Merge branch 'backstage:master' into feature/support-aoss
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-nomad-backend': patch
|
||||
---
|
||||
|
||||
Added support for the [new backend system](https://backstage.io/docs/backend-system/)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
The `HostDiscovery` export has been deprecated, import it from `@backstage/backend-app-api` instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Wrap entire app in `<Suspense>`, enabling support for using translations outside plugins.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Display log visibility button on the template panel
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Updated to import `HostDiscovery` from `@backstage/backend-app-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Moved `HostDiscovery` from `@backstage/backend-common`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': minor
|
||||
---
|
||||
|
||||
URL encode some well known unsafe characters in `RouteResolver` (and therefore `useRouteRef`)
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,222 @@
|
||||
# Declarative Integrated Search Plugin
|
||||
|
||||
> **Disclaimer:**
|
||||
> Declarative integration is in an experimental stage and is not recommended for production.
|
||||
|
||||
This is a guide for experimenting with `Search` in a declarative integrated Backstage front-end application.
|
||||
|
||||
## Main Concepts
|
||||
|
||||
Using declarative integration, you can customize your Backstage instance without writing code, see this [RFC](https://github.com/backstage/backstage/issues/18372) for more information.
|
||||
|
||||
In the new frontend system, everything that extends Backstage's core features is called an extension, so an extension can be anything from an API to a page component.
|
||||
|
||||
Extensions produces output artifacts and these artifacts are inputs consumed by other extensions:
|
||||
|
||||

|
||||
|
||||
In the image above, a `SearchResultItem` extension outputs a component and this component is injected as input to the `SearchPage` "items" attachment point. The `SearchPage` in turn uses the search result items to compose a search page element and outputs a route path and the page element so they are used as inputs attached to the `CoreRoutes` extension. Finally, the `CoreRoutes` renders the page element when the location matches the search page path.
|
||||
|
||||
The basic concepts briefly mentioned are crucial to understanding how the declarative version of the `Search` plugin works.
|
||||
|
||||
## Search Plugin
|
||||
|
||||
The search plugin is a collection of extensions that implement the search feature in Backstage.
|
||||
|
||||
### Installation
|
||||
|
||||
Only one step is required to start using the `Search` plugin within declarative integration, so all you have to do is to install the `@backstage/plugin-catalog` and `@backstage/plugin-search` packages, (e.g., [app-next](https://github.com/backstage/backstage/tree/master/packages/app-next)):
|
||||
|
||||
```sh
|
||||
yarn add @backstage/plugin-catalog @backstage/plugin-search
|
||||
```
|
||||
|
||||
The `Search` plugin depends on the `Catalog API`, that's the reason we have to install the ` @backstage/plugin-catalog` package too.
|
||||
|
||||
### Extensions
|
||||
|
||||
The `Search` plugin provides the following [extensions preset](https://github.com/backstage/backstage/blob/3f4a44aef39bd8dbf5098e60b6fdf66fd754c6d9/plugins/search/src/alpha.tsx#L246):
|
||||
|
||||
- **SearchApi**: Outputs a concrete implementation for the `Search API` that is attached as an input to the `Core` apis holder;
|
||||
- **SearchPage**: Outputs a component that represents the advanced `Search` page interface, this extension expects `Search` result items components as inputs to use them for rendering results in a custom way;
|
||||
- **SearchNavItem**: It is an extension that outputs a data that represents a `Search` item in the main application sidebar, in other words, it inputs a sidebar item to the `Core` nav extension.
|
||||
|
||||
### Configurations
|
||||
|
||||
The `Search` extensions are configurable via `app-config.yaml` file in the `app.extensions` field using the extension id as the configuration key:
|
||||
|
||||
_Example disabling the search page extension_
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- plugin.search.page: false # ✨
|
||||
```
|
||||
|
||||
_Example setting the search sidebar item label_
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- plugin.search.nav.index: # ✨
|
||||
config:
|
||||
label: 'Search Page'
|
||||
```
|
||||
|
||||
> **Known limitations:**
|
||||
> It is currently not possible to open modals in sidebar items and also configure a different icon via configuration file, but it is already on the maintainers' radar.
|
||||
|
||||
### Customizations
|
||||
|
||||
Plugin developers can use the `createSearchResultItemExtension` factory provided by the `@backstage/plugin-search-react` for building their own custom `Search` result item extensions.
|
||||
|
||||
_Example creating a custom `TechDocsSearchResultItemExtension`_
|
||||
|
||||
```tsx
|
||||
// plugins/techdocs/alpha.tsx
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const TechDocsSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
noTrack: z.boolean().default(false),
|
||||
lineClamp: z.number().default(5),
|
||||
}),
|
||||
),
|
||||
predicate: result => result.type === 'techdocs',
|
||||
component: async ({ config }) => {
|
||||
const { TechDocsSearchResultListItem } = await import(
|
||||
'./components/TechDocsSearchResultListItem'
|
||||
);
|
||||
return props => <TechDocsSearchResultListItem {...props} {...config} />;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In the snippet above, a plugin developer is providing a custom component for rendering search results of type "techdocs". The custom result item extension will be enabled by default once the `@backstage/plugin-techdocs` package is installed, that means adopters don't have to enable the extension manually via configuration file.
|
||||
|
||||
When a Backstage adopter doesn't want to use the custom `TechDocs` search result item after installing the `TechDocs` plugin, they could disable it via Backstage configuration file:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- plugin.search.result.item.techdocs: false # ✨
|
||||
```
|
||||
|
||||
Because a configuration schema was provided to the extension factory, Backstage adopters will be able to customize `TechDocs` search results **line clamp** that defaults to 3 and also **disable automatic analytics events tracking**:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- plugin.search.result.item.techdocs:
|
||||
config: # ✨
|
||||
noTrack: true
|
||||
lineClamp: 3
|
||||
```
|
||||
|
||||
[comment]: <> (TODO: Extract this explanation to a more central place in the future)
|
||||
The `createSearchResultItemExtension` function returns a Backstage's extension representation as follows:
|
||||
|
||||
```ts
|
||||
{
|
||||
"$$type": "@backstage/Extension", // [1]
|
||||
"id": "plugin.search.result.item.techdocs", // [2]
|
||||
"at": "plugin.search.page/items", // [3]
|
||||
"inputs": {} // [4️]
|
||||
"output": { // [5️]
|
||||
"item": {
|
||||
"$$type": "@backstage/ExtensionDataRef",
|
||||
"id": "plugin.search.result.item.data",
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"configSchema": { // [6️]
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"noTrack": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"lineClamp": {
|
||||
"type": "number",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
},
|
||||
"disabled": false, // [7️]
|
||||
}
|
||||
```
|
||||
|
||||
In this object, you can see exactly what will happen once the custom extension is installed:
|
||||
|
||||
- **[1] $$type**: declares that the object represents an extension;
|
||||
- **[2] id**: Is a unique identification for the extension, the `plugin.search.result.item.techdocs` is the key used to configure the extension in the `app-config.yaml` file;
|
||||
- **[3] at**: It represents the extension attachment point, so the value `plugin.search.page/items` says that the `TechDocs`'s search result item output will be injected as input on the "items" attachment expected by the search page extension;
|
||||
- **[4] inputs**: in this case is an empty object because this extension doesn't expect inputs;
|
||||
- **[5] output**: Object representing the artifact produced by the `TechDocs` result item extension, on the example, it is a react component reference;
|
||||
- **[6] configSchema**: represents the `TechDocs` search result item configuration definition, this is the same schema that adopters will use for customizing the extension via `app-config.yaml` file;
|
||||
- **[7] disable**: Says that the result item extension will be enable by default when the `TechDocs` plugin is installed in the app.
|
||||
|
||||
To complete the development cycle for creating a custom search result item extension, we should provide the extension via `TechDocs` plugin:
|
||||
|
||||
```tsx
|
||||
// plugins/techdocs/alpha.tsx
|
||||
import { createPlugin } from "@backstage/frontend-plugin-api";
|
||||
|
||||
// plugins should be always exported as default
|
||||
export default createPlugin({
|
||||
id: 'techdocs'
|
||||
extensions: [TechDocsSearchResultItemExtension]
|
||||
})
|
||||
```
|
||||
|
||||
Here is the `plugins/techdocs/alpha.tsx` final version, and you can also take a look at the [actual implementation](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/alpha.tsx) of a custom `TechDocs` search result item:
|
||||
|
||||
```tsx
|
||||
// plugins/techdocs/alpha.tsx
|
||||
import { createPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const TechDocsSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
noTrack: z.boolean().default(false),
|
||||
lineClamp: z.number().default(5),
|
||||
}),
|
||||
),
|
||||
predicate: result => result.type === 'techdocs',
|
||||
component: async ({ config }) => {
|
||||
const { TechDocsSearchResultListItem } = await import(
|
||||
'./components/TechDocsSearchResultListItem'
|
||||
);
|
||||
return props => <TechDocsSearchResultListItem {...props} {...config} />;
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export default createPlugin({
|
||||
// plugins should be always exported as default
|
||||
id: 'techdocs',
|
||||
extensions: [TechDocsSearchResultListItemExtension],
|
||||
});
|
||||
```
|
||||
|
||||
### Future Enhancement Opportunities
|
||||
|
||||
Backstage maintainers are currently working on the extension replacement feature, and with this release, adopters will also be able to replace extensions provided by plugins, so stay tuned for future updates to this documentation.
|
||||
|
||||
The first version of the `SearchPage` extension makes room for the `Search` plugin maintainers to convert filters into extensions as well in the future, if you also would like to collaborate with them on this idea, don't hesitate to open an issue and submit a pull request, your contribution is more than welcome!
|
||||
@@ -10,6 +10,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { CacheClient } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { CorsOptions } from 'cors';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import { Express as Express_2 } from 'express';
|
||||
import { Format } from 'logform';
|
||||
@@ -25,7 +26,6 @@ import { LoadConfigOptionsRemote } from '@backstage/config-loader';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { RemoteConfigSourceOptions } from '@backstage/config-loader';
|
||||
import { RequestHandler } from 'express';
|
||||
import { RequestListener } from 'http';
|
||||
@@ -114,7 +114,7 @@ export interface DefaultRootHttpRouterOptions {
|
||||
|
||||
// @public (undocumented)
|
||||
export const discoveryServiceFactory: () => ServiceFactory<
|
||||
PluginEndpointDiscovery,
|
||||
DiscoveryService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
@@ -128,6 +128,20 @@ export interface ExtendedHttpServer extends http.Server {
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
basePath?: string;
|
||||
},
|
||||
): HostDiscovery;
|
||||
// (undocumented)
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface HttpRouterFactoryOptions {
|
||||
getPath?(pluginId: string): string;
|
||||
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
/** Discovery options. */
|
||||
discovery?: {
|
||||
/**
|
||||
* Endpoints
|
||||
*
|
||||
* A list of target baseUrls and the associated plugins.
|
||||
*/
|
||||
endpoints: {
|
||||
/**
|
||||
* The target baseUrl to use for the plugin
|
||||
*
|
||||
* Can be either a string or an object with internal and external keys.
|
||||
* Targets with `{{pluginId}}` or `{{ pluginId }} in the url will be replaced with the pluginId.
|
||||
*/
|
||||
target: string | { internal: string; external: string };
|
||||
/** Array of plugins which use the target baseUrl. */
|
||||
plugins: string[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
@@ -90,8 +90,10 @@
|
||||
"mock-fs": "^5.2.0",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"configSchema": "config.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts",
|
||||
"alpha"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Config } from '@backstage/config';
|
||||
import { readHttpServerOptions } from '@backstage/backend-app-api';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
|
||||
type Target = string | { internal: string; external: string };
|
||||
|
||||
/**
|
||||
* HostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
* that can handle plugins that are hosted in a single or multiple deployments.
|
||||
*
|
||||
* The deployment may be scaled horizontally, as long as the external URL
|
||||
* is the same for all instances. However, internal URLs will always be
|
||||
* resolved to the same host, so there won't be any balancing of internal traffic.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
/**
|
||||
* Creates a new HostDiscovery discovery instance by reading
|
||||
* from the `backend` config section, specifically the `.baseUrl` for
|
||||
* discovering the external URL, and the `.listen` and `.https` config
|
||||
* for the internal one.
|
||||
*
|
||||
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
||||
* eg.
|
||||
* ```yaml
|
||||
* discovery:
|
||||
* endpoints:
|
||||
* - target: https://internal.example.com/internal-catalog
|
||||
* plugins: [catalog]
|
||||
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
||||
* plugins: [auth, permission]
|
||||
* - target:
|
||||
* internal: https://internal.example.com/search
|
||||
* external: https://example.com/search
|
||||
* plugins: [search]
|
||||
* ```
|
||||
*
|
||||
* The basePath defaults to `/api`, meaning the default full internal
|
||||
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
||||
*/
|
||||
static fromConfig(config: Config, options?: { basePath?: string }) {
|
||||
const basePath = options?.basePath ?? '/api';
|
||||
const externalBaseUrl = config
|
||||
.getString('backend.baseUrl')
|
||||
.replace(/\/+$/, '');
|
||||
|
||||
const {
|
||||
listen: { host: listenHost = '::', port: listenPort },
|
||||
} = readHttpServerOptions(config.getConfig('backend'));
|
||||
const protocol = config.has('backend.https') ? 'https' : 'http';
|
||||
|
||||
// Translate bind-all to localhost, and support IPv6
|
||||
let host = listenHost;
|
||||
if (host === '::' || host === '') {
|
||||
// We use localhost instead of ::1, since IPv6-compatible systems should default
|
||||
// to using IPv6 when they see localhost, but if the system doesn't support IPv6
|
||||
// things will still work.
|
||||
host = 'localhost';
|
||||
} else if (host === '0.0.0.0') {
|
||||
host = '127.0.0.1';
|
||||
}
|
||||
if (host.includes(':')) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
|
||||
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
||||
|
||||
return new HostDiscovery(
|
||||
internalBaseUrl + basePath,
|
||||
externalBaseUrl + basePath,
|
||||
config.getOptionalConfig('discovery'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly internalBaseUrl: string,
|
||||
private readonly externalBaseUrl: string,
|
||||
private readonly discoveryConfig: Config | undefined,
|
||||
) {}
|
||||
|
||||
private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') {
|
||||
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
|
||||
|
||||
const target = endpoints
|
||||
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
|
||||
?.get<Target>('target');
|
||||
|
||||
if (!target) {
|
||||
const baseUrl =
|
||||
type === 'external' ? this.externalBaseUrl : this.internalBaseUrl;
|
||||
|
||||
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
return target.replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
return target[type].replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'internal');
|
||||
}
|
||||
|
||||
async getExternalBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'external');
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HostDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
/** @public */
|
||||
export const discoveryServiceFactory = createServiceFactory({
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { discoveryServiceFactory } from './discoveryServiceFactory';
|
||||
export { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
@@ -28,6 +28,7 @@ import { GiteaIntegration } from '@backstage/integration';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GithubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
import { HostDiscovery as HostDiscovery_2 } from '@backstage/backend-app-api';
|
||||
import { IdentityService } from '@backstage/backend-plugin-api';
|
||||
import { isChildPath } from '@backstage/cli-common';
|
||||
import { Knex } from 'knex';
|
||||
@@ -479,18 +480,7 @@ export class GitlabUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
// @public
|
||||
export class HostDiscovery implements PluginEndpointDiscovery {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
basePath?: string;
|
||||
},
|
||||
): HostDiscovery;
|
||||
// (undocumented)
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
export const HostDiscovery: typeof HostDiscovery_2;
|
||||
|
||||
export { isChildPath };
|
||||
|
||||
@@ -751,7 +741,7 @@ export type ServiceBuilder = {
|
||||
export function setRootLogger(newLogger: winston.Logger): void;
|
||||
|
||||
// @public @deprecated
|
||||
export const SingleHostDiscovery: typeof HostDiscovery;
|
||||
export const SingleHostDiscovery: typeof HostDiscovery_2;
|
||||
|
||||
// @public
|
||||
export type StatusCheck = () => Promise<any>;
|
||||
|
||||
Vendored
-20
@@ -216,24 +216,4 @@ export interface Config {
|
||||
*/
|
||||
csp?: { [policyId: string]: string[] | false };
|
||||
};
|
||||
|
||||
/** Discovery options. */
|
||||
discovery?: {
|
||||
/**
|
||||
* Endpoints
|
||||
*
|
||||
* A list of target baseUrls and the associated plugins.
|
||||
*/
|
||||
endpoints: {
|
||||
/**
|
||||
* The target baseUrl to use for the plugin
|
||||
*
|
||||
* Can be either a string or an object with internal and external keys.
|
||||
* Targets with `{{pluginId}}` or `{{ pluginId }} in the url will be replaced with the pluginId.
|
||||
*/
|
||||
target: string | { internal: string; external: string };
|
||||
/** Array of plugins which use the target baseUrl. */
|
||||
plugins: string[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,11 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { PluginEndpointDiscovery } from './types';
|
||||
import { readHttpServerOptions } from '@backstage/backend-app-api';
|
||||
import { HostDiscovery as _HostDiscovery } from '@backstage/backend-app-api';
|
||||
|
||||
type Target = string | { internal: string; external: string };
|
||||
export type { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
* HostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
@@ -30,106 +28,7 @@ type Target = string | { internal: string; external: string };
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class HostDiscovery implements PluginEndpointDiscovery {
|
||||
/**
|
||||
* Creates a new HostDiscovery discovery instance by reading
|
||||
* from the `backend` config section, specifically the `.baseUrl` for
|
||||
* discovering the external URL, and the `.listen` and `.https` config
|
||||
* for the internal one.
|
||||
*
|
||||
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
||||
* eg.
|
||||
* ```yaml
|
||||
* discovery:
|
||||
* endpoints:
|
||||
* - target: https://internal.example.com/internal-catalog
|
||||
* plugins: [catalog]
|
||||
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
||||
* plugins: [auth, permission]
|
||||
* - target:
|
||||
* internal: https://internal.example.com/search
|
||||
* external: https://example.com/search
|
||||
* plugins: [search]
|
||||
* ```
|
||||
*
|
||||
* The basePath defaults to `/api`, meaning the default full internal
|
||||
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
||||
*/
|
||||
static fromConfig(config: Config, options?: { basePath?: string }) {
|
||||
const basePath = options?.basePath ?? '/api';
|
||||
const externalBaseUrl = config
|
||||
.getString('backend.baseUrl')
|
||||
.replace(/\/+$/, '');
|
||||
|
||||
const {
|
||||
listen: { host: listenHost = '::', port: listenPort },
|
||||
} = readHttpServerOptions(config.getConfig('backend'));
|
||||
const protocol = config.has('backend.https') ? 'https' : 'http';
|
||||
|
||||
// Translate bind-all to localhost, and support IPv6
|
||||
let host = listenHost;
|
||||
if (host === '::' || host === '') {
|
||||
// We use localhost instead of ::1, since IPv6-compatible systems should default
|
||||
// to using IPv6 when they see localhost, but if the system doesn't support IPv6
|
||||
// things will still work.
|
||||
host = 'localhost';
|
||||
} else if (host === '0.0.0.0') {
|
||||
host = '127.0.0.1';
|
||||
}
|
||||
if (host.includes(':')) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
|
||||
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
||||
|
||||
return new HostDiscovery(
|
||||
internalBaseUrl + basePath,
|
||||
externalBaseUrl + basePath,
|
||||
config.getOptionalConfig('discovery'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly internalBaseUrl: string,
|
||||
private readonly externalBaseUrl: string,
|
||||
private readonly discoveryConfig: Config | undefined,
|
||||
) {}
|
||||
|
||||
private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') {
|
||||
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
|
||||
|
||||
const target = endpoints
|
||||
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
|
||||
?.get<Target>('target');
|
||||
|
||||
if (!target) {
|
||||
const baseUrl =
|
||||
type === 'external' ? this.externalBaseUrl : this.internalBaseUrl;
|
||||
|
||||
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
return target.replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
return target[type].replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'internal');
|
||||
}
|
||||
|
||||
async getExternalBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'external');
|
||||
}
|
||||
}
|
||||
export const HostDiscovery = _HostDiscovery;
|
||||
|
||||
/**
|
||||
* SingleHostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
@@ -142,4 +41,4 @@ export class HostDiscovery implements PluginEndpointDiscovery {
|
||||
* @public
|
||||
* @deprecated Use {@link HostDiscovery} instead
|
||||
*/
|
||||
export const SingleHostDiscovery = HostDiscovery;
|
||||
export const SingleHostDiscovery = _HostDiscovery;
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { HostDiscovery, SingleHostDiscovery } from './HostDiscovery';
|
||||
export type { PluginEndpointDiscovery } from './types';
|
||||
export {
|
||||
HostDiscovery,
|
||||
SingleHostDiscovery,
|
||||
type PluginEndpointDiscovery,
|
||||
} from './HostDiscovery';
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@backstage/plugin-kubernetes-backend": "workspace:^",
|
||||
"@backstage/plugin-lighthouse-backend": "workspace:^",
|
||||
"@backstage/plugin-linguist-backend": "workspace:^",
|
||||
"@backstage/plugin-nomad-backend": "workspace:^",
|
||||
"@backstage/plugin-permission-backend": "workspace:^",
|
||||
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
|
||||
@@ -33,6 +33,7 @@ backend.add(import('@backstage/plugin-kubernetes-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-lighthouse-backend'));
|
||||
backend.add(import('@backstage/plugin-linguist-backend'));
|
||||
backend.add(import('@backstage/plugin-playlist-backend'));
|
||||
backend.add(import('@backstage/plugin-nomad-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
);
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
MiddlewareFactory,
|
||||
createHttpServer,
|
||||
ExtendedHttpServer,
|
||||
HostDiscovery,
|
||||
DefaultRootHttpRouter,
|
||||
} from '@backstage/backend-app-api';
|
||||
import { HostDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
createServiceFactory,
|
||||
BackendFeature,
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"history": "^5.0.0",
|
||||
"i18next": "^22.4.15",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-use": "^17.2.4",
|
||||
"zen-observable": "^0.10.0",
|
||||
|
||||
@@ -35,16 +35,34 @@ import {
|
||||
createRoutableExtension,
|
||||
analyticsApiRef,
|
||||
useApi,
|
||||
errorApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppManager } from './AppManager';
|
||||
import { AppComponents, AppIcons } from './types';
|
||||
import { FeatureFlagged } from '../routing/FeatureFlagged';
|
||||
import {
|
||||
createTranslationRef,
|
||||
useTranslationRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
describe('Integration Test', () => {
|
||||
const noOpAnalyticsApi = createApiFactory(
|
||||
analyticsApiRef,
|
||||
new NoOpAnalyticsApi(),
|
||||
);
|
||||
const noopErrorApi = createApiFactory(errorApiRef, {
|
||||
error$() {
|
||||
return {
|
||||
subscribe() {
|
||||
return { unsubscribe() {}, closed: true };
|
||||
},
|
||||
[Symbol.observable]() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
post() {},
|
||||
});
|
||||
const plugin1RouteRef = createRouteRef({ id: 'ref-1' });
|
||||
const plugin1RouteRef2 = createRouteRef({ id: 'ref-1-2' });
|
||||
const plugin2RouteRef = createRouteRef({ id: 'ref-2', params: ['x'] });
|
||||
@@ -175,6 +193,10 @@ describe('Integration Test', () => {
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('runs happy paths', async () => {
|
||||
const app = new AppManager({
|
||||
apis: [noOpAnalyticsApi],
|
||||
@@ -262,7 +284,7 @@ describe('Integration Test', () => {
|
||||
|
||||
it('runs success with __experimentalTranslations', async () => {
|
||||
const app = new AppManager({
|
||||
apis: [noOpAnalyticsApi],
|
||||
apis: [noOpAnalyticsApi, noopErrorApi],
|
||||
defaultApis: [],
|
||||
themes,
|
||||
icons,
|
||||
@@ -277,27 +299,41 @@ describe('Integration Test', () => {
|
||||
},
|
||||
__experimentalTranslations: {
|
||||
availableLanguages: ['en', 'de'],
|
||||
defaultLanguage: 'de',
|
||||
},
|
||||
});
|
||||
|
||||
const Provider = app.getProvider();
|
||||
const Router = app.getRouter();
|
||||
|
||||
const translationRef = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: {
|
||||
foo: 'Foo',
|
||||
},
|
||||
translations: {
|
||||
de: () => Promise.resolve({ default: { foo: 'Bar' } }),
|
||||
},
|
||||
});
|
||||
|
||||
const TranslatedComponent = () => {
|
||||
const { t } = useTranslationRef(translationRef);
|
||||
return <div>translation: {t('foo')}</div>;
|
||||
};
|
||||
|
||||
await renderWithEffects(
|
||||
<Provider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<ExposedComponent />} />
|
||||
<Route path="/foo" element={<HiddenComponent />} />
|
||||
<Route path="/" element={<TranslatedComponent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('extLink1: /')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink2: /foo')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink3: <none>')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink4: <none>')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('translation: Bar'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should wait for the config to load before calling feature flags', async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Config } from '@backstage/config';
|
||||
import React, {
|
||||
ComponentType,
|
||||
PropsWithChildren,
|
||||
Suspense,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
@@ -341,7 +342,7 @@ export class AppManager implements BackstageApp {
|
||||
}
|
||||
}
|
||||
|
||||
const { ThemeProvider = AppThemeProvider } = this.components;
|
||||
const { ThemeProvider = AppThemeProvider, Progress } = this.components;
|
||||
|
||||
return (
|
||||
<ApiProvider apis={this.getApiHolder()}>
|
||||
@@ -360,7 +361,7 @@ export class AppManager implements BackstageApp {
|
||||
appIdentityProxy: this.appIdentityProxy,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Suspense fallback={<Progress />}>{children}</Suspense>
|
||||
</InternalAppContext.Provider>
|
||||
</RoutingProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -364,4 +364,31 @@ describe('RouteResolver', () => {
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode some characters in params', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref1, 'my-route'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([[ref1, ref2]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
'/base',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref2, '/')?.({ x: 'a/#&?b' })).toBe(
|
||||
'/base/my-parent/a%2F%23%26%3Fb',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -390,4 +390,33 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode some characters in params', () => {
|
||||
const { RouteResolver } =
|
||||
require('./RouteResolver') as typeof import('./RouteResolver');
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref1, 'my-route'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([[ref1, ref2]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
'/base',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref2, '/')?.({ x: 'a/#&?b' })).toBe(
|
||||
'/base/my-parent/a%2F%23%26%3Fb',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,4 +364,31 @@ describe('RouteResolver', () => {
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode some characters in params', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref1, 'my-route'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([[ref1, ref2]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
'/base',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref2, '/')?.({ x: 'a/#&?b' })).toBe(
|
||||
'/base/my-parent/a%2F%23%26%3Fb',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
SubRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { joinPaths } from './helpers';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
/**
|
||||
* Resolves the absolute route ref that our target route ref is pointing pointing to, as well
|
||||
@@ -225,7 +226,23 @@ export class RouteResolver {
|
||||
);
|
||||
|
||||
const routeFunc: RouteFunc<Params> = (...[params]) => {
|
||||
return joinPaths(basePath, generatePath(targetPath, params));
|
||||
// We selectively encode some some known-dangerous characters in the
|
||||
// params. The reason that we don't perform a blanket `encodeURIComponent`
|
||||
// here is that this encoding was added defensively long after the initial
|
||||
// release of this code. There's likely to be many users of this code that
|
||||
// already encode their parameters knowing that this code didn't do this
|
||||
// for them in the past. Therefore, we are extra careful NOT to include
|
||||
// the percent character in this set, even though that might seem like a
|
||||
// bad idea.
|
||||
const encodedParams =
|
||||
params &&
|
||||
mapValues(params, value => {
|
||||
if (typeof value === 'string') {
|
||||
return value.replaceAll(/[&?#;\/]/g, c => encodeURIComponent(c));
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return joinPaths(basePath, generatePath(targetPath, encodedParams));
|
||||
};
|
||||
return routeFunc;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
# @backstage/plugin-nomad-backend
|
||||
|
||||
A backend for [Nomad](https://www.nomadproject.io/), this plugin exposes a service with routes that are used by the `@backstage/plugin-nomad` plugin to query Job and Group information from a Nomad API.
|
||||
A backend for [Nomad](https://www.nomadproject.io/), this plugin exposes a service with routes that are used by the `@backstage/plugin-nomad-backend` plugin to query Job and Group information from a Nomad API.
|
||||
|
||||
## New Backend System
|
||||
|
||||
The Nomad backend plugin has support for the [new backend system](https://backstage.io/docs/backend-system/), here's how you can set that up:
|
||||
|
||||
In your `packages/backend/src/index.ts` make the following changes:
|
||||
|
||||
```diff
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
const backend = createBackend();
|
||||
// ... other feature additions
|
||||
backend.add(import('@backstage/plugin-nomad-backend'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
## Set Up
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
@@ -10,6 +11,10 @@ import { Logger } from 'winston';
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public
|
||||
const nomadPlugin: () => BackendFeature;
|
||||
export default nomadPlugin;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@types/express": "*",
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './service/router';
|
||||
export { nomadPlugin as default } from './plugin';
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* Copyright 2023 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.
|
||||
@@ -13,5 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { nomadPlugin } from './plugin';
|
||||
|
||||
export type { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api';
|
||||
describe('nomad', () => {
|
||||
it('should export the nomad plugin', () => {
|
||||
expect(nomadPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2023 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 { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createBackendPlugin,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
|
||||
/**
|
||||
* Nomad backend plugin
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const nomadPlugin = createBackendPlugin({
|
||||
pluginId: 'nomad',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
config: coreServices.rootConfig,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
},
|
||||
async init({ logger, config, httpRouter }) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
/**
|
||||
* Logger for logging purposes
|
||||
*/
|
||||
logger: winstonLogger,
|
||||
config,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -112,4 +112,29 @@ describe('OngoingTask', () => {
|
||||
expect(getByTestId('cancel-button')).toHaveClass('Mui-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
it('should initially do not display logs', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
|
||||
<OngoingTask />
|
||||
</TestApiProvider>,
|
||||
{ mountedRoutes: { '/': rootRouteRef } },
|
||||
);
|
||||
await expect(rendered.findByText('Show Logs')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle logs visibility', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
|
||||
<OngoingTask />
|
||||
</TestApiProvider>,
|
||||
{ mountedRoutes: { '/': rootRouteRef } },
|
||||
);
|
||||
await act(async () => {
|
||||
const element = await rendered.findByText('Show Logs');
|
||||
fireEvent.click(element);
|
||||
});
|
||||
|
||||
await expect(rendered.findByText('Hide Logs')).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,9 @@ const useStyles = makeStyles(theme => ({
|
||||
cancelButton: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
logsVisibilityButton: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const OngoingTask = (props: {
|
||||
@@ -188,6 +191,14 @@ export const OngoingTask = (props: {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.logsVisibilityButton}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => setLogVisibleState(!logsVisible)}
|
||||
>
|
||||
{logsVisible ? 'Hide Logs' : 'Show Logs'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
||||
@@ -3955,6 +3955,7 @@ __metadata:
|
||||
"@types/zen-observable": ^0.8.0
|
||||
history: ^5.0.0
|
||||
i18next: ^22.4.15
|
||||
lodash: ^4.17.21
|
||||
msw: ^1.0.0
|
||||
prop-types: ^15.7.2
|
||||
react-router-beta: "npm:react-router@6.0.0-beta.0"
|
||||
@@ -7981,6 +7982,7 @@ __metadata:
|
||||
resolution: "@backstage/plugin-nomad-backend@workspace:plugins/nomad-backend"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
@@ -25624,6 +25626,7 @@ __metadata:
|
||||
"@backstage/plugin-kubernetes-backend": "workspace:^"
|
||||
"@backstage/plugin-lighthouse-backend": "workspace:^"
|
||||
"@backstage/plugin-linguist-backend": "workspace:^"
|
||||
"@backstage/plugin-nomad-backend": "workspace:^"
|
||||
"@backstage/plugin-permission-backend": "workspace:^"
|
||||
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
@@ -43202,9 +43205,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.21.4":
|
||||
version: 3.22.2
|
||||
resolution: "zod@npm:3.22.2"
|
||||
checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9
|
||||
version: 3.22.3
|
||||
resolution: "zod@npm:3.22.3"
|
||||
checksum: 65b05139be337078a70700b05942ab7f2ef5f11abe194df14ef257fac4e5c383476a4dc290731842996bd57fc8d5bf38e5a4c907fe8cdf8b15477f8da5bfcc00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user