From 7f29dc4e541a5ad5fb4fba43dd7965fb6e9b2c70 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 11:59:42 -0500 Subject: [PATCH 001/112] document VMware Cloud auth provider Signed-off-by: Jamie Klassen --- docs/auth/index.md | 1 + docs/auth/vmware-cloud/provider.md | 250 +++++++++++++++++++++++++++++ microsite/sidebars.json | 3 +- 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 docs/auth/vmware-cloud/provider.md diff --git a/docs/auth/index.md b/docs/auth/index.md index 806cda0ff7..f6282264e4 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -35,6 +35,7 @@ Backstage comes with many common authentication providers in the core library: - [Okta](okta/provider.md) - [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) - [OneLogin](onelogin/provider.md) +- [VMware Cloud](vmware-cloud/provider.md) These built-in providers handle the authentication flow for a particular service including required scopes, callbacks, etc. These providers are each added to a diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md new file mode 100644 index 0000000000..00a1315b6a --- /dev/null +++ b/docs/auth/vmware-cloud/provider.md @@ -0,0 +1,250 @@ +--- +id: provider +title: VMware Cloud Authentication Provider +sidebar_label: VMware Cloud +description: Adding VMware Cloud as an authentication provider in Backstage +--- + +Backstage comes with an auth provider module to allow users to sign-in with +their VMware Cloud account. This page describes some actions within the VMware +Cloud Console and within a Backstage app required to enable this capability. + +## Create an OAuth App in the VMware Cloud Console + +1. Log in to the [VMware Cloud Console](https://console.cloud.vmware.com). +1. Navigate to [Identity & Access Management > OAuth + Apps](https://console.cloud.vmware.com/csp/gateway/portal/#/consumer/usermgmt/oauth-apps) + and click the [Owned + Apps](https://console.cloud.vmware.com/csp/gateway/portal/#/consumer/usermgmt/oauth-apps/owned-apps/view) + tab -- if you are not an Organization Owner or Administrator but only a + Member, you will not see this nav entry unless the **Developer** check box is + selected for your role (see the [Organization roles and + permissions](https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-C11D3AAC-267C-4F16-A0E3-3EDF286EBE53.html#organization-roles-and-permissions-0) + docs for details). +1. Click **Create App**, choose 'Web/Mobile app' and click **Continue**. +1. Use default settings except: + - `App Name` and `App Description` of your choosing. + - `Redirect URIs`: `${baseUrl}/api/auth/vmwareCloudServices/handler/frame` + where `baseUrl` is the URL where your Backstage backend can be reached; + note that VMware Cloud does not support the combination of an `http://` + scheme and a `localhost` hostname, so when testing locally it may help to + set your backend base URL to `http://127.0.0.1:7007`. + - `Refresh Token`: check `Issue refresh token`; refresh tokens are required + to prevent forcing users to re-login when they refresh their browser. + - `Define Scopes`: check `OpenID` at the bottom. +1. Click **Create**. +1. Take note of the `App ID` in the resulting modal; this is the client ID to be + used by Backstage. + +## Install the provider in the backend + +### New backend system + +Apps using the [new backend system](../../backend-system/index.md), +can enable the VMware Cloud provider with a small modification like: + +```ts title="packages/backend-next/src/index.ts" +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'), +); +/* highlight-add-end */ +backend.start(); +``` + +### Old backend system + +This provider was added after the migration of the auth-backend plugin to the +new backend system, so no default provider factory was added. Because of this, +the installation procedure for old-style backends is slightly more involved: + +```ts title="packages/backend/src/plugins/auth.ts" +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +/* highlight-add-start */ +import { + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { + vmwareCloudAuthenticator, +} from '@backstage/plugin-auth-backend-module-vmware-cloud-provider'; +/* highlight-add-end */ + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + /* highlight-add-start */ + vmwareCloudServices: createOAuthProviderFactory({ + authenticator: vmwareCloudAuthenticator, + signInResolver: + commonSignInResolvers.emailLocalPartMatchingUserEntityName(), + }), + /* highlight-add-end */ +``` + +In the above, `commonSignInResolvers.emailLocalPartMatchingUserEntityName()` +can be replaced with a more suitable resolver for the app in question. + +## Configure Sign-in Resolution + +See [Sign-in Identities and Resolvers](../identity-resolver.md) for details. + +## Create a Utility API + +At the moment, this auth provider is not included in the default API factories, +but you can create one with a change to your frontend like: + +```ts title="packages/app/src/apis.ts" +import { + ScmIntegrationsApi, + scmIntegrationsApiRef, + ScmAuth, +} from '@backstage/integration-react'; +import { + costInsightsApiRef, + ExampleCostInsightsClient, +} from '@backstage/plugin-cost-insights'; +import { + graphQlBrowseApiRef, + GraphQLEndpoints, +} from '@backstage/plugin-graphiql'; +/* highlight-add-start */ +import { OAuth2 } from '@backstage/core-app-api'; +/* highlight-add-end */ +import { + AnyApiFactory, + /* highlight-add-start */ + ApiRef, + OpenIdConnectApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionApi, + /* highlight-add-end */ + configApiRef, + /* highlight-add-start */ + createApiRef, + /* highlight-add-end */ + createApiFactory, + discoveryApiRef, + errorApiRef, + fetchApiRef, + githubAuthApiRef, + /* highlight-add-start */ + oauthRequestApiRef, + /* highlight-add-end */ +} from '@backstage/core-plugin-api'; +import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi'; + +/* highlight-add-start */ +export const vmwareCloudAuthApiRef: ApiRef< + OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'auth.vmware-cloud-auth-provider', +}); +/* highlight-add-end */ + +export const apis: AnyApiFactory[] = [ + /* highlight-add-start */ + createApiFactory({ + api: vmwareCloudAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'vmwareCloudServices', + title: 'VMware Cloud', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), + defaultScopes: ['openid'], + }), + }), + /* highlight-add-end */ +``` + +## Add to Sign-in Page + +See the [Sign-In Configuration](../index.md#sign-in-configuration) docs for +general guidance, but as an example: + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-start */ +import { vmwareCloudAuthApiRef } from './apis'; +import { SignInPage } from '@backstage/core-components'; +/* highlight-add-end */ + +const app = createApp({ + /* highlight-add-start */ + components: { + SignInPage: props => ( + + ), + }, + /* highlight-add-end */ + // .. +}); +``` + +## Configuration + +Add the following to your `app-config.yaml` under the root `auth` configuration: + +```yaml +auth: + session: + secret: your session secret + environment: development + providers: + vmwareCloudServices: + development: + clientId: ${APP_ID} + organizationId: ${ORG_ID} +``` + +where `APP_ID` refers to the ID retrieved when creating the OAuth App, and +`ORG_ID` is the [long ID of the +Organization](https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-CF9E9318-B811-48CF-8499-9419997DC1F8.html#view-the-organization-id-1) +in VMware Cloud for which you wish to enable sign-in. + +Note that VMware Cloud requires OAuth Apps to use +[PKCE](https://oauth.net/2/pkce/) when performing authorization code flows; the +library used by this provider requires the use of Express session middleware to +do this. Therefore the value `your session secret` under `auth.session.secret` +should be replaced with a long, complex and unique string which will act as a +key for signing session cookies set by Backstage. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 187c41ad75..4dda27ea8a 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -309,7 +309,8 @@ "auth/google/gcp-iap-auth", "auth/okta/provider", "auth/oauth2-proxy/provider", - "auth/onelogin/provider" + "auth/onelogin/provider", + "auth/vmware-cloud/provider" ] }, "auth/identity-resolver", From ff3a604dae3157b26f2b618411d8d860554d583e Mon Sep 17 00:00:00 2001 From: Aramis Date: Thu, 18 Jan 2024 18:00:01 -0500 Subject: [PATCH 002/112] feat(repo-tools): Update OpenAPI command naming. fixes: https://github.com/backstage/backstage/issues/21790 Signed-off-by: Aramis --- docs/openapi/01-getting-started.md | 6 +- packages/repo-tools/src/commands/index.ts | 99 +++++++++++++------ .../schema/openapi/generate/client.ts} | 35 ++++--- .../schema/openapi/generate/server.ts} | 57 +++-------- .../test => package/schema/openapi}/init.ts | 10 +- .../{ => repo/schema}/openapi/lint.ts | 25 ++--- .../index.ts => repo/schema/openapi/test.ts} | 16 +-- .../schema => repo/schema/openapi}/verify.ts | 20 ++-- .../{commands => lib}/openapi/constants.ts | 0 .../repo-tools/src/lib/openapi/helpers.ts | 32 ++++++ .../src/{commands/openapi => lib}/runner.ts | 4 +- plugins/catalog-backend/package.json | 5 +- plugins/search-backend/package.json | 3 +- plugins/todo-backend/package.json | 4 +- yarn.lock | 4 +- 15 files changed, 195 insertions(+), 125 deletions(-) rename packages/repo-tools/src/commands/{openapi/client/generate.ts => package/schema/openapi/generate/client.ts} (73%) rename packages/repo-tools/src/commands/{openapi/schema/generate.ts => package/schema/openapi/generate/server.ts} (60%) rename packages/repo-tools/src/commands/{openapi/test => package/schema/openapi}/init.ts (90%) rename packages/repo-tools/src/commands/{ => repo/schema}/openapi/lint.ts (83%) rename packages/repo-tools/src/commands/{openapi/test/index.ts => repo/schema/openapi/test.ts} (86%) rename packages/repo-tools/src/commands/{openapi/schema => repo/schema/openapi}/verify.ts (79%) rename packages/repo-tools/src/{commands => lib}/openapi/constants.ts (100%) create mode 100644 packages/repo-tools/src/lib/openapi/helpers.ts rename packages/repo-tools/src/{commands/openapi => lib}/runner.ts (94%) diff --git a/docs/openapi/01-getting-started.md b/docs/openapi/01-getting-started.md index eb3b1c1209..ce04e75107 100644 --- a/docs/openapi/01-getting-started.md +++ b/docs/openapi/01-getting-started.md @@ -45,7 +45,7 @@ You should create a new folder, `src/schema` in your backend plugin to store you ## Generating a typed express router from a spec -Run `yarn backstage-repo-tools schema openapi generate `. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types. +Run `yarn backstage-repo-tools package schema openapi generate server `. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types. You should add this command to your `package.json` for future use. Use it like so, update your `router.ts` or `createRouter.ts` file with the following content, @@ -63,7 +63,7 @@ export async function createRouter( ## Generating a typed client from a spec -Run `yarn backstage-repo-tools schema openapi generate-client --input-spec /src/schema/openapi.yaml --output-directory `. `` should match the same backend plugin we've been using so far. `` is a new directory and npm package that you should create. The general pattern is `plugins/-client`. +From your current backend plugin directory, run `yarn backstage-repo-tools package schema openapi generate client --output-package `. `` is a new directory and npm package that you should create. The general pattern is `plugins/-client` or if you want to co-locate this with your other shared types, use `plugins/-common`. You should add this command to your `package.json` for future use. The generated client will have a directory `src/generated` that exports a `DefaultApiClient` class and all generated types. You can use the client like so, @@ -108,7 +108,7 @@ describe('createRouter', () => { + app = wrapInOpenApiTestServer(express().use(router)); ``` -This adds a wrapper around the express server that allows it to reroute traffic for `supertest`. Run `yarn backstage-repo-tools schema openapi init` to create some required config files. Now, when you run `yarn backstage-repo-tools schema openapi test` your schema will now be tested against your test data. Any errors will be reported. +This adds a wrapper around the express server that allows it to reroute traffic for `supertest`. Run `yarn backstage-repo-tools package schema openapi init` to create some required files. Now, when you run `yarn backstage-repo-tools repo schema openapi test` your schema will now be tested against your test data. Any errors will be reported. Our command is a small wrapper over [`Optic`](https://github.com/opticdev/optic) which does all of the heavy lifting. diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index d9c6d892f5..06ea2fa95c 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -18,12 +18,71 @@ import { assertError } from '@backstage/errors'; import { Command } from 'commander'; import { exitWithError } from '../lib/errors'; -function registerSchemaCommand(program: Command) { +function registerPackageCommand(program: Command) { const command = program + .command('package [command]') + .description('Various tools for working with specific packages.'); + + const schemaCommand = command + .command('schema [command]') + .description( + "Various tools for working with specific packages' API schema", + ); + + const openApiCommand = schemaCommand + .command('openapi [command]') + .description('Tooling for OpenAPI schema'); + + openApiCommand + .command('init') + .description('Initialize any required files to use the OpenAPI tooling.') + .action( + lazy(() => import('./package/schema/openapi/init').then(m => m.default)), + ); + + const generateCommand = openApiCommand + .command('generate [command]') + .description( + 'Commands for generating various things from an OpenAPI spec.', + ); + + generateCommand + .command('server') + .description( + 'Generates an express server stub using the OpenAPI schema for typings.', + ) + .action( + lazy(() => + import('./package/schema/openapi/generate/server').then(m => m.command), + ), + ); + + generateCommand + .command('client') + .description( + 'Generates a client that can interact with your backend plugin using types from your OpenAPI schema.', + ) + .requiredOption( + '--output-package ', + 'Top-level path to where the client should be generated, ie packages/catalog-client.', + ) + .action( + lazy(() => + import('./package/schema/openapi/generate/client').then(m => m.command), + ), + ); +} + +function registerRepoCommand(program: Command) { + const command = program + .command('repo [command]') + .description('Tools for working across your entire repository.'); + + const schemaCommand = command .command('schema [command]') .description('Various tools for working with API schema'); - const openApiCommand = command + const openApiCommand = schemaCommand .command('openapi [command]') .description('Tooling for OpenApi schema'); @@ -33,16 +92,9 @@ function registerSchemaCommand(program: Command) { 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.', ) .action( - lazy(() => import('./openapi/schema/verify').then(m => m.bulkCommand)), - ); - - openApiCommand - .command('generate [paths...]') - .description( - 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.', - ) - .action( - lazy(() => import('./openapi/schema/generate').then(m => m.bulkCommand)), + lazy(() => + import('./repo/schema/openapi/verify').then(m => m.bulkCommand), + ), ); openApiCommand @@ -52,27 +104,16 @@ function registerSchemaCommand(program: Command) { '--strict', 'Fail on any linting severity messages, not just errors.', ) - .action(lazy(() => import('./openapi/lint').then(m => m.bulkCommand))); + .action( + lazy(() => import('./repo/schema/openapi/lint').then(m => m.bulkCommand)), + ); openApiCommand .command('test [paths...]') .description('Test OpenAPI schemas against written tests') .option('--update', 'Update the spec on failure.') - .action(lazy(() => import('./openapi/test').then(m => m.bulkCommand))); - - openApiCommand - .command('init ') - .description('Creates any config needed for the test command.') - .action(lazy(() => import('./openapi/test/init').then(m => m.default))); - - openApiCommand - .command('generate-client') - .requiredOption('--input-spec ') - .requiredOption('--output-directory ') .action( - lazy(() => - import('./openapi/client/generate').then(m => m.singleCommand), - ), + lazy(() => import('./repo/schema/openapi/test').then(m => m.bulkCommand)), ); } @@ -139,8 +180,8 @@ export function registerCommands(program: Command) { ), ), ); - - registerSchemaCommand(program); + registerPackageCommand(program); + registerRepoCommand(program); } // Wraps an action function so that it always exits and handles errors diff --git a/packages/repo-tools/src/commands/openapi/client/generate.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts similarity index 73% rename from packages/repo-tools/src/commands/openapi/client/generate.ts rename to packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index f827f1005f..101c2c918b 100644 --- a/packages/repo-tools/src/commands/openapi/client/generate.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -16,16 +16,23 @@ import chalk from 'chalk'; import { resolve } from 'path'; -import { OPENAPI_IGNORE_FILES, OUTPUT_PATH } from '../constants'; -import { paths as cliPaths } from '../../../lib/paths'; +import { + OPENAPI_IGNORE_FILES, + OUTPUT_PATH, +} from '../../../../../lib/openapi/constants'; +import { paths as cliPaths } from '../../../../../lib/paths'; import { mkdirpSync } from 'fs-extra'; import fs from 'fs-extra'; -import { exec } from '../../../lib/exec'; +import { exec } from '../../../../../lib/exec'; import { resolvePackagePath } from '@backstage/backend-common'; +import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; -async function generate(spec: string, outputDirectory: string) { - const resolvedOpenapiPath = resolve(spec); - const resolvedOutputDirectory = resolve(outputDirectory, OUTPUT_PATH); +async function generate(outputDirectory: string) { + const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); + const resolvedOutputDirectory = cliPaths.resolveTargetRoot( + outputDirectory, + OUTPUT_PATH, + ); mkdirpSync(resolvedOutputDirectory); await fs.mkdirp(resolvedOutputDirectory); @@ -80,19 +87,19 @@ async function generate(spec: string, outputDirectory: string) { }); } -export async function singleCommand({ - inputSpec, - outputDirectory, +export async function command({ + outputPackage, }: { - inputSpec: string; - outputDirectory: string; + outputPackage: string; }): Promise { try { - await generate(inputSpec, outputDirectory); - console.log(chalk.green(`Generated client for ${inputSpec}`)); + await generate(outputPackage); + console.log( + chalk.green(`Generated client in ${outputPackage}/${OUTPUT_PATH}`), + ); } catch (err) { console.log(); - console.log(chalk.red(`Client generation failed in ${outputDirectory}:`)); + console.log(chalk.red(`Client generation failed:`)); console.log(err); process.exit(1); diff --git a/packages/repo-tools/src/commands/openapi/schema/generate.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts similarity index 60% rename from packages/repo-tools/src/commands/openapi/schema/generate.ts rename to packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index cd9f572f2d..e6c56c3122 100644 --- a/packages/repo-tools/src/commands/openapi/schema/generate.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -17,30 +17,19 @@ import fs from 'fs-extra'; import YAML from 'js-yaml'; import chalk from 'chalk'; -import { resolve } from 'path'; -import { paths as cliPaths } from '../../../lib/paths'; -import { runner } from '../runner'; -import { TS_SCHEMA_PATH, YAML_SCHEMA_PATH } from '../constants'; +import { paths as cliPaths } from '../../../../../lib/paths'; +import { TS_SCHEMA_PATH } from '../../../../../lib/openapi/constants'; import { promisify } from 'util'; import { exec as execCb } from 'child_process'; +import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; const exec = promisify(execCb); -async function generate( - directoryPath: string, - config?: { skipMissingYamlFile: boolean }, -) { - const { skipMissingYamlFile } = config ?? {}; - const openapiPath = resolve(directoryPath, YAML_SCHEMA_PATH); - if (!(await fs.pathExists(openapiPath))) { - if (skipMissingYamlFile) { - return; - } - throw new Error(`Could not find ${YAML_SCHEMA_PATH} in root of directory.`); - } +async function generate() { + const openapiPath = await getPathToCurrentOpenApiSpec(); const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); - const tsPath = resolve(directoryPath, TS_SCHEMA_PATH); + const tsPath = cliPaths.resolveTarget(TS_SCHEMA_PATH); // The first set of comment slashes allow for the eslint notice plugin to run // with onNonMatchingHeader: 'replace', as is the case in the open source @@ -63,33 +52,19 @@ export const createOpenApiRouter = async ( await exec(`yarn backstage-cli package lint --fix ${tsPath}`); if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) { - await exec(`yarn prettier --write ${tsPath}`); + await exec(`yarn prettier --write ${tsPath}`, { + cwd: cliPaths.targetRoot, + }); } } -export async function bulkCommand(paths: string[] = []): Promise { - const resultsList = await runner(paths, (dir: string) => - generate(dir, { skipMissingYamlFile: true }), - ); - - let failed = false; - for (const { relativeDir, resultText } of resultsList) { - if (resultText) { - console.log(); - console.log( - chalk.red( - `OpenAPI yaml to Typescript generation failed in ${relativeDir}:`, - ), - ); - console.log(resultText.trimStart()); - - failed = true; - } - } - - if (failed) { - process.exit(1); - } else { +export async function command(): Promise { + try { + await generate(); console.log(chalk.green('Generated all files.')); + } catch (err) { + console.log(chalk.red(`OpenAPI server stub generation failed.`)); + console.log(err.message); + process.exit(1); } } diff --git a/packages/repo-tools/src/commands/openapi/test/init.ts b/packages/repo-tools/src/commands/package/schema/openapi/init.ts similarity index 90% rename from packages/repo-tools/src/commands/openapi/test/init.ts rename to packages/repo-tools/src/commands/package/schema/openapi/init.ts index a38416aabb..56136f8882 100644 --- a/packages/repo-tools/src/commands/openapi/test/init.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/init.ts @@ -15,12 +15,12 @@ */ import fs from 'fs-extra'; import { join } from 'path'; -import { YAML_SCHEMA_PATH } from './../constants'; +import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; -import { paths as cliPaths } from '../../../lib/paths'; -import { runner } from '../runner'; +import { paths as cliPaths } from '../../../../lib/paths'; +import { runner } from '../../../../lib/runner'; import chalk from 'chalk'; -import { exec } from '../../../lib/exec'; +import { exec } from '../../../../lib/exec'; const ROUTER_TEST_PATHS = [ 'src/service/router.test.ts', @@ -31,7 +31,7 @@ async function init(directoryPath: string) { const openapiPath = join(directoryPath, YAML_SCHEMA_PATH); if (!(await fs.pathExists(openapiPath))) { throw new Error( - `You do not have an OpenAPI YAML file at ${openapiPath}. Please create one and retry this command. If you already have existing test cases for your router, see 'backstage-repo-tools schema openapi test --update'`, + `You do not have an OpenAPI YAML file at ${openapiPath}. Please create one and retry this command. If you already have existing test cases for your router, see 'backstage-repo-tools package schema openapi test --update'`, ); } const opticConfigFilePath = join(directoryPath, 'optic.yml'); diff --git a/packages/repo-tools/src/commands/openapi/lint.ts b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts similarity index 83% rename from packages/repo-tools/src/commands/openapi/lint.ts rename to packages/repo-tools/src/commands/repo/schema/openapi/lint.ts index 8aac0189b1..eacb3ac50d 100644 --- a/packages/repo-tools/src/commands/openapi/lint.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts @@ -24,24 +24,19 @@ import { Yaml } from '@stoplight/spectral-parsers'; import ruleset from '@apisyouwonthate/style-guide'; import fs from 'fs-extra'; import chalk from 'chalk'; -import { resolve } from 'path'; -import { runner } from './runner'; -import { YAML_SCHEMA_PATH } from './constants'; +import { runner } from '../../../../lib/runner'; import { oas } from '@stoplight/spectral-rulesets'; import { DiagnosticSeverity } from '@stoplight/types'; import { pretty } from '@stoplight/spectral-formatters'; +import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; -async function lint( - directoryPath: string, - config?: { skipMissingYamlFile: boolean; strict: boolean }, -) { - const { skipMissingYamlFile, strict } = config ?? {}; - const openapiPath = resolve(directoryPath, YAML_SCHEMA_PATH); - if (!(await fs.pathExists(openapiPath))) { - if (skipMissingYamlFile) { - return; - } - throw new Error(`Could not find a file at ${openapiPath}.`); +async function lint(directoryPath: string, config?: { strict: boolean }) { + const { strict } = config ?? {}; + let openapiPath = ''; + try { + openapiPath = await getPathToOpenApiSpec(directoryPath); + } catch { + return; } const openapiFileContent = await fs.readFile(openapiPath, 'utf8'); @@ -90,7 +85,7 @@ export async function bulkCommand( options: { strict?: boolean }, ): Promise { const resultsList = await runner(paths, (dir: string) => - lint(dir, { skipMissingYamlFile: true, strict: !!options.strict }), + lint(dir, { strict: !!options.strict }), ); let failed = false; diff --git a/packages/repo-tools/src/commands/openapi/test/index.ts b/packages/repo-tools/src/commands/repo/schema/openapi/test.ts similarity index 86% rename from packages/repo-tools/src/commands/openapi/test/index.ts rename to packages/repo-tools/src/commands/repo/schema/openapi/test.ts index 47b553aaef..833de181e3 100644 --- a/packages/repo-tools/src/commands/openapi/test/index.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/test.ts @@ -17,18 +17,22 @@ import fs from 'fs-extra'; import { join } from 'path'; import chalk from 'chalk'; -import { runner } from '../runner'; -import { YAML_SCHEMA_PATH } from '../constants'; -import { paths as cliPaths } from '../../../lib/paths'; -import { exec } from '../../../lib/exec'; +import { runner } from '../../../../lib/runner'; +import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; +import { paths as cliPaths } from '../../../../lib/paths'; +import { exec } from '../../../../lib/exec'; +import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; async function test( directoryPath: string, { port }: { port: number }, options?: { update?: boolean }, ) { - const openapiPath = join(directoryPath, YAML_SCHEMA_PATH); - if (!(await fs.pathExists(openapiPath))) { + let openapiPath = join(directoryPath, YAML_SCHEMA_PATH); + try { + openapiPath = await getPathToOpenApiSpec(directoryPath); + } catch { + // OpenAPI schema doesn't exist. return; } const opticConfigFilePath = join(directoryPath, 'optic.yml'); diff --git a/packages/repo-tools/src/commands/openapi/schema/verify.ts b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts similarity index 79% rename from packages/repo-tools/src/commands/openapi/schema/verify.ts rename to packages/repo-tools/src/commands/repo/schema/openapi/verify.ts index 7120dd67ad..442494610c 100644 --- a/packages/repo-tools/src/commands/openapi/schema/verify.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts @@ -21,13 +21,21 @@ import { join } from 'path'; import chalk from 'chalk'; import { relative as relativePath, resolve as resolvePath } from 'path'; import Parser from '@apidevtools/swagger-parser'; -import { runner } from '../runner'; -import { paths as cliPaths } from '../../../lib/paths'; -import { TS_MODULE, TS_SCHEMA_PATH, YAML_SCHEMA_PATH } from '../constants'; +import { runner } from '../../../../lib/runner'; +import { paths as cliPaths } from '../../../../lib/paths'; +import { + TS_MODULE, + TS_SCHEMA_PATH, + YAML_SCHEMA_PATH, +} from '../../../../lib/openapi/constants'; +import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; async function verify(directoryPath: string) { - const openapiPath = join(directoryPath, YAML_SCHEMA_PATH); - if (!(await fs.pathExists(openapiPath))) { + let openapiPath = ''; + try { + openapiPath = await getPathToOpenApiSpec(directoryPath); + } catch { + // Unable to find spec at path. return; } @@ -47,7 +55,7 @@ async function verify(directoryPath: string) { if (!isEqual(schema.spec, yaml)) { const path = relativePath(cliPaths.targetRoot, directoryPath); throw new Error( - `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema openapi generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`, + `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools package schema openapi generate\` from '${path}' to regenerate \`${TS_SCHEMA_PATH}\`.`, ); } } diff --git a/packages/repo-tools/src/commands/openapi/constants.ts b/packages/repo-tools/src/lib/openapi/constants.ts similarity index 100% rename from packages/repo-tools/src/commands/openapi/constants.ts rename to packages/repo-tools/src/lib/openapi/constants.ts diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts new file mode 100644 index 0000000000..5a5932c802 --- /dev/null +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { pathExists } from 'fs-extra'; +import { paths } from '../paths'; +import { YAML_SCHEMA_PATH } from './constants'; +import { resolve } from 'path'; + +export const getPathToOpenApiSpec = async (directory: string) => { + const openapiPath = resolve(directory, YAML_SCHEMA_PATH); + if (!(await pathExists(openapiPath))) { + throw new Error(`Could not find ${YAML_SCHEMA_PATH}.`); + } + return openapiPath; +}; + +export const getPathToCurrentOpenApiSpec = async () => { + return await getPathToOpenApiSpec(paths.targetDir); +}; diff --git a/packages/repo-tools/src/commands/openapi/runner.ts b/packages/repo-tools/src/lib/runner.ts similarity index 94% rename from packages/repo-tools/src/commands/openapi/runner.ts rename to packages/repo-tools/src/lib/runner.ts index cc354daaaa..b9700cd09b 100644 --- a/packages/repo-tools/src/commands/openapi/runner.ts +++ b/packages/repo-tools/src/lib/runner.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { resolvePackagePaths } from '../../lib/paths'; +import { resolvePackagePaths } from './paths'; import pLimit from 'p-limit'; import { relative as relativePath } from 'path'; -import { paths as cliPaths } from '../../lib/paths'; +import { paths as cliPaths } from './paths'; import portFinder from 'portfinder'; export async function runner( diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 42f40cccd2..eee268736a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -42,7 +42,9 @@ "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean" + "clean": "backstage-cli package clean", + "generate-server": "backstage-repo-tools package schema openapi generate server", + "generate-client": "backstage-repo-tools package schema openapi generate client --output-package packages/catalog-client" }, "dependencies": { "@backstage/backend-common": "workspace:^", @@ -61,6 +63,7 @@ "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@backstage/plugin-search-backend-module-catalog": "workspace:^", + "@backstage/repo-tools": "workspace:^", "@backstage/types": "workspace:^", "@opentelemetry/api": "^1.3.0", "@types/express": "^4.17.6", diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index bea288f6b2..b328083246 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -39,7 +39,8 @@ "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean" + "clean": "backstage-cli package clean", + "generate-server": "backstage-repo-tools package schema openapi generate server" }, "dependencies": { "@backstage/backend-common": "workspace:^", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index ddf3baac3d..75a070e284 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -26,7 +26,8 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", "clean": "backstage-cli package clean", - "start": "backstage-cli package start" + "start": "backstage-cli package start", + "generate-server": "backstage-repo-tools package schema openapi generate server" }, "dependencies": { "@backstage/backend-common": "workspace:^", @@ -38,6 +39,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/repo-tools": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", "leasot": "^12.0.0", diff --git a/yarn.lock b/yarn.lock index 33ac9d7aab..ef01464078 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5560,6 +5560,7 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/plugin-search-backend-module-catalog": "workspace:^" + "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" "@opentelemetry/api": ^1.3.0 "@types/core-js": ^2.5.4 @@ -9350,6 +9351,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/repo-tools": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 express: ^4.17.1 @@ -9550,7 +9552,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/repo-tools@workspace:*, @backstage/repo-tools@workspace:packages/repo-tools": +"@backstage/repo-tools@workspace:*, @backstage/repo-tools@workspace:^, @backstage/repo-tools@workspace:packages/repo-tools": version: 0.0.0-use.local resolution: "@backstage/repo-tools@workspace:packages/repo-tools" dependencies: From 4c62935c8e6e17dd48d9d53a97e5c7da5473dd7a Mon Sep 17 00:00:00 2001 From: Aramis Date: Thu, 18 Jan 2024 18:09:49 -0500 Subject: [PATCH 003/112] add changeset Signed-off-by: Aramis --- .changeset/lovely-bugs-prove.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/lovely-bugs-prove.md diff --git a/.changeset/lovely-bugs-prove.md b/.changeset/lovely-bugs-prove.md new file mode 100644 index 0000000000..5b35402dfb --- /dev/null +++ b/.changeset/lovely-bugs-prove.md @@ -0,0 +1,19 @@ +--- +'@backstage/repo-tools': minor +--- + +Renames the `schema openapi *` commands into `package schema openapi *` and `repo schema openapi *`. The aim is to make it more clear what the command is operating on, the entire repo or just a single package. + +The following commands now live under the `package` namespace, + +- `schema openapi generate` is now `package schema openapi generate server` +- `schema openapi generate-client` is now `package schema openapi generate client` +- `schema openapi init` is now `package schema openapi init` + +And these commands live under the new `repo` namespace, + +- `schema openapi lint` is now `repo schema openapi lint` +- `schema openapi test` is now `repo schema openapi test` +- `schema openapi verify` is now `repo schema openapi verify` + +This also reworks the `package schema openapi generate client` to accept only an output directory as the input directory can now be inferred. From e16864039b7653813baeff5f279dbd28c1390cc6 Mon Sep 17 00:00:00 2001 From: Aramis Date: Thu, 18 Jan 2024 18:18:47 -0500 Subject: [PATCH 004/112] fix more documentation and workflows Signed-off-by: Aramis --- .changeset/lovely-bugs-prove.md | 2 +- .github/workflows/ci.yml | 6 +- docs/openapi/generate-client.md | 4 +- docs/openapi/test-case-validation.md | 6 +- packages/repo-tools/cli-report.md | 138 +++++++++++++++++++++------ 5 files changed, 116 insertions(+), 40 deletions(-) diff --git a/.changeset/lovely-bugs-prove.md b/.changeset/lovely-bugs-prove.md index 5b35402dfb..b1cb762e73 100644 --- a/.changeset/lovely-bugs-prove.md +++ b/.changeset/lovely-bugs-prove.md @@ -2,7 +2,7 @@ '@backstage/repo-tools': minor --- -Renames the `schema openapi *` commands into `package schema openapi *` and `repo schema openapi *`. The aim is to make it more clear what the command is operating on, the entire repo or just a single package. +**BREAKING**: The `schema openapi *` commands are now renamed into `package schema openapi *` and `repo schema openapi *`. The aim is to make it more clear what the command is operating on, the entire repo or just a single package. The following commands now live under the `package` namespace, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6e2edbc3..ddc1712698 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,10 +113,10 @@ jobs: run: yarn backstage-repo-tools generate-catalog-info --ci - name: lint openapi yaml files - run: yarn backstage-repo-tools schema openapi lint + run: yarn backstage-repo-tools repo schema openapi lint - name: verify openapi yaml file matches generated ts file - run: yarn backstage-repo-tools schema openapi verify + run: yarn backstage-repo-tools repo schema openapi verify - name: verify doc links run: node scripts/verify-links.js @@ -225,7 +225,7 @@ jobs: # We run the test cases before verifying the specs to prevent any failing tests from causing errors. - name: verify openapi specs against test cases - run: yarn backstage-repo-tools schema openapi test + run: yarn backstage-repo-tools repo schema openapi test - name: ensure clean working directory run: | diff --git a/docs/openapi/generate-client.md b/docs/openapi/generate-client.md index f2c7c64574..640036960e 100644 --- a/docs/openapi/generate-client.md +++ b/docs/openapi/generate-client.md @@ -4,7 +4,7 @@ title: Generate a client from your OpenAPI spec description: Documentation on how to create a client for a given OpenAPI spec --- -## How to generate a client with `repo-tools schema openapi generate-client`? +## How to generate a client with `repo-tools package schema openapi generate client`? ### Prerequisites @@ -20,7 +20,7 @@ info: ### Generating your client -1. Run `yarn backstage-repo-tools schema openapi generate-client --input-spec --output-directory `. This will create a new folder in `/src/generated` to house the generated content. +1. Run `yarn backstage-repo-tools schema openapi generate client --output-package `. This will create a new folder in `/src/generated` to house the generated content. 2. You should use the generated files as follows, - `apis/DefaultApi.client.ts` - this is the client that you should use. It has types for all of the various operations on your API. diff --git a/docs/openapi/test-case-validation.md b/docs/openapi/test-case-validation.md index 0d93b6d5b5..28d0eb5470 100644 --- a/docs/openapi/test-case-validation.md +++ b/docs/openapi/test-case-validation.md @@ -1,15 +1,15 @@ --- id: test-case-validation title: Validate your OpenAPI spec against test data -description: Documentation on how to use the `schema openapi test` command. +description: Documentation on how to use the `repo schema openapi test` command. --- ## OpenAPI Validation using Test Cases -This is primarily performed by `backstage-repo-tools schema openapi test`. Any errors found in the generated specs can be either +This is primarily performed by `backstage-repo-tools repo schema openapi test`. Any errors found in the generated specs can be either 1. Fixed manually, this is usually relevant for request body or response body changes. -2. Fixed automatically with `backstage-repo-tools schema openapi test --update`. +2. Fixed automatically with `backstage-repo-tools repo schema openapi test --update`. 3. Fixing the test case. This can happen where a response is mocked as ```ts diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index be1e7ffceb..3a03e25352 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -15,7 +15,8 @@ Commands: api-reports [options] [paths...] type-deps generate-catalog-info [options] - schema [command] + package [command] + repo [command] help [command] ``` @@ -48,10 +49,23 @@ Options: -h, --help ``` -### `backstage-repo-tools schema` +### `backstage-repo-tools package` ``` -Usage: backstage-repo-tools schema [options] [command] [command] +Usage: backstage-repo-tools package [options] [command] [command] + +Options: + -h, --help + +Commands: + schema [command] + help [command] +``` + +### `backstage-repo-tools package schema` + +``` +Usage: backstage-repo-tools package schema [options] [command] [command] Options: -h, --help @@ -61,65 +75,127 @@ Commands: help [command] ``` -### `backstage-repo-tools schema openapi` +### `backstage-repo-tools package schema openapi` ``` -Usage: backstage-repo-tools schema openapi [options] [command] [command] +Usage: backstage-repo-tools package schema openapi [options] [command] [command] + +Options: + -h, --help + +Commands: + init + generate [command] + help [command] +``` + +### `backstage-repo-tools package schema openapi generate` + +``` +Usage: backstage-repo-tools package schema openapi generate [options] [command] [command] + +Options: + -h, --help + +Commands: + server + client [options] + help [command] +``` + +### `backstage-repo-tools package schema openapi generate client` + +``` +Usage: backstage-repo-tools package schema openapi generate client [options] + +Options: + --output-package + -h, --help +``` + +### `backstage-repo-tools package schema openapi generate server` + +``` +Usage: backstage-repo-tools package schema openapi generate server [options] + +Options: + -h, --help +``` + +### `backstage-repo-tools package schema openapi init` + +``` +Usage: backstage-repo-tools package schema openapi init [options] + +Options: + -h, --help +``` + +### `backstage-repo-tools repo` + +``` +Usage: backstage-repo-tools repo [options] [command] [command] + +Options: + -h, --help + +Commands: + schema [command] + help [command] +``` + +### `backstage-repo-tools repo schema` + +``` +Usage: backstage-repo-tools repo schema [options] [command] [command] + +Options: + -h, --help + +Commands: + openapi [command] + help [command] +``` + +### `backstage-repo-tools repo schema openapi` + +``` +Usage: backstage-repo-tools repo schema openapi [options] [command] [command] Options: -h, --help Commands: verify [paths...] - generate [paths...] lint [options] [paths...] test [options] [paths...] - init help [command] ``` -### `backstage-repo-tools schema openapi generate` +### `backstage-repo-tools repo schema openapi lint` ``` -Usage: backstage-repo-tools schema openapi generate [options] [paths...] - -Options: - -h, --help -``` - -### `backstage-repo-tools schema openapi init` - -``` -Usage: backstage-repo-tools schema openapi init [options] - -Options: - -h, --help -``` - -### `backstage-repo-tools schema openapi lint` - -``` -Usage: backstage-repo-tools schema openapi lint [options] [paths...] +Usage: backstage-repo-tools repo schema openapi lint [options] [paths...] Options: --strict -h, --help ``` -### `backstage-repo-tools schema openapi test` +### `backstage-repo-tools repo schema openapi test` ``` -Usage: backstage-repo-tools schema openapi test [options] [paths...] +Usage: backstage-repo-tools repo schema openapi test [options] [paths...] Options: --update -h, --help ``` -### `backstage-repo-tools schema openapi verify` +### `backstage-repo-tools repo schema openapi verify` ``` -Usage: backstage-repo-tools schema openapi verify [options] [paths...] +Usage: backstage-repo-tools repo schema openapi verify [options] [paths...] Options: -h, --help From 9c1c15fe1b0d11b40eeef536d20c8d7c8ad2072f Mon Sep 17 00:00:00 2001 From: Aramis Date: Tue, 23 Jan 2024 10:09:09 -0500 Subject: [PATCH 005/112] work to update init to be package focused Signed-off-by: Aramis --- .../commands/package/schema/openapi/init.ts | 25 +++++++++++-------- .../repo-tools/src/lib/openapi/helpers.ts | 22 ++++++++++------ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/repo-tools/src/commands/package/schema/openapi/init.ts b/packages/repo-tools/src/commands/package/schema/openapi/init.ts index 56136f8882..ab91aabecc 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/init.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/init.ts @@ -21,20 +21,23 @@ import { paths as cliPaths } from '../../../../lib/paths'; import { runner } from '../../../../lib/runner'; import chalk from 'chalk'; import { exec } from '../../../../lib/exec'; +import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers'; const ROUTER_TEST_PATHS = [ 'src/service/router.test.ts', 'src/service/createRouter.test.ts', ]; -async function init(directoryPath: string) { - const openapiPath = join(directoryPath, YAML_SCHEMA_PATH); - if (!(await fs.pathExists(openapiPath))) { +async function init() { + try { + await getPathToCurrentOpenApiSpec(); + } catch (err) { throw new Error( - `You do not have an OpenAPI YAML file at ${openapiPath}. Please create one and retry this command. If you already have existing test cases for your router, see 'backstage-repo-tools package schema openapi test --update'`, + `OpenAPI.yaml not found in ${YAML_SCHEMA_PATH}. Please create one and retry this command.`, ); } - const opticConfigFilePath = join(directoryPath, 'optic.yml'); + + const opticConfigFilePath = await getPathTo; if (await fs.pathExists(opticConfigFilePath)) { throw new Error(`This directory already has an optic.yml file. Exiting.`); } @@ -65,6 +68,12 @@ capture: } export default async function initCommand(paths: string[] = []) { + try { + await init(); + } catch (err) { + console.log(chalk.red(`OpenAPI tooling initialization failed.`)); + console.log(err.message); + } const resultsList = await runner(paths, dir => init(dir), { concurrencyLimit: 5, }); @@ -72,12 +81,6 @@ export default async function initCommand(paths: string[] = []) { let failed = false; for (const { relativeDir, resultText } of resultsList) { if (resultText) { - console.log(); - console.log( - chalk.red(`Failed to initialize ${relativeDir} for OpenAPI commands.`), - ); - console.log(resultText.trimStart()); - failed = true; } } diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index 5a5932c802..dda7f110a4 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -17,16 +17,24 @@ import { pathExists } from 'fs-extra'; import { paths } from '../paths'; import { YAML_SCHEMA_PATH } from './constants'; -import { resolve } from 'path'; +import { join, resolve } from 'path'; + +export const getPathToFile = async (directory: string, filename: string) => { + const path = resolve(directory, filename); + if (!(await pathExists(path))) { + throw new Error(`Could not find ${join(directory, filename)}.`); + } + return path; +}; + +export const getRelativePathToFile = async (filename: string) => { + return await getPathToFile(paths.targetDir, filename); +}; export const getPathToOpenApiSpec = async (directory: string) => { - const openapiPath = resolve(directory, YAML_SCHEMA_PATH); - if (!(await pathExists(openapiPath))) { - throw new Error(`Could not find ${YAML_SCHEMA_PATH}.`); - } - return openapiPath; + return await getPathToFile(directory, YAML_SCHEMA_PATH); }; export const getPathToCurrentOpenApiSpec = async () => { - return await getPathToOpenApiSpec(paths.targetDir); + return await getRelativePathToFile(YAML_SCHEMA_PATH); }; From 2a42f97175f2c04e7a4a60ea24150a8156a24ac3 Mon Sep 17 00:00:00 2001 From: Aramis Date: Tue, 23 Jan 2024 13:25:42 -0500 Subject: [PATCH 006/112] make init for a single package Signed-off-by: Aramis --- packages/repo-tools/src/commands/index.ts | 8 +++-- .../commands/package/schema/openapi/init.ts | 32 ++++++------------- .../repo-tools/src/lib/openapi/helpers.ts | 19 ++++++----- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 06ea2fa95c..290999687c 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -35,9 +35,13 @@ function registerPackageCommand(program: Command) { openApiCommand .command('init') - .description('Initialize any required files to use the OpenAPI tooling.') + .description( + 'Initialize any required files to use the OpenAPI tooling for this package.', + ) .action( - lazy(() => import('./package/schema/openapi/init').then(m => m.default)), + lazy(() => + import('./package/schema/openapi/init').then(m => m.singleCommand), + ), ); const generateCommand = openApiCommand diff --git a/packages/repo-tools/src/commands/package/schema/openapi/init.ts b/packages/repo-tools/src/commands/package/schema/openapi/init.ts index ab91aabecc..00e82f65df 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/init.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/init.ts @@ -14,14 +14,14 @@ * limitations under the License. */ import fs from 'fs-extra'; -import { join } from 'path'; import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; - import { paths as cliPaths } from '../../../../lib/paths'; -import { runner } from '../../../../lib/runner'; import chalk from 'chalk'; import { exec } from '../../../../lib/exec'; -import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers'; +import { + getPathToCurrentOpenApiSpec, + getRelativePathToFile, +} from '../../../../lib/openapi/helpers'; const ROUTER_TEST_PATHS = [ 'src/service/router.test.ts', @@ -37,7 +37,7 @@ async function init() { ); } - const opticConfigFilePath = await getPathTo; + const opticConfigFilePath = await getRelativePathToFile('optic.yml'); if (await fs.pathExists(opticConfigFilePath)) { throw new Error(`This directory already has an optic.yml file. Exiting.`); } @@ -48,7 +48,9 @@ async function init() { capture: ${YAML_SCHEMA_PATH}: # 🔧 Runnable example with simple get requests. - # Run with "PORT=3000 optic capture ${YAML_SCHEMA_PATH} --update interactive" in '${directoryPath}' + # Run with "PORT=3000 optic capture ${YAML_SCHEMA_PATH} --update interactive" in '${ + cliPaths.targetDir + }' # You can change the server and the 'requests' section to experiment server: # This will not be used by 'backstage-repo-tools schema openapi test', but may be useful for interactive updates. @@ -67,27 +69,13 @@ capture: } } -export default async function initCommand(paths: string[] = []) { +export async function singleCommand() { try { await init(); + console.log(chalk.green(`Successfully configured.`)); } catch (err) { console.log(chalk.red(`OpenAPI tooling initialization failed.`)); console.log(err.message); - } - const resultsList = await runner(paths, dir => init(dir), { - concurrencyLimit: 5, - }); - - let failed = false; - for (const { relativeDir, resultText } of resultsList) { - if (resultText) { - failed = true; - } - } - - if (failed) { process.exit(1); - } else { - console.log(chalk.green(`All directories have already been configured.`)); } } diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index dda7f110a4..f7e684a407 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -17,24 +17,27 @@ import { pathExists } from 'fs-extra'; import { paths } from '../paths'; import { YAML_SCHEMA_PATH } from './constants'; -import { join, resolve } from 'path'; +import { resolve } from 'path'; export const getPathToFile = async (directory: string, filename: string) => { - const path = resolve(directory, filename); - if (!(await pathExists(path))) { - throw new Error(`Could not find ${join(directory, filename)}.`); - } - return path; + return resolve(directory, filename); }; export const getRelativePathToFile = async (filename: string) => { return await getPathToFile(paths.targetDir, filename); }; +export const assertExists = async (path: string) => { + if (!(await pathExists(path))) { + throw new Error(`Could not find ${path}.`); + } + return path; +}; + export const getPathToOpenApiSpec = async (directory: string) => { - return await getPathToFile(directory, YAML_SCHEMA_PATH); + return await assertExists(await getPathToFile(directory, YAML_SCHEMA_PATH)); }; export const getPathToCurrentOpenApiSpec = async () => { - return await getRelativePathToFile(YAML_SCHEMA_PATH); + return await assertExists(await getRelativePathToFile(YAML_SCHEMA_PATH)); }; From b107fdf6090352c2c1715891b0f86e46fec67d0c Mon Sep 17 00:00:00 2001 From: Aramis Date: Wed, 24 Jan 2024 10:40:04 -0500 Subject: [PATCH 007/112] rework `generate` to take flags instead of subcommands Signed-off-by: Aramis --- .changeset/lovely-bugs-prove.md | 6 ++-- docs/openapi/01-getting-started.md | 4 +-- packages/repo-tools/cli-report.md | 29 ++-------------- packages/repo-tools/src/commands/index.ts | 34 +++++-------------- .../package/schema/openapi/generate/client.ts | 6 +--- .../package/schema/openapi/generate/index.ts | 34 +++++++++++++++++++ plugins/catalog-backend/package.json | 3 +- plugins/search-backend/package.json | 2 +- plugins/todo-backend/package.json | 2 +- 9 files changed, 55 insertions(+), 65 deletions(-) create mode 100644 packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts diff --git a/.changeset/lovely-bugs-prove.md b/.changeset/lovely-bugs-prove.md index b1cb762e73..de55f329bb 100644 --- a/.changeset/lovely-bugs-prove.md +++ b/.changeset/lovely-bugs-prove.md @@ -6,8 +6,8 @@ The following commands now live under the `package` namespace, -- `schema openapi generate` is now `package schema openapi generate server` -- `schema openapi generate-client` is now `package schema openapi generate client` +- `schema openapi generate` is now `package schema openapi generate --server` +- `schema openapi generate-client` is now `package schema openapi generate --client-package` - `schema openapi init` is now `package schema openapi init` And these commands live under the new `repo` namespace, @@ -16,4 +16,4 @@ And these commands live under the new `repo` namespace, - `schema openapi test` is now `repo schema openapi test` - `schema openapi verify` is now `repo schema openapi verify` -This also reworks the `package schema openapi generate client` to accept only an output directory as the input directory can now be inferred. +The `package schema openapi generate` now supports defining both `--server` and `--client-package` to generate both at once.This update also reworks the `--client-package` flag to accept only an output directory as the input directory can now be inferred. diff --git a/docs/openapi/01-getting-started.md b/docs/openapi/01-getting-started.md index ce04e75107..5b9908756f 100644 --- a/docs/openapi/01-getting-started.md +++ b/docs/openapi/01-getting-started.md @@ -45,7 +45,7 @@ You should create a new folder, `src/schema` in your backend plugin to store you ## Generating a typed express router from a spec -Run `yarn backstage-repo-tools package schema openapi generate server `. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types. You should add this command to your `package.json` for future use. +Run `yarn backstage-repo-tools package schema openapi generate --server` from the directory with your plugin. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types. You should add this command to your `package.json` for future use and you can combine both the server generation and the client generation below like so, `yarn backstage-repo-tools package schema openapi generate --server --client-package ` Use it like so, update your `router.ts` or `createRouter.ts` file with the following content, @@ -63,7 +63,7 @@ export async function createRouter( ## Generating a typed client from a spec -From your current backend plugin directory, run `yarn backstage-repo-tools package schema openapi generate client --output-package `. `` is a new directory and npm package that you should create. The general pattern is `plugins/-client` or if you want to co-locate this with your other shared types, use `plugins/-common`. You should add this command to your `package.json` for future use. +From your current backend plugin directory, run `yarn backstage-repo-tools package schema openapi generate --client-package `. `` is a new directory and npm package that you should create. The general pattern is `plugins/-client` or if you want to co-locate this with your other shared types, use `plugins/-common`. You should add this command to your `package.json` for future use. The generated client will have a directory `src/generated` that exports a `DefaultApiClient` class and all generated types. You can use the client like so, diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index 3a03e25352..7988c479b0 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -85,40 +85,17 @@ Options: Commands: init - generate [command] + generate [options] help [command] ``` ### `backstage-repo-tools package schema openapi generate` ``` -Usage: backstage-repo-tools package schema openapi generate [options] [command] [command] - -Options: - -h, --help - -Commands: - server - client [options] - help [command] -``` - -### `backstage-repo-tools package schema openapi generate client` - -``` -Usage: backstage-repo-tools package schema openapi generate client [options] - -Options: - --output-package - -h, --help -``` - -### `backstage-repo-tools package schema openapi generate server` - -``` -Usage: backstage-repo-tools package schema openapi generate server [options] +Usage: backstage-repo-tools package schema openapi generate [options] Options: + --client-package [package] -h, --help ``` diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 290999687c..1fa37402ce 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -44,35 +44,19 @@ function registerPackageCommand(program: Command) { ), ); - const generateCommand = openApiCommand - .command('generate [command]') - .description( - 'Commands for generating various things from an OpenAPI spec.', - ); - - generateCommand - .command('server') - .description( - 'Generates an express server stub using the OpenAPI schema for typings.', - ) - .action( - lazy(() => - import('./package/schema/openapi/generate/server').then(m => m.command), - ), - ); - - generateCommand - .command('client') - .description( - 'Generates a client that can interact with your backend plugin using types from your OpenAPI schema.', - ) - .requiredOption( - '--output-package ', + openApiCommand + .command('generate') + .option( + '--client-package [package]', 'Top-level path to where the client should be generated, ie packages/catalog-client.', ) + .option('--server') + .description( + 'Command to generate a client and/or a server stub from an OpenAPI spec.', + ) .action( lazy(() => - import('./package/schema/openapi/generate/client').then(m => m.command), + import('./package/schema/openapi/generate').then(m => m.command), ), ); } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 101c2c918b..5dfed05f93 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -87,11 +87,7 @@ async function generate(outputDirectory: string) { }); } -export async function command({ - outputPackage, -}: { - outputPackage: string; -}): Promise { +export async function command(outputPackage: string): Promise { try { await generate(outputPackage); console.log( diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts new file mode 100644 index 0000000000..1e48fe3825 --- /dev/null +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import chalk from 'chalk'; +import { OptionValues } from 'commander'; +import { command as generateClient } from './client'; +import { command as generateServer } from './server'; + +export async function command(opts: OptionValues) { + if (!opts.clientPackage && !opts.server) { + console.log( + chalk.red('Either --client-package or --server must be defined.'), + ); + process.exit(1); + } + if (opts.clientPackage) { + await generateClient(opts.clientPackage); + } + if (opts.server) { + await generateServer(); + } +} diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index eee268736a..5e1d4f4e60 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -43,8 +43,7 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", "clean": "backstage-cli package clean", - "generate-server": "backstage-repo-tools package schema openapi generate server", - "generate-client": "backstage-repo-tools package schema openapi generate client --output-package packages/catalog-client" + "generate": "backstage-repo-tools package schema openapi generate --server --client-package packages/catalog-client" }, "dependencies": { "@backstage/backend-common": "workspace:^", diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index b328083246..c68f2b816e 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -40,7 +40,7 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", "clean": "backstage-cli package clean", - "generate-server": "backstage-repo-tools package schema openapi generate server" + "generate": "backstage-repo-tools package schema openapi generate --server" }, "dependencies": { "@backstage/backend-common": "workspace:^", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 75a070e284..2d9eb469d1 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -27,7 +27,7 @@ "postpack": "backstage-cli package postpack", "clean": "backstage-cli package clean", "start": "backstage-cli package start", - "generate-server": "backstage-repo-tools package schema openapi generate server" + "generate": "backstage-repo-tools package schema openapi generate --server" }, "dependencies": { "@backstage/backend-common": "workspace:^", From dc7ae8b20a902017dea554e9d96a3c2ed32db52c Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 25 Jan 2024 16:12:42 +0100 Subject: [PATCH 008/112] fix(plugins/home/FeatureDocsCard): fall back to metadata.name if no title has been supplied Signed-off-by: secustor --- .changeset/nasty-shirts-look.md | 5 +++++ .../home/src/homePageComponents/FeaturedDocsCard/Content.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/nasty-shirts-look.md diff --git a/.changeset/nasty-shirts-look.md b/.changeset/nasty-shirts-look.md new file mode 100644 index 0000000000..2742ebd1f6 --- /dev/null +++ b/.changeset/nasty-shirts-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fall back to metadata.name if no metadata.title has been defined diff --git a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx index eb8d78b0cc..21ed43003b 100644 --- a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx +++ b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx @@ -116,7 +116,7 @@ export const Content = (props: FeaturedDocsCardProps): JSX.Element => { }/` } > - {d.metadata.title} + {d.metadata.title ?? d.metadata.name} {d.metadata.description && ( From 9fe9285b07f696ae961d17bdfd9389a06b70ebd1 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 25 Jan 2024 16:46:18 +0100 Subject: [PATCH 009/112] feat(plugins/home/FeatureDocsCard): use EntityDisplayName rather then data direct Signed-off-by: secustor --- .changeset/nasty-shirts-look.md | 4 ++-- .../src/homePageComponents/FeaturedDocsCard/Content.tsx | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.changeset/nasty-shirts-look.md b/.changeset/nasty-shirts-look.md index 2742ebd1f6..2a6e2d9c19 100644 --- a/.changeset/nasty-shirts-look.md +++ b/.changeset/nasty-shirts-look.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-home': patch +'@backstage/plugin-home': minor --- -Fall back to metadata.name if no metadata.title has been defined +Use EntityDisplayName JSX element entity information directly for FeaturedDocsCard. diff --git a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx index 21ed43003b..a7ba4d5e9b 100644 --- a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx +++ b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.tsx @@ -23,11 +23,16 @@ import { Progress, ErrorPanel, } from '@backstage/core-components'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + CatalogApi, + EntityDisplayName, +} from '@backstage/plugin-catalog-react'; import { useApi } from '@backstage/core-plugin-api'; import { EntityFilterQuery } from '@backstage/catalog-client'; import { makeStyles, Typography } from '@material-ui/core'; +import { stringifyEntityRef } from '@backstage/catalog-model'; /** * Props customizing the component. @@ -116,7 +121,7 @@ export const Content = (props: FeaturedDocsCardProps): JSX.Element => { }/` } > - {d.metadata.title ?? d.metadata.name} + {d.metadata.description && ( From 4461c556c63e914c710305d60bec1963e9d0f367 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 25 Jan 2024 18:09:50 +0100 Subject: [PATCH 010/112] adapt test Signed-off-by: secustor --- .../src/homePageComponents/FeaturedDocsCard/Content.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.test.tsx b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.test.tsx index 427a1484d5..c49b41eb18 100644 --- a/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.test.tsx +++ b/plugins/home/src/homePageComponents/FeaturedDocsCard/Content.test.tsx @@ -66,8 +66,7 @@ describe('', () => { }, ); const docsCardContent = getByTestId('docs-card-content'); - const docsEntity = getByText('Getting Started Docs'); - + const docsEntity = getByText('getting-started-with-idp'); expect(docsCardContent).toContainElement(docsEntity); }); }); From 9185966262bf2718b94a0f00465bc0660d47d845 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 26 Jan 2024 12:14:01 -0500 Subject: [PATCH 011/112] Remove utility API instructions Since #22549 adds this API to the core framework, integrators don't need to define their own. Signed-off-by: Jamie Klassen --- docs/auth/vmware-cloud/provider.md | 81 +----------------------------- 1 file changed, 1 insertion(+), 80 deletions(-) diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index 00a1315b6a..20115f9e75 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -111,85 +111,6 @@ can be replaced with a more suitable resolver for the app in question. See [Sign-in Identities and Resolvers](../identity-resolver.md) for details. -## Create a Utility API - -At the moment, this auth provider is not included in the default API factories, -but you can create one with a change to your frontend like: - -```ts title="packages/app/src/apis.ts" -import { - ScmIntegrationsApi, - scmIntegrationsApiRef, - ScmAuth, -} from '@backstage/integration-react'; -import { - costInsightsApiRef, - ExampleCostInsightsClient, -} from '@backstage/plugin-cost-insights'; -import { - graphQlBrowseApiRef, - GraphQLEndpoints, -} from '@backstage/plugin-graphiql'; -/* highlight-add-start */ -import { OAuth2 } from '@backstage/core-app-api'; -/* highlight-add-end */ -import { - AnyApiFactory, - /* highlight-add-start */ - ApiRef, - OpenIdConnectApi, - ProfileInfoApi, - BackstageIdentityApi, - SessionApi, - /* highlight-add-end */ - configApiRef, - /* highlight-add-start */ - createApiRef, - /* highlight-add-end */ - createApiFactory, - discoveryApiRef, - errorApiRef, - fetchApiRef, - githubAuthApiRef, - /* highlight-add-start */ - oauthRequestApiRef, - /* highlight-add-end */ -} from '@backstage/core-plugin-api'; -import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi'; - -/* highlight-add-start */ -export const vmwareCloudAuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'auth.vmware-cloud-auth-provider', -}); -/* highlight-add-end */ - -export const apis: AnyApiFactory[] = [ - /* highlight-add-start */ - createApiFactory({ - api: vmwareCloudAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'vmwareCloudServices', - title: 'VMware Cloud', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - defaultScopes: ['openid'], - }), - }), - /* highlight-add-end */ -``` - ## Add to Sign-in Page See the [Sign-In Configuration](../index.md#sign-in-configuration) docs for @@ -197,7 +118,7 @@ general guidance, but as an example: ```tsx title="packages/app/src/App.tsx" /* highlight-add-start */ -import { vmwareCloudAuthApiRef } from './apis'; +import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; import { SignInPage } from '@backstage/core-components'; /* highlight-add-end */ From f919be97d995c4eb5cd2b11a48f1daab9809291f Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 26 Jan 2024 12:06:19 -0500 Subject: [PATCH 012/112] Utility API for VMware Cloud auth Signed-off-by: Jamie Klassen --- .changeset/kind-wombats-draw.md | 11 ++++ packages/app-defaults/src/defaults/apis.ts | 18 +++++++ packages/core-app-api/api-report.md | 7 +++ .../src/apis/implementations/auth/index.ts | 1 + .../auth/vmwareCloud/VMwareCloudAuth.ts | 53 +++++++++++++++++++ .../implementations/auth/vmwareCloud/index.ts | 16 ++++++ packages/core-plugin-api/api-report.md | 9 ++++ .../src/apis/definitions/auth.ts | 19 +++++++ .../src/wiring/createApp.test.tsx | 1 + packages/frontend-plugin-api/api-report.md | 3 ++ .../src/apis/definitions/auth.ts | 1 + 11 files changed, 139 insertions(+) create mode 100644 .changeset/kind-wombats-draw.md create mode 100644 packages/core-app-api/src/apis/implementations/auth/vmwareCloud/VMwareCloudAuth.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/vmwareCloud/index.ts diff --git a/.changeset/kind-wombats-draw.md b/.changeset/kind-wombats-draw.md new file mode 100644 index 0000000000..29a0e9da3c --- /dev/null +++ b/.changeset/kind-wombats-draw.md @@ -0,0 +1,11 @@ +--- +'@backstage/core-plugin-api': minor +'@backstage/app-defaults': minor +'@backstage/core-app-api': minor +'@backstage/frontend-plugin-api': patch +--- + +Added a utility API for VMware Cloud auth; the API ref is available in the +`@backstage/core-plugin-api` and `@backstage/frontend-plugin-api` packages, the +implementation is in `@backstage/core-app-api` and a factory has been added to +`@backstage/app-defaults`. diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 848d204670..4e9e1a492c 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -34,6 +34,7 @@ import { AtlassianAuth, createFetchApi, FetchMiddlewares, + VMwareCloudAuth, } from '@backstage/core-app-api'; import { @@ -56,6 +57,7 @@ import { bitbucketAuthApiRef, bitbucketServerAuthApiRef, atlassianAuthApiRef, + vmwareCloudAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -259,6 +261,22 @@ export const apis = [ }); }, }), + createApiFactory({ + api: vmwareCloudAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return VMwareCloudAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), createApiFactory({ api: permissionApiRef, deps: { diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 05071255ba..8b8cc991c8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -64,6 +64,7 @@ import { SessionState } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; +import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; // @public export class AlertApiForwarder implements AlertApi { @@ -651,6 +652,12 @@ export class UrlPatternDiscovery implements DiscoveryApi { getBaseUrl(pluginId: string): Promise; } +// @public +export class VMwareCloudAuth { + // (undocumented) + static create(options: OAuthApiCreateOptions): typeof vmwareCloudAuthApiRef.T; +} + // @public export class WebStorage implements StorageApi { constructor(namespace: string, errorApi: ErrorApi); diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index b54e0424da..e02e07961a 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -25,4 +25,5 @@ export * from './onelogin'; export * from './bitbucket'; export * from './bitbucketServer'; export * from './atlassian'; +export * from './vmwareCloud'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/VMwareCloudAuth.ts b/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/VMwareCloudAuth.ts new file mode 100644 index 0000000000..83a2be0668 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/VMwareCloudAuth.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; +import { OAuthApiCreateOptions } from '../types'; + +const DEFAULT_PROVIDER = { + id: 'vmwareCloudServices', + title: 'VMware Cloud', + icon: () => null, +}; + +/** + * Implements the OAuth flow for VMware Cloud Services + * + * @public + */ +export default class VMwareCloudAuth { + static create( + options: OAuthApiCreateOptions, + ): typeof vmwareCloudAuthApiRef.T { + const { + configApi, + discoveryApi, + oauthRequestApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + defaultScopes = ['openid'], + } = options; + + return OAuth2.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/index.ts b/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/index.ts new file mode 100644 index 0000000000..023f700149 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/vmwareCloud/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { default as VMwareCloudAuth } from './VMwareCloudAuth'; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 1fa40e2d0c..2a993687b3 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -776,6 +776,15 @@ export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params; +// @public +export const vmwareCloudAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + // @public export function withApis( apis: TypesToApiRefs, diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index d9aec2ca5a..d89544cf68 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -450,3 +450,22 @@ export const atlassianAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.atlassian', }); + +/** + * Provides authentication towards VMware Cloud APIs and identities. + * + * @public + * @remarks + * + * For more info about VMware Cloud identity and access management: + * - {@link https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-53D39337-D93A-4B84-BD18-DDF43C21479A.html} + */ +export const vmwareCloudAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.vmware-cloud', +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 43bb4499b1..70f8cfa2ae 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -291,6 +291,7 @@ describe('createApp', () => { + ] " diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 4116469c2a..28204ac455 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -86,6 +86,7 @@ import { TypesToApiRefs } from '@backstage/core-plugin-api'; import { useApi } from '@backstage/core-plugin-api'; import { useApiHolder } from '@backstage/core-plugin-api'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; import { withApis } from '@backstage/core-plugin-api'; import { z } from 'zod'; import { ZodSchema } from 'zod'; @@ -1167,5 +1168,7 @@ export function useRouteRefParams( export { useTranslationRef }; +export { vmwareCloudAuthApiRef }; + export { withApis }; ``` diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index d27aebbf47..89509082f0 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -36,4 +36,5 @@ export { oktaAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, + vmwareCloudAuthApiRef, } from '@backstage/core-plugin-api'; From e2083dfb55e6b5b211ae16b40fa9d10332c6d181 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 27 Dec 2023 12:11:26 -0600 Subject: [PATCH 013/112] Added alpha support for the New Frontend System Signed-off-by: Andre Wanlin --- .changeset/beige-apples-press.md | 5 ++ packages/app-next/app-config.yaml | 7 +- plugins/azure-devops/api-report-alpha.md | 13 +++ plugins/azure-devops/package.json | 21 ++++- plugins/azure-devops/src/alpha.ts | 18 ++++ plugins/azure-devops/src/alpha/index.ts | 17 ++++ plugins/azure-devops/src/alpha/plugin.tsx | 104 ++++++++++++++++++++++ yarn.lock | 2 + 8 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 .changeset/beige-apples-press.md create mode 100644 plugins/azure-devops/api-report-alpha.md create mode 100644 plugins/azure-devops/src/alpha.ts create mode 100644 plugins/azure-devops/src/alpha/index.ts create mode 100644 plugins/azure-devops/src/alpha/plugin.tsx diff --git a/.changeset/beige-apples-press.md b/.changeset/beige-apples-press.md new file mode 100644 index 0000000000..56c46db830 --- /dev/null +++ b/.changeset/beige-apples-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops': patch +--- + +Added alpha support for the New Frontend System (Declarative Integration) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 9cc3507fa4..2ffc8967e3 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -17,8 +17,13 @@ app: config: filter: kind:component has:links - entity-card:linguist/languages + - entity-card:azure-devops/readme # Entity page content - entity-content:techdocs + - entity-content:azure-devops/pipelines + - entity-content:azure-devops/pull-requests + - entity-content:azure-devops/git-tags + # scmAuthExtension: >- # createScmAuthExtension({ @@ -32,7 +37,7 @@ app: # }) # externalRouteRefs: - # bind(catalogPlugin.externalRoutes, { + # bind(catalogPlugin.externalRoutes, # createComponent: scaffolderPlugin.routes.root, # viewTechDoc: techdocsPlugin.routes.docRoot, # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, diff --git a/plugins/azure-devops/api-report-alpha.md b/plugins/azure-devops/api-report-alpha.md new file mode 100644 index 0000000000..8040c0235f --- /dev/null +++ b/plugins/azure-devops/api-report-alpha.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-azure-devops" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +const _default: BackstagePlugin<{}, {}>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index d0b5932b31..e367cfb89f 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "frontend-plugin" @@ -30,9 +43,11 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-azure-devops-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@material-ui/core": "^4.12.2", diff --git a/plugins/azure-devops/src/alpha.ts b/plugins/azure-devops/src/alpha.ts new file mode 100644 index 0000000000..e80f131817 --- /dev/null +++ b/plugins/azure-devops/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './alpha/index'; +export { default } from './alpha/index'; diff --git a/plugins/azure-devops/src/alpha/index.ts b/plugins/azure-devops/src/alpha/index.ts new file mode 100644 index 0000000000..2f137f09ee --- /dev/null +++ b/plugins/azure-devops/src/alpha/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { default } from './plugin'; diff --git a/plugins/azure-devops/src/alpha/plugin.tsx b/plugins/azure-devops/src/alpha/plugin.tsx new file mode 100644 index 0000000000..f30b5cf219 --- /dev/null +++ b/plugins/azure-devops/src/alpha/plugin.tsx @@ -0,0 +1,104 @@ +/* + * 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 React from 'react'; +import { + createApiExtension, + createApiFactory, + createPageExtension, + createPlugin, + discoveryApiRef, + identityApiRef, +} from '@backstage/frontend-plugin-api'; +import { azureDevOpsApiRef, AzureDevOpsClient } from '../api'; +import { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; +import { + createEntityCardExtension, + createEntityContentExtension, +} from '@backstage/plugin-catalog-react/alpha'; +import { azurePullRequestDashboardRouteRef } from '../routes'; + +export const azureDevOpsApi = createApiExtension({ + factory: createApiFactory({ + api: azureDevOpsApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new AzureDevOpsClient({ discoveryApi, identityApi }), + }), +}); + +const azureDevOpsPullRequestPage = createPageExtension({ + defaultPath: '/azure-pull-requests', + routeRef: convertLegacyRouteRef(azurePullRequestDashboardRouteRef), + loader: () => + import('../components/PullRequestsPage').then(m => + compatWrapper(), + ), +}); + +const entityAzurePipelinesContent = createEntityContentExtension({ + name: 'pipelines', + defaultPath: '/pipelines', + defaultTitle: 'Pipelines', + loader: () => + import('../components/EntityPageAzurePipelines').then(m => + compatWrapper(), + ), +}); + +const entityAzureGitTagsContent = createEntityContentExtension({ + name: 'git-tags', + defaultPath: '/git-tags', + defaultTitle: 'Git Tags', + loader: () => + import('../components/EntityPageAzureGitTags').then(m => + compatWrapper(), + ), +}); + +const entityAzurePullRequestsContent = createEntityContentExtension({ + name: 'pull-requests', + defaultPath: '/pull-requests', + defaultTitle: 'Pull Requests', + loader: () => + import('../components/EntityPageAzurePullRequests').then(m => + compatWrapper(), + ), +}); + +export const entityAzureReadmeCard = createEntityCardExtension({ + name: 'readme', + loader: async () => + import('../components/ReadmeCard').then(m => + compatWrapper(), + ), +}); + +/** @alpha */ +export default createPlugin({ + id: 'azure-devops', + extensions: [ + azureDevOpsApi, + entityAzureReadmeCard, + entityAzurePipelinesContent, + entityAzureGitTagsContent, + entityAzurePullRequestsContent, + azureDevOpsPullRequestPage, + ], +}); diff --git a/yarn.lock b/yarn.lock index 42811acc21..9649405700 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4909,10 +4909,12 @@ __metadata: dependencies: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-azure-devops-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" From e6bb058f8d1a205dde05ffdfa645a65d62d65dc4 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 27 Dec 2023 12:58:21 -0600 Subject: [PATCH 014/112] Clean up Signed-off-by: Andre Wanlin --- packages/app-next/app-config.yaml | 3 +-- plugins/azure-devops/src/alpha/plugin.tsx | 14 ++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 2ffc8967e3..9af6b6c438 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -24,7 +24,6 @@ app: - entity-content:azure-devops/pull-requests - entity-content:azure-devops/git-tags - # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', @@ -37,7 +36,7 @@ app: # }) # externalRouteRefs: - # bind(catalogPlugin.externalRoutes, + # bind(catalogPlugin.externalRoutes, { # createComponent: scaffolderPlugin.routes.root, # viewTechDoc: techdocsPlugin.routes.docRoot, # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, diff --git a/plugins/azure-devops/src/alpha/plugin.tsx b/plugins/azure-devops/src/alpha/plugin.tsx index f30b5cf219..f601baf7c0 100644 --- a/plugins/azure-devops/src/alpha/plugin.tsx +++ b/plugins/azure-devops/src/alpha/plugin.tsx @@ -34,6 +34,7 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { azurePullRequestDashboardRouteRef } from '../routes'; +/** @alpha */ export const azureDevOpsApi = createApiExtension({ factory: createApiFactory({ api: azureDevOpsApiRef, @@ -43,7 +44,8 @@ export const azureDevOpsApi = createApiExtension({ }), }); -const azureDevOpsPullRequestPage = createPageExtension({ +/** @alpha */ +export const azureDevOpsPullRequestPage = createPageExtension({ defaultPath: '/azure-pull-requests', routeRef: convertLegacyRouteRef(azurePullRequestDashboardRouteRef), loader: () => @@ -52,7 +54,8 @@ const azureDevOpsPullRequestPage = createPageExtension({ ), }); -const entityAzurePipelinesContent = createEntityContentExtension({ +/** @alpha */ +export const entityAzurePipelinesContent = createEntityContentExtension({ name: 'pipelines', defaultPath: '/pipelines', defaultTitle: 'Pipelines', @@ -62,7 +65,8 @@ const entityAzurePipelinesContent = createEntityContentExtension({ ), }); -const entityAzureGitTagsContent = createEntityContentExtension({ +/** @alpha */ +export const entityAzureGitTagsContent = createEntityContentExtension({ name: 'git-tags', defaultPath: '/git-tags', defaultTitle: 'Git Tags', @@ -72,7 +76,8 @@ const entityAzureGitTagsContent = createEntityContentExtension({ ), }); -const entityAzurePullRequestsContent = createEntityContentExtension({ +/** @alpha */ +export const entityAzurePullRequestsContent = createEntityContentExtension({ name: 'pull-requests', defaultPath: '/pull-requests', defaultTitle: 'Pull Requests', @@ -82,6 +87,7 @@ const entityAzurePullRequestsContent = createEntityContentExtension({ ), }); +/** @alpha */ export const entityAzureReadmeCard = createEntityCardExtension({ name: 'readme', loader: async () => From a3243f206001e6567937c7858777ba772775a683 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 27 Jan 2024 13:08:37 -0600 Subject: [PATCH 015/112] Standardized naming based on feedback Signed-off-by: Andre Wanlin --- plugins/azure-devops/src/alpha/plugin.tsx | 33 ++++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/plugins/azure-devops/src/alpha/plugin.tsx b/plugins/azure-devops/src/alpha/plugin.tsx index f601baf7c0..40305c1714 100644 --- a/plugins/azure-devops/src/alpha/plugin.tsx +++ b/plugins/azure-devops/src/alpha/plugin.tsx @@ -55,7 +55,7 @@ export const azureDevOpsPullRequestPage = createPageExtension({ }); /** @alpha */ -export const entityAzurePipelinesContent = createEntityContentExtension({ +export const azureDevOpsPipelinesEntityContent = createEntityContentExtension({ name: 'pipelines', defaultPath: '/pipelines', defaultTitle: 'Pipelines', @@ -66,7 +66,7 @@ export const entityAzurePipelinesContent = createEntityContentExtension({ }); /** @alpha */ -export const entityAzureGitTagsContent = createEntityContentExtension({ +export const azureDevOpsGitTagsEntityContent = createEntityContentExtension({ name: 'git-tags', defaultPath: '/git-tags', defaultTitle: 'Git Tags', @@ -77,18 +77,19 @@ export const entityAzureGitTagsContent = createEntityContentExtension({ }); /** @alpha */ -export const entityAzurePullRequestsContent = createEntityContentExtension({ - name: 'pull-requests', - defaultPath: '/pull-requests', - defaultTitle: 'Pull Requests', - loader: () => - import('../components/EntityPageAzurePullRequests').then(m => - compatWrapper(), - ), -}); +export const azureDevOpsPullRequestsEntityContent = + createEntityContentExtension({ + name: 'pull-requests', + defaultPath: '/pull-requests', + defaultTitle: 'Pull Requests', + loader: () => + import('../components/EntityPageAzurePullRequests').then(m => + compatWrapper(), + ), + }); /** @alpha */ -export const entityAzureReadmeCard = createEntityCardExtension({ +export const azureDevOpsReadmeEntityCard = createEntityCardExtension({ name: 'readme', loader: async () => import('../components/ReadmeCard').then(m => @@ -101,10 +102,10 @@ export default createPlugin({ id: 'azure-devops', extensions: [ azureDevOpsApi, - entityAzureReadmeCard, - entityAzurePipelinesContent, - entityAzureGitTagsContent, - entityAzurePullRequestsContent, + azureDevOpsReadmeEntityCard, + azureDevOpsPipelinesEntityContent, + azureDevOpsGitTagsEntityContent, + azureDevOpsPullRequestsEntityContent, azureDevOpsPullRequestPage, ], }); From 7dd8463935af94b62b3c0412def46cd974a44804 Mon Sep 17 00:00:00 2001 From: secustor Date: Mon, 29 Jan 2024 12:20:34 +0100 Subject: [PATCH 016/112] chore(plugins/auth-backend): migrate from passport-saml package to @node-saml/passport-saml Signed-off-by: secustor --- .changeset/tasty-oranges-rescue.md | 5 + plugins/auth-backend/package.json | 2 +- .../src/providers/saml/provider.ts | 31 ++-- yarn.lock | 132 +++++++++++------- 4 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 .changeset/tasty-oranges-rescue.md diff --git a/.changeset/tasty-oranges-rescue.md b/.changeset/tasty-oranges-rescue.md new file mode 100644 index 0000000000..cc57aaffad --- /dev/null +++ b/.changeset/tasty-oranges-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +migrate from 'passport-saml' to '@node-saml/passport-saml' diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 29c34b84e1..140b4b470a 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -52,6 +52,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@google-cloud/firestore": "^7.0.0", + "@node-saml/passport-saml": "^4.0.4", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", @@ -81,7 +82,6 @@ "passport-microsoft": "^1.0.0", "passport-oauth2": "^1.6.1", "passport-onelogin-oauth": "^0.0.1", - "passport-saml": "^3.1.2", "uuid": "^8.0.0", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 3d35f46628..0c09f1e987 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -15,12 +15,12 @@ */ import express from 'express'; -import { SamlConfig } from 'passport-saml/lib/passport-saml/types'; +import { SamlConfig } from '@node-saml/passport-saml'; import { Strategy as SamlStrategy, Profile as SamlProfile, VerifyWithoutRequest, -} from 'passport-saml'; +} from '@node-saml/passport-saml'; import { executeFrameHandlerStrategy, executeRedirectStrategy, @@ -62,17 +62,22 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; this.resolverContext = options.resolverContext; - this.strategy = new SamlStrategy({ ...options }, (( - fullProfile: SamlProfile, - done: PassportDoneCallback, - ) => { - // TODO: There's plenty more validation and profile handling to do here, - // this provider is currently only intended to validate the provider pattern - // for non-oauth auth flows. - // TODO: This flow doesn't issue an identity token that can be used to validate - // the identity of the user in other backends, which we need in some form. - done(undefined, { fullProfile }); - }) as VerifyWithoutRequest); + this.strategy = new SamlStrategy( + { ...options }, + (( + fullProfile: SamlProfile, + done: PassportDoneCallback, + ) => { + // TODO: There's plenty more validation and profile handling to do here, + // this provider is currently only intended to validate the provider pattern + // for non-oauth auth flows. + // TODO: This flow doesn't issue an identity token that can be used to validate + // the identity of the user in other backends, which we need in some form. + done(undefined, { fullProfile }); + }) as VerifyWithoutRequest, + // TODO: Validate logout + () => {}, + ); } async start(req: express.Request, res: express.Response): Promise { diff --git a/yarn.lock b/yarn.lock index 1d31e0fc44..1d9d11f9b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4789,6 +4789,7 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@google-cloud/firestore": ^7.0.0 + "@node-saml/passport-saml": ^4.0.4 "@types/body-parser": ^1.19.0 "@types/cookie-parser": ^1.4.2 "@types/express": ^4.17.6 @@ -4830,7 +4831,6 @@ __metadata: passport-microsoft: ^1.0.0 passport-oauth2: ^1.6.1 passport-onelogin-oauth: ^0.0.1 - passport-saml: ^3.1.2 supertest: ^6.1.3 uuid: ^8.0.0 winston: ^3.2.1 @@ -13122,6 +13122,39 @@ __metadata: languageName: node linkType: hard +"@node-saml/node-saml@npm:^4.0.4": + version: 4.0.5 + resolution: "@node-saml/node-saml@npm:4.0.5" + dependencies: + "@types/debug": ^4.1.7 + "@types/passport": ^1.0.11 + "@types/xml-crypto": ^1.4.2 + "@types/xml-encryption": ^1.2.1 + "@types/xml2js": ^0.4.11 + "@xmldom/xmldom": ^0.8.6 + debug: ^4.3.4 + xml-crypto: ^3.0.1 + xml-encryption: ^3.0.2 + xml2js: ^0.5.0 + xmlbuilder: ^15.1.1 + checksum: 7d97575111a381ef2d0f16e1fc85ae3f84322ccba06dcb0594b00cf598e429658f45e479b78836943f69f249c08a8593e5168404acf7f1ed659ead53ceef465e + languageName: node + linkType: hard + +"@node-saml/passport-saml@npm:^4.0.4": + version: 4.0.4 + resolution: "@node-saml/passport-saml@npm:4.0.4" + dependencies: + "@node-saml/node-saml": ^4.0.4 + "@types/express": ^4.17.14 + "@types/passport": ^1.0.11 + "@types/passport-strategy": ^0.2.35 + passport: ^0.6.0 + passport-strategy: ^1.0.0 + checksum: 75178669d7d47038c33bb0602454cb5030fc9b3ecdcae9163a35cef436bc6c22e68e57d06213e0118ff1cb0dcd2f2fa25112672ebe4cbad90578df21bec67fce + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -18017,7 +18050,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.17, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": +"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.14, @types/express@npm:^4.17.17, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -18664,7 +18697,7 @@ __metadata: languageName: node linkType: hard -"@types/passport@npm:*, @types/passport@npm:^1.0.3": +"@types/passport@npm:*, @types/passport@npm:^1.0.11, @types/passport@npm:^1.0.3": version: 1.0.16 resolution: "@types/passport@npm:1.0.16" dependencies: @@ -19300,7 +19333,26 @@ __metadata: languageName: node linkType: hard -"@types/xml2js@npm:*, @types/xml2js@npm:^0.4.7": +"@types/xml-crypto@npm:^1.4.2": + version: 1.4.6 + resolution: "@types/xml-crypto@npm:1.4.6" + dependencies: + "@types/node": "*" + xpath: 0.0.27 + checksum: e53516a2f5e4e018e164eb1cb9fc922294b9a339624e567c1c00a2b1496e9f86826210473e62ceb0b45949638c9d149da088b3598f6b3acd86e933f0a2b23f2c + languageName: node + linkType: hard + +"@types/xml-encryption@npm:^1.2.1": + version: 1.2.4 + resolution: "@types/xml-encryption@npm:1.2.4" + dependencies: + "@types/node": "*" + checksum: 1ef957dfb47cf55b12e114755e271a2343f73eb4c59ab6c68b0b7d1b8111d7e1bd8d2bfe0601d2aea09be83c66355bc77fc59f9b71aeff9bb9e15371bcfef5d3 + languageName: node + linkType: hard + +"@types/xml2js@npm:*, @types/xml2js@npm:^0.4.11, @types/xml2js@npm:^0.4.7": version: 0.4.14 resolution: "@types/xml2js@npm:0.4.14" dependencies: @@ -19960,14 +20012,7 @@ __metadata: languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.7.0, @xmldom/xmldom@npm:^0.7.6, @xmldom/xmldom@npm:^0.7.9": - version: 0.7.13 - resolution: "@xmldom/xmldom@npm:0.7.13" - checksum: b4054078530e5fa8ede9677425deff0fce6d965f4c477ca73f8490d8a089e60b8498a15560425a1335f5ff99ecb851ed2c734b0a9a879299a5694302f212f37a - languageName: node - linkType: hard - -"@xmldom/xmldom@npm:^0.8.3": +"@xmldom/xmldom@npm:^0.8.3, @xmldom/xmldom@npm:^0.8.5, @xmldom/xmldom@npm:^0.8.6, @xmldom/xmldom@npm:^0.8.8": version: 0.8.10 resolution: "@xmldom/xmldom@npm:0.8.10" checksum: 4c136aec31fb3b49aaa53b6fcbfe524d02a1dc0d8e17ee35bd3bf35e9ce1344560481cd1efd086ad1a4821541482528672306d5e37cdbd187f33d7fadd3e2cf0 @@ -36408,21 +36453,6 @@ __metadata: languageName: node linkType: hard -"passport-saml@npm:^3.1.2": - version: 3.2.4 - resolution: "passport-saml@npm:3.2.4" - dependencies: - "@xmldom/xmldom": ^0.7.6 - debug: ^4.3.2 - passport-strategy: ^1.0.0 - xml-crypto: ^2.1.3 - xml-encryption: ^2.0.0 - xml2js: ^0.4.23 - xmlbuilder: ^15.1.1 - checksum: 8e885af4d44c2d862b2ea0d051ab2a36bc6f9a70e62f90daf7ce4eefd126ac2ab4d5fc070693eba05f5e1be248af23fa018611bbfa7fad31708371f387f5dd77 - languageName: node - linkType: hard - "passport-strategy@npm:1.x.x, passport-strategy@npm:^1.0.0": version: 1.0.0 resolution: "passport-strategy@npm:1.0.0" @@ -36430,6 +36460,17 @@ __metadata: languageName: node linkType: hard +"passport@npm:^0.6.0": + version: 0.6.0 + resolution: "passport@npm:0.6.0" + dependencies: + passport-strategy: 1.x.x + pause: 0.0.1 + utils-merge: ^1.0.1 + checksum: ef932ad671d50de34765c7a53cd1e058d8331a82a6df09265a9c6c1168911aee4a7b5215803d0101110ab7f317e096b4954ca7e18fb2c33b9929f0bd17dbe159 + languageName: node + linkType: hard + "passport@npm:^0.7.0": version: 0.7.0 resolution: "passport@npm:0.7.0" @@ -44755,24 +44796,24 @@ __metadata: languageName: node linkType: hard -"xml-crypto@npm:^2.1.3": - version: 2.1.5 - resolution: "xml-crypto@npm:2.1.5" +"xml-crypto@npm:^3.0.1": + version: 3.2.0 + resolution: "xml-crypto@npm:3.2.0" dependencies: - "@xmldom/xmldom": ^0.7.9 + "@xmldom/xmldom": ^0.8.8 xpath: 0.0.32 - checksum: 387ed6aa812f9ea7fb33385bd3e934042152ee9a97870f28ebfa5c7931eee23a7a2d36ca35916fbe5eadd65163ce9483db661cf3f569c9177773e8efa1acfa37 + checksum: 6c4974a7518307ea006dcfc1405f61c6738b45574b4d9d1e62f53b602bfcf894d34017f99d618f26f67c40a5e6d78e6228116ded2768b2ca5b2df5c8bf7774b7 languageName: node linkType: hard -"xml-encryption@npm:^2.0.0": - version: 2.0.0 - resolution: "xml-encryption@npm:2.0.0" +"xml-encryption@npm:^3.0.2": + version: 3.0.2 + resolution: "xml-encryption@npm:3.0.2" dependencies: - "@xmldom/xmldom": ^0.7.0 + "@xmldom/xmldom": ^0.8.5 escape-html: ^1.0.3 xpath: 0.0.32 - checksum: a454445704c5e3aa3f992128c413c02f3c00c346cb0d63b01beae4b6a341cfc0a52a0219ccec47dcce250e336ba7b09d95909913b1f199ca43604961a00a1995 + checksum: aac1b987d5de5becfc747c88c3a656c00799a153ab541078b875a69e1ac1f1c2f29bf85f22eab6a78382dc2919f79401a916cc392aba7994475919e0695893eb languageName: node linkType: hard @@ -44790,16 +44831,6 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:^0.4.23": - version: 0.4.23 - resolution: "xml2js@npm:0.4.23" - dependencies: - sax: ">=0.6.0" - xmlbuilder: ~11.0.0 - checksum: ca0cf2dfbf6deeaae878a891c8fbc0db6fd04398087084edf143cdc83d0509ad0fe199b890f62f39c4415cf60268a27a6aed0d343f0658f8779bd7add690fa98 - languageName: node - linkType: hard - "xml2js@npm:^0.5.0": version: 0.5.0 resolution: "xml2js@npm:0.5.0" @@ -44848,6 +44879,13 @@ __metadata: languageName: node linkType: hard +"xpath@npm:0.0.27": + version: 0.0.27 + resolution: "xpath@npm:0.0.27" + checksum: 51f45d211a9a552a8f6a12a474061e89bafb07e0aecd4bad18a557411feb975919c158e1a66e4ea0542198c6ed442481d9f709c625cca57b97aaedeaeded902e + languageName: node + linkType: hard + "xpath@npm:0.0.32": version: 0.0.32 resolution: "xpath@npm:0.0.32" From aa91cd69acffd06db4febf10768d9003f1fd8426 Mon Sep 17 00:00:00 2001 From: Brian Hudson Date: Mon, 29 Jan 2024 15:54:06 -0500 Subject: [PATCH 017/112] Fix safeEntityName createOrMergeEntity was replacing capital letters with -. For example "@internal/pluginApi" would result in "internal-plugin-pi". Updated to correctly handle capital letters, prefixing them with - unless they are the first character. For example "@internal/pluginApi" would result in "internal-plugin-api". Signed-off-by: Brian Hudson m> --- .changeset/calm-onions-exercise.md | 5 ++++ .../generate-catalog-info.ts | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-onions-exercise.md diff --git a/.changeset/calm-onions-exercise.md b/.changeset/calm-onions-exercise.md new file mode 100644 index 0000000000..f1c58a173f --- /dev/null +++ b/.changeset/calm-onions-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Resolved an issue with generate-catalog-info where it was replacing upper case characters with -. diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts index b8a473c67b..c15f478bd0 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts @@ -165,8 +165,17 @@ async function fixCatalogInfoYaml(options: FixOptions) { relativePath('.', yamlPath), ); const safeName = packageJson.name - .replace(/[^a-z0-9_\-\.]+/g, '-') - .replace(/^[^a-z0-9]|[^a-z0-9]$/g, ''); + .replace(/^[^\w\s]|[^a-z0-9]$/g, '') + .replace(/[^A-Za-z0-9_\-.]+/g, '-') + .replace(/([A-Z])/g, (_, letter, index, original) => { + if (index !== 0) { + const previousChar = original[index - 1]; + if (previousChar !== '-') { + return `-${letter.toLowerCase()}`; + } + } + return letter.toLowerCase(); + }); let yamlJson: BackstagePackageEntity; try { @@ -241,8 +250,17 @@ function createOrMergeEntity( existingEntity: BackstagePackageEntity | Record = {}, ): BackstagePackageEntity { const safeEntityName = packageJson.name - .replace(/[^a-z0-9_\-\.]+/g, '-') - .replace(/^[^a-z0-9]|[^a-z0-9]$/g, ''); + .replace(/^[^\w\s]|[^a-z0-9]$/g, '') + .replace(/[^A-Za-z0-9_\-.]+/g, '-') + .replace(/([A-Z])/g, (_, letter, index, original) => { + if (index !== 0) { + const previousChar = original[index - 1]; + if (previousChar !== '-') { + return `-${letter.toLowerCase()}`; + } + } + return letter.toLowerCase(); + }); return { ...existingEntity, From a245fad6c75c784ae644d07c59fbc36b685337c6 Mon Sep 17 00:00:00 2001 From: Brian Hudson Date: Tue, 30 Jan 2024 07:34:38 -0500 Subject: [PATCH 018/112] Add util function and related tests Signed-off-by: Brian Hudson --- .../generate-catalog-info.ts | 29 +++------------ .../generate-catalog-info/utils.test.ts | 36 +++++++++++++++++++ .../commands/generate-catalog-info/utils.ts | 17 +++++++++ 3 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 packages/repo-tools/src/commands/generate-catalog-info/utils.test.ts diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts index c15f478bd0..1893bd25c8 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts @@ -33,6 +33,7 @@ import { isFulfilled, readFile, writeFile, + safeEntityName, } from './utils'; import { CodeOwnersEntry } from 'codeowners-utils'; @@ -164,18 +165,7 @@ async function fixCatalogInfoYaml(options: FixOptions) { codeowners, relativePath('.', yamlPath), ); - const safeName = packageJson.name - .replace(/^[^\w\s]|[^a-z0-9]$/g, '') - .replace(/[^A-Za-z0-9_\-.]+/g, '-') - .replace(/([A-Z])/g, (_, letter, index, original) => { - if (index !== 0) { - const previousChar = original[index - 1]; - if (previousChar !== '-') { - return `-${letter.toLowerCase()}`; - } - } - return letter.toLowerCase(); - }); + const safeName = safeEntityName(packageJson.name); let yamlJson: BackstagePackageEntity; try { @@ -249,18 +239,7 @@ function createOrMergeEntity( owner: string, existingEntity: BackstagePackageEntity | Record = {}, ): BackstagePackageEntity { - const safeEntityName = packageJson.name - .replace(/^[^\w\s]|[^a-z0-9]$/g, '') - .replace(/[^A-Za-z0-9_\-.]+/g, '-') - .replace(/([A-Z])/g, (_, letter, index, original) => { - if (index !== 0) { - const previousChar = original[index - 1]; - if (previousChar !== '-') { - return `-${letter.toLowerCase()}`; - } - } - return letter.toLowerCase(); - }); + const entityName = safeEntityName(packageJson.name); return { ...existingEntity, @@ -269,7 +248,7 @@ function createOrMergeEntity( metadata: { ...existingEntity.metadata, // Provide default name/title/description values. - name: safeEntityName, + name: entityName, title: packageJson.name, ...(packageJson.description && !existingEntity.metadata?.description ? { description: packageJson.description } diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.test.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.test.ts new file mode 100644 index 0000000000..40c1bb1e19 --- /dev/null +++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { safeEntityName } from './utils'; + +describe('utils', () => { + describe('safeEntityName', () => { + it('should remove non-alphanumeric characters at the start and end', () => { + const result = safeEntityName('%entityname$'); + expect(result).toBe('entityname'); + }); + + it('should replace non-alphanumeric characters, except - and _, with -', () => { + const result = safeEntityName('entity@#name$'); + expect(result).toBe('entity-name'); + }); + + it('should replace capital letters with - followed by the same letter in lowercase', () => { + const result = safeEntityName('EntityName'); + expect(result).toBe('entity-name'); + }); + }); +}); diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts index b00fbd57a8..3f23b35112 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts @@ -48,3 +48,20 @@ export const isRejected = ( export const isFulfilled = ( input: PromiseSettledResult, ): input is PromiseFulfilledResult => input.status === 'fulfilled'; + +/** + * Generates a suitable entity name from a package name by slugifying the given package name. + * + * @param packageName - The package name to generate an entity name from. + * @returns The generated entity name, a slugified version of the package name. + */ +export const safeEntityName = (packageName: string): string => { + return packageName + .replace(/^[^\w\s]|[^a-z0-9]$/g, '') + .replace(/[^A-Za-z0-9_\-.]+/g, '-') + .replace( + /([a-z])([A-Z])/g, + (_, a, b) => `${a}-${b.toLocaleLowerCase('en-US')}`, + ) + .replace(/^(.)/, (_, a) => a.toLocaleLowerCase('en-US')); +}; From 09a9c95f7963a2ea91d45a014280a11f68252739 Mon Sep 17 00:00:00 2001 From: AmbrishRamachandiran Date: Wed, 31 Jan 2024 12:07:05 +0530 Subject: [PATCH 019/112] Add intro to azure sites plugin Signed-off-by: AmbrishRamachandiran --- .changeset/cool-islands-impress.md | 5 +++++ plugins/azure-sites/README.md | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/cool-islands-impress.md diff --git a/.changeset/cool-islands-impress.md b/.changeset/cool-islands-impress.md new file mode 100644 index 0000000000..df6df1ada2 --- /dev/null +++ b/.changeset/cool-islands-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites': patch +--- + +Updated README diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md index a19b314b77..21e97d348e 100644 --- a/plugins/azure-sites/README.md +++ b/plugins/azure-sites/README.md @@ -1,5 +1,7 @@ # Azure Sites Plugin +Azure Sites (Apps & Functions) plugin support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + ![preview of Azure table](docs/functions-table.png) _Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ From 12afbbc8dc77aa77cf6a0eb6a9025f2a4dd548ff Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 31 Jan 2024 15:14:44 +0100 Subject: [PATCH 020/112] Add alpha export for new fe system Signed-off-by: Philipp Hugenroth --- plugins/org/api-report-alpha.md | 25 ++++++++++++++++ plugins/org/package.json | 17 +++++++++++ plugins/org/src/alpha.tsx | 52 +++++++++++++++++++++++++++++++++ yarn.lock | 2 ++ 4 files changed, 96 insertions(+) create mode 100644 plugins/org/api-report-alpha.md create mode 100644 plugins/org/src/alpha.tsx diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md new file mode 100644 index 0000000000..b9605a45f5 --- /dev/null +++ b/plugins/org/api-report-alpha.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-org" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +const _default: BackstagePlugin< + {}, + { + catalogIndex: ExternalRouteRef; + } +>; +export default _default; + +// @alpha (undocumented) +export const EntityGroupProfileCard: ExtensionDefinition<{ + filter?: string | undefined; +}>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/org/package.json b/plugins/org/package.json index 4355e265aa..01aa872e32 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -4,6 +4,21 @@ "version": "0.6.20-next.1", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "license": "Apache-2.0", "publishConfig": { "access": "public", @@ -31,8 +46,10 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@material-ui/core": "^4.12.2", diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx new file mode 100644 index 0000000000..30cda2a4c7 --- /dev/null +++ b/plugins/org/src/alpha.tsx @@ -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 { + compatWrapper, + convertLegacyRouteRefs, +} from '@backstage/core-compat-api'; +import { createPlugin } from '@backstage/frontend-plugin-api'; +import React from 'react'; +import { catalogIndexRouteRef } from './routes'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +/** @alpha */ +export const EntityGroupProfileCard = createEntityCardExtension({ + name: 'group-profile', + loader: async () => + import('./components/Cards/Group/GroupProfile/GroupProfileCard').then(m => + compatWrapper(), + ), +}); + +/** @alpha */ +/* export const EntityMembersListCard = createEntityCardExtension({ + name: 'members-list', + loader: async () => + import('./components/').then(m => + compatWrapper(), + ), + }); + */ + +/** @alpha */ +export default createPlugin({ + id: 'org', + extensions: [EntityGroupProfileCard], + externalRoutes: convertLegacyRouteRefs({ + catalogIndex: catalogIndexRouteRef, + }), +}); diff --git a/yarn.lock b/yarn.lock index 75fde2bc1b..bd654c7dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7839,9 +7839,11 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" From 491e0705c23b70628f79c5545c4944dac0d34a84 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 31 Jan 2024 15:38:45 +0100 Subject: [PATCH 021/112] Add catalog cards Signed-off-by: Philipp Hugenroth --- packages/app-next/app-config.yaml | 9 +++++++ packages/app-next/src/App.tsx | 2 ++ plugins/org/src/alpha.tsx | 40 ++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index a79bb866db..0b842a0b91 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -5,6 +5,7 @@ app: bindings: catalog.viewTechDoc: techdocs.docRoot catalog.createComponent: catalog-import.importPage + # org.catalogIndex: catalog.catalogIndex extensions: # - apis.plugin.graphiql.browse.gitlab: true @@ -19,6 +20,14 @@ app: # Entity page content - entity-content:techdocs + # Org Plugin + - entity-card:org/group-profile: + config: + filter: kind:group + # - entity-card:org/members-list + # - entity-card:org/ownership + # - entity-card:org/user-profile + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 03a6649c27..bb39d8d40e 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -24,6 +24,7 @@ import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; +import orgPlugin from '@backstage/plugin-org/alpha'; import { coreExtensionData, @@ -125,6 +126,7 @@ const app = createApp({ userSettingsPlugin, homePlugin, appVisualizerPlugin, + orgPlugin, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 30cda2a4c7..3d02aec88b 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -33,19 +33,41 @@ export const EntityGroupProfileCard = createEntityCardExtension({ }); /** @alpha */ -/* export const EntityMembersListCard = createEntityCardExtension({ - name: 'members-list', - loader: async () => - import('./components/').then(m => - compatWrapper(), - ), - }); - */ +export const EntityMembersListCard = createEntityCardExtension({ + name: 'members-list', + loader: async () => + import('./components/Cards/Group/MembersList/MembersListCard').then(m => + compatWrapper(), + ), +}); + +/** @alpha */ +export const EntityOwnershipCard = createEntityCardExtension({ + name: 'ownership', + loader: async () => + import('./components/Cards/OwnershipCard/OwnershipCard').then(m => + compatWrapper(), + ), +}); + +/** @alpha */ +export const EntityUserProfileCard = createEntityCardExtension({ + name: 'user-profile', + loader: async () => + import('./components/Cards/User/UserProfileCard/UserProfileCard').then(m => + compatWrapper(), + ), +}); /** @alpha */ export default createPlugin({ id: 'org', - extensions: [EntityGroupProfileCard], + extensions: [ + EntityGroupProfileCard, + EntityMembersListCard, + EntityOwnershipCard, + EntityUserProfileCard, + ], externalRoutes: convertLegacyRouteRefs({ catalogIndex: catalogIndexRouteRef, }), From 6e1bf50fe0dd29e5c79fc896ae1a35a4b5746090 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 31 Jan 2024 15:48:33 +0100 Subject: [PATCH 022/112] Add changeset Signed-off-by: Philipp Hugenroth --- .changeset/slimy-rats-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-rats-fly.md diff --git a/.changeset/slimy-rats-fly.md b/.changeset/slimy-rats-fly.md new file mode 100644 index 0000000000..81fcae5bb2 --- /dev/null +++ b/.changeset/slimy-rats-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Added basic support for the new frontend system, exported from the `/alpha` subpath. From 3e1c6e2318f32bbb16c506d0ed8c125460d93461 Mon Sep 17 00:00:00 2001 From: Michal Hitka Date: Tue, 30 Jan 2024 14:26:10 +0100 Subject: [PATCH 023/112] feat(edge): show arrow heads for relation edges - reverts part of https://github.com/backstage/backstage/pull/13673/ Signed-off-by: Michal Hitka --- .changeset/forty-beers-sniff.md | 5 +++++ .../src/components/DependencyGraph/Edge.tsx | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/forty-beers-sniff.md diff --git a/.changeset/forty-beers-sniff.md b/.changeset/forty-beers-sniff.md new file mode 100644 index 0000000000..aa7c2faa52 --- /dev/null +++ b/.changeset/forty-beers-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added arrow heads for graph edges in `DependencyGraph` for better understandability. diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index 05e068eca4..57aaa38727 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -19,7 +19,7 @@ import * as d3Shape from 'd3-shape'; import isFinite from 'lodash/isFinite'; import makeStyles from '@material-ui/core/styles/makeStyles'; import { DependencyGraphTypes as Types } from './types'; -import { EDGE_TEST_ID, LABEL_TEST_ID } from './constants'; +import { ARROW_MARKER_ID, EDGE_TEST_ID, LABEL_TEST_ID } from './constants'; import { DefaultLabel } from './DefaultLabel'; import dagre from 'dagre'; @@ -43,7 +43,7 @@ export type DependencyGraphEdgeClassKey = 'path' | 'label'; const useStyles = makeStyles( theme => ({ path: { - strokeWidth: 1, + strokeWidth: 1.3, stroke: theme.palette.textSubtle, fill: 'none', transition: `${theme.transitions.duration.shortest}ms`, @@ -126,7 +126,12 @@ export function Edge({ return ( <> {path && ( - + )} {labelProps.label ? ( Date: Wed, 31 Jan 2024 10:10:54 +0100 Subject: [PATCH 024/112] feat(catalog-graph): allow showing arrow heads for relation edges Signed-off-by: Michal Hitka --- .changeset/forty-beers-sniff.md | 3 ++- packages/core-components/api-report.md | 1 + .../src/components/DependencyGraph/DependencyGraph.tsx | 8 ++++++++ .../src/components/DependencyGraph/Edge.tsx | 4 +++- plugins/catalog-graph/api-report.md | 1 + .../EntityRelationsGraph/EntityRelationsGraph.tsx | 3 +++ 6 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.changeset/forty-beers-sniff.md b/.changeset/forty-beers-sniff.md index aa7c2faa52..6adb7e98b4 100644 --- a/.changeset/forty-beers-sniff.md +++ b/.changeset/forty-beers-sniff.md @@ -1,5 +1,6 @@ --- '@backstage/core-components': patch +'@backstage/plugin-catalog-graph': patch --- -Added arrow heads for graph edges in `DependencyGraph` for better understandability. +Added possibility to show arrow heads for graph edges in `DependencyGraph` for better understandability. diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b5d54dd606..27b9c62653 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -265,6 +265,7 @@ export interface DependencyGraphProps rankMargin?: number; renderLabel?: DependencyGraphTypes.RenderLabelFunction; renderNode?: DependencyGraphTypes.RenderNodeFunction; + showArrowHeads?: boolean; zoom?: 'enabled' | 'disabled' | 'enable-on-click'; } diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index d4907d2a18..b1449d1b70 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -160,6 +160,12 @@ export interface DependencyGraphProps * Default: 'curveMonotoneX' */ curve?: 'curveStepBefore' | 'curveMonotoneX'; + /** + * Controls if the arrow heads should be rendered or not. + * + * Default: true + */ + showArrowHeads?: boolean; /** * Controls if the graph should be contained or grow * @@ -201,6 +207,7 @@ export function DependencyGraph( defs, zoom = 'enabled', curve = 'curveMonotoneX', + showArrowHeads = true, fit = 'grow', ...svgProps } = props; @@ -436,6 +443,7 @@ export function DependencyGraph( render={renderLabel} edge={edge} curve={curve} + showArrowHeads={showArrowHeads} /> ); })} diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index 57aaa38727..6a1f8861bf 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -67,6 +67,7 @@ export type EdgeComponentProps = { edge: Types.DependencyEdge, ) => dagre.graphlib.Graph<{}>; curve: 'curveStepBefore' | 'curveMonotoneX'; + showArrowHeads?: boolean; }; const renderDefault = (props: Types.RenderLabelProps) => ( @@ -79,6 +80,7 @@ export function Edge({ id, edge, curve, + showArrowHeads, }: EdgeComponentProps) { const { x = 0, y = 0, width, height, points } = edge; const labelProps: Types.DependencyEdge = edge; @@ -129,7 +131,7 @@ export function Edge({ )} diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index ba75943843..8f75e52289 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -123,6 +123,7 @@ export type EntityRelationsGraphProps = { renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; + showArrowHeads?: boolean; }; // @public diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 654aaff969..35f76a7de0 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -84,6 +84,7 @@ export type EntityRelationsGraphProps = { renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; + showArrowHeads?: boolean; }; /** @@ -107,6 +108,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { renderNode, renderLabel, curve, + showArrowHeads, } = props; const theme = useTheme(); @@ -154,6 +156,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { labelOffset={theme.spacing(1)} zoom={zoom} curve={curve} + showArrowHeads={showArrowHeads} /> )} From 84b2d781e26e66de70b333903d412f83bfb6f9e7 Mon Sep 17 00:00:00 2001 From: Michal Hitka Date: Wed, 31 Jan 2024 10:17:46 +0100 Subject: [PATCH 025/112] feat(catalog-graph): hide arrow heads for relation edges by default Signed-off-by: Michal Hitka --- .../src/components/DependencyGraph/DependencyGraph.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index b1449d1b70..ee6b74f97a 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -163,7 +163,7 @@ export interface DependencyGraphProps /** * Controls if the arrow heads should be rendered or not. * - * Default: true + * Default: false */ showArrowHeads?: boolean; /** @@ -207,7 +207,7 @@ export function DependencyGraph( defs, zoom = 'enabled', curve = 'curveMonotoneX', - showArrowHeads = true, + showArrowHeads = false, fit = 'grow', ...svgProps } = props; From 96655af9eeb0984fc0fc5078ac55e8a788d176a2 Mon Sep 17 00:00:00 2001 From: Michal Hitka Date: Wed, 31 Jan 2024 14:16:57 +0100 Subject: [PATCH 026/112] chore: document showArrowHeads in changeset Signed-off-by: Michal Hitka --- .changeset/forty-beers-sniff.md | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/.changeset/forty-beers-sniff.md b/.changeset/forty-beers-sniff.md index 6adb7e98b4..e1f478c1e1 100644 --- a/.changeset/forty-beers-sniff.md +++ b/.changeset/forty-beers-sniff.md @@ -3,4 +3,36 @@ '@backstage/plugin-catalog-graph': patch --- -Added possibility to show arrow heads for graph edges in `DependencyGraph` for better understandability. +Added possibility to show arrow heads for graph edges for better understandability. + +In order to show arrow heads in the catalog graph page, add `showArrowHeads` attribute to `CatalogGraphPage` component +(typically in `packages/app/src/App.tsx`): + +```diff + +``` + +In order to show arrow heads in entity graphs, add `showArrowHeads` attribute to `EntityCatalogGraphCard` components +(typically multiple occurrences in `packages/app/src/components/catalog/EntityPage.tsx`): + +```diff +- ++ +``` From 7f4062f91360a7bca8d5239d1780d0c30ab7e76d Mon Sep 17 00:00:00 2001 From: Michal Hitka Date: Wed, 31 Jan 2024 14:32:30 +0100 Subject: [PATCH 027/112] chore: simplify showArrowHeads example in changeset Signed-off-by: Michal Hitka --- .changeset/forty-beers-sniff.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/.changeset/forty-beers-sniff.md b/.changeset/forty-beers-sniff.md index e1f478c1e1..130726f94f 100644 --- a/.changeset/forty-beers-sniff.md +++ b/.changeset/forty-beers-sniff.md @@ -9,24 +9,8 @@ In order to show arrow heads in the catalog graph page, add `showArrowHeads` att (typically in `packages/app/src/App.tsx`): ```diff - +- ++ ``` In order to show arrow heads in entity graphs, add `showArrowHeads` attribute to `EntityCatalogGraphCard` components From bc644fb451db508ff888c5e9557322d66a6723c4 Mon Sep 17 00:00:00 2001 From: Tim Eyzermans Date: Thu, 1 Feb 2024 08:10:12 +0100 Subject: [PATCH 028/112] Add latest_report_status tag to PuppetDB nodes Signed-off-by: Tim Eyzermans --- .changeset/wet-lions-crash.md | 5 +++++ plugins/catalog-backend-module-puppetdb/api-report.md | 1 + .../src/providers/PuppetDbEntityProvider.test.ts | 8 ++++---- .../src/puppet/read.test.ts | 2 ++ .../src/puppet/transformers.test.ts | 3 ++- .../src/puppet/transformers.ts | 8 +++++++- .../catalog-backend-module-puppetdb/src/puppet/types.ts | 4 ++++ 7 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 .changeset/wet-lions-crash.md diff --git a/.changeset/wet-lions-crash.md b/.changeset/wet-lions-crash.md new file mode 100644 index 0000000000..795e60588d --- /dev/null +++ b/.changeset/wet-lions-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-puppetdb': patch +--- + +Added `latest_report_status` parameter from the PuppetDB node api and added it as a tag to the nodes. The status is valuable information as it displays which nodes are compliant to your configuration and which ones are failing are making changes. diff --git a/plugins/catalog-backend-module-puppetdb/api-report.md b/plugins/catalog-backend-module-puppetdb/api-report.md index a4954ce6bd..51c98bf5bf 100644 --- a/plugins/catalog-backend-module-puppetdb/api-report.md +++ b/plugins/catalog-backend-module-puppetdb/api-report.md @@ -65,6 +65,7 @@ export type PuppetNode = { timestamp: string; certname: string; hash: string; + latest_report_status: string; producer_timestamp: string; producer: string; environment: string; diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts index 42091fd6d6..660ddc2bdd 100644 --- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts @@ -108,7 +108,7 @@ describe('PuppetEntityProvider', () => { annotations: { [ANNOTATION_PUPPET_CERTNAME]: 'node1', }, - tags: ['windows'], + tags: ['windows', 'unchanged'], description: 'Description 1', }, spec: { @@ -127,7 +127,7 @@ describe('PuppetEntityProvider', () => { annotations: { [ANNOTATION_PUPPET_CERTNAME]: 'node2', }, - tags: ['linux'], + tags: ['linux', 'unchanged'], description: 'Description 2', }, spec: { @@ -173,7 +173,7 @@ describe('PuppetEntityProvider', () => { 'catalog.providers.puppetdb.baseUrl', )}/${ENDPOINT_NODES}/node1`, }, - tags: ['windows'], + tags: ['windows', 'unchanged'], description: 'Description 1', }, spec: { @@ -201,7 +201,7 @@ describe('PuppetEntityProvider', () => { 'catalog.providers.puppetdb.baseUrl', )}/${ENDPOINT_NODES}/node2`, }, - tags: ['linux'], + tags: ['linux', 'unchanged'], description: 'Description 2', }, spec: { diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts index c3b750a417..ac28f04503 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts @@ -53,6 +53,7 @@ describe('readPuppetNodes', () => { producer_timestamp: 'producer_time1', producer: 'producer1', environment: 'environment1', + latest_report_status: 'unchanged', facts: { data: [ { @@ -84,6 +85,7 @@ describe('readPuppetNodes', () => { hash: 'hash2', producer_timestamp: 'producer_time2', producer: 'producer2', + latest_report_status: 'unchanged', environment: 'environment2', facts: { data: [ diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts index a56f7b549b..ca0e8ae61b 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts @@ -33,6 +33,7 @@ describe('defaultResourceTransformer', () => { producer_timestamp: 'producer_time1', producer: 'producer1', environment: 'environment1', + latest_report_status: 'unchanged', facts: { href: 'facts1', data: [ @@ -71,7 +72,7 @@ describe('defaultResourceTransformer', () => { [ANNOTATION_PUPPET_CERTNAME]: 'node1', }, description: 'ipaddress1', - tags: ['linux'], + tags: ['linux', 'unchanged'], }, spec: { type: 'virtual-machine', diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.ts index 9ec607b83e..af9aeaa0ca 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.ts @@ -37,6 +37,7 @@ export const defaultResourceTransformer: ResourceTransformer = async ( ? 'virtual-machine' : 'physical-server'; const kernel = node.facts?.data?.find(e => e.name === 'kernel')?.value; + const latest_report_status = node.latest_report_status; return { apiVersion: 'backstage.io/v1beta1', @@ -50,7 +51,12 @@ export const defaultResourceTransformer: ResourceTransformer = async ( description: node.facts?.data ?.find(e => e.name === 'ipaddress') ?.value?.toString(), - tags: kernel ? [kernel.toString().toLocaleLowerCase('en-US')] : [], + tags: kernel + ? [ + kernel.toString().toLocaleLowerCase('en-US'), + latest_report_status.toString().toLocaleLowerCase('en-US'), + ] + : [], }, spec: { type: type, diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/types.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/types.ts index 5bfac5c998..dd5ab3b781 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/types.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/types.ts @@ -50,6 +50,10 @@ export type PuppetNode = { /** * A hash of the factset's certname, environment, timestamp, facts, and producer_timestamp. */ + latest_report_status: string; + /** + * The status of the latest report. Possible values come from Puppet's report status failed, changed, or unchanged. + */ hash: string; /** * The most recent time of fact submission for the relevant certname from the Puppet Server. From 3489d05ed43e62e8c25979526f9659505bd51ab1 Mon Sep 17 00:00:00 2001 From: Kyle Smith Date: Thu, 1 Feb 2024 19:30:23 -0600 Subject: [PATCH 029/112] feat(backend-common/FetchUrlReader): support token authentication Builds on interface enhancements introduced by #22521 to support token-based authentication in FetchUrlReader#readUrl(). Signed-off-by: Kyle Smith --- .changeset/fast-onions-own.md | 5 +++++ .../src/reading/FetchUrlReader.test.ts | 21 +++++++++++++++++++ .../src/reading/FetchUrlReader.ts | 1 + 3 files changed, 27 insertions(+) create mode 100644 .changeset/fast-onions-own.md diff --git a/.changeset/fast-onions-own.md b/.changeset/fast-onions-own.md new file mode 100644 index 0000000000..6977b035bc --- /dev/null +++ b/.changeset/fast-onions-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +`FetchUrlReader#readUrl()` now supports passing an optional `token` to authenticate requests. diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index ad834c64ef..7b5e4ebc05 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -223,6 +223,27 @@ describe('FetchUrlReader', () => { ).rejects.toThrow(NotModifiedError); }); + it('should send Authorization header if token is provided', async () => { + expect.assertions(1); + + worker.use( + rest.get( + 'https://backstage.io/requires-authentication', + (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe('Bearer mytoken'); + return res(ctx.status(200)); + }, + ), + ); + + await fetchUrlReader.readUrl( + 'https://backstage.io/requires-authentication', + { + token: 'mytoken', + }, + ); + }); + it('should return etag from the response', async () => { const response = await fetchUrlReader.readUrl( 'https://backstage.io/some-resource', diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 90c1cf6c26..475a98ffdc 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -131,6 +131,7 @@ export class FetchUrlReader implements UrlReader { ...(options?.lastModifiedAfter && { 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), }), + ...(options?.token && { Authorization: `Bearer ${options.token}` }), }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. From 796d4277acaf9a9b5e9382d4512e2c8c31d13e05 Mon Sep 17 00:00:00 2001 From: Radu Ciopraga <1442639+raduciopraga@users.noreply.github.com> Date: Thu, 1 Feb 2024 22:09:16 +0100 Subject: [PATCH 030/112] Use the EntityDisplayName component for rendering Group nodes Signed-off-by: Radu Ciopraga <1442639+raduciopraga@users.noreply.github.com> --- .changeset/strong-news-develop.md | 5 ++++ .../GroupsDiagram.test.tsx | 8 ++++-- .../GroupsExplorerContent/GroupsDiagram.tsx | 13 +++++----- .../GroupsExplorerContent.test.tsx | 26 ++++++++----------- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 .changeset/strong-news-develop.md diff --git a/.changeset/strong-news-develop.md b/.changeset/strong-news-develop.md new file mode 100644 index 0000000000..1cbe4dda07 --- /dev/null +++ b/.changeset/strong-news-develop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Use the EntityDisplayName component for rendering Group nodes diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx index 7c57de84bc..1329d3c522 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx @@ -21,6 +21,7 @@ import { } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { GroupsDiagram } from './GroupsDiagram'; @@ -55,7 +56,7 @@ describe('', () => { }), }; - const { getByText } = await renderInTestApp( + await renderInTestApp( , @@ -65,6 +66,9 @@ describe('', () => { }, }, ); - expect(getByText('Group A', { selector: 'div' })).toBeInTheDocument(); + + expect( + screen.getByRole('link', { name: 'group:my-namespace/group-a' }), + ).toBeInTheDocument(); }); }); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index f7f0eab0bd..dd0705908a 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -15,7 +15,6 @@ */ import { - GroupEntity, parseEntityRef, RELATION_CHILD_OF, stringifyEntityRef, @@ -31,8 +30,8 @@ import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, - humanizeEntityRef, getEntityRelations, + EntityDisplayName, } from '@backstage/plugin-catalog-react'; import { makeStyles, Typography, useTheme } from '@material-ui/core'; import ZoomOutMap from '@material-ui/icons/ZoomOutMap'; @@ -140,7 +139,9 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { rx={theme.shape.borderRadius} className={classes.groupNode} /> - {props.node.name} + + <EntityDisplayName entityRef={props.node.id} hideIcon disableTooltip /> + ) {
- {props.node.name} +
@@ -210,9 +211,7 @@ export function GroupsDiagram(props: { nodes.push({ id: stringifyEntityRef(catalogItem), kind: catalogItem.kind, - name: - (catalogItem as GroupEntity).spec?.profile?.displayName || - humanizeEntityRef(catalogItem, { defaultKind: 'Group' }), + name: '', }); // Edge to parent diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 9b461fba7d..0e992c220d 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import React from 'react'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; @@ -73,48 +73,44 @@ describe('', () => { ]; catalogApi.getEntities.mockResolvedValue({ items: entities }); - const { getByText } = await renderInTestApp( + await renderInTestApp( , mountedRoutes, ); - await waitFor(() => { - expect( - getByText('my-namespace/group-a', { selector: 'div' }), - ).toBeInTheDocument(); - }); + expect( + screen.getByRole('link', { name: 'group:my-namespace/group-a' }), + ).toBeInTheDocument(); }); it('renders a custom title', async () => { catalogApi.getEntities.mockResolvedValue({ items: [] }); - const { getByText } = await renderInTestApp( + await renderInTestApp( , mountedRoutes, ); - await waitFor(() => - expect(getByText('Our Teams', { selector: 'h2' })).toBeInTheDocument(), - ); + expect( + screen.getByText('Our Teams', { selector: 'h2' }), + ).toBeInTheDocument(); }); it('renders a friendly error if it cannot collect domains', async () => { const catalogError = new Error('Network timeout'); catalogApi.getEntities.mockRejectedValueOnce(catalogError); - const { getAllByText } = await renderInTestApp( + await renderInTestApp( , mountedRoutes, ); - await waitFor(() => - expect(getAllByText(/Error: Network timeout/).length).not.toBe(0), - ); + expect(screen.getAllByText(/Error: Network timeout/).length).not.toBe(0); }); }); From 7f11009c50a30bcff2cc98be9fd97e5f4c86765f Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Wed, 10 Jan 2024 17:45:29 -0500 Subject: [PATCH 031/112] [feat] include stack trace display option in error page Signed-off-by: Jithen Shriyan --- .changeset/real-grapes-sing.md | 5 ++ .../src/layout/ErrorPage/ErrorPage.test.tsx | 11 +++ .../src/layout/ErrorPage/ErrorPage.tsx | 70 ++++++++++++------- 3 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 .changeset/real-grapes-sing.md diff --git a/.changeset/real-grapes-sing.md b/.changeset/real-grapes-sing.md new file mode 100644 index 0000000000..80fb9edab0 --- /dev/null +++ b/.changeset/real-grapes-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Included stack trace display option in ErrorPage component diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx index 2efdc5da33..64a209e4b8 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx @@ -99,4 +99,15 @@ describe('', () => { 'https://error-page-test-support-url.com', ); }); + + it('should render with stack trace if stack is provided', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect(getByText(/this is my stack trace!/i)).toBeInTheDocument(); + }); }); diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 4639e8de59..830abe6ec3 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -15,11 +15,13 @@ */ import Grid from '@material-ui/core/Grid'; +import Box from '@material-ui/core/Box'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import React from 'react'; import { useNavigate } from 'react-router-dom'; import { Link } from '../../components/Link'; +import { LogViewer } from '../../components'; import { useSupportConfig } from '../../hooks'; import { MicDrop } from './MicDrop'; @@ -28,6 +30,7 @@ interface IErrorPageProps { statusMessage: string; additionalInfo?: React.ReactNode; supportUrl?: string; + stack?: string; } /** @public */ @@ -35,12 +38,18 @@ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; const useStyles = makeStyles( theme => ({ - container: { + parent: { padding: theme.spacing(8), [theme.breakpoints.down('xs')]: { padding: theme.spacing(2), }, }, + container: { + marginBottom: theme.spacing(5), + [theme.breakpoints.down('xs')]: { + marginBottom: theme.spacing(4), + }, + }, title: { paddingBottom: theme.spacing(5), [theme.breakpoints.down('xs')]: { @@ -62,37 +71,44 @@ const useStyles = makeStyles( * */ export function ErrorPage(props: IErrorPageProps) { - const { status, statusMessage, additionalInfo, supportUrl } = props; + const { status, statusMessage, additionalInfo, supportUrl, stack } = props; const classes = useStyles(); const navigate = useNavigate(); const support = useSupportConfig(); return ( - - - - ERROR {status}: {statusMessage} - - - {additionalInfo} - - - Looks like someone dropped the mic! - - - navigate(-1)}> - Go back - - ... or please{' '} - contact support if you - think this is a bug. - + + + + + ERROR {status}: {statusMessage} + + + {additionalInfo} + + + Looks like someone dropped the mic! + + + navigate(-1)} + > + Go back + + ... or please{' '} + contact support if you + think this is a bug. + + + - - + {stack && } + ); } From 589006c37d75cbb7055aa4ebf3533709278f211f Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Thu, 11 Jan 2024 16:09:14 -0500 Subject: [PATCH 032/112] [feat] include stack trace in accordion by default and update existing error refs Signed-off-by: Jithen Shriyan --- .../app-defaults/src/defaults/components.tsx | 2 +- .../src/layout/ErrorPage/ErrorPage.tsx | 33 +++++++++++++++++-- .../components/PlaylistPage/PlaylistPage.tsx | 1 + .../components/ActionsPage/ActionsPage.tsx | 1 + 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/app-defaults/src/defaults/components.tsx b/packages/app-defaults/src/defaults/components.tsx index 1e2fddca07..9b58694f2b 100644 --- a/packages/app-defaults/src/defaults/components.tsx +++ b/packages/app-defaults/src/defaults/components.tsx @@ -49,7 +49,7 @@ const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { // TODO: figure out a nicer way to handle routing on the error page, when it can be done. return ( - + ); }; diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 830abe6ec3..823571d8b3 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -21,7 +21,7 @@ import Typography from '@material-ui/core/Typography'; import React from 'react'; import { useNavigate } from 'react-router-dom'; import { Link } from '../../components/Link'; -import { LogViewer } from '../../components'; +import { CopyTextButton, WarningPanel } from '../../components'; import { useSupportConfig } from '../../hooks'; import { MicDrop } from './MicDrop'; @@ -60,6 +60,17 @@ const useStyles = makeStyles( subtitle: { color: theme.palette.textSubtle, }, + text: { + fontFamily: 'monospace', + whiteSpace: 'pre', + overflowX: 'auto', + marginRight: theme.spacing(2), + }, + copyTextContainer: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-start', + }, }), { name: 'BackstageErrorPage' }, ); @@ -108,7 +119,25 @@ export function ErrorPage(props: IErrorPageProps) { - {stack && } + {stack && ( + + + + Stack Trace + + {stack} + + + + + + + + )} ); } diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistPage.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistPage.tsx index 4c9a0edbb7..b17fbd2fb7 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistPage.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistPage.tsx @@ -84,6 +84,7 @@ export const PlaylistPage = () => { ); } diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 9957d22456..9c0276fa28 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -142,6 +142,7 @@ export const ActionsPage = () => { ); } From e5117941859506885885b830d0bb8fcb9427bd85 Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Thu, 11 Jan 2024 23:10:00 -0500 Subject: [PATCH 033/112] [feat] reposition stack trace accordion below content grid item and adjust colors Signed-off-by: Jithen Shriyan --- .../src/layout/ErrorPage/ErrorPage.tsx | 110 +++++++++--------- .../src/layout/ErrorPage/MicDrop.tsx | 2 +- 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 823571d8b3..9bbf600aee 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -15,7 +15,6 @@ */ import Grid from '@material-ui/core/Grid'; -import Box from '@material-ui/core/Box'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import React from 'react'; @@ -38,18 +37,12 @@ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; const useStyles = makeStyles( theme => ({ - parent: { + container: { padding: theme.spacing(8), [theme.breakpoints.down('xs')]: { padding: theme.spacing(2), }, }, - container: { - marginBottom: theme.spacing(5), - [theme.breakpoints.down('xs')]: { - marginBottom: theme.spacing(4), - }, - }, title: { paddingBottom: theme.spacing(5), [theme.breakpoints.down('xs')]: { @@ -60,6 +53,13 @@ const useStyles = makeStyles( subtitle: { color: theme.palette.textSubtle, }, + goBackTitle: { + color: theme.palette.textSubtle, + marginBottom: theme.spacing(5), + [theme.breakpoints.down('xs')]: { + marginBottom: theme.spacing(4), + }, + }, text: { fontFamily: 'monospace', whiteSpace: 'pre', @@ -88,56 +88,52 @@ export function ErrorPage(props: IErrorPageProps) { const support = useSupportConfig(); return ( - - - - - ERROR {status}: {statusMessage} - - - {additionalInfo} - - - Looks like someone dropped the mic! - - - navigate(-1)} - > - Go back - - ... or please{' '} - contact support if you - think this is a bug. - - + + + + ERROR {status}: {statusMessage} + + + {additionalInfo} + + + Looks like someone dropped the mic! + + + navigate(-1)}> + Go back + + ... or please{' '} + contact support if you + think this is a bug. + + {stack && ( + + + + Stack Trace + + {stack} + + + + + + + + )} + + - {stack && ( - - - - Stack Trace - - {stack} - - - - - - - - )} - + ); } diff --git a/packages/core-components/src/layout/ErrorPage/MicDrop.tsx b/packages/core-components/src/layout/ErrorPage/MicDrop.tsx index d55c50bd29..a16de3f5e6 100644 --- a/packages/core-components/src/layout/ErrorPage/MicDrop.tsx +++ b/packages/core-components/src/layout/ErrorPage/MicDrop.tsx @@ -21,7 +21,7 @@ import MicDropSvgUrl from './mic-drop.svg'; const useStyles = makeStyles( theme => ({ micDrop: { - maxWidth: '60%', + maxWidth: '80%', bottom: theme.spacing(2), right: theme.spacing(2), [theme.breakpoints.down('xs')]: { From 969daf7d6b0a754430cd68875b7ecfb586c74bd1 Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Thu, 11 Jan 2024 23:49:44 -0500 Subject: [PATCH 034/112] [feat] add overflow scroll with fixed max accordion height Signed-off-by: Jithen Shriyan --- packages/core-components/src/layout/ErrorPage/ErrorPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 9bbf600aee..9b10067fa6 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -60,7 +60,9 @@ const useStyles = makeStyles( marginBottom: theme.spacing(4), }, }, - text: { + stackTraceText: { + maxHeight: '30vh', + overflow: 'scroll', fontFamily: 'monospace', whiteSpace: 'pre', overflowX: 'auto', @@ -117,7 +119,7 @@ export function ErrorPage(props: IErrorPageProps) { Stack Trace From 39579f03b74972f17b505b96f20b7c61ca06522d Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Mon, 22 Jan 2024 16:18:24 -0500 Subject: [PATCH 035/112] [update] use code snippet component, remove warning panel Signed-off-by: Jithen Shriyan --- .changeset/real-grapes-sing.md | 5 +- .../src/layout/ErrorPage/ErrorPage.tsx | 50 ++---------- .../src/layout/ErrorPage/MicDrop.tsx | 2 +- .../src/layout/ErrorPage/StackDetails.tsx | 77 +++++++++++++++++++ 4 files changed, 87 insertions(+), 47 deletions(-) create mode 100644 packages/core-components/src/layout/ErrorPage/StackDetails.tsx diff --git a/.changeset/real-grapes-sing.md b/.changeset/real-grapes-sing.md index 80fb9edab0..4656d57a2d 100644 --- a/.changeset/real-grapes-sing.md +++ b/.changeset/real-grapes-sing.md @@ -1,5 +1,8 @@ --- '@backstage/core-components': patch +'@backstage/app-defaults': minor +'@backstage/playlist': patch +'@backstage/scaffolder': minor --- -Included stack trace display option in ErrorPage component +Added stack trace display to `ErrorPage` and updated existing refs diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 9b10067fa6..131df3da3a 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -20,9 +20,9 @@ import Typography from '@material-ui/core/Typography'; import React from 'react'; import { useNavigate } from 'react-router-dom'; import { Link } from '../../components/Link'; -import { CopyTextButton, WarningPanel } from '../../components'; import { useSupportConfig } from '../../hooks'; import { MicDrop } from './MicDrop'; +import { StackDetails } from './StackDetails'; interface IErrorPageProps { status?: string; @@ -53,26 +53,6 @@ const useStyles = makeStyles( subtitle: { color: theme.palette.textSubtle, }, - goBackTitle: { - color: theme.palette.textSubtle, - marginBottom: theme.spacing(5), - [theme.breakpoints.down('xs')]: { - marginBottom: theme.spacing(4), - }, - }, - stackTraceText: { - maxHeight: '30vh', - overflow: 'scroll', - fontFamily: 'monospace', - whiteSpace: 'pre', - overflowX: 'auto', - marginRight: theme.spacing(2), - }, - copyTextContainer: { - display: 'flex', - justifyContent: 'flex-end', - alignItems: 'flex-start', - }, }), { name: 'BackstageErrorPage' }, ); @@ -91,7 +71,7 @@ export function ErrorPage(props: IErrorPageProps) { return ( - + Looks like someone dropped the mic! - + navigate(-1)}> Go back @@ -113,29 +93,9 @@ export function ErrorPage(props: IErrorPageProps) { contact support if you think this is a bug. - {stack && ( - - - - Stack Trace - - {stack} - - - - - - - - )} - - - + {stack && } + ); } diff --git a/packages/core-components/src/layout/ErrorPage/MicDrop.tsx b/packages/core-components/src/layout/ErrorPage/MicDrop.tsx index a16de3f5e6..d55c50bd29 100644 --- a/packages/core-components/src/layout/ErrorPage/MicDrop.tsx +++ b/packages/core-components/src/layout/ErrorPage/MicDrop.tsx @@ -21,7 +21,7 @@ import MicDropSvgUrl from './mic-drop.svg'; const useStyles = makeStyles( theme => ({ micDrop: { - maxWidth: '80%', + maxWidth: '60%', bottom: theme.spacing(2), right: theme.spacing(2), [theme.breakpoints.down('xs')]: { diff --git a/packages/core-components/src/layout/ErrorPage/StackDetails.tsx b/packages/core-components/src/layout/ErrorPage/StackDetails.tsx new file mode 100644 index 0000000000..b46a937485 --- /dev/null +++ b/packages/core-components/src/layout/ErrorPage/StackDetails.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Typography from '@material-ui/core/Typography'; +import React from 'react'; +import { useState } from 'react'; +import { Link } from '../../components/Link'; +import { CodeSnippet } from '../../components'; +import { makeStyles } from '@material-ui/core/styles'; + +interface IStackDetailsProps { + stack: string; +} + +const useStyles = makeStyles( + theme => ({ + title: { + paddingBottom: theme.spacing(5), + [theme.breakpoints.down('xs')]: { + paddingBottom: theme.spacing(4), + fontSize: theme.typography.h3.fontSize, + }, + }, + }), + { name: 'BackstageErrorPageStackDetails' }, +); + +/** + * Error page details with stack trace + * + * @public + * + */ +export function StackDetails(props: IStackDetailsProps) { + const { stack } = props; + const classes = useStyles(); + + const [detailsOpen, setDetailsOpen] = useState(false); + + if (!detailsOpen) { + return ( + + setDetailsOpen(true)}> + Show more details + + + ); + } + + return ( + <> + + setDetailsOpen(false)}> + Show less details + + + + + ); +} From 6fc1ed6b012b9c78f309ccf471ebb1c5427314c1 Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Mon, 22 Jan 2024 16:28:38 -0500 Subject: [PATCH 036/112] [update] update show details test case Signed-off-by: Jithen Shriyan --- .../core-components/src/layout/ErrorPage/ErrorPage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx index 64a209e4b8..d7036d0a5b 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx @@ -100,7 +100,7 @@ describe('', () => { ); }); - it('should render with stack trace if stack is provided', async () => { + it('should render show details if stack is provided', async () => { const { getByText } = await renderInTestApp( ', () => { stack="this is my stack trace!" />, ); - expect(getByText(/this is my stack trace!/i)).toBeInTheDocument(); + expect(getByText(/Show more details/i)).toBeInTheDocument(); }); }); From c70ab42a31f16b4f76e9923068d5fa16f8b7fc36 Mon Sep 17 00:00:00 2001 From: Jithen Shriyan Date: Fri, 26 Jan 2024 20:05:58 -0500 Subject: [PATCH 037/112] [fix] changeset package names Signed-off-by: Jithen Shriyan --- .changeset/real-grapes-sing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/real-grapes-sing.md b/.changeset/real-grapes-sing.md index 4656d57a2d..484fc7969a 100644 --- a/.changeset/real-grapes-sing.md +++ b/.changeset/real-grapes-sing.md @@ -1,8 +1,8 @@ --- '@backstage/core-components': patch '@backstage/app-defaults': minor -'@backstage/playlist': patch -'@backstage/scaffolder': minor +'@backstage/plugin-playlist': patch +'@backstage/plugin-scaffolder': minor --- Added stack trace display to `ErrorPage` and updated existing refs From 36625b6d5220dc8e00844cba99e5542c3634fe58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 2 Feb 2024 11:33:30 +0100 Subject: [PATCH 038/112] update the fetch recommendations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../help-im-behind-a-corporate-proxy.md | 115 +++++++++++------- .../adr013-use-node-fetch.md | 30 ++--- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md index 9ec3f1ca64..6ae6d10fac 100644 --- a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md +++ b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md @@ -1,66 +1,65 @@ # Running the backend behind a Corporate Proxy -Let's admit it, we've all been there. Sometimes you've gotta run stuff with no way out to the public internet, only the smallest of corporate proxy tunnels. +This article helps you get your backend installation up and running making calls through corporate proxies. -Whilst this isn't supported natively by Backstage, this might help you get your installation up and running making calls through the said proxy tunnel. +## Background -## backend +Let's admit it, we've all been there. Sometimes you have to run stuff with no way out to the public internet, except via the smallest of corporate proxy tunnels. It's most likely that you're going to run into these issues from the backend part of Backstage as that's the part that isn't helped by your browser or OS settings for the corporate proxy. -Unfortunately, `nodejs` does not respect `HTTP(S)_PROXY` environment variables by default, and the library that we use to provide `fetch` functionality `node-fetch` (provided by `cross-fetch`) does not also respect these environment variables. +Unfortunately, neither the Node.js native `fetch` nor the other frequently used library `node-fetch` (see [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013)) respect `HTTP(S)_PROXY` environment variables by default. As an additional complication, there is no single solution for configuring both native `fetch` and `node-fetch` at once, uniformly. -There are however some ways to get this to work without too much effort. It's most likely that you're going to run into these issues from the `backend` part of `backstage` as that's the part that isn't helped by your browser or OS's settings for the corporate proxy. +There are however some ways to get this to work without too much effort. -**Note:** You're gonna want to be in your backend working directory for these solutions as that's where the requests come from that don't go through this proxy. +## Installation -### Using `global-agent` +**Note:** You're going to want to be in your backend working directory for these solutions as that's where the requests come from that don't go through this proxy. -1. Install `global-agent` using `yarn add global-agent` -2. Go to the entry file for the backend (`src/index.ts`) -3. At the top of the file paste the following: +1. Install the required packages in your backend, by running the following command inside your backend directory (typically `packages/backend` under your repository root). -```ts -import 'global-agent/bootstrap'; -``` + ```bash + yarn add undici global-agent + ``` -4. Start the backend with the `global-agent` variables + `undici` exposes the settings for native `fetch`, and `global-agent` can set things up for `node-fetch`. -```sh -export GLOBAL_AGENT_HTTP_PROXY=$HTTP_PROXY -export GLOBAL_AGENT_NO_PROXY=$NO_PROXY -yarn start -``` +1. Go to the entry file for the backend (typically `packages/backend/src/index.ts`), and add the following at the top: -More information and more options for configuring `global-agent` including just using the default environment variables can be found here: https://github.com/gajus/global-agent + ```ts + import 'global-agent/bootstrap'; + import { setGlobalDispatcher, ProxyAgent } from 'undici'; -### Using `proxy-agent` + const proxyEnv = + process.env.GLOBAL_AGENT_HTTP_PROXY || + process.env.GLOBAL_AGENT_HTTPS_PROXY; -`proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request. + if (proxyEnv) { + const proxyUrl = new URL(proxyEnv); + setGlobalDispatcher( + new ProxyAgent({ + uri: proxyUrl.protocol + proxyUrl.host, + token: + proxyUrl.username && proxyUrl.password + ? `Basic ${Buffer.from( + `${proxyUrl.username}:${proxyUrl.password}`, + ).toString('base64')}` + : undefined, + }), + ); + } + ``` -1. Install `proxy-agent` using `yarn add proxy-agent` -2. Go to the entry file for the backend (`src/index.ts`) -3. At the top of the file paste the following: + The first import automatically bootstraps `global-agent`, which addresses `node-fetch` proxying. The lines of code below that peeks into the same environment variables as `global-agent` uses, and also leverages them to set up the `undici` package which affects native `fetch`. Does that seem weird? Yes, we think so too. But in the current state of the Node.js ecosystem, that's how it works. -```ts -import ProxyAgent from 'proxy-agent'; -import http from 'http'; -import https from 'https'; + This code is kept brief for illustrative purposes. You may want to adjust it slightly if you need support for [no-proxy excludes](https://gist.github.com/zicklag/1bb50db6c5138de347c224fda14286da) or only do proxying in local development etc. Also see [the `global-agent` docs](https://github.com/gajus/global-agent) for information about its configuration options. -/* - Something to note here, this might need different configuration depending on your own setup. - If you only have an http_proxy then you'll need to set that as both the http and https globalAgent instead. -*/ -if (process.env.HTTP_PROXY) { - http.globalAgent = new ProxyAgent(process.env.HTTP_PROXY); -} +1. Start the backend with the correct environment variables set. For example: -if (process.env.HTTPS_PROXY) { - https.globalAgent = new ProxyAgent(process.env.HTTPS_PROXY); -} -``` + ```sh + export GLOBAL_AGENT_HTTP_PROXY=http://username:password@proxy.example.net:8888 + yarn start + ``` -4. Start the backend with `yarn start` - -## config +## Configuration If your development environment is in the cloud (like with [AWS Cloud9](https://aws.amazon.com/cloud9/) or an instance of [Theia](https://theia-ide.org/)), you will need to update your configuration. @@ -82,3 +81,33 @@ backend: ``` The app port must proxy web socket connections in order to make hot reloading work. + +## Alternatives to `global-agent` + +The `proxy-agent` package can be used as an alternative to `global-agent` (do not install both!), and also ensures that the `node-fetch` library correctly respects proxy settings, but [does NOT work](https://github.com/TooTallNate/proxy-agents/issues/239) for modern `undici` based native Node.js `fetch`, so you'll have to also do the `undici` steps in the section above in addition to this. + +`proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request. + +1. Install `proxy-agent` using `yarn add proxy-agent` +2. Go to the entry file for the backend (`src/index.ts`) +3. At the top of the file paste the following: + + ```ts + import ProxyAgent from 'proxy-agent'; + import http from 'http'; + import https from 'https'; + + /* + Something to note here, this might need different configuration depending on your own setup. + If you only have an http_proxy then you'll need to set that as both the http and https globalAgent instead. + */ + if (process.env.HTTP_PROXY) { + http.globalAgent = new ProxyAgent(process.env.HTTP_PROXY); + } + + if (process.env.HTTPS_PROXY) { + https.globalAgent = new ProxyAgent(process.env.HTTPS_PROXY); + } + ``` + +4. Start the backend with `yarn start` diff --git a/docs/architecture-decisions/adr013-use-node-fetch.md b/docs/architecture-decisions/adr013-use-node-fetch.md index d802988ae4..e7d1494b0b 100644 --- a/docs/architecture-decisions/adr013-use-node-fetch.md +++ b/docs/architecture-decisions/adr013-use-node-fetch.md @@ -2,7 +2,7 @@ id: adrs-adr013 title: 'ADR013: Proper use of HTTP fetching libraries' # prettier-ignore -description: Architecture Decision Record (ADR) for the proper use of fetchApiRef, node-fetch, and cross-fetch for data fetching. +description: Architecture Decision Record (ADR) for the proper use of fetch libraries for data fetching. --- ## Context @@ -12,13 +12,13 @@ support burden of keeping said package up to date. ## Decision -Backend (node) packages should use the `node-fetch` package for HTTP data -fetching. Example: +Newly written backend (Node.js) packages should use the native `fetch` call for HTTP data +fetching. Additionally, in legacy code, using the `node-fetch` library continues to be allowed. Other third party fetching libraries (for example `axios`, `got` etc) are not allowed. Example: ```ts -import fetch from 'node-fetch'; import { ResponseError } from '@backstage/errors'; +// note that this is the global fetch, not imported from anywhere const response = await fetch('https://example.com/api/v1/users.json'); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -26,19 +26,19 @@ if (!response.ok) { const users = await response.json(); ``` -Frontend plugins and packages should prefer to use the -[`fetchApiRef`](https://backstage.io/docs/reference/core-plugin-api.fetchapiref). -It uses `cross-fetch` internally. Example: +Frontend plugins and packages should use the [`fetchApiRef`](https://backstage.io/docs/reference/core-plugin-api.fetchapiref). Example: ```ts -import { useApi } from '@backstage/core-plugin-api'; -const { fetch } = useApi(fetchApiRef); +import { useApi, fetchApiRef } from '@backstage/core-plugin-api'; -const response = await fetch('https://example.com/api/v1/users.json'); -if (!response.ok) { - throw await ResponseError.fromResponse(response); -} -const users = await response.json(); +const MyComponent = () => { + const { fetch } = useApi(fetchApiRef); + const response = await fetch('https://example.com/api/v1/users.json'); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + const users = await response.json(); + // ... ``` Isomorphic packages should have a dependency on the `cross-fetch` package for @@ -67,5 +67,5 @@ export class MyClient { ## Consequences We will gradually transition away from third party packages such as `axios`, -`got` and others. Once we have transitioned to `node-fetch` we will add lint +`got` and others. Once we have transitioned to native `fetch` and `node-fetch` we will add lint rules to enforce this decision. From 198affd2599f93c6491eeba1ef1dfe5f78b3384a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 4 Feb 2024 14:45:06 +0100 Subject: [PATCH 039/112] just keep the updated docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../help-im-behind-a-corporate-proxy.md | 2 +- .../adr013-use-node-fetch.md | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md index 6ae6d10fac..deef356d84 100644 --- a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md +++ b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md @@ -84,7 +84,7 @@ The app port must proxy web socket connections in order to make hot reloading wo ## Alternatives to `global-agent` -The `proxy-agent` package can be used as an alternative to `global-agent` (do not install both!), and also ensures that the `node-fetch` library correctly respects proxy settings, but [does NOT work](https://github.com/TooTallNate/proxy-agents/issues/239) for modern `undici` based native Node.js `fetch`, so you'll have to also do the `undici` steps in the section above in addition to this. +The `proxy-agent` package can be used as an alternative to `global-agent` (do not install both!), and also ensures that the `node-fetch` library correctly respects proxy settings, but [does NOT work](https://github.com/TooTallNate/proxy-agents/issues/239) for modern `undici` based native Node.js `fetch`, so you'll still have to also do the `undici` steps in the section above in addition to this. `proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request. diff --git a/docs/architecture-decisions/adr013-use-node-fetch.md b/docs/architecture-decisions/adr013-use-node-fetch.md index e7d1494b0b..d802988ae4 100644 --- a/docs/architecture-decisions/adr013-use-node-fetch.md +++ b/docs/architecture-decisions/adr013-use-node-fetch.md @@ -2,7 +2,7 @@ id: adrs-adr013 title: 'ADR013: Proper use of HTTP fetching libraries' # prettier-ignore -description: Architecture Decision Record (ADR) for the proper use of fetch libraries for data fetching. +description: Architecture Decision Record (ADR) for the proper use of fetchApiRef, node-fetch, and cross-fetch for data fetching. --- ## Context @@ -12,13 +12,13 @@ support burden of keeping said package up to date. ## Decision -Newly written backend (Node.js) packages should use the native `fetch` call for HTTP data -fetching. Additionally, in legacy code, using the `node-fetch` library continues to be allowed. Other third party fetching libraries (for example `axios`, `got` etc) are not allowed. Example: +Backend (node) packages should use the `node-fetch` package for HTTP data +fetching. Example: ```ts +import fetch from 'node-fetch'; import { ResponseError } from '@backstage/errors'; -// note that this is the global fetch, not imported from anywhere const response = await fetch('https://example.com/api/v1/users.json'); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -26,19 +26,19 @@ if (!response.ok) { const users = await response.json(); ``` -Frontend plugins and packages should use the [`fetchApiRef`](https://backstage.io/docs/reference/core-plugin-api.fetchapiref). Example: +Frontend plugins and packages should prefer to use the +[`fetchApiRef`](https://backstage.io/docs/reference/core-plugin-api.fetchapiref). +It uses `cross-fetch` internally. Example: ```ts -import { useApi, fetchApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; +const { fetch } = useApi(fetchApiRef); -const MyComponent = () => { - const { fetch } = useApi(fetchApiRef); - const response = await fetch('https://example.com/api/v1/users.json'); - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - const users = await response.json(); - // ... +const response = await fetch('https://example.com/api/v1/users.json'); +if (!response.ok) { + throw await ResponseError.fromResponse(response); +} +const users = await response.json(); ``` Isomorphic packages should have a dependency on the `cross-fetch` package for @@ -67,5 +67,5 @@ export class MyClient { ## Consequences We will gradually transition away from third party packages such as `axios`, -`got` and others. Once we have transitioned to native `fetch` and `node-fetch` we will add lint +`got` and others. Once we have transitioned to `node-fetch` we will add lint rules to enforce this decision. From c3a91f821390e1bfb3a2d1c0f2f37d0e3cac891b Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 26 Jan 2024 17:13:59 -0500 Subject: [PATCH 040/112] fix(auth): add support for additional alg values Signed-off-by: Aramis --- plugins/auth-backend/src/identity/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index d994f44d37..38e48f0a5d 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -34,7 +34,7 @@ export function createOidcRouter(options: Options) { jwks_uri: `${baseUrl}/.well-known/jwks.json`, response_types_supported: ['id_token'], subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256'], + id_token_signing_alg_values_supported: ['RS256', 'ES256'], scopes_supported: ['openid'], token_endpoint_auth_methods_supported: [], claims_supported: ['sub'], From f6dfb17b786fc5cedd9db14f249e82f56828b767 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 26 Jan 2024 17:21:27 -0500 Subject: [PATCH 041/112] add additional scopes Signed-off-by: Aramis --- plugins/auth-backend/src/identity/router.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index 38e48f0a5d..c9ae9e685a 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -34,7 +34,18 @@ export function createOidcRouter(options: Options) { jwks_uri: `${baseUrl}/.well-known/jwks.json`, response_types_supported: ['id_token'], subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256', 'ES256'], + id_token_signing_alg_values_supported: [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA', + ], scopes_supported: ['openid'], token_endpoint_auth_methods_supported: [], claims_supported: ['sub'], From 97f8724e91c69aabf3f36f28bc6866ca919c7b8b Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 26 Jan 2024 17:25:23 -0500 Subject: [PATCH 042/112] add changeset Signed-off-by: Aramis --- .changeset/dirty-cheetahs-shave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-cheetahs-shave.md diff --git a/.changeset/dirty-cheetahs-shave.md b/.changeset/dirty-cheetahs-shave.md new file mode 100644 index 0000000000..8f9a476a25 --- /dev/null +++ b/.changeset/dirty-cheetahs-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Support additional algorithms in the `/.well-known/openid-configuration` endpoint. From 4a89bc48b16d58d9a030c3e7151d86bffa1d785d Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 00:47:01 -0500 Subject: [PATCH 043/112] add support for service-to-service auth external callers Signed-off-by: Aramis --- plugins/auth-backend/src/identity/router.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index c9ae9e685a..422b7907c0 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -44,6 +44,9 @@ export function createOidcRouter(options: Options) { 'PS256', 'PS384', 'PS512', + 'HS256', + 'HS384', + 'HS512', 'EdDSA', ], scopes_supported: ['openid'], From ee3cbed39d889ba7661a9f1526611c260489cd6d Mon Sep 17 00:00:00 2001 From: Aramis Date: Tue, 30 Jan 2024 22:47:16 -0500 Subject: [PATCH 044/112] remove symmetric keys Signed-off-by: Aramis --- plugins/auth-backend/src/identity/router.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index 422b7907c0..c9ae9e685a 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -44,9 +44,6 @@ export function createOidcRouter(options: Options) { 'PS256', 'PS384', 'PS512', - 'HS256', - 'HS384', - 'HS512', 'EdDSA', ], scopes_supported: ['openid'], From 903ea055d58e4ea29ca5b442c60247d90c92562d Mon Sep 17 00:00:00 2001 From: Aramis Date: Tue, 30 Jan 2024 22:47:24 -0500 Subject: [PATCH 045/112] update changeset Signed-off-by: Aramis --- .changeset/dirty-cheetahs-shave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dirty-cheetahs-shave.md b/.changeset/dirty-cheetahs-shave.md index 8f9a476a25..1691adec2d 100644 --- a/.changeset/dirty-cheetahs-shave.md +++ b/.changeset/dirty-cheetahs-shave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend': minor +'@backstage/plugin-auth-backend': patch --- Support additional algorithms in the `/.well-known/openid-configuration` endpoint. From 43766556fb3e30b8dbd545285333c45a5ccf4915 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Sun, 4 Feb 2024 16:49:58 +0100 Subject: [PATCH 046/112] Added permissionIntegrationRouter for azure-sites-backend routes Signed-off-by: Deepankumar Loganathan --- .changeset/tall-frogs-clap.md | 5 +++++ plugins/azure-sites-backend/src/service/router.ts | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/tall-frogs-clap.md diff --git a/.changeset/tall-frogs-clap.md b/.changeset/tall-frogs-clap.md new file mode 100644 index 0000000000..1e87ca316c --- /dev/null +++ b/.changeset/tall-frogs-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites-backend': patch +--- + +Added `permissionIntegrationRouter` for azure-sites-backend routes diff --git a/plugins/azure-sites-backend/src/service/router.ts b/plugins/azure-sites-backend/src/service/router.ts index 9ed198cc20..bd5d20c05e 100644 --- a/plugins/azure-sites-backend/src/service/router.ts +++ b/plugins/azure-sites-backend/src/service/router.ts @@ -27,8 +27,10 @@ import { } from '@backstage/plugin-permission-common'; import { azureSitesActionPermission, + azureSitesPermissions, AZURE_WEB_SITE_NAME_ANNOTATION, } from '@backstage/plugin-azure-sites-common'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import { CatalogApi } from '@backstage/catalog-client'; import { AzureSitesApi } from '../api'; @@ -47,8 +49,13 @@ export async function createRouter( ): Promise { const { logger, azureSitesApi, permissions, catalogApi } = options; + const permissionIntegrationRouter = createPermissionIntegrationRouter({ + permissions: azureSitesPermissions, + }); + const router = Router(); router.use(express.json()); + router.use(permissionIntegrationRouter); router.get('/health', (_, response) => { logger.info('PONG!'); From 6178a67e838500a49f1f591c451676d868b3be33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:02:06 +0000 Subject: [PATCH 047/112] chore(deps): update dependency husky to v9 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6deae7611e..4a90b25e7b 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "eslint-plugin-react": "^7.28.0", "eslint-plugin-testing-library": "^6.0.0", "fs-extra": "10.1.0", - "husky": "^8.0.0", + "husky": "^9.0.0", "lint-staged": "^15.0.0", "minimist": "^1.2.5", "node-gyp": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index 5f84cbbc51..868817d2e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29839,12 +29839,12 @@ __metadata: languageName: node linkType: hard -"husky@npm:^8.0.0": - version: 8.0.3 - resolution: "husky@npm:8.0.3" +"husky@npm:^9.0.0": + version: 9.0.10 + resolution: "husky@npm:9.0.10" bin: - husky: lib/bin.js - checksum: 837bc7e4413e58c1f2946d38fb050f5d7324c6f16b0fd66411ffce5703b294bd21429e8ba58711cd331951ee86ed529c5be4f76805959ff668a337dbfa82a1b0 + husky: bin.mjs + checksum: 55f4b6db6706ff0bc181607d6a64f55310cbb18b4d7db2a5b85608c87a80abafc14ac3a8c4d0b44d6272f4a62d4c2d3d5491d6a13e2cc7ac4895309e5e5f0ec2 languageName: node linkType: hard @@ -40403,7 +40403,7 @@ __metadata: eslint-plugin-react: ^7.28.0 eslint-plugin-testing-library: ^6.0.0 fs-extra: 10.1.0 - husky: ^8.0.0 + husky: ^9.0.0 lint-staged: ^15.0.0 minimist: ^1.2.5 node-gyp: ^10.0.0 From ed1dbb456b3671deb1c9da0358e34781846ca969 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:02:12 +0000 Subject: [PATCH 048/112] chore(deps): update microsoft/setup-msbuild action to v2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/verify_e2e-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 1bddd792b7..fdedf56fb7 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -61,7 +61,7 @@ jobs: python-version: '3.10' - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 - name: Setup gyp env run: | From b3dfcb6da607367814c784dbdd3d9540c9e3825b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:07:28 +0000 Subject: [PATCH 049/112] chore(deps): update peter-evans/repository-dispatch action to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy_packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index ea390a3903..7e4f297933 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -196,7 +196,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Dispatch repository event - uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2.1.2 + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} event-type: release-published From 9aac2b0d36bdb8095ea747fe5e5490cfea1c9f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 4 Feb 2024 20:10:44 +0100 Subject: [PATCH 050/112] move cwd as the first argument to yarn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/hot-horses-matter.md | 108 ++++++++++++++++++ .../tutorials/authenticate-api-requests.md | 4 +- .../building-backends/08-migrating.md | 10 +- docs/features/kubernetes/installation.md | 4 +- docs/features/search/getting-started.md | 4 +- docs/features/techdocs/addons.md | 2 +- docs/features/techdocs/getting-started.md | 4 +- docs/getting-started/configuration.md | 2 +- .../configure-app-with-plugins.md | 2 +- docs/getting-started/homepage.md | 2 +- docs/integrations/aws-s3/discovery.md | 2 +- docs/integrations/azure/discovery.md | 2 +- docs/integrations/azure/org.md | 2 +- docs/integrations/bitbucketCloud/discovery.md | 2 +- .../integrations/bitbucketServer/discovery.md | 2 +- docs/integrations/gerrit/discovery.md | 2 +- docs/integrations/github/discovery.md | 4 +- docs/integrations/github/org.md | 4 +- docs/integrations/gitlab/discovery.md | 2 +- docs/integrations/gitlab/org.md | 2 +- docs/integrations/ldap/org.md | 2 +- docs/permissions/getting-started.md | 2 +- docs/permissions/plugin-authors/01-setup.md | 4 +- docs/plugins/backend-plugin.md | 2 +- .../tutorials/configuring-plugin-databases.md | 4 +- docs/tutorials/switching-sqlite-postgres.md | 2 +- ...-04-30-how-to-quickly-set-up-backstage.mdx | 2 +- packages/app-defaults/README.md | 2 +- packages/backend-app-api/README.md | 2 +- packages/backend-common/README.md | 2 +- packages/backend-defaults/README.md | 2 +- packages/backend-dev-utils/README.md | 2 +- packages/backend-plugin-api/README.md | 2 +- packages/backend-tasks/README.md | 2 +- packages/core-app-api/README.md | 2 +- plugins/adr-backend/README.md | 2 +- plugins/airbrake/README.md | 4 +- plugins/allure/README.md | 2 +- plugins/analytics-module-ga/README.md | 2 +- plugins/analytics-module-ga4/README.md | 2 +- .../README.md | 2 +- .../api-docs-module-protoc-gen-doc/README.md | 2 +- plugins/api-docs/README.md | 2 +- plugins/app-backend/README.md | 2 +- plugins/azure-devops-backend/README.md | 2 +- plugins/azure-devops/README.md | 8 +- plugins/azure-sites-backend/README.md | 2 +- plugins/azure-sites/README.md | 2 +- plugins/badges-backend/README.md | 2 +- plugins/badges/README.md | 2 +- plugins/bazaar-backend/README.md | 2 +- plugins/bazaar/README.md | 2 +- plugins/bitrise/README.md | 2 +- .../README.md | 2 +- .../catalog-backend-module-msgraph/README.md | 2 +- .../catalog-backend-module-openapi/README.md | 2 +- .../catalog-backend-module-puppetdb/README.md | 2 +- .../README.md | 2 +- plugins/catalog-backend/README.md | 2 +- plugins/catalog-graph/README.md | 2 +- plugins/catalog-import/README.md | 2 +- .../catalog-unprocessed-entities/README.md | 2 +- plugins/catalog/README.md | 2 +- plugins/circleci/README.md | 2 +- plugins/code-climate/README.md | 2 +- plugins/code-coverage-backend/README.md | 2 +- plugins/code-coverage/README.md | 2 +- plugins/codescene/README.md | 2 +- plugins/cost-insights/README.md | 6 +- .../contrib/aws-cost-explorer-api.md | 2 +- plugins/devtools-backend/README.md | 2 +- plugins/devtools/README.md | 10 +- plugins/dynatrace/README.md | 2 +- plugins/entity-feedback-backend/README.md | 2 +- plugins/entity-validation/README.md | 2 +- .../events-backend-module-aws-sqs/README.md | 2 +- plugins/events-backend-module-azure/README.md | 2 +- .../README.md | 2 +- .../events-backend-module-gerrit/README.md | 2 +- .../events-backend-module-github/README.md | 2 +- .../events-backend-module-gitlab/README.md | 2 +- plugins/events-backend/README.md | 2 +- plugins/explore-backend/README.md | 6 +- plugins/firehydrant/README.md | 2 +- plugins/fossa/README.md | 2 +- plugins/github-actions/README.md | 2 +- plugins/github-deployments/README.md | 2 +- plugins/graphiql/README.md | 2 +- plugins/graphql-voyager/README.md | 2 +- plugins/home/README.md | 2 +- plugins/ilert/README.md | 2 +- plugins/jenkins-backend/README.md | 2 +- plugins/jenkins/README.md | 2 +- plugins/lighthouse-backend/README.md | 2 +- plugins/lighthouse/README.md | 2 +- .../lighthouse/src/components/Intro/index.tsx | 2 +- plugins/linguist-backend/README.md | 2 +- plugins/linguist/README.md | 2 +- plugins/microsoft-calendar/README.md | 2 +- plugins/newrelic/README.md | 2 +- plugins/nomad-backend/README.md | 2 +- plugins/nomad/README.md | 2 +- plugins/octopus-deploy/README.md | 2 +- plugins/opencost/README.md | 2 +- plugins/periskop/README.md | 4 +- plugins/playlist-backend/README.md | 2 +- plugins/rollbar-backend/README.md | 2 +- plugins/rollbar/README.md | 2 +- .../README.md | 2 +- .../README.md | 2 +- .../README.md | 2 +- .../scaffolder-backend-module-rails/README.md | 2 +- .../README.md | 2 +- .../README.md | 2 +- plugins/scaffolder-backend/README.md | 2 +- plugins/scaffolder/README.md | 2 +- .../search-backend-module-catalog/README.md | 2 +- .../search-backend-module-explore/README.md | 2 +- .../README.md | 2 +- .../search-backend-module-techdocs/README.md | 2 +- plugins/sentry/README.md | 2 +- plugins/shortcuts/README.md | 2 +- plugins/sonarqube-backend/README.md | 2 +- plugins/sonarqube/README.md | 2 +- plugins/splunk-on-call/README.md | 2 +- .../README.md | 2 +- plugins/tech-insights-backend/README.md | 4 +- plugins/tech-insights/README.md | 2 +- plugins/tech-radar/README.md | 2 +- plugins/techdocs-react/README.md | 2 +- plugins/vault-backend/README.md | 2 +- plugins/vault/README.md | 2 +- plugins/xcmetrics/README.md | 4 +- scripts/verify-lockfile-duplicates.js | 4 +- 134 files changed, 269 insertions(+), 161 deletions(-) create mode 100644 .changeset/hot-horses-matter.md diff --git a/.changeset/hot-horses-matter.md b/.changeset/hot-horses-matter.md new file mode 100644 index 0000000000..1b544c0854 --- /dev/null +++ b/.changeset/hot-horses-matter.md @@ -0,0 +1,108 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-events-backend-module-bitbucket-cloud': patch +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +'@backstage/plugin-catalog-backend-module-unprocessed': patch +'@backstage/plugin-analytics-module-newrelic-browser': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-openapi': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-search-backend-module-catalog': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-events-backend-module-gerrit': patch +'@backstage/plugin-events-backend-module-github': patch +'@backstage/plugin-events-backend-module-gitlab': patch +'@backstage/plugin-events-backend-module-azure': patch +'@backstage/plugin-entity-feedback-backend': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-analytics-module-ga4': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/backend-plugin-api': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-azure-sites-backend': patch +'@backstage/backend-dev-utils': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-lighthouse-backend': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-defaults': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-sonarqube-backend': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-explore-backend': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/backend-tasks': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-techdocs-react': patch +'@backstage/app-defaults': patch +'@backstage/core-app-api': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-nomad-backend': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-vault-backend': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-adr-backend': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-opencost': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-home': patch +--- + +Use `--cwd` as the first `yarn` argument diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 4c44c8ae65..8b71151ee9 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -88,7 +88,7 @@ Install cookie-parser: ```bash # From your Backstage root directory -yarn add --cwd packages/backend cookie-parser +yarn --cwd packages/backend add cookie-parser ``` Update routes in `packages/backend/src/index.ts`: @@ -223,7 +223,7 @@ Install cookie-parser: ```bash # From your Backstage root directory -yarn add --cwd packages/backend cookie-parser @types/cookie-parser +yarn --cwd packages/backend add cookie-parser @types/cookie-parser ``` Create a custom configured `rootHttpRouterService` in `packages/backend/src/customRootHttpRouterService.ts`: diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index fc1a6f1859..9eada30ef6 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -83,7 +83,7 @@ following command: ```bash # from the repository root -yarn add --cwd packages/backend @backstage/backend-defaults @backstage/backend-plugin-api +yarn --cwd packages/backend add @backstage/backend-defaults @backstage/backend-plugin-api ``` You should now be able to start this up with the familiar `yarn workspace @@ -616,7 +616,7 @@ if you didn't already have one. ```bash # from the repository root -yarn add --cwd packages/backend @backstage/plugin-catalog-node +yarn --cwd packages/backend add @backstage/plugin-catalog-node ``` Here we've placed the module directly in the backend index file just to get @@ -681,7 +681,7 @@ if you didn't already have one. ```bash # from the repository root -yarn add --cwd packages/backend @backstage/plugin-events-node +yarn --cwd packages/backend add @backstage/plugin-events-node ``` Here we've placed the module directly in the backend index file just to get @@ -715,7 +715,7 @@ And of course you'll need to install those separately as well. ```bash # from the repository root -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-github +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-github ``` You can find a list of the available modules under the [plugins directory](https://github.com/backstage/backstage/tree/master/plugins) in the monorepo. @@ -766,7 +766,7 @@ if you didn't already have one. ```bash # from the repository root -yarn add --cwd packages/backend @backstage/plugin-scaffolder-node +yarn --cwd packages/backend add @backstage/plugin-scaffolder-node ``` Here we've placed the module directly in the backend index file just to get diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 5111b5ad2e..bdd14098ff 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -17,7 +17,7 @@ application. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-kubernetes +yarn --cwd packages/app add @backstage/plugin-kubernetes ``` Once the package has been installed, you need to import the plugin in your app @@ -55,7 +55,7 @@ Navigate to `packages/backend` of your Backstage app, and install the ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-kubernetes-backend +yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend ``` Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 1787189e91..7989b60ae2 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -18,7 +18,7 @@ If you haven't setup Backstage already, start ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-search @backstage/plugin-search-react +yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react ``` Create a new `packages/app/src/components/search/SearchPage.tsx` file in your @@ -135,7 +135,7 @@ Add the following plugins into your backend app: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend @backstage/plugin-search-backend-node +yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-node ``` Create a `packages/backend/src/plugins/search.ts` file containing the following diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 2e17891917..471e62a57f 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -54,7 +54,7 @@ Addons are rendered in the order in which they are registered. ## Installing and using Addons -To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn add --cwd packages/app @backstage/plugin-techdocs-module-addons-contrib` +To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` Addons can be installed and configured in much the same way as extensions for other Backstage plugins: by adding them underneath an extension registry diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index f798384697..eaa6c2de72 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -23,7 +23,7 @@ Navigate to your new Backstage application directory. And then to your ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-techdocs +yarn --cwd packages/app add @backstage/plugin-techdocs ``` Once the package has been installed, you need to import the plugin in your app. @@ -108,7 +108,7 @@ Navigate to `packages/backend` of your Backstage app, and install the ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-techdocs-backend +yarn --cwd packages/backend add @backstage/plugin-techdocs-backend ``` Create a file called `techdocs.ts` inside `packages/backend/src/plugins/` and diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 5d797ecb4b..0c813b1aee 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -67,7 +67,7 @@ App. Use the following commands to start the PostgreSQL client installation: ```bash # From your Backstage root directory -yarn add --cwd packages/backend pg +yarn --cwd packages/backend add pg ``` Use your favorite editor to open `app-config.yaml` and add your PostgreSQL diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 5611117636..724746ca16 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -23,7 +23,7 @@ to an entity in the software catalog. ```bash # From your Backstage root directory - yarn add --cwd packages/app @circleci/backstage-plugin + yarn --cwd packages/app add @circleci/backstage-plugin ``` Note the plugin is added to the `app` package, rather than the root diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 04dfb13d6a..18f23f6607 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -30,7 +30,7 @@ Now, let's get started by installing the home plugin and creating a simple homep ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-home +yarn --cwd packages/app add @backstage/plugin-home ``` #### 2. Create a new HomePage component diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index ceb780141f..d664466a16 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -64,7 +64,7 @@ the AWS catalog plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-aws +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws ``` Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 3b25a9ac75..742fbb6076 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -98,7 +98,7 @@ the Azure catalog plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-azure +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 061baf7301..38c2e7618a 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -16,7 +16,7 @@ The package is not installed by default, therefore you have to add `@backstage/p ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-msgraph +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` Next add the basic configuration to `app-config.yaml` diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 53315a4c48..a7477a2a61 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -21,7 +21,7 @@ package. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbucket-cloud +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` ### Installation without Events Support diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index ad59006ff1..f37732b600 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -21,7 +21,7 @@ package. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbucket-server +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` And then add the entity provider to your catalog builder: diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index ee843d2454..e2922c1f76 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -18,7 +18,7 @@ the Gerrit provider plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gerrit +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit ``` Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 3c3d90d6e9..461fab8af8 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -23,7 +23,7 @@ package. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` And then add the entity provider to your catalog builder: @@ -250,7 +250,7 @@ package, plus `@backstage/integration` for the basic credentials management: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/integration @backstage/plugin-catalog-backend-module-github +yarn --cwd packages/backend add @backstage/integration @backstage/plugin-catalog-backend-module-github ``` And then add the processors to your catalog builder: diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 1404b20559..263ceda6c6 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -27,7 +27,7 @@ to `@backstage/plugin-catalog-backend-module-github` to your backend package. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` > Note: When configuring to use a Provider instead of a Processor you do not @@ -308,7 +308,7 @@ install and register it in the catalog plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` ```typescript title="packages/backend/src/plugins/catalog.ts" diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 235d93ab03..d41f4f8de5 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -39,7 +39,7 @@ the gitlab catalog plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab ``` Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index ca40cefa9e..f2a483c9d1 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -15,7 +15,7 @@ As this provider is not one of the default providers, you will first need to ins ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab ``` Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 7306ab4105..9bc2667aa7 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -26,7 +26,7 @@ to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-ldap +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` > Note: When configuring to use a Provider instead of a Processor you do not diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 01a268f7ef..e241f0e38e 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -48,7 +48,7 @@ The permissions framework uses a new `permission-backend` plugin to accept autho ```bash # From your Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-permission-backend + yarn --cwd packages/backend add @backstage/plugin-permission-backend ``` 2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything. diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 8134fc0c39..a674d82e25 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -41,8 +41,8 @@ The source code is available here: ```sh # From your Backstage root directory - yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common - yarn add --cwd packages/app @internal/plugin-todo-list + yarn --cwd packages/backend add @internal/plugin-todo-list-backend @internal/plugin-todo-list-common + yarn --cwd packages/app add @internal/plugin-todo-list ``` 3. Include the backend and frontend plugin in your application: diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index d2e93bb08c..22d4bae371 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -69,7 +69,7 @@ to your backend. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @internal/plugin-carmen-backend@^0.1.0 # Change this to match the plugin's package.json +yarn --cwd packages/backend add @internal/plugin-carmen-backend@^0.1.0 # Change this to match the plugin's package.json ``` Create a new file named `packages/backend/src/plugins/carmen.ts`, and add the diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 8cd5ce99a9..528d2c6325 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -39,10 +39,10 @@ both of them. ```bash # From your Backstage root directory # install pg if you need PostgreSQL -yarn add --cwd packages/backend pg +yarn --cwd packages/backend add pg # install SQLite 3 if you intend to set it as the client -yarn add --cwd packages/backend better-sqlite3 +yarn --cwd packages/backend add better-sqlite3 ``` From an operational perspective, you only need to install drivers for clients diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index 87afabda60..2ef481dbf3 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -21,7 +21,7 @@ First, add PostgreSQL to your `backend` package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend pg +yarn --cwd packages/backend add pg ``` ## Add PostgreSQL configuration diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx index 4884762560..b59f943245 100644 --- a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx @@ -102,7 +102,7 @@ Install in your app’s package folder (`/packages/app`) with: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin- +yarn --cwd packages/app add @backstage/plugin- ``` After that, you inject the plugin into the application where you want it to be exposed. Please read the documentation for the specific plugin you are installing for more information. diff --git a/packages/app-defaults/README.md b/packages/app-defaults/README.md index cfdaf36acb..ebaaacafe3 100644 --- a/packages/app-defaults/README.md +++ b/packages/app-defaults/README.md @@ -8,7 +8,7 @@ Install the package via Yarn: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/app-defaults +yarn --cwd packages/app add @backstage/app-defaults ``` ## Documentation diff --git a/packages/backend-app-api/README.md b/packages/backend-app-api/README.md index 3fd6170b4f..7ac3767ece 100644 --- a/packages/backend-app-api/README.md +++ b/packages/backend-app-api/README.md @@ -8,7 +8,7 @@ Add the library to your backend app package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/backend-app-api +yarn --cwd packages/backend add @backstage/backend-app-api ``` ## Documentation diff --git a/packages/backend-common/README.md b/packages/backend-common/README.md index 578fbc0830..71a15bfb5a 100644 --- a/packages/backend-common/README.md +++ b/packages/backend-common/README.md @@ -9,7 +9,7 @@ Add the library to your backend package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/backend-common +yarn --cwd packages/backend add @backstage/backend-common ``` then make use of the handlers and logger as necessary: diff --git a/packages/backend-defaults/README.md b/packages/backend-defaults/README.md index 01e42d42c4..a1bf881929 100644 --- a/packages/backend-defaults/README.md +++ b/packages/backend-defaults/README.md @@ -8,7 +8,7 @@ Add the library to your backend app package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/backend-defaults +yarn --cwd packages/backend add @backstage/backend-defaults ``` ## Documentation diff --git a/packages/backend-dev-utils/README.md b/packages/backend-dev-utils/README.md index 744a9d2ca0..f3666ea33e 100644 --- a/packages/backend-dev-utils/README.md +++ b/packages/backend-dev-utils/README.md @@ -8,7 +8,7 @@ Add the library to your backend plugin or module package: ```bash # From your Backstage root directory -yarn add --cwd plugins/-backend @backstage/backend-dev-utils +yarn --cwd plugins/-backend add @backstage/backend-dev-utils ``` ## Documentation diff --git a/packages/backend-plugin-api/README.md b/packages/backend-plugin-api/README.md index b827ecc50c..1e5e4cf3b1 100644 --- a/packages/backend-plugin-api/README.md +++ b/packages/backend-plugin-api/README.md @@ -8,7 +8,7 @@ Add the library to your backend plugin or module package: ```bash # From your Backstage root directory -yarn add --cwd plugins/-backend @backstage/backend-plugin-api +yarn --cwd plugins/-backend add @backstage/backend-plugin-api ``` ## Documentation diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index afaca06e55..57cd63b0e0 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -8,7 +8,7 @@ Add the library to your backend package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/backend-tasks +yarn --cwd packages/backend add @backstage/backend-tasks ``` then make use of its facilities as necessary: diff --git a/packages/core-app-api/README.md b/packages/core-app-api/README.md index ffdc0ef94a..94dd618625 100644 --- a/packages/core-app-api/README.md +++ b/packages/core-app-api/README.md @@ -7,7 +7,7 @@ This package provides the core API used by Backstage apps. Install the package via Yarn: ```bash -yarn add --cwd packages/app @backstage/core-app-api +yarn --cwd packages/app add @backstage/core-app-api ``` ## Documentation diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index 6ce2f13ebf..ebf99b4ee1 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -20,7 +20,7 @@ Here's how to get the backend up and running: ```sh # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-adr-backend +yarn --cwd packages/backend add @backstage/plugin-adr-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/adr.ts`, and add the diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index 573910a1cf..603311f92c 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -8,14 +8,14 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-airbrake + yarn --cwd packages/app add @backstage/plugin-airbrake ``` 2. Install the Backend plugin: ```bash # From your Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-airbrake-backend + yarn --cwd packages/backend add @backstage/plugin-airbrake-backend ``` 3. Add the `EntityAirbrakeContent` and `isAirbrakeAvailable` to `packages/app/src/components/catalog/EntityPage.tsx` for all the entity pages you want Airbrake to be in: diff --git a/plugins/allure/README.md b/plugins/allure/README.md index 3a1137fc39..22348385a3 100644 --- a/plugins/allure/README.md +++ b/plugins/allure/README.md @@ -6,7 +6,7 @@ Welcome to the Backstage Allure plugin. This plugin add an entity service page t ```shell # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-allure +yarn --cwd packages/app add @backstage/plugin-allure ``` ## Configure diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index 6fdfc91306..dfea532403 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -12,7 +12,7 @@ This plugin contains no other functionality. ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-analytics-module-ga +yarn --cwd packages/app add @backstage/plugin-analytics-module-ga ``` 2. Wire up the API implementation to your App: diff --git a/plugins/analytics-module-ga4/README.md b/plugins/analytics-module-ga4/README.md index 2d8c930e30..dad9b88a27 100644 --- a/plugins/analytics-module-ga4/README.md +++ b/plugins/analytics-module-ga4/README.md @@ -12,7 +12,7 @@ This plugin contains no other functionality. ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-analytics-module-ga4 +yarn --cwd packages/app add @backstage/plugin-analytics-module-ga4 ``` 2. Wire up the API implementation to your App: diff --git a/plugins/analytics-module-newrelic-browser/README.md b/plugins/analytics-module-newrelic-browser/README.md index 2fe3956d2d..d607a4f446 100644 --- a/plugins/analytics-module-newrelic-browser/README.md +++ b/plugins/analytics-module-newrelic-browser/README.md @@ -10,7 +10,7 @@ This plugin contains no other functionality. ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-analytics-module-newrelic-browser +yarn --cwd packages/app add @backstage/plugin-analytics-module-newrelic-browser ``` 2. Wire up the API implementation to your App: diff --git a/plugins/api-docs-module-protoc-gen-doc/README.md b/plugins/api-docs-module-protoc-gen-doc/README.md index a921fafc0b..5bf75766e0 100644 --- a/plugins/api-docs-module-protoc-gen-doc/README.md +++ b/plugins/api-docs-module-protoc-gen-doc/README.md @@ -8,7 +8,7 @@ This package contains ApiDefinitionWidgets for the following projects: ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-api-docs-module-protoc-gen-doc +yarn --cwd packages/app add @backstage/plugin-api-docs-module-protoc-gen-doc ``` ## Add the GrpcDocsApiWidget to your apis diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index 46d9df4628..9b8eb1669e 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -29,7 +29,7 @@ To link that a component provides or consumes an API, see the [`providesApis`](h ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-api-docs +yarn --cwd packages/app add @backstage/plugin-api-docs ``` 2. Add the `ApiExplorerPage` extension to the app: diff --git a/plugins/app-backend/README.md b/plugins/app-backend/README.md index 590c6e8eb7..0f0c235ab5 100644 --- a/plugins/app-backend/README.md +++ b/plugins/app-backend/README.md @@ -8,7 +8,7 @@ Add both this package and your local frontend app package as dependencies to you ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-app-backend app +yarn --cwd packages/backend add @backstage/plugin-app-backend app ``` By adding the app package as a dependency we ensure that it is built as part of the backend, and that it can be resolved at runtime. diff --git a/plugins/azure-devops-backend/README.md b/plugins/azure-devops-backend/README.md index 0066e43856..f69951f70d 100644 --- a/plugins/azure-devops-backend/README.md +++ b/plugins/azure-devops-backend/README.md @@ -37,7 +37,7 @@ Here's how to get the backend up and running: ```sh # From your Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-azure-devops-backend + yarn --cwd packages/backend add @backstage/plugin-azure-devops-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 7119cb0980..f897588987 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -146,7 +146,7 @@ To get the Azure Pipelines component working you'll need to do the following two ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-azure-devops + yarn --cwd packages/app add @backstage/plugin-azure-devops ``` 2. Second we need to add the `EntityAzurePipelinesContent` extension to the entity page in your app. How to do this will depend on which annotation you are using in your entities: @@ -204,7 +204,7 @@ To get the Azure Repos component working you'll need to do the following two ste ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-azure-devops + yarn --cwd packages/app add @backstage/plugin-azure-devops ``` 2. Second we need to add the `EntityAzurePullRequestsContent` extension to the entity page in your app: @@ -241,7 +241,7 @@ To get the Git Tags component working you'll need to do the following two steps: ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-azure-devops + yarn --cwd packages/app add @backstage/plugin-azure-devops ``` 2. Second we need to add the `EntityAzureGitTagsContent` extension to the entity page in your app: @@ -277,7 +277,7 @@ To get the README component working you'll need to do the following two steps: ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-azure-devops + yarn --cwd packages/app add @backstage/plugin-azure-devops ``` 2. Second we need to add the `EntityAzureReadmeCard` extension to the entity page in your app: diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 5017be8a8f..892cbe4024 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -37,7 +37,7 @@ Here's how to get the backend plugin up and running: ```sh # From the Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-azure-sites-backend + yarn --cwd packages/backend add @backstage/plugin-azure-sites-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/azure-sites.ts`, and add the following to it: diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md index a19b314b77..540daeb7df 100644 --- a/plugins/azure-sites/README.md +++ b/plugins/azure-sites/README.md @@ -48,7 +48,7 @@ azure.com/microsoft-web-sites: func-testapp ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-azure-sites +yarn --cwd packages/app add @backstage/plugin-azure-sites ``` 2. Add widget component to your Backstage instance: diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index 22c3311ff1..1737482080 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -15,7 +15,7 @@ Install the `@backstage/plugin-badges-backend` package in your backend package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-badges-backend +yarn --cwd packages/backend add @backstage/plugin-badges-backend ``` Add the plugin using the following default setup for diff --git a/plugins/badges/README.md b/plugins/badges/README.md index 4b8ff38d47..06c0e3e413 100644 --- a/plugins/badges/README.md +++ b/plugins/badges/README.md @@ -80,7 +80,7 @@ Install the `@backstage/plugin-badges` package in your frontend app package: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-badges +yarn --cwd packages/app add @backstage/plugin-badges ``` ### Register plugin diff --git a/plugins/bazaar-backend/README.md b/plugins/bazaar-backend/README.md index 266841b26b..eec1574b7a 100644 --- a/plugins/bazaar-backend/README.md +++ b/plugins/bazaar-backend/README.md @@ -8,7 +8,7 @@ Welcome to the Bazaar backend plugin! ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-bazaar-backend +yarn --cwd packages/backend add @backstage/plugin-bazaar-backend ``` ## Adding the plugin to your `packages/backend` diff --git a/plugins/bazaar/README.md b/plugins/bazaar/README.md index 941979d4d3..c340baec3e 100644 --- a/plugins/bazaar/README.md +++ b/plugins/bazaar/README.md @@ -22,7 +22,7 @@ First install the plugin into your app: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-bazaar +yarn --cwd packages/app add @backstage/plugin-bazaar ``` Modify your app routes in `packages/app/src/App.tsx` to include the `Bazaar` component exported from the plugin, for example: diff --git a/plugins/bitrise/README.md b/plugins/bitrise/README.md index e5d5c2122c..80c3dadac9 100644 --- a/plugins/bitrise/README.md +++ b/plugins/bitrise/README.md @@ -9,7 +9,7 @@ Welcome to the Bitrise plugin! ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-bitrise +yarn --cwd packages/app add @backstage/plugin-bitrise ``` Bitrise Plugin exposes an entity tab component named `EntityBitriseContent`. You can include it in the diff --git a/plugins/catalog-backend-module-incremental-ingestion/README.md b/plugins/catalog-backend-module-incremental-ingestion/README.md index c5ed3e6367..0894d32031 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/README.md +++ b/plugins/catalog-backend-module-incremental-ingestion/README.md @@ -41,7 +41,7 @@ The Incremental Entity Provider backend is designed for data sources that provid ## Installation -1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-incremental-ingestion` from the Backstage root directory. +1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-incremental-ingestion` from the Backstage root directory. 2. In your catalog.ts, import `IncrementalCatalogBuilder` from `@backstage/plugin-catalog-backend-module-incremental-ingestion` and instantiate it with `await IncrementalCatalogBuilder.create(env, builder)`. You have to pass `builder` into `IncrementalCatalogBuilder.create` function because `IncrementalCatalogBuilder` will convert an `IncrementalEntityProvider` into an `EntityProvider` and call `builder.addEntityProvider`. ```ts diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index b70134cba3..e3ed302fa9 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -104,7 +104,7 @@ By default, all users are loaded. If you want to filter users based on their att ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-msgraph +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` 4. The `MicrosoftGraphOrgEntityProvider` is not registered by default, so you diff --git a/plugins/catalog-backend-module-openapi/README.md b/plugins/catalog-backend-module-openapi/README.md index 6a8028895d..a79416e901 100644 --- a/plugins/catalog-backend-module-openapi/README.md +++ b/plugins/catalog-backend-module-openapi/README.md @@ -12,7 +12,7 @@ This is useful for OpenAPI and AsyncAPI specifications. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-openapi +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-openapi ``` ### Adding the plugin to your `packages/backend` diff --git a/plugins/catalog-backend-module-puppetdb/README.md b/plugins/catalog-backend-module-puppetdb/README.md index 9319299a06..a3291693e8 100644 --- a/plugins/catalog-backend-module-puppetdb/README.md +++ b/plugins/catalog-backend-module-puppetdb/README.md @@ -12,7 +12,7 @@ to your backend package: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-puppetdb +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-puppetdb ``` Update the catalog plugin initialization in your backend to add the provider and schedule it: diff --git a/plugins/catalog-backend-module-unprocessed/README.md b/plugins/catalog-backend-module-unprocessed/README.md index f2ea7b7741..0d9f9f8cb9 100644 --- a/plugins/catalog-backend-module-unprocessed/README.md +++ b/plugins/catalog-backend-module-unprocessed/README.md @@ -11,7 +11,7 @@ A `pending` entity has not been processed yet. ## Installation ```shell -yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-unprocessed +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-unprocessed ``` ### backend diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index cd921499d3..7f886de155 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -27,7 +27,7 @@ restoring the plugin, if you previously removed it. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-catalog-backend +yarn --cwd packages/backend add @backstage/plugin-catalog-backend ``` ### Adding the plugin to your `packages/backend` diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index f00d11810f..1b972082d5 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -27,7 +27,7 @@ To use the catalog graph plugin, you have to add some things to your Backstage a 1. Add a dependency to your `packages/app/package.json`: ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-catalog-graph + yarn --cwd packages/app add @backstage/plugin-catalog-graph ``` 2. Add the `CatalogGraphPage` to your `packages/app/src/App.tsx`: diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index 602e67503d..d4ede8af7b 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -19,7 +19,7 @@ Some features are not yet available for all supported Git providers. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-catalog-import +yarn --cwd packages/app add @backstage/plugin-catalog-import ``` 2. Add the `CatalogImportPage` extension to the app: diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md index 3c9f649e37..c2e5109d5c 100644 --- a/plugins/catalog-unprocessed-entities/README.md +++ b/plugins/catalog-unprocessed-entities/README.md @@ -29,7 +29,7 @@ Requires the `@backstage/plugin-catalog-backend-module-unprocessed` module to be ## Installation ```shell -yarn add --cwd packages/app @backstage/plugin-catalog-unprocessed-entities +yarn --cwd packages/app add @backstage/plugin-catalog-unprocessed-entities ``` Import into your `App.tsx` and include into the `` component: diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index b35110053e..1ce7b2bf69 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -20,7 +20,7 @@ plugin, if you previously removed it. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-catalog +yarn --cwd packages/app add @backstage/plugin-catalog ``` ### Add the plugin to your `packages/app` diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md index 86af7081c7..67e342e8c8 100644 --- a/plugins/circleci/README.md +++ b/plugins/circleci/README.md @@ -15,7 +15,7 @@ ```bash # From your Backstage root directory -yarn add --cwd packages/app @circleci/backstage-plugin +yarn --cwd packages/app add @circleci/backstage-plugin ``` 2. Add the `EntityCircleCIContent` extension to the entity page in your app: diff --git a/plugins/code-climate/README.md b/plugins/code-climate/README.md index 4140f8f1aa..8aff5cd9af 100644 --- a/plugins/code-climate/README.md +++ b/plugins/code-climate/README.md @@ -10,7 +10,7 @@ The Code Climate Plugin displays a few stats from the quality section from [Code ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-code-climate +yarn --cwd packages/app add @backstage/plugin-code-climate ``` 2. Add the `EntityCodeClimateCard` to the EntityPage: diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 2b22b0ad1d..f47e966466 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -6,7 +6,7 @@ This is the backend part of the `code-coverage` plugin. It takes care of process ```sh # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-code-coverage-backend +yarn --cwd packages/backend add @backstage/plugin-code-coverage-backend ``` First create a `codecoverage.ts` file here: `packages/backend/src/plugins`. Now add the following as its content: diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md index d4daa6ec00..4298c69b11 100644 --- a/plugins/code-coverage/README.md +++ b/plugins/code-coverage/README.md @@ -6,7 +6,7 @@ This is the frontend part of the code-coverage plugin. It displays code coverage ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-code-coverage +yarn --cwd packages/app add @backstage/plugin-code-coverage ``` Finally you need to import and render the code coverage entity, in `packages/app/src/components/catalog/EntityPage.tsx` add the following: diff --git a/plugins/codescene/README.md b/plugins/codescene/README.md index 8c84c9015c..1a713c1589 100644 --- a/plugins/codescene/README.md +++ b/plugins/codescene/README.md @@ -12,7 +12,7 @@ The CodeScene Backstage Plugin provides a page component that displays a list of ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-codescene +yarn --cwd packages/app add @backstage/plugin-codescene ``` 2. Add the routes and pages to your `App.tsx`: diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index a7bcb0cb0a..b7bfa053cf 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -17,7 +17,7 @@ Learn more with the Backstage blog post [New Cost Insights plugin: The engineer' ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-cost-insights +yarn --cwd packages/app add @backstage/plugin-cost-insights ``` ## Setup @@ -224,8 +224,8 @@ costInsights: ### Engineer Threshold (Optional; default 0.5) -This threshold determines whether to show 'Negligible', or a percentage with a fraction of 'engineers' for cost savings or cost excess on top of the charts. -A threshold of 0.5 means that `Negligible` is shown when the difference in costs is lower than that fraction of engineers in that time frame, +This threshold determines whether to show 'Negligible', or a percentage with a fraction of 'engineers' for cost savings or cost excess on top of the charts. +A threshold of 0.5 means that `Negligible` is shown when the difference in costs is lower than that fraction of engineers in that time frame, and show `XX% or ~N engineers` when it's above the threshold. ```yaml diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 4fbf8a0cbe..abe6d10120 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -34,7 +34,7 @@ Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over mak ```bash # From your Backstage root directory -yarn add --cwd packages/app @aws-sdk/client-cost-explorer +yarn --cwd packages/app add @aws-sdk/client-cost-explorer ``` ## Usage of the SDK diff --git a/plugins/devtools-backend/README.md b/plugins/devtools-backend/README.md index 528e864783..c9226a92ab 100644 --- a/plugins/devtools-backend/README.md +++ b/plugins/devtools-backend/README.md @@ -10,7 +10,7 @@ Here's how to get the DevTools Backend up and running: ```sh # From the Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-devtools-backend + yarn --cwd packages/backend add @backstage/plugin-devtools-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/devtools.ts`, and add the diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index cb6fbb95fd..f6db8d1417 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -14,7 +14,7 @@ Lists helpful information about your current running Backstage instance such as: #### Backstage Version Reporting -The Backstage Version that is reported requires `backstage.json` to be present at the root of the running backstage instance. +The Backstage Version that is reported requires `backstage.json` to be present at the root of the running backstage instance. You may need to modify your Dockerfile to ensure `backstage.json` is copied into the `WORKDIR` of your image. ```sh @@ -66,7 +66,7 @@ To setup the DevTools frontend you'll need to do the following steps: ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-devtools + yarn --cwd packages/app add @backstage/plugin-devtools ``` 2. Now open the `packages/app/src/App.tsx` file @@ -206,7 +206,7 @@ To use the permission framework to secure the DevTools sidebar option you'll wan ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-devtools-common + yarn --cwd packages/app add @backstage/plugin-devtools-common ``` 2. Then open the `packages/app/src/components/Root/Root.tsx` file @@ -236,7 +236,7 @@ To use the permission framework to secure the DevTools route you'll want to do t ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-devtools-common + yarn --cwd packages/app add @backstage/plugin-devtools-common ``` 2. Then open the `packages/app/src/App.tsx` file @@ -341,7 +341,7 @@ To use this policy you'll need to make sure to add the `@backstage/plugin-devtoo ```sh # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-devtools-common +yarn --cwd packages/backend add @backstage/plugin-devtools-common ``` You'll also need to add these imports: diff --git a/plugins/dynatrace/README.md b/plugins/dynatrace/README.md index 9143b0ff9c..5e2394cee3 100644 --- a/plugins/dynatrace/README.md +++ b/plugins/dynatrace/README.md @@ -28,7 +28,7 @@ The Dynatrace plugin will require the following information, to be used in the c ``` # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-dynatrace +yarn --cwd packages/app add @backstage/plugin-dynatrace ``` 2. We created in our catalog the interface for using the integration with Dynatrace. diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md index 49a8ce20e0..a3aae695e2 100644 --- a/plugins/entity-feedback-backend/README.md +++ b/plugins/entity-feedback-backend/README.md @@ -12,7 +12,7 @@ out of the box, this plugin will not work when you test it. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-entity-feedback-backend +yarn --cwd packages/backend add @backstage/plugin-entity-feedback-backend ``` ### Adding the plugin to your `packages/backend` diff --git a/plugins/entity-validation/README.md b/plugins/entity-validation/README.md index 7816262836..f831007a45 100644 --- a/plugins/entity-validation/README.md +++ b/plugins/entity-validation/README.md @@ -10,7 +10,7 @@ First of all, install the package in the `app` package by running the following ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-entity-validation +yarn --cwd packages/app add @backstage/plugin-entity-validation ``` Add the new route to the app by adding the following line: diff --git a/plugins/events-backend-module-aws-sqs/README.md b/plugins/events-backend-module-aws-sqs/README.md index 397de35c10..7ca4bb280d 100644 --- a/plugins/events-backend-module-aws-sqs/README.md +++ b/plugins/events-backend-module-aws-sqs/README.md @@ -38,7 +38,7 @@ events: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-aws-sqs +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-aws-sqs ``` ```ts title="packages/backend/src/index.ts" diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md index 457fd81fbc..61b3b63175 100644 --- a/plugins/events-backend-module-azure/README.md +++ b/plugins/events-backend-module-azure/README.md @@ -28,7 +28,7 @@ Install this module: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-azure +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-azure ``` ### Add to backend diff --git a/plugins/events-backend-module-bitbucket-cloud/README.md b/plugins/events-backend-module-bitbucket-cloud/README.md index d70a70c87e..0a40ab2eea 100644 --- a/plugins/events-backend-module-bitbucket-cloud/README.md +++ b/plugins/events-backend-module-bitbucket-cloud/README.md @@ -28,7 +28,7 @@ Install this module: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-cloud +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-bitbucket-cloud ``` ### Add to backend diff --git a/plugins/events-backend-module-gerrit/README.md b/plugins/events-backend-module-gerrit/README.md index 34b2d154be..b658fba366 100644 --- a/plugins/events-backend-module-gerrit/README.md +++ b/plugins/events-backend-module-gerrit/README.md @@ -27,7 +27,7 @@ Install this module: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gerrit +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gerrit ``` ### Add to backend diff --git a/plugins/events-backend-module-github/README.md b/plugins/events-backend-module-github/README.md index 7731d10d24..072877ee86 100644 --- a/plugins/events-backend-module-github/README.md +++ b/plugins/events-backend-module-github/README.md @@ -28,7 +28,7 @@ Install this module: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-github +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-github ``` Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: diff --git a/plugins/events-backend-module-gitlab/README.md b/plugins/events-backend-module-gitlab/README.md index 13e30659a6..3d4919302f 100644 --- a/plugins/events-backend-module-gitlab/README.md +++ b/plugins/events-backend-module-gitlab/README.md @@ -27,7 +27,7 @@ Install this module: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gitlab +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gitlab ``` Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 2fd980f003..b8470101aa 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -21,7 +21,7 @@ to the used event broker. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend @backstage/plugin-events-node +yarn --cwd packages/backend add @backstage/plugin-events-backend @backstage/plugin-events-node ``` ### Add to backend diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index aae3ff94b8..6767749eab 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -13,7 +13,7 @@ Install dependencies ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-explore-backend +yarn --cwd packages/backend add @backstage/plugin-explore-backend ``` Add feature @@ -45,7 +45,7 @@ Install dependencies ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-explore-backend +yarn --cwd packages/backend add @backstage/plugin-explore-backend ``` You'll need to add the plugin to the router in your `backend` package. You can @@ -90,7 +90,7 @@ Install dependencies ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-explore-backend @backstage/plugin-explore-common +yarn --cwd packages/backend add @backstage/plugin-explore-backend @backstage/plugin-explore-common ``` You'll need to add the plugin to the router in your `backend` package. You can diff --git a/plugins/firehydrant/README.md b/plugins/firehydrant/README.md index 4472536245..aa56bd066a 100644 --- a/plugins/firehydrant/README.md +++ b/plugins/firehydrant/README.md @@ -18,7 +18,7 @@ The [FireHydrant](https://firehydrant.io) plugin brings incident management to B ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-firehydrant +yarn --cwd packages/app add @backstage/plugin-firehydrant ``` 2. Add the plugin to `EntityPage.tsx`, inside the `const overviewContent`'s parent `` component: diff --git a/plugins/fossa/README.md b/plugins/fossa/README.md index 10ccba5ba7..2c69a9b309 100644 --- a/plugins/fossa/README.md +++ b/plugins/fossa/README.md @@ -10,7 +10,7 @@ The FOSSA Plugin displays code statistics from [FOSSA](https://fossa.com/). ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-fossa +yarn --cwd packages/app add @backstage/plugin-fossa ``` 2. Add the `EntityFossaCard` to the EntityPage: diff --git a/plugins/github-actions/README.md b/plugins/github-actions/README.md index d0b5ceb54d..4be62346a5 100644 --- a/plugins/github-actions/README.md +++ b/plugins/github-actions/README.md @@ -39,7 +39,7 @@ TBD ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-github-actions +yarn --cwd packages/app add @backstage/plugin-github-actions ``` 2. Add to the app `EntityPage` component: diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 5cc6ccdeaa..111e028eed 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -14,7 +14,7 @@ The GitHub Deployments Plugin displays recent deployments from GitHub. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-github-deployments +yarn --cwd packages/app add @backstage/plugin-github-deployments ``` 2. Add the `EntityGithubDeploymentsCard` to the EntityPage: diff --git a/plugins/graphiql/README.md b/plugins/graphiql/README.md index 452dfa807d..ed47b96967 100644 --- a/plugins/graphiql/README.md +++ b/plugins/graphiql/README.md @@ -13,7 +13,7 @@ Start out by installing the plugin in your Backstage app: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-graphiql +yarn --cwd packages/app add @backstage/plugin-graphiql ``` ```diff diff --git a/plugins/graphql-voyager/README.md b/plugins/graphql-voyager/README.md index 66ea63191b..d37ffdecca 100644 --- a/plugins/graphql-voyager/README.md +++ b/plugins/graphql-voyager/README.md @@ -13,7 +13,7 @@ To get started, first install the plugin with the following command: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-graphql-voyager +yarn --cwd packages/app add @backstage/plugin-graphql-voyager ``` ### Adding the page diff --git a/plugins/home/README.md b/plugins/home/README.md index 066e637725..35f9cb0738 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -10,7 +10,7 @@ If you have a standalone app (you didn't clone this repo), then do ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-home +yarn --cwd packages/app add @backstage/plugin-home ``` ### Setting up the Home Page diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 814baea507..434cf8c0f0 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -27,7 +27,7 @@ Install the plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-ilert +yarn --cwd packages/app add @backstage/plugin-ilert ``` Add it to the `EntityPage.tsx`: diff --git a/plugins/jenkins-backend/README.md b/plugins/jenkins-backend/README.md index d6f8fa5472..4cca86f86d 100644 --- a/plugins/jenkins-backend/README.md +++ b/plugins/jenkins-backend/README.md @@ -28,7 +28,7 @@ This plugin needs to be added to an existing backstage instance. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-jenkins-backend +yarn --cwd packages/backend add @backstage/plugin-jenkins-backend ``` Typically, this means creating a `src/plugins/jenkins.ts` file and adding a reference to it to `src/index.ts` diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index b62eaab514..bdbe0ac0d9 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -14,7 +14,7 @@ Website: [https://jenkins.io/](https://jenkins.io/) ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-jenkins +yarn --cwd packages/app add @backstage/plugin-jenkins ``` 2. Add and configure the [jenkins-backend](../jenkins-backend) plugin according to it's instructions diff --git a/plugins/lighthouse-backend/README.md b/plugins/lighthouse-backend/README.md index 687467f1d5..06455d90b4 100644 --- a/plugins/lighthouse-backend/README.md +++ b/plugins/lighthouse-backend/README.md @@ -8,7 +8,7 @@ Lighthouse Backend allows you to run scheduled lighthouse Tests for each Website ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-lighthouse-backend +yarn --cwd packages/backend add @backstage/plugin-lighthouse-backend ``` 2. Create a `lighthouse.ts` file inside `packages/backend/src/plugins/`: diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md index e834ad39c1..ee0e1dfd1d 100644 --- a/plugins/lighthouse/README.md +++ b/plugins/lighthouse/README.md @@ -29,7 +29,7 @@ When you have an instance running that Backstage can hook into, first install th ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-lighthouse +yarn --cwd packages/app add @backstage/plugin-lighthouse ``` Modify your app routes in `App.tsx` to include the `LighthousePage` component exported from the plugin, for example: diff --git a/plugins/lighthouse/src/components/Intro/index.tsx b/plugins/lighthouse/src/components/Intro/index.tsx index 8701c473aa..4c102b7587 100644 --- a/plugins/lighthouse/src/components/Intro/index.tsx +++ b/plugins/lighthouse/src/components/Intro/index.tsx @@ -52,7 +52,7 @@ When you have an instance running that Backstage can hook into, first install th \`\`\`sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-lighthouse +yarn --cwd packages/app add @backstage/plugin-lighthouse \`\`\` Modify your app routes in \`App.tsx\` to include the \`LighthousePage\` component exported from the plugin, for example: diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md index d2ed7f5f03..3bc441c238 100644 --- a/plugins/linguist-backend/README.md +++ b/plugins/linguist-backend/README.md @@ -14,7 +14,7 @@ Here's how to get the backend up and running: ```sh # From the Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-linguist-backend + yarn --cwd packages/backend add @backstage/plugin-linguist-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/linguist.ts`, and add the diff --git a/plugins/linguist/README.md b/plugins/linguist/README.md index e7cad1c8e0..7ad70f1db6 100644 --- a/plugins/linguist/README.md +++ b/plugins/linguist/README.md @@ -55,7 +55,7 @@ To setup the Linguist Card frontend you'll need to do the following steps: ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-linguist + yarn --cwd packages/app add @backstage/plugin-linguist ``` 2. Second we need to add the `EntityLinguistCard` extension to the entity page in your app: diff --git a/plugins/microsoft-calendar/README.md b/plugins/microsoft-calendar/README.md index 3dcf58c418..f5ba17bde3 100644 --- a/plugins/microsoft-calendar/README.md +++ b/plugins/microsoft-calendar/README.md @@ -25,7 +25,7 @@ The following sections will help you set up the Microsoft calendar plugin. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-microsoft-calendar +yarn --cwd packages/app add @backstage/plugin-microsoft-calendar ``` 2. Import the Microsoft calendar React component from `@backstage/plugin-microsoft-calendar`. diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index 14049ba39c..14e2c0005d 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -48,7 +48,7 @@ APIs. 2. Add a dependency to your `packages/app/package.json`: ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-newrelic + yarn --cwd packages/app add @backstage/plugin-newrelic ``` 3. Add the `NewRelicPage` to your `packages/app/src/App.tsx`: diff --git a/plugins/nomad-backend/README.md b/plugins/nomad-backend/README.md index 231f1ddccb..9f530bd943 100644 --- a/plugins/nomad-backend/README.md +++ b/plugins/nomad-backend/README.md @@ -22,7 +22,7 @@ In your `packages/backend/src/index.ts` make the following changes: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-nomad-backend +yarn --cwd packages/backend add @backstage/plugin-nomad-backend ``` 2. Create a `nomad.ts` file inside `packages/backend/src/plugins/`: diff --git a/plugins/nomad/README.md b/plugins/nomad/README.md index da14a0a3f3..443669b71a 100644 --- a/plugins/nomad/README.md +++ b/plugins/nomad/README.md @@ -31,7 +31,7 @@ If your Nomad cluster has ACLs enabled, you will need a `token` with at least th ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-nomad +yarn --cwd packages/app add @backstage/plugin-nomad ``` ### Configuration diff --git a/plugins/octopus-deploy/README.md b/plugins/octopus-deploy/README.md index 3699b76678..ee7d9d8543 100644 --- a/plugins/octopus-deploy/README.md +++ b/plugins/octopus-deploy/README.md @@ -14,7 +14,7 @@ To get started, first install the plugin with the following command: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-octopus-deploy +yarn --cwd packages/app add @backstage/plugin-octopus-deploy ``` ### Setup diff --git a/plugins/opencost/README.md b/plugins/opencost/README.md index 05c63206a2..1dec15dcc1 100644 --- a/plugins/opencost/README.md +++ b/plugins/opencost/README.md @@ -11,7 +11,7 @@ All of the code was originally ported from https://github.com/opencost/opencost/ 1. Add the OpenCost dependency to the `packages/app/package.json`: ```sh # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-opencost + yarn --cwd packages/app add @backstage/plugin-opencost ``` 2. Add the `OpenCostPage` to your `packages/app/src/App.tsx`: diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index 192948402a..85bced7c1b 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -19,7 +19,7 @@ Each of the entries in the table will direct you to the error details in your de ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-periskop +yarn --cwd packages/app add @backstage/plugin-periskop ``` 3. Add to the app `EntityPage` component: @@ -53,7 +53,7 @@ annotations: ### Instances -The periskop plugin can be configured to fetch aggregated errors from multiple deployment instances. +The periskop plugin can be configured to fetch aggregated errors from multiple deployment instances. This is especially useful if you have a multi-zone deployment, or a federated setup and would like to drill deeper into a single instance of the federation. Each of the configured instances will be included in the plugin's UI via a dropdown on the errors table. The plugin requires to configure _at least one_ Periskop API location in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml): diff --git a/plugins/playlist-backend/README.md b/plugins/playlist-backend/README.md index b5c9b8797b..d4474e2331 100644 --- a/plugins/playlist-backend/README.md +++ b/plugins/playlist-backend/README.md @@ -8,7 +8,7 @@ Welcome to the playlist backend plugin! ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-playlist-backend +yarn --cwd packages/backend add @backstage/plugin-playlist-backend ``` ### Adding the plugin to your `packages/backend` diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md index d28ee60181..1f5977f5df 100644 --- a/plugins/rollbar-backend/README.md +++ b/plugins/rollbar-backend/README.md @@ -8,7 +8,7 @@ Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-rollbar-backend +yarn --cwd packages/backend add @backstage/plugin-rollbar-backend ``` 2. Create a `rollbar.ts` file inside `packages/backend/src/plugins/`: diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md index 99a3deb763..2b1fb7e40d 100644 --- a/plugins/rollbar/README.md +++ b/plugins/rollbar/README.md @@ -10,7 +10,7 @@ Website: [https://rollbar.com/](https://rollbar.com/) ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-rollbar +yarn --cwd packages/app add @backstage/plugin-rollbar ``` 3. Add to the app `EntityPage` component: diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/README.md b/plugins/scaffolder-backend-module-confluence-to-markdown/README.md index 87f6acc6fe..1e64004041 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/README.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/README.md @@ -12,7 +12,7 @@ From your Backstage root directory run: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-confluence-to-markdown ``` Then configure the action: diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index 0e8e11ef7d..62c6eede97 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -10,7 +10,7 @@ You need to configure the action in your backend: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-cookiecutter +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-cookiecutter ``` Configure the action: diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index c946c8b476..bb31769cef 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -10,7 +10,7 @@ Here you can find all Gitlab related features to improve your scaffolder: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-gitlab +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-gitlab ``` Configure the action: diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md index f4ec086775..a7afd1429e 100644 --- a/plugins/scaffolder-backend-module-rails/README.md +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -15,7 +15,7 @@ You need to configure the action in your backend: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-rails +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-rails ``` Configure the action (you can check diff --git a/plugins/scaffolder-backend-module-sentry/README.md b/plugins/scaffolder-backend-module-sentry/README.md index 14751185c4..5cb1f1b810 100644 --- a/plugins/scaffolder-backend-module-sentry/README.md +++ b/plugins/scaffolder-backend-module-sentry/README.md @@ -12,7 +12,7 @@ You need to configure the action in your backend: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-sentry +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-sentry ``` Configure the action (you can check diff --git a/plugins/scaffolder-backend-module-yeoman/README.md b/plugins/scaffolder-backend-module-yeoman/README.md index b5c7b4d026..05bbf6064b 100644 --- a/plugins/scaffolder-backend-module-yeoman/README.md +++ b/plugins/scaffolder-backend-module-yeoman/README.md @@ -10,7 +10,7 @@ You need to configure the action in your backend: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-yeoman +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-yeoman ``` Configure the action: diff --git a/plugins/scaffolder-backend/README.md b/plugins/scaffolder-backend/README.md index ba9183c5cc..0aa57fd90c 100644 --- a/plugins/scaffolder-backend/README.md +++ b/plugins/scaffolder-backend/README.md @@ -21,7 +21,7 @@ restoring the plugin, if you previously removed it. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend +yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend ``` ### Adding the plugin to your `packages/backend` diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 80d59160c4..a6af441d62 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -20,7 +20,7 @@ the plugin, if you previously removed it. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-scaffolder +yarn --cwd packages/app add @backstage/plugin-scaffolder ``` ### Add the plugin to your `packages/app` diff --git a/plugins/search-backend-module-catalog/README.md b/plugins/search-backend-module-catalog/README.md index a2825c17cd..cbabe13d6d 100644 --- a/plugins/search-backend-module-catalog/README.md +++ b/plugins/search-backend-module-catalog/README.md @@ -8,7 +8,7 @@ Add the module package as a dependency: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-catalog +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-catalog ``` Add the collator to your backend instance, along with the search plugin itself: diff --git a/plugins/search-backend-module-explore/README.md b/plugins/search-backend-module-explore/README.md index 9ba33ae326..5bccf8c737 100644 --- a/plugins/search-backend-module-explore/README.md +++ b/plugins/search-backend-module-explore/README.md @@ -8,7 +8,7 @@ Add the module package as a dependency: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-explore +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-explore ``` Add the collator to your backend instance, along with the search plugin itself: diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md index 4790917375..e2d516ace5 100644 --- a/plugins/search-backend-module-stack-overflow-collator/README.md +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -73,7 +73,7 @@ Add the module package as a dependency: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow-collator +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-stack-overflow-collator ``` Add the collator to your backend instance, along with the search plugin itself: diff --git a/plugins/search-backend-module-techdocs/README.md b/plugins/search-backend-module-techdocs/README.md index 04c884efb0..68bb4b956e 100644 --- a/plugins/search-backend-module-techdocs/README.md +++ b/plugins/search-backend-module-techdocs/README.md @@ -8,7 +8,7 @@ Add the module package as a dependency: ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-techdocs +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-techdocs ``` Add the collator to your backend instance, along with the search plugin itself: diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index 7cc015eb13..78f31952b2 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -10,7 +10,7 @@ The Sentry Plugin displays issues from [Sentry](https://sentry.io). ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-sentry +yarn --cwd packages/app add @backstage/plugin-sentry ``` 2. Add the `EntitySentryCard` to the EntityPage: diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index 46b878a579..18deb7ae37 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -8,7 +8,7 @@ The shortcuts plugin allows a user to have easy access to pages within a Backsta ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-shortcuts +yarn --cwd packages/app add @backstage/plugin-shortcuts ``` ### Register plugin: diff --git a/plugins/sonarqube-backend/README.md b/plugins/sonarqube-backend/README.md index 4eeac67498..6206cd8257 100644 --- a/plugins/sonarqube-backend/README.md +++ b/plugins/sonarqube-backend/README.md @@ -22,7 +22,7 @@ This plugin needs to be added to an existing backstage instance. ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-sonarqube-backend +yarn --cwd packages/backend add @backstage/plugin-sonarqube-backend ``` Typically, this means creating a `src/plugins/sonarqube.ts` file and adding a reference to it to `src/index.ts` in the backend package. diff --git a/plugins/sonarqube/README.md b/plugins/sonarqube/README.md index ab85d128aa..9d3bc701f9 100644 --- a/plugins/sonarqube/README.md +++ b/plugins/sonarqube/README.md @@ -10,7 +10,7 @@ The SonarQube Plugin displays code statistics from [SonarCloud](https://sonarclo ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-sonarqube +yarn --cwd packages/app add @backstage/plugin-sonarqube ``` 2. Add the `EntitySonarQubeCard` to the EntityPage: diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index fd14b311c6..ceeec2fa4c 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -21,7 +21,7 @@ Install the plugin: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-splunk-on-call +yarn --cwd packages/app add @backstage/plugin-splunk-on-call ``` Add it to your `EntityPage`: diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index c405ac34ef..33b3ad7718 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -10,7 +10,7 @@ To add this FactChecker into your Tech Insights you need to install the module i ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-tech-insights-backend-module-jsonfc +yarn --cwd packages/backend add @backstage/plugin-tech-insights-backend-module-jsonfc ``` ### Add to the backend diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 29b374c18c..56b927a10e 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -10,7 +10,7 @@ as well as a framework to run fact retrievers and store fact values in to a data ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-tech-insights-backend +yarn --cwd packages/backend add @backstage/plugin-tech-insights-backend ``` ### Adding the plugin to your `packages/backend` @@ -214,7 +214,7 @@ To add the default FactChecker into your Tech Insights you need to install the m ```bash # From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-tech-insights-backend-module-jsonfc +yarn --cwd packages/backend add @backstage/plugin-tech-insights-backend-module-jsonfc ``` and modify the `techInsights.ts` file to contain a reference to the FactChecker implementation. diff --git a/plugins/tech-insights/README.md b/plugins/tech-insights/README.md index 3185d40441..9145e892e7 100644 --- a/plugins/tech-insights/README.md +++ b/plugins/tech-insights/README.md @@ -14,7 +14,7 @@ Main areas covered by this plugin currently are: ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-tech-insights +yarn --cwd packages/app add @backstage/plugin-tech-insights ``` ### Add boolean checks overview (Scorecards) page to the EntityPage: diff --git a/plugins/tech-radar/README.md b/plugins/tech-radar/README.md index 31d90de938..a28ced777e 100644 --- a/plugins/tech-radar/README.md +++ b/plugins/tech-radar/README.md @@ -27,7 +27,7 @@ For either simple or advanced installations, you'll need to add the dependency u ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-tech-radar +yarn --cwd packages/app add @backstage/plugin-tech-radar ``` ### Configuration diff --git a/plugins/techdocs-react/README.md b/plugins/techdocs-react/README.md index 53e9fc6a3a..e0f792064c 100644 --- a/plugins/techdocs-react/README.md +++ b/plugins/techdocs-react/README.md @@ -6,5 +6,5 @@ This package provides frontend utilities for TechDocs and Addons. ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-techdocs-react +yarn --cwd packages/app add @backstage/plugin-techdocs-react ``` diff --git a/plugins/vault-backend/README.md b/plugins/vault-backend/README.md index 117bcaacbb..2c7dd06e52 100644 --- a/plugins/vault-backend/README.md +++ b/plugins/vault-backend/README.md @@ -16,7 +16,7 @@ To get started, first you need a running instance of Vault. You can follow [this ```bash # From your Backstage root directory - yarn add --cwd packages/backend @backstage/plugin-vault-backend + yarn --cwd packages/backend add @backstage/plugin-vault-backend ``` 2. Create a file in `src/plugins/vault.ts` and add a reference to it in `src/index.ts`: diff --git a/plugins/vault/README.md b/plugins/vault/README.md index ce2df6e47a..fe86a30bd2 100644 --- a/plugins/vault/README.md +++ b/plugins/vault/README.md @@ -18,7 +18,7 @@ To get started, first you need a running instance of Vault. You can follow [this ```bash # From your Backstage root directory - yarn add --cwd packages/app @backstage/plugin-vault + yarn --cwd packages/app add @backstage/plugin-vault ``` 2. Add the Vault card to the overview tab on the EntityPage: diff --git a/plugins/xcmetrics/README.md b/plugins/xcmetrics/README.md index 7c003722a0..0823cbbb24 100644 --- a/plugins/xcmetrics/README.md +++ b/plugins/xcmetrics/README.md @@ -1,6 +1,6 @@ # XCMetrics -[XCMetrics](https://xcmetrics.io) is a tool for collecting build metrics from XCode. +[XCMetrics](https://xcmetrics.io) is a tool for collecting build metrics from XCode. With this plugin, you can view data from XCMetrics directly in Backstage. ![XCMetrics-overview](./docs/XCMetrics-overview.png) @@ -9,7 +9,7 @@ With this plugin, you can view data from XCMetrics directly in Backstage. ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-xcmetrics +yarn --cwd packages/app add @backstage/plugin-xcmetrics ``` In `packages/app/src/App.tsx`, add the following: diff --git a/scripts/verify-lockfile-duplicates.js b/scripts/verify-lockfile-duplicates.js index ddf99f4fd6..5c9adb72f0 100644 --- a/scripts/verify-lockfile-duplicates.js +++ b/scripts/verify-lockfile-duplicates.js @@ -95,11 +95,11 @@ async function main() { if (failed) { if (!fix) { - const command = `yarn dedupe${ + const command = `yarn${ lockFile.directoryRelativeToProjectRoot === '.' ? '' : ` --cwd ${lockFile.directoryRelativeToProjectRoot}` - }`; + } dedupe`; const padding = ' '.repeat(Math.max(0, 85 - 6 - command.length)); console.error(''); console.error( From 03c5bbef1f2a8457fa81cb8e627ad0d4ca4cfa17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:08:26 +0000 Subject: [PATCH 051/112] fix(deps): update dependency date-fns to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-97e6aa5.md | 5 +++++ plugins/opencost/package.json | 2 +- yarn.lock | 9 ++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/renovate-97e6aa5.md diff --git a/.changeset/renovate-97e6aa5.md b/.changeset/renovate-97e6aa5.md new file mode 100644 index 0000000000..51f4edccd2 --- /dev/null +++ b/.changeset/renovate-97e6aa5.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-opencost': patch +--- + +Updated dependency `date-fns` to `^3.0.0`. diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json index 2046d04c47..b5b37d6f2b 100644 --- a/plugins/opencost/package.json +++ b/plugins/opencost/package.json @@ -32,7 +32,7 @@ "@material-ui/styles": "^4.11.5", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "axios": "^1.4.0", - "date-fns": "^2.30.0", + "date-fns": "^3.0.0", "lodash": "^4.17.21", "recharts": "^2.5.0" }, diff --git a/yarn.lock b/yarn.lock index 5f84cbbc51..0519d4cbd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7818,7 +7818,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 axios: ^1.4.0 - date-fns: ^2.30.0 + date-fns: ^3.0.0 lodash: ^4.17.21 recharts: ^2.5.0 peerDependencies: @@ -24615,6 +24615,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^3.0.0": + version: 3.3.1 + resolution: "date-fns@npm:3.3.1" + checksum: 6245e93a47de28ac96dffd4d62877f86e6b64854860ae1e00a4f83174d80bc8e59bd1259cf265223fb2ddce5c8e586dc9cc210f0d052faba2f7660e265877283 + languageName: node + linkType: hard + "dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" From 742e0fefde56f7cd4e42cb49fcf53b0ae5d6bdac Mon Sep 17 00:00:00 2001 From: Frida Jacobsson Date: Mon, 5 Feb 2024 09:09:35 +0100 Subject: [PATCH 052/112] Adding Umami analytics plugin to microsite Signed-off-by: Frida Jacobsson --- microsite/data/plugins/umami.yaml | 10 ++++++++++ microsite/static/img/umami-logo.svg | 1 + 2 files changed, 11 insertions(+) create mode 100644 microsite/data/plugins/umami.yaml create mode 100644 microsite/static/img/umami-logo.svg diff --git a/microsite/data/plugins/umami.yaml b/microsite/data/plugins/umami.yaml new file mode 100644 index 0000000000..308b09c90c --- /dev/null +++ b/microsite/data/plugins/umami.yaml @@ -0,0 +1,10 @@ +--- +title: Umami Analytics +author: AxisCommunications +authorUrl: https://github.com/AxisCommunications +category: Monitoring +description: Track usage of your Backstage instance using Umami Analytics. +documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main/plugins/analytics-module-umami/README.md +iconUrl: /img/umami-logo.svg +npmPackageName: '@axis-backstage/plugin-analytics-module-umami' +addedDate: '2024-02-05' \ No newline at end of file diff --git a/microsite/static/img/umami-logo.svg b/microsite/static/img/umami-logo.svg new file mode 100644 index 0000000000..b139531324 --- /dev/null +++ b/microsite/static/img/umami-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From 307474d1a3831d617d2ab9f8506f5f4c7f45e53a Mon Sep 17 00:00:00 2001 From: Frida Jacobsson Date: Mon, 5 Feb 2024 10:17:24 +0100 Subject: [PATCH 053/112] Run prettier Signed-off-by: Frida Jacobsson --- microsite/data/plugins/umami.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/umami.yaml b/microsite/data/plugins/umami.yaml index 308b09c90c..5f1fc208d4 100644 --- a/microsite/data/plugins/umami.yaml +++ b/microsite/data/plugins/umami.yaml @@ -7,4 +7,4 @@ description: Track usage of your Backstage instance using Umami Analytics. documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main/plugins/analytics-module-umami/README.md iconUrl: /img/umami-logo.svg npmPackageName: '@axis-backstage/plugin-analytics-module-umami' -addedDate: '2024-02-05' \ No newline at end of file +addedDate: '2024-02-05' From 7fb7a79dce0b40260ffabfde3621eea6b3dddd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 5 Feb 2024 13:11:46 +0100 Subject: [PATCH 054/112] add a config declaration for workingDirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/wise-papayas-cough.md | 5 +++++ packages/backend-common/config.d.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 .changeset/wise-papayas-cough.md diff --git a/.changeset/wise-papayas-cough.md b/.changeset/wise-papayas-cough.md new file mode 100644 index 0000000000..955e7fefcc --- /dev/null +++ b/.changeset/wise-papayas-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add a config declaration for `workingDirectory` diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 8bcc5465d5..700d9a6c6c 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -67,6 +67,22 @@ export interface Config { }; }; + /** + * An absolute path to a directory that can be used as a working dir, for + * example as scratch space for large operations. + * + * @remarks + * + * Note that this must be an absolute path. + * + * If not set, the operating system's designated temporary directory is + * commonly used, but that is implementation defined per plugin. + * + * Plugins are encouraged to heed this config setting if present, to allow + * deployment in severely locked-down or limited environments. + */ + workingDirectory?: string; + /** Database connection configuration, select base database type using the `client` field */ database: { /** Default database client to use */ From da21baa80c5edce1c1ef1f532f147c652f1ed00b Mon Sep 17 00:00:00 2001 From: Corey Daley Date: Mon, 5 Feb 2024 08:44:43 -0500 Subject: [PATCH 055/112] Swapping Red Hat partner logo for all white version Signed-off-by: Corey Daley --- microsite/static/img/partner-logo-redhat.png | Bin 15999 -> 19240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/microsite/static/img/partner-logo-redhat.png b/microsite/static/img/partner-logo-redhat.png index ad9bc9cebabc138bf0491718a8f5ad3cbdbf1ddc..0b98532c2c5b12348c12e5b39f3f1d9e1db85b4d 100644 GIT binary patch literal 19240 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 literal 15999 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 From c3ec819026179a4b99c74318c34cfdd0d66d8579 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 5 Feb 2024 15:54:54 +0100 Subject: [PATCH 056/112] Adjust to review Signed-off-by: Philipp Hugenroth --- packages/app-next/app-config.yaml | 12 +++++------- packages/app-next/src/App.tsx | 2 -- plugins/org/api-report-alpha.md | 6 ------ plugins/org/src/alpha.tsx | 12 ++++++++---- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 0b842a0b91..4e54d96d7c 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -5,7 +5,7 @@ app: bindings: catalog.viewTechDoc: techdocs.docRoot catalog.createComponent: catalog-import.importPage - # org.catalogIndex: catalog.catalogIndex + org.catalogIndex: catalog.catalogIndex extensions: # - apis.plugin.graphiql.browse.gitlab: true @@ -21,12 +21,10 @@ app: - entity-content:techdocs # Org Plugin - - entity-card:org/group-profile: - config: - filter: kind:group - # - entity-card:org/members-list - # - entity-card:org/ownership - # - entity-card:org/user-profile + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile # scmAuthExtension: >- # createScmAuthExtension({ diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index bb39d8d40e..03a6649c27 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -24,7 +24,6 @@ import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; -import orgPlugin from '@backstage/plugin-org/alpha'; import { coreExtensionData, @@ -126,7 +125,6 @@ const app = createApp({ userSettingsPlugin, homePlugin, appVisualizerPlugin, - orgPlugin, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index b9605a45f5..3cee584db0 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -4,7 +4,6 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) @@ -16,10 +15,5 @@ const _default: BackstagePlugin< >; export default _default; -// @alpha (undocumented) -export const EntityGroupProfileCard: ExtensionDefinition<{ - filter?: string | undefined; -}>; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 3d02aec88b..3fbf5bcc41 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -24,8 +24,9 @@ import { catalogIndexRouteRef } from './routes'; import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; /** @alpha */ -export const EntityGroupProfileCard = createEntityCardExtension({ +const EntityGroupProfileCard = createEntityCardExtension({ name: 'group-profile', + filter: 'kind:group', loader: async () => import('./components/Cards/Group/GroupProfile/GroupProfileCard').then(m => compatWrapper(), @@ -33,8 +34,9 @@ export const EntityGroupProfileCard = createEntityCardExtension({ }); /** @alpha */ -export const EntityMembersListCard = createEntityCardExtension({ +const EntityMembersListCard = createEntityCardExtension({ name: 'members-list', + filter: 'kind:group', loader: async () => import('./components/Cards/Group/MembersList/MembersListCard').then(m => compatWrapper(), @@ -42,8 +44,9 @@ export const EntityMembersListCard = createEntityCardExtension({ }); /** @alpha */ -export const EntityOwnershipCard = createEntityCardExtension({ +const EntityOwnershipCard = createEntityCardExtension({ name: 'ownership', + filter: 'kind:group,user', loader: async () => import('./components/Cards/OwnershipCard/OwnershipCard').then(m => compatWrapper(), @@ -51,8 +54,9 @@ export const EntityOwnershipCard = createEntityCardExtension({ }); /** @alpha */ -export const EntityUserProfileCard = createEntityCardExtension({ +const EntityUserProfileCard = createEntityCardExtension({ name: 'user-profile', + filter: 'kind:user', loader: async () => import('./components/Cards/User/UserProfileCard/UserProfileCard').then(m => compatWrapper(), From feea2211231674e5a31e99a66cef7914a1e96273 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 29 Jan 2024 11:54:04 +0100 Subject: [PATCH 057/112] feat: configure alpha subpath exports Signed-off-by: Camila Belo --- plugins/catalog-graph/package.json | 19 ++++++++++++++++--- plugins/catalog-graph/src/alpha.ts | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 plugins/catalog-graph/src/alpha.ts diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 1dba15d840..4aad12b5bc 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "frontend-plugin" diff --git a/plugins/catalog-graph/src/alpha.ts b/plugins/catalog-graph/src/alpha.ts new file mode 100644 index 0000000000..94adcafa53 --- /dev/null +++ b/plugins/catalog-graph/src/alpha.ts @@ -0,0 +1,15 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ From a36b357b5af88e430f467e77407579dccd8f2a60 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 29 Jan 2024 11:55:36 +0100 Subject: [PATCH 058/112] feat: migrate catalog-graph plugin Signed-off-by: Camila Belo --- plugins/catalog-graph/package.json | 2 ++ plugins/catalog-graph/src/alpha.ts | 14 ++++++++++++++ yarn.lock | 2 ++ 3 files changed, 18 insertions(+) diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 4aad12b5bc..b92cf201e4 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -44,8 +44,10 @@ "dependencies": { "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/types": "workspace:^", "@material-ui/core": "^4.12.2", diff --git a/plugins/catalog-graph/src/alpha.ts b/plugins/catalog-graph/src/alpha.ts index 94adcafa53..345a632dea 100644 --- a/plugins/catalog-graph/src/alpha.ts +++ b/plugins/catalog-graph/src/alpha.ts @@ -13,3 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createPlugin } from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRef } from '@backstage/core-compat-api'; +import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; + +export default createPlugin({ + id: 'catalog-graph', + routes: { + catalogGraph: convertLegacyRouteRef(catalogGraphRouteRef), + }, + externalRoutes: { + catalogEntity: convertLegacyRouteRef(catalogEntityRouteRef), + }, + extensions: [], +}); diff --git a/yarn.lock b/yarn.lock index 5f84cbbc51..5cd5a0cbf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5783,9 +5783,11 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" From 1b06d313d89ce94f51f0947387ca37f4b529899f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 10:08:56 +0100 Subject: [PATCH 059/112] feat: migrate catalog graph page Signed-off-by: Camila Belo --- plugins/catalog-graph/src/alpha.ts | 29 -------------- plugins/catalog-graph/src/alpha.tsx | 62 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 29 deletions(-) delete mode 100644 plugins/catalog-graph/src/alpha.ts create mode 100644 plugins/catalog-graph/src/alpha.tsx diff --git a/plugins/catalog-graph/src/alpha.ts b/plugins/catalog-graph/src/alpha.ts deleted file mode 100644 index 345a632dea..0000000000 --- a/plugins/catalog-graph/src/alpha.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { createPlugin } from '@backstage/frontend-plugin-api'; -import { convertLegacyRouteRef } from '@backstage/core-compat-api'; -import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; - -export default createPlugin({ - id: 'catalog-graph', - routes: { - catalogGraph: convertLegacyRouteRef(catalogGraphRouteRef), - }, - externalRoutes: { - catalogEntity: convertLegacyRouteRef(catalogEntityRouteRef), - }, - extensions: [], -}); diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx new file mode 100644 index 0000000000..bf2b0ffce8 --- /dev/null +++ b/plugins/catalog-graph/src/alpha.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + createPageExtension, + createPlugin, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +import { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; +import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; +import { Direction } from './components'; + +const CatalogGraphPage = createPageExtension({ + defaultPath: '/catalog-graph', + routeRef: convertLegacyRouteRef(catalogGraphRouteRef), + configSchema: createSchemaFromZod(z => + z.object({ + path: z.string().default('/catalog-graph'), + selectedKinds: z.array(z.string()).optional(), + selectedRelations: z.array(z.string()).optional(), + rootEntityRefs: z.array(z.string()).optional(), + maxDepth: z.number().optional(), + unidirectional: z.boolean().optional(), + mergeRelations: z.boolean().optional(), + direction: z.nativeEnum(Direction).optional(), + showFilters: z.boolean().optional(), + curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + }), + ), + loader: ({ config: { path, ...initialState } }) => + import('./components/CatalogGraphPage').then(m => + compatWrapper(), + ), +}); + +export default createPlugin({ + id: 'catalog-graph', + routes: { + catalogGraph: convertLegacyRouteRef(catalogGraphRouteRef), + }, + externalRoutes: { + catalogEntity: convertLegacyRouteRef(catalogEntityRouteRef), + }, + extensions: [CatalogGraphPage], +}); From 0e7340cb25e9b713579386a7e665c54be9a954fa Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 14:03:33 +0100 Subject: [PATCH 060/112] feat(catalog-react): accepts entity cards config schema Signed-off-by: Camila Belo --- plugins/catalog-react/src/alpha.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 330cd573cd..f5f4dfdb74 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -17,6 +17,7 @@ import { AnyExtensionInputMap, ExtensionBoundary, + PortableSchema, ResolvedExtensionInputs, RouteRef, coreExtensionData, @@ -48,6 +49,7 @@ export const catalogExtensionData = { // TODO: Figure out how to merge with provided config schema /** @alpha */ export function createEntityCardExtension< + TConfig extends { filter?: string }, TInputs extends AnyExtensionInputMap, >(options: { namespace?: string; @@ -55,13 +57,23 @@ export function createEntityCardExtension< attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; + configSchema?: PortableSchema; filter?: | typeof catalogExtensionData.entityFilterFunction.T | typeof catalogExtensionData.entityFilterExpression.T; loader: (options: { + config: TConfig; inputs: Expand>; }) => Promise; }) { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + filter: z.string().optional(), + }), + ) as PortableSchema); return createExtension({ kind: 'entity-card', namespace: options.namespace, @@ -77,15 +89,11 @@ export function createEntityCardExtension< filterExpression: catalogExtensionData.entityFilterExpression.optional(), }, inputs: options.inputs, - configSchema: createSchemaFromZod(z => - z.object({ - filter: z.string().optional(), - }), - ), + configSchema, factory({ config, inputs, node }) { const ExtensionComponent = lazy(() => options - .loader({ inputs }) + .loader({ inputs, config }) .then(element => ({ default: () => element })), ); From c888887b65308af6e16d6b63a6d13a84a7a853ec Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 14:06:30 +0100 Subject: [PATCH 061/112] feat(catalog-graph): create entity card extension Signed-off-by: Camila Belo --- plugins/catalog-graph/src/alpha.tsx | 80 ++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index bf2b0ffce8..00be9e8e69 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -24,29 +24,85 @@ import { compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; import { Direction } from './components'; +type Zod = Parameters[0]>[0]; + +function getCompoundEntityRefConfigSchema(z: Zod) { + return z.object({ + kind: z.string(), + namespace: z.string(), + name: z.string(), + }); +} + +function getEntityGraphRelationsConfigSchema(z: Zod) { + // Mapping EntityRelationsGraphProps to config + // TODO: Define how className and render functions will be configured + return z.object({ + rootEntityRef: getCompoundEntityRefConfigSchema(z) + .or(z.array(getCompoundEntityRefConfigSchema(z))) + .optional(), + kinds: z.array(z.string()).optional(), + relations: z.array(z.string()).optional(), + maxDepth: z.number().optional(), + unidirectional: z.boolean().optional(), + mergeRelations: z.boolean().optional(), + direction: z.nativeEnum(Direction).optional(), + relationPairs: z.array(z.tuple([z.string(), z.string()])).optional(), + zoom: z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), + curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + }); +} + +const CatalogGraphEntityCard = createEntityCardExtension({ + name: 'catalog-graph', + configSchema: createSchemaFromZod(z => + z + .object({ + // Filter is a config required to all entity cards + filter: z.string().optional(), + title: z.string().optional(), + height: z.number().optional(), + variant: z.enum(['flex', 'fullHeight', 'gridItem']).optional(), + }) + .merge(getEntityGraphRelationsConfigSchema(z)), + ), + loader: async ({ config: { filter, ...props } }) => + import('./components/CatalogGraphCard').then(m => + compatWrapper(), + ), +}); + const CatalogGraphPage = createPageExtension({ defaultPath: '/catalog-graph', routeRef: convertLegacyRouteRef(catalogGraphRouteRef), configSchema: createSchemaFromZod(z => z.object({ + // Path is a default config required to all pages path: z.string().default('/catalog-graph'), - selectedKinds: z.array(z.string()).optional(), - selectedRelations: z.array(z.string()).optional(), - rootEntityRefs: z.array(z.string()).optional(), - maxDepth: z.number().optional(), - unidirectional: z.boolean().optional(), - mergeRelations: z.boolean().optional(), - direction: z.nativeEnum(Direction).optional(), - showFilters: z.boolean().optional(), - curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + // Mapping intialState prop to config + initialState: z + .object({ + selectedKinds: z.array(z.string()).optional(), + selectedRelations: z.array(z.string()).optional(), + rootEntityRefs: z.array(z.string()).optional(), + maxDepth: z.number().optional(), + unidirectional: z.boolean().optional(), + mergeRelations: z.boolean().optional(), + direction: z.nativeEnum(Direction).optional(), + showFilters: z.boolean().optional(), + curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + }) + .merge(getEntityGraphRelationsConfigSchema(z)) + .optional(), }), ), - loader: ({ config: { path, ...initialState } }) => + loader: ({ config: { path, ...props } }) => import('./components/CatalogGraphPage').then(m => - compatWrapper(), + compatWrapper(), ), }); @@ -58,5 +114,5 @@ export default createPlugin({ externalRoutes: { catalogEntity: convertLegacyRouteRef(catalogEntityRouteRef), }, - extensions: [CatalogGraphPage], + extensions: [CatalogGraphPage, CatalogGraphEntityCard], }); From d73d7dd5b1824eb5c22cb94ac4ad5582c040c89f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 31 Jan 2024 10:21:06 +0100 Subject: [PATCH 062/112] feat(app-next): install catalog in app-next Signed-off-by: Camila Belo --- packages/app-next/app-config.yaml | 1 + plugins/catalog-graph/src/alpha.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index a79bb866db..7402493179 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -16,6 +16,7 @@ app: config: filter: kind:component has:links - entity-card:linguist/languages + - entity-card:catalog-graph/entity-relations # Entity page content - entity-content:techdocs diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index 00be9e8e69..6d0c9d2403 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -58,7 +58,7 @@ function getEntityGraphRelationsConfigSchema(z: Zod) { } const CatalogGraphEntityCard = createEntityCardExtension({ - name: 'catalog-graph', + name: 'entity-relations', configSchema: createSchemaFromZod(z => z .object({ From 4d27f6ae69a7c98effc00e6aa621c5f8970fc3dd Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 1 Feb 2024 09:55:51 +0100 Subject: [PATCH 063/112] docs: start drafting the card documentation Signed-off-by: Camila Belo --- plugins/catalog-graph/EXPERIMENTAL.md | 224 ++++++++++++++++++++++++++ plugins/catalog-graph/README.md | 3 + plugins/catalog-graph/src/alpha.tsx | 28 ++-- 3 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 plugins/catalog-graph/EXPERIMENTAL.md diff --git a/plugins/catalog-graph/EXPERIMENTAL.md b/plugins/catalog-graph/EXPERIMENTAL.md new file mode 100644 index 0000000000..5870b30e4b --- /dev/null +++ b/plugins/catalog-graph/EXPERIMENTAL.md @@ -0,0 +1,224 @@ +# catalog-graph + +> **Disclaimer:** +> This documentation is made for those using the experimental new Frontend system. +> If you are not using the new Backstage frontend system, please go [here](./README.md). + +Welcome to the catalog graph plugin! The catalog graph visualizes the relations +between entities, like ownership, grouping or API relationships. + +The plugin comes with these features: + +- Catalog entity relations graph card: + A card that displays the directly related entities to the current entity. + This card is for use on the entity page. + The card can be customized, for example filtering for specific relations. +

N!RC|bd^*~(wKaYw39MSwD4S69QPk`~RG^};+ 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 lnH6Gs8OD8DVMRtFB;ymE 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 R&NINj-%hA1kbg0{^gr2Fv>*Tg literal 0 HcmV?d00001 diff --git a/plugins/catalog-graph/catalog-graph-entity-relations-page.png b/plugins/catalog-graph/catalog-graph-entity-relations-page.png new file mode 100644 index 0000000000000000000000000000000000000000..21f964b6741638cafd20841fefc2991d6f467b46 GIT binary patch literal 158626 zcmX_nbyQUE_cbuWfTSWIT?!J?-AF4X2#B<_boT(#4bl<=(hbtx-3>!`3_a8g47~VR zzwi6UUH8mg&mVU^v+p_koV}kgRb@Hc=VZ@OP*89c8p+tfqm>Lx2JWrt-eKLB`bfw zGrd{ezuVIzp1afAeS#EmxI84P6OZC>0o@ z+cZUaAnN_A=l4wVZUAfk`z=@?>RAdoL~wYN6P7E-L1q>|iOFNi`=}3vhSo z6dCF}k-o3xG)RB$0#ZxI*~OI%SU+H;6PgSX8$s9TyrDeg_e-V1yuuE))=5zAi z+)`+MLNL!PT$1%9V(a3CW`S6bR#9g12c*IK`d58#MT7OzahwM@T<)(Bu38zXn47-@ zNwCQx-?D4go_`qd)v9I8B`~OS_Kn$G9#C3}A3_DUgUNPO`9i{IuTrOl!F#W<*L5@|9i^bwD@KZ(xC1lBk? zv^F(Bbv{Bn(TCXwTeTc_LrW!tSldIfP(x*(fo&%ooWd{@vX zQAmYN$#18*L&SK?jv0iuxoIcm-Oy3Kc-8s*r=g_G zQ7A6&4{J`@sNV~`RWgKLaB}pQFNnO*$P(XuruqJgfFPH(^}WncotEq4p70Im^mMrq zIablSl6nJgbs8VBbTT^`Sr&FPBet%c)g)KK>;!ZId~Q0}*afTDg-BTU3l{~p-J0ua zjj!$-zZoV}@5L;J@c<-u(hKdQ7F46`e8 zJE%q0cl;--IJx=wnA3%+%W2R9+dOX0n|TMj-JHmGO+*BZ&CK$#9ku8o)syEOYp?BA z^dY^1pnLt@yP|75sOS}b%aO`X%pK;cZ{Ou|j1(qRkZ#mt@7 zxr-2iqD59`At2RjitrR;rmpEC!oG)Hx1qy&Ar_yO5Qe_uH$uPD=)NKgzIUNBJBH;C5QHzMv53C z7#Kzex?WUkGHjadnRyk1Cc3owE#zK|-GU?_T}_fA9UC_&cMRV?ddJ4rRmL!Pky0CO z`=6gX1b{g|jUcY3UFq=HRk}=c3u(KL{LjPAIpBOF=ryE^e8SwuXYndc*5#bE{91K-!!|clrba zl|^f_T7A_($ZDVcKBLtZ^>p}sB5_Vp4HCMwVy`(s_3qczNRm6ZfO%V>O=ZX>b*kC@aQzi z06FNxM&x%4fN25|7dn&{PF>F%gii2yIsq#;9b@buvzs8kgwQ^7u@KwTHOH2<0hg7( zwQ*ByFOB_pz*6mpLPF+i6YM%Z$0J*E4^?EgD+m{lLE3VEVAJPhvO?_Kv<6_)1dq)o z)uGb$S(S#l>2l3t11hya5P=8>1=Z``p1wat8ypN@Ux=t~qKe2xZP16fyHGIfcoLoc z8E0{KZOA;jMz4rL22l7S4zG7_*u1F6@6N|=TJ9E3y~Yvyb$Po>CSPWnwHGyvWL>pW zGTz?~%vMjHajogpC%ZbdQ@1C>LB*|0KgLVt;v1+&qvw724cFkSFq_-@uf&=i9$)G^ z$VAqms5L_O-jdyPjk)HYPq6v%+C@NH$lGW^J3FM}8QpTwz2X2eyrb9Bq2B0ohyv2Z zu-=CGmzD|3{<&(=blAMV45D~58~=hd+`i8)HiFKJ_~TDy#!MirP2Zs%RNQ6hL|7=S zFjN633UDPneLGXPdr+iaK?`t3KA!<*MC_Pm>fzRd%1RZZ^!d13F}ue8)bO2;@cU>4 za*K6roTDsJeESl}-A4U{UR>5E^saMw_!(0`q+7%ZY$gIr2{KbsDv{efnth|C+`3fGy4}0<3)EHcX%yv}Etiz5A(4@+ zk#)s2@s5+OiLU682*U}=W|ECap!KVL6sqraAh*XUDQC6RhP4ud-a_l~{Q`MfGD_dZtd;E1EOzd0MKln;ORogS#u| zV1lXMI@PvOddSI3S_iRYxysghDhmkT)!c6yACOlT$yUB6RcAlewdn=USR{75!&M7$ zVbg)X!{X`>YwN#hbOEL7{{9{)=KYH{emh43>C{^MH6W&s=$Qd8Ra`ytpz|@Kd*_W6 z$E){ZZ8t1@!l^yX3mAwwa&T$4KJ}W{(rTYkcz?euOJDs_FP!_Y276t84WYwOolvs= zp(r`)<6qd{wu5w2NCv1lZ>9x3_t-KV_wP|l^$BCgP3F~(`DdFd>tTBgii{VAXA%Q!D zrnsfrAPwcdIMRPSo;H=ce-5X-qeE8Jem!RlS(}@tJ~w%sKC%AzN98bmy$1_1uCfzrxEet;B-F6@_cpC7rpTydG^0 z#e&~BUoJ+`NMC+t&;D5PH--f%Z+`v-Ile7cYULFtdTtgIZ2aqR=#_T^4mqaA4#R`Th{NU0BPt7j)!vyknVLji_O z4kmssu9s0RY1h7&B-tY52{`OrTPlBDXzsj-L$Rk1CM}|Ki2ZSo-N2}6EnfETs|C1! zrM-(&_RM@hcdXN3(7!$64)bw6%-xbAx=i#2g|5DLo?;5weU%r#RvY7u6?r^v5l+gqR(ZT{|^0Y=TuY0DjRRtH5MCeLvD?CLO3-mA6W;`!YOkLEuT>W+j6 za*iRA*JCTlaCT!-%|t)hH?|))r={QPkR6hkDAsNE;uAV7>{6KHGJ=wwf6FYg`7&pQ z+8AuZgFI2x{Yh117%*Y|-l+L#JNa}(zjD=MhS_W2y>>wsu*8|Mx4o|4r?lSLwdgp` zFi3~_C@3C&*b`5b$bCuzgJ)A<5#J9gwi4b|03+W?q-ihFB6Wm|)@2hzCCF@3drjxC zV7U&N#_szIO!pWoG4)Z*uZuk!t^>Q(BLa-;(@kN^S3SaC4 zXGm0LSMf_+Y}m_}K!Rt*LZIT_n^m;%6*b0Q7FeM$)wNe8ot1u5=V_Xo%ERByPEvdN z2;%pi2Be?mv8kS3ItrL1=1fic=*HkHaHnkZdCR|IWg}0i1?u$UD{;u#q)9b{9|o4?EmUsr8|vXX`on;|3PcuOmAB$ zrnxs^gDT`>%e%~!bROd=GsnCpP|=jMeikm3y(kC6-izqLA(~Y_R9UKv^8l)L3FVF) z^GT!g?&W0+Zed}oxiTHQxl%nVGc)r)vs`Ll1*%_nf+`s>miV+Jy>ezoG)Zo=$1A6PO(@$yhcKmS%qZOGRzxS0aB7$pa);aQ?eY;paJOAbY)T<+}$bnB#NHDuA~g_VypRF-AebW3I5H zJ$xR7HVE>7;i*uJ>~CODoqw$JMxN~T?I@nvW9Y_DJ+J8m=v_)oK5w8SvM?bKSZDr2 zetqkHFK}d*w9p^tgf$q;P<2wthFg?6#|jY|;Uiw;14A1?sw_ib@-^p_F$>MgxcN>(coHexaA ze8(@JH$v-y_9~Gv!-oQk#g5;t={~PHTc>ec(1C0jA+J3DE+uy>x%@!DmwgeClkQUk zJ7*@I_UkH22!`cjmW^HUnvd3Gg1AOgW{h~#^4SZqy1bt72FQ0&a*8tX#0rSK%_pa~ zo~fq=o>@1MF2P#6qNi}wyD?}c;U4^APqXjWz|%H_Iekz&lc*4#xDK`(E`SA~0=8%T ze^(6h?!<=(+=b%l+W^}zPfPdXE1JJk5L}$mp&pPGQ0>ckk}8u!I}bC15r-X~Ld{a> z;CbwOoC3U3#ww=7l@KAgF2Qmoqh>z)aFFu?} z=;22Wz5yW4o^sDmARYdCKya^T zySUdHi&LI;tuGdb*xsDKEn_B`zTSKJMf%4bi@Z~O?L&)aPU^+fO7(`{(qa4_2|`Zt zu)cyvniMA0>f$hwh~9#qmfrL6<})dyEVZ3BBI%gOMdRSD9`O&Lsd~wG@1;*rwj`;< zzv<3NpSJFy*7cJW++)e?g4aYcmd!xN_K2;b07#3pq05k(jZ2hosRvgnp*oC&j@*t9i2S)| zyn?m85{-qaS2}6I1R?|?s>l8&rn<8x6w+54@a&QA)K^gk%)-Gqp5B`!Yawg%ZbYzm z(FN6oF~{1LqsA>4r|^{RG&lC8UJZSO4{Wt^b7U~&+jXQsCd<*ZAHq>-0Nfw?_)9Woo4}`>@m{svpElW z12?QMFK8Ih&xl?h@YvD=Z}E>4@3x-PFNC40l2T+H1Qm0B4iTC4m-#R-e))u~m7q&u z*kmWY7YFI?;wNt7TH4EVAj4tB&sw6(sLE2Y=Bw7S&k0fwPfPt;PK&c_#M8N%BMzy@ zRy&gWCOKN)_)qix2i)fqf1bhsznW2-DBX1D z)J!6BR=|&{Nf!+R&Pd#k{vH zIfi0x*cMly{q;DMp5D z80#iaN7=^{ytSrf+kM{1)J~ij!#?@HZKz$FOS=@tul$~_^X&7g2w`+A+ydPOz4q`B zIZU24yH)e0X?gkFo07Asjl13DR(`8QT;s$RuC9P}l!QE^bOlQ;1<@^6X0MK6ee(@V zeXG!fmJmzy=HR;0FIS`~&okqc?}|a=WQTaSz|%~330#py=PCh$wXS)?Sz6RGN{6G^ zJt)h=24yyI1o|0KajEvxfS1n&coG6|_H}oy);pZUAmipWwoB31?MvBOb*@We-X7VY zy}LYpRtz4E&Koh^dQgToJ~41@zTLx!Df;D#5qkQCdc#w5Ru4>~Xs9oghBAnc z{j##SNlXd@EcXay26JUJU=Qx~5mtQMYnx;8q(e((bNo~B%YKen^FyzyK8m*PMSq2y zTC6kGK?=VK06zsm2#<49ucGv9-c&iN{bDINV0w+imFVV$bzqt;L@A?sPdNO1>b#cW zI6|n<0{z=7Fk8$AY+8z|;&(F&>}d_?zUk#9?7f@V$ig;hGY&C-wRX>pvC`?bSGHGj zJKdMBBBuxA{rTuq7nG%soo_k!%%SW(JGqM%r4}-9I!b?zE|Utb#&=A<$zo?8GE{)CQ-oQxm=Aq=KGTSKaB$(UYz#CL+Yv{3j9#iX zWY|ov+;QZV70H!*FNWOcS_hA=h`$D`qw{I@Zt#;-FjTQ@9aUFd;x~-LKFC}t^pT(+ zWyHy6+7}*+cCwiA9 zAn*Vccr#|$jgWHi<0Bx}CMZ$(6&64KoOuw1s1I$QUxM7)lTOrYMOMWKiuU z{)KevGxeS^eD@Yq%q~=+;1f=RTMwi!cijGIijrJ}Tv?4axj@O}q3lf7IJE9rI#igo zMLL-{j*g;Pkdl200{Ef!*lHhLck# zCpr`#+mtk7(?N>s;!(;{gB&J|O{tM$EoZ4Wci7|p8y&|t#R6g@4_9LaN8ZRWkE}W> z3oGHVkhG+gm%AoudhP344y_Pj@mWB-xzAlhovGchS4SJH)y^BR&|(S zhS#n@D3*`zG>iI+JT&Hh#BqkBDdZTJIp39>>m~uZS&VD6M9XLRmZI%V-|%-()W3ZR^S4lo+T$a(t#ow8lp!dpk)lH%+Nbq=P*svu+V3Sr154?9^cJAb zS_XO)dUGR@*2fIg0;T=K*3DdOV%*WP!W^@sJL$MU$5lQLg7IMNx02mLd3GirR$s%F7zx;7Miz~s)@Y|FCjLT&OJ73%?8@n7#^9B{KL zKt7K=s%f!5eQ6}MbX5Rdr1qJ3I7giTF{*`!+yafiPL+BJ0@lx9b7y0IP7Aspy(ZwC z_Nt2BVv;wVhM+|;aH)$;Z+skH1|ruO=ta30I>=ud+)XS_i_;Dau42G#pZ^(DE6Py_ ztbkyQ8FcNJ3TP8Cn)ktdQAK@`l||aoK;(JYpwfFCTJcT*YzW}qII7|Bl~(Ju2toi~ z(<&NN$68bI#>kLe);P`k^6o6@N%{CyaXM?|4-rz>PrH$XM2p}|J=HnZfVl}+c1ht; zgF2dKPvTQbYk{ze&$`l;i`WqrWg?c8J35R1IF?j4#2;BUSAf>;`jXqXLW#BX6Zxj< zoribKYJv_sUOq9v2}b;y)^WIl4CU7ZqWw9TMUR!FGd`y;OK1g(DfLMYC`iXERn??L z&butScUGkZ@3L~#w9K?*_>DmO?_z+{f|X1spQz;i<{)T`lX4o`5p?_E(>2q0zv%;7-XE{ou(y29JwMb%E8Y_9Y0#eZDnfZVyiC5 zDMo9&v~N8ov^dutBXr3Y1*&tL9y)7zD2i-bvTQ>P-!48*?uzJ&mt3S)k5xNu+A#^R zB#$@M(a7Z7b$p;-z7_ zB&(kNXe~{SI@Hs()!q0Q@U9-GACD*g?=SwB%P zgRV>lN%LhNPzkO1#P!{wIdZB}wvO=SXul`;&GpuYWP^&!4dk_^j?HY%kE@|{8h&@f z>L(&aWUoZ|vra5f%-xVm_?t@jRj0b1x_@*7Yt}SXlBy}Ra^V2-dEONBt&K=*LO zLFz6(ni39W=C_%iLkZyu5e{ja*_I0(4!(In>(O0hA|$e=ksHmYZDEvGMEZIJu;A$U z<6JG^y_^*0(>&(2GJa%K6q#brMvH(oy*B?X9_`sLm)PuNA5D!mZx=7v9bz?SAH&v5 zW8+Jn*EZ!JpNl5}|W);;G%MhPwgYLkeac&wb z5)kN)XQXckJQtA{mRwth2U#Cy@v*6V$js*92zCtum!2v7jJKs!l~Pt0Y7P%|EJ}CB zsRFu(D;LBUjH|kJmA3QvyHm<%>VL4Y%A^9#ssne=>G6avr461079j%Y(BXuUDFg%l zlM(;$hvJg9PiVbAHc50!uX@Dj0OCcp*$RY@4oyW5zmh=VUlM-8W@F-TuL8;Xgu9;& z4ldoQiu{Uoz>^GUS5Ckle7+>?&79? zP(d?-kKfzXbuN>%-f;AX*TYm`P3>2gKa5)!!CIvfrVm}h7z2wmXon)?ji+xe+2}pS z$!vX<(w5!VkvMILR(S7ec{}+I^+{I=>XR<=u6m8~wBdW)YAum(p&Q+JQhOTsrHBXr zRKR^Re6;(@CpKB1r~qjola{v1wU#i8eb*?}RdW>0(F)3#i4!aymRQj@eF>GOd89&x z+YyN%ZTov{1C~1o)*P)?^xf;Un%Wp|yyCq}T>s$u0yN;cOB#R4ceuV*hYmS z9DJNc#DjCAw9oLL;g5*puLB0o&pGcl0Ao=w0`d!~aPN2eTEjNExY3-o%DobGvQ4B)FAD}QBJ8#&Dj%ypj2LhE`Iw@KE z2U4i#!Y!nEb8bGA=>KMxK1mX!|MKg5d{!~Tift&(7@}Z37`KRsjkMK*^`ri>o%5|t zcm67jp7K6&yyKViLZeZ4dZnDXgSPIN`QPhLr>5->jt;XWoEb2GVyy$d3O?TqU1QcH zgo5XDt%J#rnYao~z3&;|Z)^4xm_(vgtiIqu-}Gor=yQwCtA;sm84teGOpyCRZOyqT zCK}F2m1jQkT^axz`1NyLtCJ~uKup2h`}rUJ-Ya&YVSIP%inj?k&!Z8^@m25Il!cCS zD1=`1qJcKqOoR-F?1OZWKS|IpTxcm>xL}M|+(VKKy+0zyq>Pa=$~kDC480$TQc2(# zc<-LaY9;FTMBENg>B?NCT%~f@viHQu&!c)wJ7jp#{^`|f-TG^TKV+*}mp?L--EchA zv&2QH=Q3rfs#d>=xxUVhJ)us_7vfx?sD5H7m`g{rnVMO{TO~h7{lidbi(B;sc`roX zUoT%^%Q9c5^cm-hW2GznFvqs+mCjG==l2H;w1eL^!LgawVSTC(qL=u6R=0FyHfIa& zEmI#)+#<1T&RTrqU3K;p41n;-*9v!U+Nk0jwQDRKmO(Nm&RS^k*CpCQvo4Tv9^dw9 zLLap?7dNT951!koNnV>V4fll0)4mB(`v~xPpr}eR4)mP<>gC^d#*?nj0=w9<)m@yr z$KE^4TTJn2Ac6lGNl0M(1ZZQP zGMqmAn&i;DjiT+DDUvaRZ$3$wFc8py_wM=@zOY*Vy`arE2eH=@p$?Z8f)LIKmpTV} zZ%aW3hYgBFi5#sngy~pZM*GmGGzF=z@brjB$uRKgB)y zw}mvZ3O7U6K1#I#emRCC_22!G_hp?+ieclBBY@@+t`sr8zfv#vaXWfG@9vLmyAKOT zg9jbtGM2&~^0yyOWgmHI_gI37>=nX-BG&yMJPiq0)_?D+;vfD#`;$W|ZDDmvW9{OS zRC~jAth#~Zpr9O(Ltmj`@(2M(NMoXnU!W{QG>R@E2mJ zOk^nPo>4IgEKRxzK<|#k!2RNFbx-B3h5y`JnyT=)!Cd6{rtugf!_5HM)&^_bGZKEP z?ICEcRu}o`8`>vgbvo*zS4)E_`5w^+K%0Mr%o94iHg)_W%}(whZ#m_u=O_RGyw3b$ zfX-X--snWw&JNU~Vl6VbHCEJ5#SA&XKIKIke&piv{R5!JC2ROo`u&q&U2GWdgJ`U1 z#}@ny;D`&TSK_WZ|26@ypnpR)?~i7T>w``A4xs<=qnUd~E0OBYIn- zQBQph@iJToeUCK`DBGC(N*^a)>?Il9YAvdRhLN@NNTgy^N-cCA!X8|*&)@G{fBe3S zbPa#N(GdLkAOBkSU-#d*?kH5ekBnyH`ul7+(~2kJJ+mnn8!YJ3-rRWg8$V~`JH@uq zr77%SAbx@u%3kkAz1cCDmoi0NL5~SV-=xw{;W1mTx)3%m*%XmGXXEsRo)g>2AI(bx zm%IHi-82-wS1vlPccrfrb%d`n+!(g^xOLy_9x3^XT$gA!j(_;rln-1*gpEmt}=*!isAas3T8Qub@N9afm< z{P#7u`wUpxEFSx~oxz*4jO~%(sO?G<#y`2l`%TYQj}Rg%X@stO<4yVo9iQi-qmJGE z2R*{*?a5W{H+I;{WoPd^F*0jfTC@Gw`nwKM85yHi9G(`Uh1a`b`KyT%`{5ndTY2 z2e)@x{*}&KNF`jvJyFnF)g^LO(Dq~es}>cf#gfm}-JFiWgX5`YF2dEBXmjZe`^xuE z+>l_-vwu|*2MnC(I2hz0IZf=fA)C`QK)s>?xCp<6=m4iiY!U6{88gQpKzW+Lrj{O~ zfJ`2yE>e{R6I1HrtsyZIIT7(6*}4lMja)1JO#-k9%pd+lkuzFA&Co*?F5Eo6O^;X} zXAuXLP@iR+&Yi*Ra*C91D~wz`w)MMl73Y<2-?qKl75xgF!dcz=Cnfybh6Ntc4zrL( zmVd~T*IAH$(i*541enc(*3C^EDC(Gg*Ty?kGbr}C6eP&X%8qVtCt!AyGk`WJ*Yo%A z=aT<8p13x#u&>7f_a8g&0LD5kXMOlO`5Xl1Wa;%YIH^PT6{Bxn4biqh5 z)a!bzHqh`W|8NFI;`-dFTJ#(qDRAG>P_zWl*p}*kL@>F4z$er|7N$WIOK4R=&QbOGL@wl{QHN$ z)X!?WN`gXULKNScnPt6cFK#0t7nt8|Y1F3i+9GH|7{72Yah2&HXxrn5`!r%?*8Uph zPXCs3;NNCqQk|Xo?RnARk9K#4A2U^P+1i@%S*ss>exv>=X=n~Ss@d*dh+X>68z^<; zA9Uduv!os!CA+qGcg^cQxWu=Pe(a6cc71ntce{dEp1u;RlWI>!X4c)!HTv&T{u`o* z-0x}F4!Mf>E)h9!y_nC%I-gyW*0L2nKSO5O+{~WpoO;8ZhI4Fl{;g$lNFSSi9P>NGX zR{#7^d@mB}?A_a=n(G^v@Tk0GZKcVoW0ud5dw<$Ey!`i2S?6JZ|2CL*2L1y(AUZ#u zN6do-y0;Qj7&`A?gLP4R7v?1eCo7x6^YDvl%j}WzTgbMzqDG%s5bH2eJvd}Ikth$= zskhBYP0}Fwk3!bKo-9p{|Ku)U?)#SGCk_u35TROM_{p-xr)hP@fY^&^T>c@ag=Qr-&7M(3x6Iz9LnX)U)YJVP3}&2Xy!d6u^RCtU{HhgKO*iV4oGwFUiqY)8 zsYl-|J@8X%Ux!3k6l!)HKVFceP*Oe0I~_1Sf*YZKOM-{hJAbeHvw0b&?TP0NzpUPz zVA>6mJ96{$nEc$IYK$i!T~|5)67;$`1diPJ!mMD4)Q{!sw1=U#Ujb9-H=irlQGdGc-WA&!7z8|8Hy z|5EAkWlw+VTH7gjl)C+Tl+Bbk6pQM$82-BH85&&ZS=8%xs>yi%I(1d{B!&L;SLg9# zZPu>aF@x)OxQrs>_d5pr zppo?Ms51WLkCDRk0;7N@a|(@GY^L)^^aB&OVg9f^8T{LRle9nc;xLb0UX>Y8eiz=d ztfutC)%iFdyKh(gXo2s&4=f7&xG<-#*07Uy(Y!Ev*@7s87NqIF0iXK~`sMih9xl{R zUl5s{Qo1UBSwXJ*Q0gg^Hd$nWbqy6hV6`KQ!}ij`i0+4&(6=t7^>@*Rh8S32)53ha z1?sqC_T6wRoOdM3(VKP35Udvi+{jB(kb)=yuQkU0Oa3xK>PfRH?h=GhofMzGP?k^U zqP;r`f%;NIur}_!=(WLxjMb4tk~p^CLFBl^46qau3Dv}ylQ++kMxu%b^agFvXouT;DUfFJ5nM;qIQ;;Td zk1{Bko2Hk>b~6i-^PS%_;%XI75YfS@DZ#Pq!zBGD`@Tq*e%sRi(so+WJL}{c5+4hL zpN|2L@rdASHQ7DrTv`26XVjp!!`2kY&?Oy|Z;0JpCE9;U8oHt(V8Iw4jDGC&J!a;S z?>{!JanOcXx-dtEoEv|)xu#<-R~B*jAo>tU(MKqBFzBATwc2>^rXu9qgzT^t`nova zEV7iHVQ%>FQg?`vJ;OP6E8e%8Mfk0Z1?q1*mDa6 zNm7y1*<~&h(`!pwc&qx-+DE=%Z_#dC*&L+%T6`%!W@o&5;}_ypvXlP9pFMVSUAjwm zynbQ6qmR}4t_7(}90idv>^8{QNDgwcpMeM3WAB0w$nq_iIEad3-(|pois(a1GR%Po zU*e5a)~Lg$^%)+%?ELzZegDM-IOrvIkUOLi125Uu%FpqQbsNQ^Jetk!dI(g3?L%od zV%B)9h7rDHjBB=Vcm)o_ItOWpK}smp*W+0f@*D+20F$DSfB`a#A~((18VYt?zSOH* z>%51ttXfbtK~G+N#v&Ocak3M7&^FUr%s)GAyGFO1{}MvBgijkf-?`pt^BYT$V#IUSf`?6=oKNANeaqHFAtSC{xytImdS?&v4b zcch3DQb)Ys4Dr^98IVs84=Zs_oN2^4pfuRlyIK^twz7DBm;Ws0n3i&yaG9IZbD_V{ zhkVO2B5myU)lv1MT9g0f=f}{N%1(S#Li_4!?(1a5zQtF22Gqw+JaQH>S1|(-uwKU# za;f~xCgZe)%s{qWo%y#9m=6kf^fNIv;(S#IPHthwC1g1#wj43!n2FJZEBXeFYwSO) zzZUm0(O1-|&_-?fD0yi3czErD-ui9=d^QZE!*n?wN3gG&P0oH25xwv2 z)_8?^?vR@K%1|U*7_{r2qB=x*qyS<*`%>C{qg>GM@i1czJ}L%H>qqG|*pO7Y*g(M~TNn&15%2s>6d z(T40XN1uy41d{L&-LD>;-_pkcd=wV1Bz-GjB*WCV&(br!r{irR_lg@dNc1S`@*?e` zQTTB27b+BTjpTBogX=I|>FagaR=T`zj;U)1nLIA|=9k7bg@l2`c>b zG$*AZ*b<4FRwnsQQ`-J<+WoWeFC41g?{)k^ef)|O#`KkHAj$<*4Hie6wVsqGBLa(L z2;5pghe*IZnc`nd@XSPVJ1WXZ1+yE9TE|&aiD`aMLt|sSBq!1VUq|IZDMqcfqw?c! zqojySBu|MVuSH0n*Et(+r}PzI^G|R*LlLc%)cjDE7LPx*{q}ZY zp>Tx>ty!i)Qe3^k(&en>8~|T_XnoFTKq!ygF8bd&iPK=nzKTozKY45AqL5y8oC?zM;W@vr082xaS3~Ks|W-V}l4;&%c8T2BLocSRaUvWFPb&BlmWd+tROx!M~kAZrSjyE0&#( zT^CooTdC{Cwfhw(eG|L|%lU-E`tV5DqkB=~_nlfkD!{8}BoKKbm8at;l-Snq{?6Bq z=Q%k{ROdkX0E9dcu6jsP zx^I>l-yY?#gYp{BhCYpo`aU=@gT&}q*=@YOqzQBO9GG|E$-onzPyls@BmxzH7C;Mh z0?kE9YrNcEMaESddHCE|xNG@R6Lg?Ea3`Vkux3dOZ)4HkVfwd89;SHiEDpHvUs;rq3|q2l@eaDQa%E|Bx^&dj z=h#)zc>Ax}TwRH=T*39HvRhT&>g1fD%5gLY z^yw|Ef#QI>NWkI1ya!!tu+3$>!LY64sGF3MyIReWt$N%n=CP|0uer2jGwKHarVBj& z2gCJ}xx#J^gQBK|(c2^(_GNFLPMt)rbksE8*ApBQVwJU%;M?)%7KOS4+WKszEKF76 zY!%Pg3H;vo7vvMCo`*R!zu&1jQrjY$7;C`K$9-1!zFr#rVx?t)$ZJNQ;RP=H`jdYr zzc`)j_hh=r3H5L8F_q}zpi0rAPoBl#J+dP8xw*f}u8Lph{N!9nuXAjlN{M2Hw8D)5XTifL!%cz0(VfXF_L0=VM zX{7g&KR!#jTNV33vaO_zR7H7NJLN_pY1__3;M!-fWZHnEB`uDFOV-=s^Oy_yZ-F^c z5=(063ILDaNdhz$pHgLKOkWd4S1;Fe4GWyTD>z8`IR!2AlUU_CN3@P6&Q%8M z;%_aafj1d49kp&}7rb41^2|@x{fg-#^Atl7>6$H?ZSnQb?-$bV{r3859r>n__{(-w zy44WH;iUEj+WxtahW4qgqd)F$rIa3AkL^_E+8%$#DOGlyTp8!Ie2IJ$6CvR8wG>Te zDSUc~!Vm(J$feGiZJeni-5?HvpBX!Pgqt3?{-$Q&wN0*?^Fe|BUs#&D;N^Qc3iQX} z3nl7Tm2}_s;_I3#14R?@=WRFh?uXjT^&y&l;D$tx*@t!B;f$ZEV~|HSHs_*C3K6E$ zuXy13RP)U*2fINH!3}+Zk5A(>?Ut{7f{t&Lj6#tN=&$2Yn%Y0?-rrBUcD;1P4839w z6+lMBUr1#77A14U#5h3gLIEca@@c@5TbO$jnUT2`*pmXz#cBD zd7DG6PMmD+>DjH^H>wV11f3x5lW?i;(QzQKQb8DMZYa{VInQzm>SP)ukr;X016=@~ zItOH+MSgciW(J?{)s8Rcp6|AknQrTVJA_s)MaBDWjSwO2r^}i|^tNB*K5lm~gu>Q! z^@mmU)>A+6!TnU*r@e8jO+3v&&m$GSW7^K2tmjl~n&UGrcqmYfOd1|tkiQNZkTI3kaAEBbu({!T_ zKx#A4CDeeydw~T}tK#b#z6&HtH@9ts;d!mj!J@tY2tG{Ek5Dbym{&qBs@EPhzhhr! zk5RlAbD!$Q<8Cfp_%>u$(l91scoGR6@klnXW^3jv=Kl8L{-(5_A}?no`H>q!cSZWr zcR}i!Hzq;DJLke>R%Tw`6y&=yyQ{_3=v}ff_r~zoZlD~sB~|UI|L&yMsrgBK$P`Pi zz8+}nqtmYMW3s4vX;=$r>aSvU{JchGxle)=;^|Y=AaJbYiXs4fKC=kr*-YZ-^S}+g zWIz@AGiyJ6LEGgSVB_)Obt&+xmIeB`ZAVK8{=UOjQQG-`j${UWfq;URdgVFYDY!%( zzY;A{Mr;!m;qJ$>MPsZCxmZj+*gg`VZVcGfdqW*=sPX)#vvv`|S%UC$q?GuJUl9%1 zUKuBc*zVis>meIwfK0%7H#Y%N$-v8o>K>RI4W8Z-Hp0(C1p6fR!VTh)!Cg?}Q z)3|IB>7^c^&VC!{5F0kwocj!jsp%PU>jSk~E695EO9Y&<&%8(7f|Y{vw+i0UZ~3NHb&&?e#D!r{bDlXq7?ips09T2lc}{?1 zicaJBOIF_2wNvNiquuU)p?9T4>6Y!lPL2_b@!Pc2Br=A*pP7igypf!ilf-zrEv^SD z<5QW&%isfbVu#^|`AP4yD|@bx!=&wuSd3tjbCOY+`lY|3&<(ki7HtV*_<2MYZ?d5e zi7eR;fI9vO3;i%4>)FlHch!rtv}LoY%!ff{BFR#-44EA!d&R$^U#_F z#xO-4z_nxk;_V-_q+X#q{X*wcRYf)sUu-pcsGZ&07s> zUsl%LzFtI8`&ov0iy8ImwmQug+S5g}@NDaW5C?lP4{QEdS=H5Mlq#_BE-78?rjw`r zokjZy6MR`^(qIJvzW{)5G9O=F)XCt_F{NhUc|{7T`C6Mv-V|y`SipBT5sC$5bi~BO zJ5!awS|KM$08u>2dC*g)`l>1!H8Txd@S+cK!?D981c=*Kk|wk8mdLqom>s^)fd+2~ zS1RY0yJoA>R2|`s2kUWMnZ|aOi~KKr21vp_d$t>ZA^0`ByypR{1RRj`?;b1y!&DxW zw)M7)Tmlo*XoG>VEHc11(*4jhy>+B>!I9tTE0wNEf1r19V72_eq25lkr|W0Tn^pI2 z-@Lmjx@N}vfY57h%8S0z{C>$6*fx?-pG^@QnmMH&Y_$7(b;NZ z$krEH489ilyhJxzn>7>MJxG)y%uumTt-Rtn!Sx%idEgYG*?}xyUh83_tW<%0%kyYJ z@Y{{n>np)`sDOS?rNz2>qlc)g-zqP@QIsbinWJ5At;DyV+z%qo4`{Gg(K{;R{FVTrC;3o#CVvLE^FHHEc4_W!W}jCe;>7m_=&{pJgR}t0O*NgF@Ru!#>R|oybh=$=Q_wdpV0K>9n>vs1rf+PMMQpC+M z@lbM-`Sao zm~DqsNA2q?(m~_n;Y#1yyLESn-PrAdkgO@cZ>mz6?_HGrczRG#>UHAad`$Z#$LJuy z%l;eLlp0wuUzIOTsm~(&;!r2^) z`)_F0y-$BG@uTe0<{HKN=C=PglFJor_`4#xQd0Nf_gv%x;#^jF)hj~cJE>q$RtN4e z*{mrIsvtXY=pIP=AFl-I!R2Fks4R!t;0yRCL(l(GNH5g7WVTU_qb?>-sbF+niW-)x z{%s)lQ*Y-8WD~#FyC`)h^Oi%=`4d0`Z|bh3kPd&;3YdwXSHOg&fM>xU+UM;rM!k6h z-FmaOTHXR&HluUn5yI=B_ zy?`>Ni_itn*r`5^PsQhUn(Ba_-6JJ|bd%040O}Rqk2m^#RqV0}%HnPcvII~Vig#Q@ zGO8uPLRBHH)>}U153wP<`2|aF8#pJwZZGP8Pw;X9C0 z-i{jgU9duax9LMqO@|W`?%^yNL;gv%j~nXsHx|GAxGZn-P|u%uM)0cMXV+IMosDpH zi48q75?UtFTDr+5eXaX8Xez8$w6Yzlb%9(iLz?7=D2M?M3K@3;5k`r>c~pJp8Ae*R zeLIALFdO}rI|dtu?b82o$$mt?sva7C031Kfv+`SsSo(R$&Z>Vey-cv zFgf7^G^>b9P{rGoZRSk>cL>+1?4@9CLjsRNs5n&y#UG3B6-HUL6k{6gl|J2LF=Wfw zn@1m@sKf+Ud}?Q$Vs@qlQDKt|^Bb!DOVPvEj<&Z9irLKZYtsU*i}75SZRONrjW0{ zC^NK4CpB`3vs9};hc-ewYsf-Lt4(<@L^8F*oC`!C7wE!!(MxyBe9Jr?Gkb#}uKShY zXd7v(|D8>6;zeu7>E_uB#&tfo`_6bZQk<1_cPNRre1U%+Al}%IiWAetVel zFxme5sq0o|bt6A`#)WpjjV9~a+%Y=X_a?wy>b0tZUrK^pXD!Rrot+|t$5{OR$e@v? z@MuF7J%S2QcwX>)LWLx;dFw*1aP`fX?H7!Xy)P1??JfOs;7(URB-39X>L*}NTo(<= zo)ae;T|OoPEnXGnJsydCNUFtF%lcfo?6FzzTd3UEqMnX#!Mr(JRt@-E!16gt)JAjP0HgCI5dv~3#`%}H$D^M(dYO^H^Y{Vg##JYu=99E{Sp<7N;S0v*Xz6*( zE6cx+EDQgZLXnlA5YaXQsCWViJ^Vsc|4m7EVqW)koiW>4GyT^00_W8>hL5TkV_Vx{ zZ;e=iVfoE^8pfzxJ1|X1i^w0y@G|T1Z5LhT`<`a;%UtaR5-&yrhTY+| zLmmKMvcAuoyb-xxZGZ>NdgOw})s*PQW#Rt(ed_5{yC7_j*@CGEzI}L-#_@Ps7I?zr zbTtH3<@NLGx&Sq{V$YqtehvG1+2%g4Be!s{1f7Z5VqTlxN)X!o8Nzg_q)NYqO8ulM zU|?^j=W0>7tPnCN7f5-cV>Z^jF6!KrF#68<;6U*`RhMl8oF@3?7A1FVJDD)j zkCK60?wO&`^yf;x&hwBjJvEZI08Us~&D$dyd(9xmq@dSX{vLPJ1umk)bosO-zdaN` z=oj#aqsw>>QIqHQ!(nwCaXW-S|H!O31Ddn*P+}*Kzcxm}jW4d}#lIYKE$rJ?r#~c7 z4%w?VG>wL1KI&xcrK|&vq7Ge8RcD!n&3Vr7$INwG+{$$zpP4qdzQ1Ut>YslGvd2tG zm?GdPCcHf~CR|tX8gl6)-n^T75t8Xdw3lj_52<)WuhGLsJ$j*J6GLR<`4Y^;24b`4jaP#T1Ct5$3?4oOpV?;6jTSe*Vz8J?{qa_{DFm5+i&yr zOQmi`L^ko7*$_*6CC^#4ev-sd5r8-)a-2Z~N zi3z+JJ5Rind>}G0DPhW(zvSU3c*R7=x_&d0bxY@a14{O@4#<<(P<$j~JIrn)-#hgx z(x9O`w9v~SWGBoYR+L%ZiDd^K!k(wMc(U1_D?enFPE@_aMIp9Jp@s5Cr3^q9Y||D>ve7|;P8d*cE%xWZ|FZSo| za>wYYV^PG?`V(7(HgAP7aruR@a9dyzV$Sv1trz1ruUv+xM=d8@p{mP4_!UOhoc^Yi zN>&bzALr4zH+F)N2bVrvMY3J*g##ih(PNwT(SQEvi~%e-dYEovoeamn8o81 zshm&C-}zG`wtM%D@~Z9Bv7gbZv2lMEJhFSV1;*4kZ+E)B8FAzWdK5diuk@i8v(c83 zIcTL)Y7N$>0J*hnnnt`~Gk!<$7N-qaXCxO-n!7tgv}Zvz+^4u%h(Mt6pW8L8QZsCS z)r^ZaMpD-opgKDbhZS9cAsa5uQ|Whwk55-~X{vrVBRFd%67V=7n@XS2Y7FYM{Ef1< z@qw*nv{FFAK4na6{Pf-?12@*dpuj4=QG5*$7S})2Z5Cpor_&kcasx;HakXKIy{Nk` z<^GE$9wkG1weCsae}B*+HNTrjyxVnVBd9_zklYUjrz1~D4=7dXpxWcfY?t9kgL;2;xX3-3zlK>b#JV-K9@_80L+#BliRXB(a&}y zD$VtGb0_h|rKBTX%f-E~39VXI;LXvmZ3WhXW{|eT(BbZPVSqK}VC305ZBwha(4Vc| zTz(Wz1pwHcUCcZEfk*Dl#$csVv=Z%ZVQ(zuR0L7(W<QRf6floQOhfiGNTlKMQTvZ*6xN%HPu+pIsM-@! zFDAJ*KmrRIic%9c%vmxjaXC0F{4 z##tk6(p&O#k3EdNbv_u%g$F^5cN1|)=-M(Wh0Sv^w<&!0K8skZRK;z{o8Lc!S8!iD z&{}$|p7>gyvcAk-@r!920cXO~>Erv(O3m)>b|Hvz9KexuSG;V*Xl;yWA64DOF}bix zFdZE;n9wKvsV$wvw6^9?pAeY=ZDjRV^lBSKq)`@Fx5;gNtVP!TlkDFV(S*bL+*%5J zqA^hfgn?mwai9(BarO=}pw!`Cq|gY3@8JlK*5-e_v%YUV`0YQ|f_P$W0@=r24X+uQ z`T?B?L&DgmQz4#O{Cb3M(u~KmuaO$ zj%7wSLq8ftw%RB4e%moGmC|S2L~#VF@8>PkQ`Jp}97sJcH(J8d)AuI0c9q%9Pwx$= zJUPuZ(W(qCeUmOA5Qa=Fq5babt#PF+VoT8bC&X!TCb>+)yz^>9IMc@f>7nHOpNH~s zihfg;z{Tdm3*8EP`=8JgNe@u&mOM44d-g|4VE0gr&O&{S$FqgiG(zu$7B(b0)Owdj_Y_KuTgHNA$WzYS0CfD#*lg~%;auwG7lf^y_{<~6#~Oy)^A5*AgFu#)4zsrL#WK zRzyG9!Fg8;SEt1HV`?=46C7HSKK$yFzS4bjK~;de*}LNIh+S|Aix5DC2|7FE)k(fU z)SxK|U+G_`_?2QLndMY_=MG5U-jZ#ktZ%|M9FV`DRpue2l;s^gC8urviT&4sBB%6ZK04VO_Uv)JT0>ebfSME$4_ z(HI`l!l||-|3ni3HJR`;9R2yYUg|E*hLr@COzMVkElWNaXIVM90xZ-vsge`pxV=b7 zer#~j7JP)%Z0+o?7fi@_-Yltap$|+uUAao1OfVu>MZ?gO+JobroSKYn8Pgh@hVll! zhr>qDf)&t$L^oVcs}yQo#JJ_^=}2|Bpm#X8K`O*^2*&_SsFfT@th&g&G~%uvuweu7 zxUYCMEJ)Rxf4JLo=+1EeP|O_$6IgZ>;UIqN?EP0#swe-tgozzZ{)umHreP)h^!(Sh zJq8sgMssJ@cQhcf_KbQlbl6pju(XSHepar}{Z4Eeqj5BxWapXAw)+H99ye%M&>Fj1 z>O5rKZY(Yy!_+8Rog{b{M-t?8t;gI(98=0_ubUc8V1uDk?y=yiz2uR6;-!Fx|XFu>;O{7FKzd%{q+Mrd||DurI@s&B9F4$tHJKFf3MJr6;Tx!VYLM$pN?I4K#TX+X;y;iX!T*8GM(L62 z!sRoH=QGA8oZrrzLffr!Y;c9lVPvT~I&BinASXc)Jl1XRZfTfN(hzymzrn%h_;qKJ znuzJj%c~k&5nngi<;FvLu3uU_p4y!xG8A3x!MX6(_Ze1CKNs8i?_2M_OEiy{{Xx=g zHu9Gkc*1jNR~ARzanuck0cF~**$-tSo>Ig}P=VL>(kLKt_NOp}Z-9r<_Gr!%wibC> z6WVHm3p4QBTGxVnd1WmzoZ*2x@Q{Aju$|=$nPS|T>m|o8?hcI{P8x_th7pL(+scA# z0diKqY96hde_1bx{;HXPjk?(eTA(iZd(-i}n0R>O?>`^Ozv4 zS3#t#m^DpwD=?9@`(+fOlV<3{s|psd%Lw)n-Iz;PYbzbD_o>KL2fK>;kEJ-g1&*c- z>2M|5i}<1d(FqMxJ$g$Gh@@P|XVkNYcjOrVoeTgyd-BO;_>s43xi#cRMPV{cQTiu0Xffl!MwFcix&5l>| z=|Byr=6LSd4~B?d!_2|)WTZe(4c$FO>XOIEK~d^+tpwPm7fqvT-Ze#O87E4^*D;V& zcU@}-%gdX=*-$foBip#(#^AYc^dne5ez~A>vdlO*u7(PL0Pt>+tmXxBIIPwN@l0`-_Wz4HMi{Kg-Vn z?#|wRuOt@Tl#B%iQeLYbMvo7CIt;}?wwut$oQjSTrqq)8f5O<64w=7`Po68S*2DHqKGOr5S~etWy^?PZ21QQw|;PGzqniHm{U>t{KH3c?$VYdL&ZV} zCaB+SeuSN&(&9>0kpu8O>D9`usL1KFc7X?FycZ@q)$TYr8BpmEI|=Ovt7f_e>tr~y zgAXc;#5Vu#5PBVEgtu>ujSZEFxh?j3KiHn|e1#Qsr@^e87e?p6)~4vQ06TpvyEbHc zdXaLqLIHV%FlD%ZSHW4yQh^D)i7>KK@wK@Z<;&NH?5j9$+X|#fta#Q6A!3qKlO@hr zQo}kE+FQpyXo=;3de&C(^R-*pGE?eUGdQ#5+xH1|-6A8l4lD(h$tqJ?E>^vk8Ed7P z@FRq`Y`65e1m=0xjcYaBi`Rl!?UM&NnMH5ZVtBF=oDa2z?>w?CzB1^sXF5xHmaaoh zQR}VbZN2vAv2-LEQT=&-u(IF;~dulYtaDeaH2xv!Y1_;j!Ft4pLp{a(IK zu5Dee@^xz_-ku*4jwPVYxX~k8XnNLm1^GSxx)F9i`>MzV*|razyDARNJfl!Im&mTK zme0})89t>aUkT@^B`c*XG!>bHsZqUXTh06{*VZUIQ*PE@1{8$dqga7Og54Y^(UIwH z<0a{cP<*0E($%d0%)~05Jmr7g4=eBz47s;|fnDustM5Dio2Z4rmAaX5mn}Sf6Y-?v{rKH3f)v;}R-pS7<`3dR0(_9*Lf5Y*@buuqlKhNTxy;?Ru>o7E|Uj9LaO(}SpkF(aXUISYRiq%|5N3Xq`y%6 zx? z84kUcm{0fVM8t}shsMNOT!blPkaXXX}2**@;u-jp0FY9!8 zO6get{lve*Z;FT4FwBS-5?i}ru4b|MqiW60>s86y$Th=9zc!`zogflDb7o%q)>;jt zVu6j((rCvZ$Ai(26GJgqg?h2|pAC^Xw=Pl7RU>xB@hZs%yl(dCZBa`Gj7NOE!};2c ztj&(mjzIhJWe`#!p4EJy?X5Ct%<8x**SgOAJah z4k8WQ>1_rOq&MD%9PSW|O|zAG=><1sEd_@4k;W#Wg$g1)&EJmI$_Zn1O6@wTfqwrC zHWHM38W{r6>Y2$1RqPXT6i8gh_1TWr+^^9v(nGQlUd&4hMOy!A7!1h#;E;uDCBqW9 zwqnwX35f*#6iWJ(#Uyscp&58DXY3q@bIGVMzx;hLU1(ylm@34-3)Uo>W6C|`}x#_Qip;hEu z-{tmdfAsfJ;|F-Uci2NF59Ei6A5=u$&U3d`m49wrUQwXuOb54L{y?(sd7aBdB{W00ui~<*w4ejs38|z{; zxoWjqhMtf+MFc%Y)sR~>)mJR^cCAHE7%9u*t9Z@iPr*WSf*_&c!orbGuaocW@TtT& zK!P}@#1)}jzG<1hYlsdrMt81v6&1V&Tpqxq3} zHPBz^g68kN*QYA+HI~5c&-Oc*dMCLp9ZfU0>yAwc4%Q+!n?0l7_dk!ON_E-$SJ*+r{WD<&G>_CZ#?RVKU5q9B-w`Gx&lRNw&*lOu|;QJ}0JL}VM z+}erDwes}AeFQQ^-L>Cd`uilpE?i|LkL_?NW$c#s3khQe!FReg2;BKGs+O-H2KKzL zpHsnob3xwqO@NKJ=cAZuCW#SK zZ&+SG5R#K2WgW^ska z$-@HU?Mz65K*lcBK<#)!i4tpI_q&gT%wL|!6x1iqj542;hd5Z=VU$P5(r=BLB+e@6 zuJq;7w^j6|teG$3xOM65gdHWZMpOgM5G}AClsrj)oz+&C3{~*)tpLLtl{rr!;KSoN z#Ps&varh)(KGErdcMt0Ou@C36k+XIQpoyX(g`vOVYb8!BisR{6WHIB#iFZn;rjN5t zh*WdBi#M3!(FV;fC8Eiy!?Ow2hol>W7o{~Eqa3K8?nAa#FwUuWAz6>BP?;u(sbm_> z?LJP=m7-ihg^Z`wQ?aJFdhbUKl_Tyh1%9QT^$;u?vB3ZZfQ{|I5iSyiq`)yjXUrW9 z`jV0^vQvuq;t{P&c_9>sL7n1)HpWIrhty_isVgqV#ALj|KfRs)0Bn9{HP(zKVhB$< znUVWoFSdq>Xur64vDN~Fh6^Sa;S^D)y{L&J8=AA)XojM=e8#lwxG>p8ClR^kjTDhL zF>fVFSXaBRTz*pSVyQ_bw(mjDY1pho{Bq@Nxkf1l2|IoDc7xF8o%E z!#e_@sY5~*u3f`xFA+{D#uv0k_mv1vBaay{Q5qKT_BPn2|2C=8r}J@Rv9#BsPt@kb zXH=r?@DKMLix>I!^8u-B1X21~pktdbVsZ`a%#nO9!D#?D@_AYs!DxT7&p3~{>yzsr zRZ#V=0S-@Pf%DpkWuG!$@4lO@{NAwU2~HPwPG(Z5h#4#4kgXZOP__w?rK1!Fzm3Ke!k>cWD%R4Fm$S_J1d%* zbP^WkJ#UK;JXZ(VSN()>DNSa0O!9q-S!AkHQ6orchP(za2e2oOd7#TQJAE88i+O@x zKUD}9@`$5B+c}7{zCiZqIjB!)K1;k$P?Yxchq0uGO4eoMeJ3{8?_*{)?S-dDmL9n! zo-H`m^Q%{HDjEb|CWvs$pQ`q~DG6QA`v2eA?=eKS*9GWK7!SiutUS-EQH_pe?9CsG zGmpY2R~)}r9-aila~}(AW<#Nf++C{C_~xCe@VK7ujnx__wmD0oLgi=!ty9^*p=v#H@+zn)vkKfG3?sv5lrL-Nqb3jFWJ{jbSe ze`R!Ak}=&xmaZkrwOvX$wvc@0?el-KYbO$Ax8Hp4-Y&6z8zr9YM=VMXkqzc*`!2yE zLZGLG0$K+c`X=h_imf!RyF-P(xKbfM zYoW`Z`BVD_6_Sfy7GEC&`+h<$TvcB}e$xB%2tj zo=s0HPA4l)@L62*aodLEH9T$tColc);vTaQu(e(5Djw&=#xi?p{W2Sgs_3TUjs=-P ztS~+-EXKPIxS-Z=R?|Hefi+e0feOu0ti*j;2A{{z@0@i6VE5H(cIXp#Swizp{bl~3 zT}|7jzt@p1d&|A*U+vLz*OAR$jh)&`pV~S{n0I94faG|)WHnyV{;Qt;6MoDUA7mz3 zoc0brA7Pj{LFL-fxx-WG1-{#i@iVSyg$vgfC>@JBt)Y!bq(7b~tR1%^#4l0--Xb)y zvWF6gqagF`Fb+;Hn_3@wd*+4N^AQNJVN~P=jc3GA;M;adYK0eAcHgkzo%WeOqP^Fc z)oXHg(KrNq;a}k*LqjcX^-G<`zv|yQST6NNiqGJhb$CEK$_7=t}~EtwBQwFg>DZ$Et(KLf1Q>&Zw(>#6e6P4P{`T4!Og z80npxx4${@nqbw`Kqq-*|K+o8J15#g37VIsLrGM%A}eMN15KSVG1IJ90iPK}9aA88)Hi68a#h&(uaO)$ zFmaEddFl4BEhO;ZEQouxrWDG*h$YmbS&)%ZDlTH@e6BZOjg-eb3Ss_NHK5VD$0`au zVn<+0X`|2_)LfuExp8**%$<0EXM=Zo+$~{T^nX-?`0ll9)qFYOLh1Rb1L z_Qo<%3^CmlC!;;&G9mQpqtTRfD1pT_jGPBFTzl2qe6NwlxQ+ zo9QKA!}Yj0YDi7?y=U0bKt{?IkQqzzIFZ2~Y+u<8GQZa8bUM}u0>RHcq(sT9jgz9@ zHzABRs-An@qvP<6shv5$<`Lh&Znv2aX&Kr!8Tkhh1rde3KbVi$o2&|iY+~9#9%ZOr zcf^@CXA$;|vnm}1HRoxJ8Dwj~rH4_DiravutIQT0tpStmrN-80?%l|ip4yig4~+sp zM62`Y1;8gk$Y$J!$gzLuhxFr6*P0)c)%+@LhJ^^H(dgn%HKlTBUuKTObMtJxR2z)% zedBU5nB2gm-F-w&c;eS^{b{{ zR3c;5GS`XCUn|ExB)P!9|E%>!_II1K+pCFFon70~BW|FGr$5RY9fBe6Q$GagKkpLN z%{ZRa+*sfC(L|4Mx#2Th0bEOsnA6s0Ye?PyP>Jo|}? zh}pvjC5c;yrT;M&g1Y;XtyKG6`M>s0d+}J1=3U7r_w6RZLu(CtA|bZnjK{jH*fZCh zKSnRJd@F|Kgb&qgKOn)6sVNCzjWgzMAEPTv2p9MBERB#MQe3W6d=o)YT3NL}6Z1=- z%Y30dlz}EhVS-wA8~x?XVF7p`!IbsREmG~?KdIgUHcGdR5;kUG0OnX@Vl?LB%PlIK zjUGZ6CKuL6nt|uks2+9E!q?8~L~i~7ly=FVes3uSZM*)|0|e3bSVBOq>FW$p*38<^ z=&AV`?6-16PdjLKz%|cc_~)M?rA5&ab<4^}6!=pfI$(@AZEB-nnU5I|Y%|IsX2=QK z4s$ZRzMi#O7pAf6J>Gg;3urLf>0)|%SkizPj~h>+x+mJcFuG028?D`bK0!-;>7Ta{U!y-!v1z{JjI#g-e`E-FTs6fR3MF5 z{8FWb?JZOu%>!ysI#Mff&+a_Pr+u)ur@IPSw`L8bf161660Zm7r|N@P%764DBmNS= zGmZ$li%|9BmBnnBaJNhSHBp%JG1tam}52*Ncr*m*Zcg8fqK5A z%0Y7W1?cv(%R`EZ&s@pzwD(=2^aQ$R#2)ER5+%4gCa3+g9pN@~#eQFL8$K{?_Vwi^ z|9)o`%D&-3{qKMKf2AOekY?}T2VuK-A_k;f@_$_+jSz*6kq~TBD%*d-IGEJBsz`0d zpMinF?Y!I$5P7(1_P57lI^=yE{s2>Tvo6zKSLrLQ;Aa6y$T|NT{PiXxi=s-y+P;eL z(j~*$oZ9bRag`>OK1D~9p5#XPSuiBgJf?sAm?yk1!ez+=r-X1Qc$QH1*w40sB%uAX z^ooc0)Va=F&_GC{Hrl@E#Ul6a)6`w`tE~sx812pT^W7v$tMd@LK)qLdH7f~MsPL%q2?nrBBO@5pa)qd_X z$5ER5JWOjq_jLiF*p7>MJJs@ddr|=ke5WRxvA%P_DYx2I$&+Aunu)ma?S*BB z2a*tq*wX_{N)CfB2CZRh@b@6~%7>Bu5gveh9dNr^l?UUv&YE3>X|*c%P^QL}Zfxq^ z%M>NgPKma)){7<%{+LxS1kS>cW&;?4xQC=6+75{>VsCtwsTwcrutwD$05&QjyCGQ7 z5w+tI<93v{;ooBfLP|+2Sm>ozo$r%JheYgoDn*XT%?ER3OH6TAA=eREDXM-a;GI~X zYaY2{_s{XsaA%l1HrcyOAuDrR=aO-m)mpJ_T9woJu+n#hdcpr`$ z6@RoSylqsaAsuVl64-&R!J7-*&8#2c$jVS9d^!Zlx5)<%jJ2daxvz9DGYC8EEFaYf zW1C1+PvMqs6D({UupS4L(_wr>C)h^ZkAJ$8fMe|>_EXv8y?u+YK5m@xe#BVgmVoS` zT6ei_TH7e=GBdJkqHzzCL0Rpa2GcJG@l)JIA7q1EkvlEkuJX_sbmqrz@oTQ|`4&Qp2U zS5_|lY6Q8uKi~IfJcn+MFhC8`n+e7b=;c@ayM76SZ({%UsFVeR)6%%-&B;QfzP~wi zKsQguTy6L!F7^pUhtKz?O_*Z4Vy&P>xJ8!j)W{?#x) z&={lVC?HoTA6i!Ve+(?vF6gb=f)?&BE5oZRf7!D`ztgkV4W)rp-c)@6twCf(_MfM! z#KSnx`%CCWLQ#>KY`P6)h^8Z^?2&1P$Bj+_pgq?>*)?uF$aVWPHL=QZHN(VrrYFQY z*;?Repr`-o59PSUSIPOz(|+o2#I12hD(&UMH(WiVWnC-Y1lo)R$-uiG>Tc6=17|WN z>KS`m5@#92Syh5()8J(MQ{E`m%js76OJ}ODogp_4x1ULu)GLw!Pv}C*k1Bk+ew6<# zQ>pfn?%s>Kmd1y_G4gHxy}b`NEpcbR`rAv;#}tXXeEB zj-!5GZ40!knrx=S2Fr6hX3VR?hy(I1(ya4J$vg-0u8_Ep!nYY`Rao%%@%TcEq_)gv zKZAjJQA)!gMc|@)Xm`GW8ZZu{nc#fuBMW#9!KNbG>{__MwI%iP4;m?dPo&Gb8*4q+ z!(k0rhi*5;L~(%9v71i|KG81g$YaN}dGpYM84 zV`K_2o!2vcYf|HgHeXD>yuEQ{7iew=?m-mbn({ zNWYrBDOAsT2)hACR5o|@ZQxY2A(uv*TAQXO-7$ey&Hay`*geI|i=J~_RciRTKIJ-= z$swn-1-*m=UmfF!QCuDqLEq>))B9k?aHVeal1!sI9G`fD{X<5@Zw*WB*+Io1a$t$E z{jYbyT`s!)pOi#9OfH^f`nzcB|9iKZ_%DNr{`F0yi5n`~@lqP}ze5RW18S2XjGsY} zi6bVtqTS_vxD~N(u z%#Zf}e!+Pq`W8x)q(M2)zvjJT-@7(@(%cHGeDPxMezp%Ticl9jspR%X%iM+T(zb&1 z0umc=$3j@ARNKUY)a?{rq{L;u!|C!7tNOC4ckDih%GVGvz_^Bs3Mrw_3wYWi>~va$gy}kDtn7dx>RZATs`~!$syoU*o~jS zLSgOp5Hw;0KpY0zel$!Fy+XEQSk2z#P^$PWd5u7Cj>=X=c5ld11-y@;>^d*AVy<#N zy2@|I4bVLdJzv&aRh*U#{;_+22KWXee!3TLx#LafCTVm`E zc7L~Ox5<@kR=!ordOT?T-jMd^{IA!_#fEXxP9-qOq>Oc@pl}4oQP4TTI^U!8)+hWY3ZNnx-Bi9rTfS3@PWsRkA0)90vG11 ztv6ave|-+N!iWiXdjN5SapdHLEN&?ux?@0mDIUlSU_;Ia&Whh%d<`C3*U_gSFcqrk zL6n~QB$3jXBatR?!H0quS5an*l>UL#F}oXF#SJwTOp#MXo|4{$Cy*rh8BULnN2urA z{SJKFIv6?(;*f8HY8BF41k#8Rs-HX)@J=U0%{NJJ-=hycd)`(dMp70W5~sa1+V}px zBlVIv%&9{u8$(>Gf89$a;Wc2!W9{4omkQ1)fTt*sYJ8M>mg*8FnC z%&D!fI(Q*`TV)Wf8KSb6n@p~hAK!H8XP6mmx5E0bp(D!TGOV7?Il70wXvr-rphfnzItiAs*QW3dl|{$9&yC$68? z?wn5UCM}jB^*no=&#byNtN-AN*%qjM0X)8J^uYck5)-TUxgJU2b_v!Fi36<`6Izt7 z=B9`5+v)p$E%;f>e)`QNiMnsaNjjVF`Ij8s0ClVlF*SGG=?a6E8lDKIATdKxT<1Yk zeWxTUL!0RR-a|R zzpqKN2yjFv>2mlbS6UKI04_5UwHi9_TJT7FOI>c0WOVupT$38#aLI{=mh&!Q!R*#V zwEK@d5KeUT2B6Uuk&I>%LVhvQ)%f53}VWN_@OC;v{2J@qQeq@m!S;9h)!349wAIz$i9Dg&>CuS++rCC$lnebdJ zRItE!lR=Byy`$mm$^6sTg37Gm)BZ!8gzwhP@5mZy?N0ULGa#nyE5+i=Xb@v%A;ZMRj)UM}7 zyzpXT$mxoD)N|>U)Dw!oPc1mn-{lW_IO-sMJdX8HOgGZ%W}Sqr-|#<&Drh0w0_u;( zkLs-AYgmDGA5o!IV8N+KDbcjB(fUb{MNfG}#lnZ>@CIx*DpYBC2IYI0Hu!#h?Ol;WKuc70M&9z;!j5~~b zZ{<BfsIWkO_$F8D?_X_=61be@I()95x zGm-2jemEpX=Ah&?e zKnuT|s8ZF^q>X0@zIwtzipo3M2VeKHJ`6Y;j?96KjjU%W(-!#(;P2|Jm%=YKOB!DH z(CO>3t5hOqOup&I@6c~mU(s)J9Y!djph`t@$jfrD+rdU@9ZG9MiRcb&jlOr*SdbEr zL*ju75}TJDS%hGGYa0Psj{pV}XHYz!pPk4?&LlK@dbGYNvIw$=L)UQ~$^<%zS9f2g z=@sq9&B0!(Z^{@J@p*e)9;x0p zg};!LFrQR&CZ`~8)8lBN=|y7KOz?HJO^(=MN|Uk1oPmx&wIc@#=9}9I{J;#%LJ#G7 zk%BIghYJ34FXHS_c~*|5|~->@YeQW&nFntX@GEQkccs7+fPyp;6%gqlQbX@8Aniv7=~4nxl`19l1PQ(O7J88$Isrn$8~^Khzh-95%*s0V zo_+S&``qa0XeY<-39Hn^9B%=^mu^EPtdt^kqhcTOG?Q0)>$L(@8RU0E<+-H}8SZg^ zvx&Ij?>(j;KP+VkY!&CXVrNFg6odGX`gt+pI#lyAI#MxjCPrTRr10AwfJ9VO6RsJ`Le_o)$7^9QK{YAT7dYIS zv>2bixO8#=2b=cr;8}Y$=ylMV%~*BSv1uBGS$B+<9|ql7{+Nd}^o2h;X5jzN0_djb zAncaEtjTqxO!QY|c+)-Qq7NxdVGnzYhvGw8eu|gP=Qf>vJ^Lx&dCuhlc3Ap)#W!TM zP$%da>9*iIQ3yt=W4Cs65JDT2*+_AkUqkEOOv(3em2--&_E$~qzx^6U;Q;I9&ccMd zEv<)h1_es_C|Zehj0*|ITcX@$I35$ehn!-$%%gUCjcI&3pu$G3S?iQwCh>pHPNLnw zV)N}rZL>9UADnFb9v6!a2|p{con_d-jr>s#H0)DvCbEfHD!H(hc{UU@PSs!_tT+oC zrx<_+G~F@kvwq{S(D_0JuC9VLKBwLwiUKR}DfA?L$vyY^{B7Oh<0iR}&$+0bfCtlK z%Sp=Y&A}Yj4ZGwXsEa>-tTI3xvHZ34(^lBfbJlb}+!w^b=V;zNfn)9QKmB9{`c11M z#vBy5BmLLC`m()LN^{eyTEk}keixlCHK*^1&o)bD$>H-HI5O9n_^0ZSU{;mKtP@rf z!q?NviHP5eKK^7g-R?}lq69kE*C2P|-CaH&wE@t|_*K7e<`msjGqBIOsJlCUn@Gl4 z3AX_nHuT2}B#E_Zcikl`L3{73w)yPJq7sWWQ&=)z*6R%gAZ40AIISYQ<|^cR6K!y;3H8?$ z=%J(=FBZzFzepYjsoEz82(xE+&_iC}jxTrLvTq|vg?EgnRNv){y;&EupQArC0bSnO zImo;P@hzv8aprSzkW%k3XdX+EcNDS(;IJi=;!Eg^;2tX$*T0j+-B+vy+Kt7*@|)UU z{$t&w-mez-`#o0LV};S?A2>9FN1Wq2amuN0sE=%=uuxW~Z94Zh>jZ&BWkx+ahuN&M zmSzkenDKuhcf-`TIqR2~Q@4ueWaz8PJV1SVquENA#O@yofnUkA>jsY7$`Vf_UR zbJMG-d}n8|A7${f4W)vKyn)bc-MLM(>$_bT{zUv^#$CD`z_*KYuNUW6->oH8KkS~- zy?#99B#-6O7D(1+$~iC#B4Pb$-Bx_r@4XX!l6|`>n)t8vW&BBY7iL53ad1^jI^C}~ zyukz5^M;pVC!L$?n&_j!1~@gGg?isCm-NOJmtcJ+Hvl^9^KL#|84%fo9?o9y-2tp^ zK;ucS)3xV%Hm$GfGa!{Do+;8Xl$SbFbHC~&Iyh;dQ^*kkul;`O7q^q-|45L3Y8xN| zQqM>z3oL(C8ZW%NbvKx8*w2+`?yJj3o~!TtM{M5XFJYc9RW<=I*P^(#A@ z>Ah}mkjv}Ir%g1|R=CbK_XpF3vH3HMT^V(FQ2CA%4r<*FN$J!~373SXVG*$?td{FS70McDJ zf+BA&%TEHS8=GXg7gFE6gS=meg^EA&ysfndt=;$7wPl5?Ehg$ET}w^w^BkgWgP6u1P@+NQ#VAe%M>PeSRfPLy$fho}& zB~(Hhbl#y!JOm8PGWyJD{AyoK+F7-Z?y_1lW;Nb@4KS7mH&2#jj`NR5K1=5BWz-2b zZ{=PJ?XnHF)_G!lOw%pG8ZoNkF+fAHCJt#)?eW9`CG}zqZ zGQG8+?4I?SA&BVpM_>Q~Tj%X^#}9kWac$I||c@*W{drdvh95qI2R>a^C-RW2Dl z)**1l$!0H?HJgD`n%+*2|IHsCAc!OFd2t@yT6eO!1>|@w^?kg}*IMVhv@deYeS=I~ zJivn?QRmzamj+S97(qLwQCg8~;_gDw+KOu8&hm zPm*wma*#HCBG#AkV_l|=lCryzjmNHsbYi#;kefhrtt9m=@j^0?xVB4+Ju4D-=l)^1e_>Zl{ zx3;GuR;g)e3+?S+N7C<+9#+!`Y*ZPinsQmAo`m#K5)-n~&f}m>mN%!r{hv(uIZca|j6Kai8>Bt}gkLS7)_==>p*Ro4#JDJl*{QTFszP zmmWr)u1LPsi2Hnmz;-)*87QHfxM<{EO2l3{fu=dQwTS9PiAwsuWN)&U*%A4nS>h!( zX8q(j@*5LfE|~eh>O02%BFRUG-Hg5;w;A7+k~xmTYf%=Zq*eQ!Vp(otC8HE9dDYI( z6OR9tjD*6<-#+Dk@yseoNqFI_V)9_ceVy#{r0rV8NZK<(tOHV3<>L$4GzvFcb=A@? zt!947BelMcO}VUTDCEB_`{tH;A{MMG&9TX|MJ8{*VHtg&fZFppf+W{^Mmb?m94G5f zZ1iW)x`jz?f_scRTXbt;kh<04Vfc!;UG@e3D1t=s9rxSd;cT5R0-dgek%v-$Gl#X; z6?Yp%m;5~|??cS+0#6^zDMTI%jA_fgSW$wD$PZO&_um<=2=o4MhMC*JQTA7w3_NvD zhyFU+ba=J%EucdJQ8maW`yZGu{zL39=UsYYd$-zKjtURUj-TJBaDbq-H}*bvr<`Qj zac|oc&vmplxEtNQsD&7~vFh8rVDx;*@3<^u?;({XZJz*sb%#DZ@2^?w^fV{z9xZl= zv4X-TKar~6c$(#5uy3W1q@ZS>_bbvv3(N2GKfw2E95!1AKS6ZJk+#T|xP6cID>WI1 zNe7RC$}I6FonsSno-l@6ENsLM`*92|9t%%VNoqS}Z?w)V-xVa<-wj9wJB1oNYE;7$ zYQp;qi`QfuaTi+q+8kwCv=ywU1ZS2+;e40!3+py;%QOABVv68$YpBWWqZXnUs=8s% zbi#Q6;~!1B#u4e zW@j6_D3v>YRjXXnr#snN{x-*9~TqK9=8D zDV)(!Gwf7=0FM27(DJoM%V~OmhZDS$#p)^S$-*Au+5ZA^JcJ0#`soAV`>E9pSK-2c zncvj11`kTU`7_-ow>KcQ%J{yk2*Rul86_oNmguPyvi8m0Vsy!29ZE&n;W#UgB!GI! zevBF27qtfd?76T*GuZ;M#Zw+IzqRZSH6N-7cG~VZwi7@lankyL+7v7_9Oq*)c875% zB6dBmRb3{0g}#`7*f}>}S{`x8?2&vi`y%l9nE?=03%H~-<^af3TpUwg2+Ah?R97xk zu0Xn41~A{_;-T#aCm-AY0_mgMioaYk&JO!mwUFt|3?Bu@Tg$u9@F`jbk{`_&m@mSf zcbC_Ov)QjpKDZ@I;;76}|4a`2Ke#@(DRvzyuxB2RDu7Nt75X4Xy1Ga$!1V5<+hcjQVcD$3bv2m#NHdKWhtHLK-6 zKWXckn7EPuc;xSe?E_qtxa=)M@CNeuU5yZr>U2eVk_r($xttq>8L)-~nP76aLO70;*dOIW`E@T`uFBEulGvs6M(F<^3^>czHHz?t~ z`^z=^gSP`Ou0)QDvj-ktra!aT0sQ6_6;Y#{jXD1D8r`o@4k{GDkEH||1k{E4p4~nJ zjnZ*$LBD@L3}(gi#?FH@D)#_UgT-`FG!JwN=(75a9#v}Y9Wg&d2tFHolYO%~;QEa~ zTngELjDJQu!enuMvEKGf$nX1~XG0gS-`ud@K7Y+CQ4{~LC($P~ge0&5w)6$t1St(T zL-fM&pg1keg)%NDU?T0x?Oq1RsIVu=i~P#wFJ$28R^Z2_Tr-EBF@(80Qdz#7AZOth+O<}Yw=&PjKe6rOr1F% zm*E*kRCs7`WqvchG)(t2)crPU?(rw&+Ao7>K;+{n4Q3W#&CEC|+ zCVd=YlMo8U$y`%#l z;@!n8!uA0WJ|75YUIxp&5g?ys+5zXol^DnJ1lE)xCLupuC@-I7R!PahqWOOWn8FN< z|EkN*NyJz;A>L@!n9H(XTPKNn+s zlE16Q(dJ^4fhX|VaNED}73^pC_tQfOMvCQ+6Hdu1aRL-qybFp&KWT_jW%8 zbcwUturUIQCnM9s!ersMdgMz@W9+0PLeJACO|O!!8oas1v)ryS;+vAYCURw3C0503 z(w(j*^Eh|u`ZJ{`-?GjP9#as`!oD3kxVlI!Hsm}b0XTpqNmT<+lffTrPURWpzLYC&gS#v0f|D4v1v)7VM|706|c=~j*l z-g061dI%wu9KX{`ydOsASAd-s!TVssL6FSmOY-_Db;nWJ4OP(k3i|3`X)n;fTidR) z9MB-#enz%%w$B%(Gu`#$dDB^DAu7fLW*GX7e2!n1$I6re71eI`7Nq5PHuGK1msEto zp+DMV=P-eXhxC*3i?g2+ygb<>t8r|OKHcxoR%->|Z}_uVqASj8X+B1i%7ZKcFD0|= z$7V=x2Q@juY!O8YP2aM)50Ipb^p5lr`;vb(;epB})Cy(Dd#W-c)lqT#gq+*ua759| z#;TL8#@eItEQIKP%+A^SoW9-|4pAHE_C;yBqQCvD+zhxhP<+v?gS{!j0uw;i>0SxqIbyrs>kVPjLin9v;cJD~ap(zDimxG)!dg zi?$i<16Euhl?`Xpd~0Kmp9E45)z8Ay!5Rir>|zx*suUekgc!bYdHW?r%IjY+|9nN% z`qY=Ei4ft&Z|B-`$_R{mrn~Tht1Aiyu0yRg?PFB#3?EzI2`_PaN~QT%a@J19L6lym z40t54dAM$VGdhiB;QPZi5D3-b862k_n_<67l1ve2)8P=MC*6Y^85^HrHm1rATPYnj ze~k(63;R}-;RZ?L;cZT6#n_Zr_;D#3^2o z`NSDK;y5+@XjBV)}9k$8{op6yKOsx2Uap;DyEH=+Eg6--eg$1#ON5lur zEO-nX122X)veAqpJ{5k+U~b=m>MJbY6mLb*uNoMMH)HmZPB_+I|!H z3!K`J+~)I$B`+sjrdJ~m6q4tM4%2q_LN9FsuTuo(H!I+M+VEEN7}RYEdjy&_-^g63 z0pO&)X2!0&os9PJ=kEbt@_HU=+y1j$!!DcIM~{;#GluNvO(Dkz3yB{#S%h&081%6J zm;yeJPO5p>!xV9;g24|%`T~aC#}&4}b~kJ){H1g}7!&rIjGb=0Iidr&3rxxU`Kx$j zdh;#q%75?U%-_Z1sObhT{F>^*0v6M}DUY`BYAw8(tu#?%Kqp#*I&eGUIR_6n>zN&= zn-LdSx&{X9qub~SKj^B-`Kfht1ZdgaPstPAFJ-hTEPjbkAu)k>pJM_p#i5%+tv4Cy zt+`*#*M|Dd7!$l$z1{S0x&6iBCewg(>Uk+aD8%hjb%ErK%;2F9%x}iF*zX$XH#of@ z2Ri5Qn%cH*GQGAQcFlAUJ^IE2oKzU=+lybq=%e93h3GWEjbjcz&9knL>uzs;zy7wd z)=myx3#>kH^vWnaJJD|J2)MS_Z`6itXe?$uleaXCzA!%%Ph~6|EjM+I=R(;lxk==# z?7ZJv4hmp;^|8ssFySI0o)4)VUfugk-IfcPbnrEGvU3LNpP}`;h0r%Dd64AfoQvn^ zc|pzhc{#cSL$hv0I2CU5NW!UFXyaGitm}Q1$8ecG@2hyF%X@h-_iptORz~z2p<8=m zHfDsS#qIu)PE`iX+$C;g=>DfGupdUE)ZlAAaqg4frEGA>^_khqM~6@nnLbw}GW-*U zLWMP4M9SF5A1U2m0G2>9ST%<@{6{rLMYCZW=5w#E{*nHXjJJ|rEYJM5#IwINt+sNE zzDXzX{IJcq>gkeo&N%yC6{2%3dtTkjp#?Db#Y2DR^_yib3Zr$?E~oZ&!@D_`YVv*? z5;5^>V%MU5{zfEIsB+BBvhGb^_7~Zkziv?1`Z>uckXE+7>2|7vT(180i8U&8Ib^{Lza4?zU26WL zuus3)NWOr1?sch;*sD2zkU4`*8*ZHl*fBKqGHp1|MDuOtZ04N)M?K$-IDS=+gg<0{ z0BHuxxZfll?89bdUIS|67tIV`piN9XD}Db`FZ3lM(f!kb{n?Ny<#`!QGs16vmd*`| zJC~Z5BIsBZFr$o(s`6Wc7`4o*b^|)dg*{^Bd`2S+M~f zh?s}mj4WM=?BJKD1ILej+FMV?0d5$kqG7**PRE*A?0zP~8pX9QIsjOPpW!m+r=6Pq z1T6Kf(+#99{1h@3H_Tz4`RanYxg68~#0;Ei#YAa0XF~il{lZd4w|Srz8bk&BO81uP& zP3VKBLr&8$?!0DfbOZxFUvH6Gmv`+$-fKFMdn0hJnZu+={@;eQ$f&l@gcYm(bqrSE znzXAslM`Q2L=$Y%pUkB9zrJt6Z!R7Y*+;k9Tzb3In)m$g)OzU74J%qEH67VXtm$qr zzxhCsi#Btm6A3)m%{Zb{P6g5>Aq z&oU#yb%e~7n5b5>s1H@K`L!hQYldysAep1^SZbjV_=xaR1=n!+k~e)m)y^PBtr zDwVpAsaQFK#PPMatzx3QIYc`taxK4dc6vKW`0RG zoPZ}ML*6u2`XM1$5p+%frU5!1kmff&jQ=LLQIIsh@3n|V3eJ5VEnPZ?GzX+Uod3b* zdUM^a@CUbG4MYpWQrt1mvJoD$H@l7v8-6B%)4@1&ohN$E04B6<{Bx*s@xmB0gt##^ zfg>ONVg%ufuNfa-T&)#qw{~#fz?T=zOIwzm+2;%)XCCNLg5YA|0txU&R9`?Z#x}u| za~YB{ur1)>w)RC;#D2m!2!Dv3k2izN9a%c1*9R^up!@9ME6BZ?$SQr*(}meDYhbU% zZTx}@j#5x856wYIuFoGqDg zeg(xZ&9jA%AGGd@TvNT!e2j*v^$1kl6k!MS;d?;vN=3bzQdt)EPdsEDztSgg9Xii2 zZM+7wy#MXvQ3OLMl+Hn>-{DW6T#Puo{}nTO1tT|(8&zH06s~U|fCxl)>&hi)t@5bt zu)0_t8E~k+oUt5$D-Yikxe50>MGl>9u5+-(EvXa+9^cs}TKcd3?$l4fo8rp2M3C=IOoph1XVwzwUk~*sO%swnbzm zC{8~Wbrd`SI+{2CrF?kyjj=qwO%Ho$@8i=8Aqt;yk(*`E?*Caypt64KvIJLYS0=rm z;6W4?zN-G&2rro*yH~oAi@Z7fB#myqvglJL^n3Uz%4V6hy7vUFvhGCmx=6!L#LB;! zFnyW8KA)1~2h6?Od~~JqTOli}eO(vaO>C?wz@3cjok*%=TF7a+-EVpc^FM4^ zI~n#L36j0$Jq){2-Qw&!Ry~Orr8#&GWd;d8*?2fSEw@@bv~|8u5Fr}eDF*WyY}Ke} z+uU-Tiqbr<;3ORptG49r)4P3{`=@uiIBVxUu1M?4sCmDlqufP153qAz&YL&WUzw?l z$W4;iD_pC3!Dm8)$@pp*fft(f4ADgIn4yN0r1{J_nw=;Tu-X*=@Cx)iG8{=8@A zb0n3l=G*LAw=}<%Bxuf^n7ikKhRj`*4Z99?(53I4aI#je*!W(dnO6hpK-k)Kf!3Bw zZu-a?`VlgA^VFD<1$yc4b>p|tZETNsBIJO^8nDKYwLCf!9bCLPtDmfy`JFqqHbPmGu8b=P!@aTvr*S@i8L z_5EHU_r+JoEh7yt9KZZ-IxX37O454#WZ~_Qdjg1N-omGgE(GQdEm39 z5qsikZjnzGIH0d0)N10t&hIMYW1@m%2hH4;v;4lA8PDuYGKR_f-s9$i9MjUEGY!1l zXRaB0p7N4WzoFmH->eS7`ZU6>*=OvOS$r(1mlI-hJGCq`^&`8X#*}sUs?WgvoLR23 zGm$#LN~_jSzL??bym<>LUPuf`6LM^&z`YuG)hi7PGtvKIQzo;)rA;mSC!{Xa#tQS@ z=VIsXd*twRCIN|39~V41KQ$a%;PWYWLLFyvVF7wIIRJH zUBK~}6(xC%L$TjpWj8L@6&o4A<`)XN`2oyMj4UtWu-hfEY4xl%lRra-!dXUcyn8IJ z|KqgPICfyQ+x`tH%Lm11kK1)(v~7FG)q!vq^MGE;*vQ?0fwcvCSauAqx7ZcVc{~AP z;)?3aS+Y?{%zCCBk*pn#`tP!Mq4D}QnJBJJz^nIUpPBag0mX;KG^2;0swHj7Z}mr< zaTl+1+Ap8a?B2Q@F)39`8uhG0b!8eT_U(^`?I%d~Un z;jbJ~?JXcn!Q`WuYB0<$_Ra&q5EG&9`Q*CYEw%OY7_I8Hkl|f$zUCk!?J(E?Hn-Qd z<}ZFzmYjbgP$U}kth>^7M8_#p8!l)#-$X!z!r~3s1Te@aA8erJH9+V5{x)C2UJrej zp_bT%4UucdP!aMo6C2s2bDMkdHjWN)EV8drFN5*PtZM0km2`wI<1mZ;5eC(f?KsK2 zaN=96r{NRg=j5n2Cf0kP#*D*-5ZAkoiw>7rv&ZFzj_!Wt@6TbRY&7rzOP_n%ht#l7^rg7Bl74$! z3~WPZ&3wkKstDktf+e01Rb?3$%Q?Y{m^q@MQZZBo=$sJmDS>VA4$zg+FI?Bc176EkFoysM98Dg@J;h|_^$c!XIGJ!uG4pqwzSQ9 z`o(@zst)))*Ahp{uKcN{ROcPvy(|_RcUF+WDL?vKS(Q$8${F}jGGW+|CAj8gJiGB( zlFMR{pjnATL#jLx(s|HDvGp~MZRyE`}52U4erSeH>=|8d~PPKM*6AF#L zvrGWEn)dscaZBt=fRS-CFN!HiC!BQq@pV6}U-dUm`@N7ZM30f+ z24RgE%F?P(8ORyU0eQAl>zBUyu|_OU@4kMkbu2XSvvFCzj0v=x})0g?7PWl|9fSOZ;mbI|9ejf{3%^( zDh4<17Y}H8ZEJL6%M()WcKKbw30k$bctp>F7F)DtW*Z6goCSV4FW687mYNDyrmL(l z52NC$TrN{PwOL5vNn4%qoG;~f-@hcVD`Q?Ft>@ga!-xJ-_ea+|6EaNS0c(F~F4jP! zm^if~8Bgv+_5K>T6PMF!xYKX?ooIY`ELvDphOFB;hiu31j_(68D=DzLzBk!MM2q#M z`Bm`zu7*$G-S(Q57r4)CyA1td{+gUV&=loEjlr$xRG)#j{_@b_;e$HPAlgP{a=MwR zD#Eyi9pxp#y8H&^R*l6R z_%?@W9A7}e(HdsQrlwdG48L5+{vb?fvwFo9KTGE^z389R+&(m!Srfc$54o~lc<=B8 zjes0Fwcc3)%pLE5uC#-NHm@F6Z9oT18fp$8P32#j=ja;P{cp(ALCV01g zul=~Fd7bc^QuUuTo)3ygI=mVJE+?WL`6lLXW6dn1>!M2~>FZMi6B6PLvZLJ$|JP96 zHTd+u7C!~?2iFfqvd2jUwP8{p{&A87VR5aH@1e^*)6*(M?ydh_!5I;*5GX}T(zl?a z{C@83E%OHe+pEn-_TettR-lU?cT(@)Vxb}SKlR-!0jt>pDp$(7Mi|cI^8DwgUtP`8 zO%~?QJlX;9!RrJ9lJq?8+08t?V>kYvX`+k`XAOgCD#_u1s_~~}{9DkvTwojXqo0gs z?=~Lpu!TAekBwG_7cSMwPw96#Ex~O{S02vy^+m~rE;Otcu?KI7_x-HbC!-4X2zeoW z8LoMISQ;3=9?c82B^$MOypF^E26JBar^$C`TO>J4)9o?l_(wK(>U~2is zpMPg;naOU!e1?3PuLPdlM)P>WP}3DRo?qwytv20Nw;C`N#^_C9p9)l~eZAj>!Da&# zzEw=9$>F0gjTV%#<7TQ)_Aumk^R!yzkZDcLWbVoCw5gn+Ey3Tx;dLohd2X8%@RtPl zFbd?uAIFVGpsZ4KA`RM_|$G0?2pY2$!*WE zoL1QP{Y3O7Bk)f^?~q*x@&&cyq~0uXlGp64cf2%C)9dJV{1Lv=Z%Dg&`Vp`=%ijsa z5s?xs5JB6FRHen%EZ^N?&3qZcnk{DZB^rF@vzS5W8U3aIM|QSXHdx9BwcypPudo}2 zUOf?U*OopQMQ> z{nlGFiT*Nk1^=CK>rTOZ-EmgS2Im&&g7!~%Iv}4pCFz{RWG-zrs#jWis_|Kr=zpeZ zQ?VhZ_gThdKecfVtS>mE0HZ4Q^xDaZnby~COV!UX-!`4{=@KGzo_?nIG%if-U%qhy#Uj=-}2h^3!#us+zUJ=zpTB?cuqy-p5bU`PO{(8C=OA0*_# z<`;y!)^d9gfi)3-q<55(p1V{If?+>pns2Ih$dhOBt)vKF)V88HHIL6}s)haJxNV;&tj^??qPxMEdJjscO1Qv69kXxPvp%UZ*20|5cV zA)JqT-1AzxRZ;%xgM&t+-+`LrV$S#mL4nlFcmlQ3{9XP`i=s@5wCYyHK%PrCQ6WJW zM{-}@$!FRMeJ2ecQ(o=cikwQ+H}Z=0o94OsquPjsMfs1Ln*0r1^AYj9+4n$f;NbQs zY4|1E!6_>CsFl8KUBV-gCk#}Ha5>RJ2TC@cZ{F-IU72^q*5i6`X$Y)6R((v*C}T+K zX3`4nQFNkxW2L?6gb*L?JzS27un~lIjVaDqm^$W5B@=U(157&4kU3~iEo!lVD zyH2NpSX%blsv9Jmtp7~FkY16(89ifzRLdckN%%DmsgGFCu{L~#?{hO<$8WVlw(&UZ z;))42OJiXi;1k^3y&AanOJLr*0zQ_;C+QfB>;ul6CK5P(W7ownv_AgQZNa{>wF`&G z-i)#>i1vD3D2vtJ5m&efW%{4O|K=q1SfAcJ#!XH zg#y18pLkz`^i1k!&eL^`Q+)fbj3frlSNweybEN~0!zN6aItZQ(Y*Y-kvM&m>gJBSA zfLa3YFov29SUl?HF?|{R-lmgrM%RUDn)5TmC$3h0^Ivlhi*-NfuT2Jqd304d{f(Eb z{*;+RH*GkYc7eH&b^;$Equ4~|A3C5uwK&Mx0k~O_8Dg#qq(qO=y8pi^B^lbXHNn{? zQfO;KAlQX@!GAO!Y~@G$T*YS!&#CHaXvkgn2h=9HzuFIAJbu=CTsalrwW0s7dD}$) zqP4rS$E9VhzOSALzb7+t3j8a~OT#1ECtW=kP<;NAm1k<9`bwX>Ta2Xy>D?{L57%s_ z{*;W6^=0XFPJcm#1M{$Ux`>z^U?E_?ySn&<47g@kPH5c_}YYjY!19CR1Q$W zuC-Z#rICtpO+&7B>KPR_9;Am3aZ7!De->fMllcOl87XG4hyNt<`wBnZ^FGLi2HicR z7yzA6hP|fQU8dr^b7WL~(y9kWRdJqI*Ag1AmE4CZQ&CzECp}igCs7Vh4)jp4BT&o9 zvwFALAscZH7(&qX;vET=A^ykx?G@^NtN!T)m@lnJP>7dsz>X3NUsiXc3ha?%0?J%K zq3YY7r^5zQU9z7;*wXjy1&A3!+hQ24AgSop!v<56l`Xr~p_h3igNN_AACV?C{gG2WR zPU4CY0rlAH>5AjPix80y_vH`HFZ)Q9g}Zc2BT5{&g@4h!;!{ayyv>|xs>{;-<;7WO zwCT?e@k_crwk5?!0jz%m9_>-@GY2jR_?)tps>Vxawen4K+)8rDmmMhj;1TgcI%6t( zYE%Ne@a({Irh}!+*0m?zT~}%Qr`q z)(E^jQT)Aa$2v(R? zy=n~ef)U!~R$))vB=6nYn)MYCOJdGBwP&Lv*}y%BC?O1tV9*I-ZpEVgT3Q$z7bg=rCG)&Vt<&## z$*_zstL_PM$_{uv*3=Y}@os}Zp~Nv0QUvgsyVtCb<8S1{q;>GaKMPw}tkCZSz0`-|8o;fM>p8fs=y~HQ4pUsV)8;pjg~-TJ>FzI@ zs*h+*If7$~QmBNcwwNkAIaa3CJZykpN%6KygAUuc)%Cr_w41wrWUF@Dk zNz6Hl39GHUwk+paAj6~LS#HxWre&6QQ)!}W?>e?r=u_EP!yINE0X}6{rc31HTpm33 zwY&%4?y*iS{L6UnLx|W1j;w@?39DD_?Oc~gP;WFWqRkU^%fC1~)3Clg8rA(z#!12z zwMR$jtVL4=p9K-^V5_ifYjm}jctk8;yS>@+TJ+Egf zHJ@45hDd(cV5Y5&qh9O88Kk&7gWR%Sed02CNcOjjE-1tK;#DGUK$RiNH@MfJ=soOf zOzMM;v`cQQ8FhD>%gO?&J0FsvvPTRsR$|R_%il;C80GM4KG5sb8RzA{@l#`rl(;;| z{%g)ERWKPva(Pfc!i2pb7jw(*ZHcfI;;?M9Q4ZWW9Rb4r1lA$hzh6Dk9I@OxsE`|A z(OYTViq!U?t8-h^1Fxc+L05TxH=FoT?J*CVu7RK53$J#p|7%uh_^Uki(xEJ}_q48L zgE@WbDl+1=10;rFx}jqIS9oc1`YlT0*3ZVn9%iN4nCN|O7uZCB@rI=1ShzZZpc`Vo z)!>919vJ*(=Yo(uJvp`#TFDbWp+75S?Bw~&D}ndZ7EZ+Gg3_jfE_x(~4BE`wX{J=)`v?yv zn7f1ruAJSKITNQkv-a1*u3i{;eEBNIX0hZO2b~89-V|h;;|8`yj-77sy-~b^nx+Qt zjZeE1mYM+^MJoO~*+|xKfH?x?XTn9gX1qob@kIglP{{W;EJuE4y>re>AGwIIB4%2k z4VjtV54@mGC%v296~+ZRgUb=n56i?F*N^akt95GLLB6{_mA`wXW<8Vo9cT66lSrv& z&yga>JOYh};F+7Bgql6@xJ!!{+Pc**2JS*#+!;0@c-a%>jdUW2$0IFa?XbT}K;`4EnB6YZG`StstNC70bzX^NA7E&DqZ{F?K*Mo1Pjn^%Io%in4oSD)K z3x-s0YV^4n4H7CLtq!(a{hX_-l*Jc5YVB&kci1#V+Y2F-nBw=K{=1`Lcefq}Us?m< zhG8bCAQg{WQ#9`)TTV*7AKN91_Pu8l3U7RQ!VuQqS&@VaW(w?SorO zOM6sW9qLfi1FYE>Lt??P9l-chXDAd%wzhA>KaM$1Dv7_>1(egEdQx*Qq zfRcDh=k1c0pTWhVr8b~EK`bKwZKmI5JxjjD{p@h z++Pp0@~?-z7g23a3I)NO)`W@kpVi-4w`q}i*)rT>4_-S@8T^8!;wfT+uHoERlHZCe z+>pK)CQe?7=FJ}k9)rjKkUVJ7qg5qdkG4UjZj7@ySl@dX;t*P@lp%|xBN#!cI`uVl z+ZiS`Cf>hU&NT^@2k%8aoYa`=PWQAL7u-^u)pya|u}$%PF32|5?!1hXfZr8#p)m|g zeLDKf3-F^XYeET5WtH{N78;;4_ezL+^}(e${|3@K6v=*iLVbegLjJmQ;R5Gk$5SDU zz{}c(>B2sv1}}2UR7MGI)`p`uy#IPNw4{sb{Gb^c{g0Y;J?aycHbw2)|;T2HU zC!SUEPl)&=$d5j*R?wrC(K&ZE+WDmKdT{!tQrbjhobzT|mB=bC`^jHtf(R#7DTJQe zag&-Pt~co_;r%98U#Nt5%IJR>b2v9sd*sd{o-!LABn48WSI^MpmFMSSlmcZKaD z%k0v~IrpXBJo@fOV?z(}^8>F4I<~zx+hkL!T(Dw&;1Ijj>U|E-~RO&=Vq}WCAnRs?W4(8skGLhbva7*o+*Jd zMAzm$btb6i&acYaxU(-z!_FbR&dg8a_swwyH;;49J=6?DekS?TTcyXU=q%<-r(QPj zflqwDG6YO${NJO%hsruuTm{dLbxS%kP}Q9`MSf4>z(V2#wc|gT`wV#kW40%$^i*Cp zfu3p3tqrRO|3lr|?d_7FBS{d{K}yEXaH()`jEgk1?dnC)@Vx;o-JF%VqAC(O%E!i8 zdCvs{9I$t~84sjSBISoWlpZ;=TKj%sI~m}y%_7Jc;|$*@aC8#>rv1}SR1BfL z{Sih3a(uL76}YqIzzX`f-Nhzqr_;>XvY^>b*G9G;gbX0#fX=@=E!RX|>&HDH?FbmU zCR>~oQN~6nn2;WiIHoq+w0h}`e`Or{F%RDS#%5yVPENb$Wv}Y-`Q;glO=!Zou#v#B zN}HBW=nEDKhsO0EdA+$VISkX`KexsI%v8^Z$6AV4h^`p$-)z?~ki2Y)^%N0XB1|y= zCqq6$=ep|dVCP^zpSM0_8IzC$I8F{lApgv>44=&v$EJC0c(YQF8ZSzJ1K58z+~XpG zWzPQ&=>B-ddlVbiCXL;X4J&YMUc9^9nyBNKalocFm*@N}e)TLIS-WAKpFxi#NeMhyHKmJk;Gs>$_Y-cy?wF-%vk;0GAi(vGS~eEV3}^z*E2pPv9e19rJ25=4H` zAg?sxTC#W5-+%ZZvgX@f!)433Ax406I&y88mZl3a{qF8Qy>!y?D! z%uPS0%>|3pDa-iyk7nvU6VbjDGOVAubas=ZOw;Vht`Z3MOKcT`z<2if z*0z^!7YaET{!>-KB8Wu0329WLzfpwHuoj(7zM9i69kBsz_pP5oNejUaoBwY6r$6$e zsA79k{*9zo)wuxw{XXNi`jfh(8Ix4Sl?PRiLXfyQa2Sia(SSqe4IQ(El0{pzy6ogh&s?BNQ% z{z-ZSeJI)W%x&{cebwMlM9rkLDjmQ?#31vRzs>FW4-648v#7UKG zB-`34AGI+x>+C7ZdLIReufEBe`A=Zg;(?T~>Dxj{VWwT_O@c_%i%|d0Ur&8*?)Au; z>@pCrvu5G>MM@|y@7`kLXpKxjmK}$sQM%Z7AgAmcGrGyxqfBlJu`GQC z)Q4v{9Xv^3oMY`;T68qI5HzPhXr=Hpv9VqmcZ&j0(r+8Owr<+ zmdNTb^y>KS-=k;O{)*|3JK`^&z|C9c&w@?^8eW@p!{18nQ0bqD^{#ctVJnq;F{4Hc z=Gl}sw|(d`pR%6}HL>(3HD$l}q*tFome~^%FY=9_6?^WTMCTv$>n>v52RJ&abgi?G zYVs((+3}hr!`j0nHYg()ToPFmUk15#Bk!&yloi+n1diiZVFMxo}p7+q9(H9L zY4!ok(zX|DI`4={ta~_OD~!%ms;S zGytU)=@w~bhHe-_Km|di8%gONdMN1z$)UT3?ihZX=Q-zn-|z3J=lKIZFbsR&v+h{e zy4G6PZmV;aQC?_dojP7`O4})UXg(^LVf4L@ro>q~-d*A(hkH@Dmy|KRySrQK3v-O= zMZ3<>JX_|Zvr5gswEz-sg(X%9*$`heu3tYdVS2itctHA5GS}+~NtcH(-3?oRjgZ(2 z$3?9vTc1SP-R0}+>CA+*N>P%-F&suj;i}VJdK*0rt%=;Jli#MiA z;_qs|SGJyh^f8ocwtTFz>kNE-v#$fQsTsrQWSK*~w%9HOh=HkZlc3@y zkxXcR8KL_cM-{7)F;(?xlcAIlCnP{Y;CbMzcDYiSo*a-Y(V1RZ?UrLmcy{q4x+pvD zou?W&Zqdf};zy9*P-+gukr?;M2wO5c<~OB;*y~{?E&O*MxR*;Y#vXq+3j_;P))=$9 ze;cKFe(Q;$9NCJyt2nb$JJD4S=v}g@-pX|w58C|_qA zHd|JP%q-NW`M?8X$_KuW1myNvY*`YISXwwYU*EPFg{O)cE;r@f66aNO>W+QbS$()d zRH4PmKUVEUHlso5`SX!w&5XZf9q+bBpHJ(#`FCu58T0Rt zt=2*7j0=6YELV2W>p9l`^co#smNXo0Pu}zJlle30uzB!@>V=$~u zli3BT>PltLQ0vOoaU`U` zx?9iUPu{Nlb%0v1_9{=*8d5*!goF$LTBsdr`Sh3}wKr1!)_F0O1Qb&YUgO~bZomS& zPru)PWFO1KU9Jx3|0{kli>=5nBYi$Pl+Y&%*&T=M`n5ZIfaoZq5kij* zkOWKHNNAW1fc2glm)P=7n=0ke4G>faVybCBNxlZMzpUxzo3Uier{%wa`!iVN!%S_% z!M)Ovep-^0b&}|{6^rYFrA03u>HSa>Cw>We#1{MM?D+xT{k;Pis(ij^Z&rOoeFSDY zVowq$sJu=a=NK(yEmZjysmKkq%Li7*q3(!`u*#xf8@j@y!DOo6P(=A2jap6gMR?v`~3ahk7Vf zZupHQX8}lH)}geY@IdWb6ZkuI%9Ip7#ho@CD!B_HWrPHSwQ;;xs4^mD{v#s#P+f&s z;%?|{2fIDCr!Z?UcmMSKsvJ( zuBpbSYM>HV(p77+Lmk5%Kdq*G0Gl~7c4U4`MR1hLxy_RdnoRzX4?OY5(IGD%>S@+I zsB1hgs$jqWH>Ri&XbVyj2c18O4v4&JG9HLL4q5~nP+#;_cH5)lg>v0;1`>^GUJB6f zHEmxlIsYpkTH=j?d_1llVOC+0T&joS`|@4JM`NWgX*s^=m<(LvkF>tv?7c(M^lp*y zbZ=Zz2nWmgG9<7u{M2r5i+&fBHu;md1gd#zu<0uGyVsh#H^2*u}ukk%*pft@os!dNh$uK zlvSyc=gx75RO!Sk$Phe3%*i@W{|Eeu)&2zaS2 za^!>AcV*6BI#T3*XeDJ)HCHt&o!ygF-FVcQNvS9fI%YFWQfy=pD}=3q6Ru%7UYp<# z_q84^#53G}xvz$a@eM{2pF*y1(|b0jPd7H~jc-B0sd`^O%Yt(-9~sunzU5QbZu2Pamo%&ah>en89to-CHcBb`qkmeVFy0nsn`Z0l}y1)-hago`STV5r>m# zl^&h?Xa40HVANFy9M6v1)BSbE|rF2XD~MH&Uzt0+j?N3ME8N4owkaKgb*d7w zOj^S^i-qxBBzQ<}5P!h5bpAPRFT-JIK3iS*7wTDAt@PuJP`4K)$3^1OHND==4@&p| zdGXMQRF#`5J^?2MWV(;vNrADq?<3qtATWsuAFQ*9{w7(=%%X31l2-x68)CJEjt$D& zg(s__v~dDnhJ5g!Its${X=TwH!e1@lSU#Gf57esUj~k7a|4wo3Zr`f&jR(r9P06tk zZE%`rnkoTvH{psyq^n_aoptI&lN)8`bU3sL@kr2+Ii6*1jY%-EaF+1vj=4(qU;u`C z6-ravNnX@RAR6feDVT&bjqC)&#L)b%4_+)YreK9cn$TK$Xry}9@7TRS+fECkxsUw={V zOhBhaawR21XG&Tyi*&Fn@?=mag=`!sPqU4#DO^hts7lq>)hU=YJ z`?(3-mo)P2zD3Y=x(Yi2Qa))kKfA~v6VU3EYwGCC#&7nESnzbvevz*k6p@&!gp4 zzT5>KG+Ove#!i3vyVa6YRno=%PzyRcb+Re1pTxITRh2G3-?>!1lLW9B>NQ-|weE*i z{wJ7}U{&AJ=GFkE7u;y@>s-oyLp2=vQq;1v=(u~yUzz8MYpHv^^WPj8Dkif|u>eaNcu zlt{{|^@@k?@WmpkJB}U1l6I&TR_eStiAc8?SGtMH#9SrXbna*30cAit)&t>UjfU5$ zdPx%9=-uoBI?X8=kC_sABu8;^ub&rJ6k=UkOQ&4Yj5k|7TXe)E-Xm4>48a*=eJ*|v zx87uBVNJs*nYJF<&|^6b2lq4Z>Fm6;?&887-|F$pCmuUZm9(2Lj?QC zHEi3ZzRR^8HoYz@^6b8wCj8x=;dZ61bWCZ?N?>5+3#4!tqNZltGdy%qJh0*5e#IkG z^(=YtQe_AjyvD$lr^UYIg!&N~E_~y3bllwRhhlLZU0ytuRW|wezG=m>3m;oychl-T zCmpyNG|rE!(3gp#`lqMp!Y5HN%_JOvZ&zbPgGybF`|=GUE7fcBDRaMFw$`uSQYPN3C2$)4o91k&JB-LM+KcFA$fx6zet@ z6u(3J{VBSTxxZ zlYSOuE4siE-ZC)g3Rx&3xckcqh|hlLyJTqi^Qym+-BDXPKlc@d|rdBH7q9Tvq1@XboqQO`>iykM99vC{b~N~R%AzEcieKyWXM;&(BphocWMR3w{H?> z&E*-B@K1T%dRZ@uIV5I9BROVi16=&Wi@DhjUX6SgoA>F7eOjm3>OnM5OG0@dydXky zBWC<#{iD_XVL@}v(9S6>_&2jx78|38CLj#mJVZ?|lZ-;YTa7;;QhPv*xk39FVAr2M zZVUMIzCHuyWE6?g*9do2+>-9IRVw)M!DOYY%0^Lt&B?^rnBFvNqMH4%6`k zip!ybh-(cRZOxPxcRj2qozhonck+Sm2S2t!Yrhb&!bYTdFhLL-accbedk0FoPQ+LC zT^>F{x26V5fC+<uMjlp~GqV?Fde7stbg62T)^?vXxp`Zc$i(SX85lU90+~y+Qg8Xs~8XqJ0ogR4lkKu6hI)WSq z;x}b~We;lc3LE>L{8f-g1qf+grhOj^%wvgix?h>+gm^>4O~3xc;IAa_T)k7}RlCPu zj$7-QH;N0)GruZ{FNYMgJgYH97W9IZdZ;!eZ(er&Ow5$p#XMQo0q74+QXEqPCJ9G3nn3 z4G!|~uXlTlL7^P|1CR+g{7@BOoCYkmg^BMcNe=EJ78j{=87@zlWH20Q%}TdQUQ|Rx zTok!X?rCBQl~+_8FF7WunyBCbi3qdA@iq>k)DADrTQj2T1f^fd@5;2S_u1Tdjv!!o zX=1>?LTb<%xicl%{KbHdI7J}JY{oukcagsICbYWb3u|Ls{azvW%&O#jUd4hah4OHN z9JOzg+L4x>vtX|U-2PFONY1NP9H1)+j54}w{!GNTw`1tE{{1Wny11Yavp zPm594yyuN5z26p+@Wf#5D}34I)$k$EsHwazwWN&j{AjnQHemC_fMTzn$JOwDW|wMI zxFh)^pUFrrdk6{c$Qd6-$k|eNrqb=Q#Fkv;4YNdV?&;fP7TcOTra$%#R8oDt-(41E^)V+Z>1~h`R_M?YXIm?>xb>Aox*d6#kvc zhAzD@qL=;Ij78{X7n`K!ngVj=qpyZ3y5}swUqBBs&+SZ4ZvkTF=O2Z0KQ_tx5wEulNq{w_ z%YsPG4y2cBn#)mUd+V`TFpKp{lt6dTp;2iHNYN|MZ% zR9`-+A0PN;!Y@l;t{+|!O=-6NT<}K|(R3KyK7UH!+V-@E@jPh3$R&fc?vAXdH19E0 zn~~8WpQ{tY>E5HySh&;V0sLh=hvh3D{Lqj2Fpb0K%Oo~-7p+GUl`Y=rDl1831S`V~ z{T)_nCT^S_y{(O|UTvEVpb#G~(?o{xFOnADveziVg4KXf$DTs_?ZB4J{sHFGrxPRQYS6eW# zoee$CGCslH|FL7N9^bCD5?2pQvIjbpwKsDEeqCk;(}Vo zOvE>yUv`bIq)@qrn|8Q8Kc`9gK-T~XbX*9J5&u&UJsg$fp(6~5f%byVtYWj ztc1$XpELe_YVG4hyQuZY5``db2rFwS@=h>iBRtFwFb$`M!KOux;KF?`ILdFf==32O z0@CPq1b9!Nr&80*6-}l%-ctm`^Y#b`Qpu4J#R`p+Ou?SG?V8RGKOHsf)JTR zA6lfi_NMa4)VNvdVT6axrNH8W^YE7BSm>d<6SCJfiV~S+gB3$>2XzHQCazFM1W7~H{Nu+V_ghC~jJ3Bo*9pqURAK?X7EFRRE zdr&`9qUbqUxG~H)94GPU$;mQR^aa+Y!I~NUTJP~|SPPw0E^D&ps^2nUBI+7Jpo17g z1NwpIxNP;Qc{XX)jb2U`NvX}I0~+B}*4Mi}2dve$yWbClXM~c`pF}0r35ngs^-w&3WPr8lQKDS}8xi9QncMxQrgc=V$1wUB}X z{g0%yCSuZ;COcXlGONL9MluTSaJXrTbEBQ&n*`q zFy;@;vk~p1)_0F;=|r~X^ZMm#@j+*#h5{2_b$1oR9ArX}rH^l>xVVnoO28DtzWc$y zew0kuR_27(=EkjAHD&Vj$KhdH;d6O>H@|a~sy2a49%HT5?G_bW7ZbRAl8ELS=(^d2 zkHrv9>s6ok9*_u(O&4hhN^zqdX^r0cIl*x-(19f!RduE8CMnCM)=M*(c599^@tG3h zv-YQ*91a3DaApnQiyunAScJVeIXrIzXcm6aeQ;`SwctpB_fxvhg5TJc`FLR+qUkS? zudz8%bUdh`>I(ggyv!`w8x=6G_~TKg269o-Uwj@$@J-tyU71Vq_Gd#%?$nt5dv1S_8^z_HAg!w!RH zb9RYjZ?*z4wW`PEz9DapffNd*QBO|O zq&aq0|JZ-BRlG|-l*Bnj5D~*Vz_1Y8ft$`XW`nJ{Nd!b4u$%1QWlvQg>u&W(QWCn{ z1TJ?+zta(SOS3vK9?9gJX(1`1#PhMkmN=g!t&WJGa-ONjEU&6sgRMmOYYqY=Hw6{w z3SV7`O8%AZ`{gezf!>$(x79+eJnE|**VQj|iuGgweQF#eb+(K<-AIgO5KBD^* zTbC%(<#rzxC>np6xIF?ItvO%7DViqx-;4lU`+F$H)XrXOgw4c9J4^L?@fl3gZISB5 zNS(E1m51-@iSBr7*-U({pnZthss`1b6!i0F5Bq!b{ocfePw#l+3kyS@emFwATuS!i zS7b^YWNpUV_t-8cxdmTkUeBMGC@2hxny#A`KP(D$gnF*)Ogh({P2TSk=H7HHaIX}qT~*u+po7@J zjssyU+*K=gjtJ|v2{)fIo_;=Sla=5liIB7ZNM5niLRZo*6q|)&{qXKDZu0j=0si8% zD(kJIGVp5K3zI~=Dkc6r1p)Qii`#5sL>Cg4ClWQ;q(H)lY&7&;)1PWDC&9cRpv{M& zuE83xAvKLk_sz;y&n@?9d*=yTT*aGoi@eV`yr=5W>ve!O4{!fLD8^b7wGJjO4Mdby zzW_!V{`8L<;4c%F1W5@WQRq8Y;g|>)U1Mt+0t;hpJ|=bhJneKn3_Lp=o~qQS=Xo&X z1={o_kZq*Fdo-gubvZV%+GCILdGhu5dXOmlA5~x9C z$;r&K{!p){xe!tV5Zc@+III(sCrgqwZZ#`R^Oe zlmZi?`oS5Upjh7=2K5ff8P`9^)-YMFH=08!qZgFVvn#f(g%ui&)>~}SCmtLZHEzr_ z_(9GNQ6ypN(Qn;<+`ToMst3C`*sS-#@@hoomh@aHNA0=Wp@HYCS53pOhnb2xZ|+mL z93hTs8Q>)oweIClM+-brRxy?eqYb@`-_0l>P?sjUiGhKyXFpgaW=R+x0oBVJX4NBz z!?MkCul4d`;hOrx==X#=Dr|*eBowaCDFhphc8M*RBuDh!^V~<9ZY}BW#f}`K1jsLe zyA&v|Qx&-Q*R>4C+6{h^5~k>B7tZsF+vE*%{O<;NgWY&^@!7EohgqYjS;puMcf5I% zfckeT#Zn_)!Z3Ta$s*&{2@S+9)e$h({sR6cZ2Iz~^WCVri6|nL9}n6CUKI4g(0dRQ zlje&RS;<`rV+^b4rydGD7YnSCI}|o}m)n7o6XM%Gr2CxnA-BY}V}DFKPor2*$Yx3h zmF%G&^K_o0btiFkJCr$VD&BRMXLVZ|oknw8(-+mSW!B>dn6iN%YRYRCxe~0iUL>Rd z+P1sFtP^u_4yEB#^8erms5wL}YAvnBkEuZ^8A7*baIvOw zfj!|*qrW0?(yn|8zFB$kmYzDNrSQ`|^|*`8Ep)zt?a?QwN0V|3@UWDYhBFsyqYrs( zy%J~ENBfzQlL@vhwEB!7yD=k!v^K1qWqsg+XC)1VA4!%+jb2^jTzv5Dcw$*XCHeE+pB+v?? zNNa`{rsK%Ka%|IbQ$4fxxS8j$ziuyK>#dGxz^c)kO;Yp>={ZNGtoYhXw6(BO#zoK8 zxe_&hyh2=3Qtx?@2{Zh|M444o^-`=AJ-q%$j6FaIe{6~Y8fO+cFF3^FQHSWGx@p;_ z!*`#J8o7RcYL^ynTiL9bC(-{1FqeOF!v8+T9fGAY&FYP$=~((2^VllW@4+K}sE5ze zLT8=^R+v!O8&w>;fx+EZC()*DsC#@<+D+a5!?5jE?3*n3ux&raieAr6d$08u@vBZr zXA`YZ!ge*AnbpGDdkj5MP1KQ$r+tAFvJ$)GTc7w)*^d;+-W*J$%BMHEt2cN9LB3CK z%>>%dK41@waI(P*!mqTkl2;h`7Z3I)WB)Y`3fC~}3bW;h;-%*A(X&)P=)VT*DC8ia zu%ET8e91Z;EfkLxh%a1GjAFgr^lqdVz0rHRH#mhpEEiR(cpQNL077#N?1%BBrhq(- zB9pfpO5z8vCA9)-48IbE*v=O&*aD*C*?vtOm8@d3_>`5L`4x?x8^(-#@Y)R%9zYyv z!|WovDpMR~aZ4z7B@B>t{-X=}*L*~iV7s}~IK3rh9T&#O?O4A<5vzo_t*D@4L`4g? z{2|ce#%YI3LT>{*G=nYa?|zLs&bvo$3hdq5d4{d+oSsd@86>d1hLF3WUL0@=_M_Ys zoQqe^AZbEPLvQic<#DUKNaKXVprRM2oI<{lPRG+g5~{2NFHKL`z`eEYRKG+Xk6A(p zAoJ^GcYV2`L0#d8=L{C3;e(g|Xu;t5;NSJ0Bow-G_&?G%>etFs+JzK^0T<*rmR*sL zYO)lFr^WStphW{+uK*(jc|={d4U;T_gYfI_xEj2Ry`{kDWF(L@v2n)+a6&!#JH@*H z;D}&*hM}XPB|r)^&gKe>+%6<1>%*RqVKZ5L`QI)ecoU>4HFQE9Zo*_Wmq$c< z0zAy+pKOjU0???D?OyH;PSO7cy#77)^`&>cx!43Sg;44D&i=j0{tF;Bz2;L^CZJf~ ztzglv@{b0F=aVH1XqsB!O#Gw?Bcl9=D~}4>;o3xQc=*OY{Re#P5Jt&Oc-Omh-d6yI z0%S((V0G01@|pyI#i~tHr{XnC4^*M^mi|#au0$Uf507#S9#EYA=Z)Z_L^fJ#5r%xr zHJU{O_hM_4ihk7t`k)d>*I!d{fgr; z23a3joQ#;R8S>m4*RhxD z^=!MC@L!(lyk@mFR{afXa(*GDm zUWK<25U6MC@+r$yG?hZzY+!rk^tpe~q$-baLW=f&+gfZjI`T59_6#E|`_ZK^8uIOE z$~}xloEn?mmz>O36Mg4H3fY4jR4Vt2o_&&2`B3!L@cyWj^kW=Kc0pfXN=)gr7ZhI* z@@AbdSnGtXQ*>TeSI?$b@7_jFf_ty{(L}FFpPByD(MEZ6sdk{ErAFZ!=@iyw4rYNN z1hRO9Ba+={h%=(2-;?THPkVbi_6$Lz-@+|2ys~V;E>1+&Vv!# zjOaO!0S(fo)3D3hNv|`MdX3AD>XbXH#NnuMWOVdOt@xtjXra;JYGzVZk&;L>Jk@@= zXE;ZLhcI4QK_NJT?Pa6oP_{{}`w^T&ExTgyXmiqv&$O!$jD(*As3rm3+B#*HLmp?N zQW%)nWdHHar;#gp23c60k^WSrB@P6(Oe&ao!-F{RN2-%4c_y79sYFN`#Sh164(IlH zP;LG5kleZEpm8;AFv`*HWAg7P>xQ-pigY zP7slwm61GSkf>#7FL5G>s-B^M7%X;}py2CONakfB`>g{`8?gj~DhGV4{?dR+(VD>K z=H^lVCf2i4TnMFn^)w)FA!6hFx;8Y?#{6~B16ph{yeL4qh!SHDT2xebUp6N8u;9+Q z&W!t(CSWntj}HduUHvMDEOx03?-(JalikH`(Q>oiMD?`i=q)k{m#PuLvKP%F#bJ`1OMc4oeFt$MYgoa=xG zD{s0|apEX1f+Mm>l2oMk;DwS>8B_>WY&N)5bU8!rzI}SjY2o+&99z9?mR4#7 zCaHESd<${bgEQ0fXg#+SMn%4i+yXnS{bV1+h~Y&6H*-anL!HO$JBCGFchj!;pKOf+ zNS=0?9cJa}!Me_3M+A7eC&B6bct@f2XsLk{gvaEK@gLtPtugaXs4J%8hFu4}cxE!J zg;0A2bV|$Lm7PZ3o!K4mL{#;Nl!Q219m$_`5IzztysXja%vR$_;9LkO;~d|DA;+CJ zwYV&iTj-iGiBr2U!unOI)mP1f2mJ_A2|cSM&oXv!PT-_~#W(PC!j9VgZ9~z5?t&L& zG)W^rlvpxH>kKZtdaQItdcbP(nb#Pg&CZhJa|ME$cG}@K3-u)VNnVN`v&K1^2!|?9 zgx|}GZ(G^z-a(DHP2ps0Z6E?ocJrm_*F6t)t+MkB>d@gt6)cLe*u=vYO1PxAO1X64 zn*Ib-xo_LQLSi}iP5^>s4EO?@CAwx@1+l})kFk#CVT{&ye_d=-6_xI~to<~Q z75Yrgmsb-W2V=Z^=nFoQ$SLwHwD~l7y5cT)6tv{jF!m`|LmLC~GdZN8YT0r+Y8q|F zqt;JH#I-=j znR&DW3fC&jUd)p_F+ju}ge2uHTeVj*HZfT|c=4lNv8q7KE>VC*_ zdK&*+4sK%M`MPJfBWS!rqS}eEw8O%L!>=qh0yb6$UdEiBWrU4Gmq{-}-3z`(N}|Ti z76(9Hp;H4RBdm20^U8aGK@L_QLb31M`#fkDu63X>lMS8ThxugJ7*~v&MCqAI2T>~u z)qt$^gF-`{7nu(vL`23S;(tD}sdhpYS2?a%K?sJgaIr;Dz%Bd76=hZ<q<`%ep0E|#oJ8K=8rLCJ(z0!cZNZCVpvm3p^8%w{5nyohsotT{xEt7h zK<+X3`IfWOMgP3uK|`lUCbrHN*HG!4_9@&-*_YwD@SIa5+smJi%D{%HxJkyuR*^@J zVs0B1T>3?0wV}#7et9o1vYjlp)DK~`+Et#59gzw;d#LDwy2}>y4}(LE?M`b>ZT=~3 z{_LeH2FVy}0OUT`k=!iwtUg&LDp@#h4L$^eZ2Hmfq8ULr3Q`RZBD{lNTmAh0GQ+Br zy~1fVJ?bl=sw90acx2Ai>h4PlXAhm1s>ksE#bz-TR zt`5%#_5=A>jkBq&O0>J9MHK2O&x#z*aQCgj6a&S}yjG5uYH*y(o3Tbw;{qK>4Nf6s zs5^;PcgN~TiVgx$;L{HqkMLOL)Y%+Eo}El>NjgmQ)GZktCY48y7X~3u4wqX$rKR2D zo!OoDGw#8A_KGPd82-G6`T7Wleoe(tz8(^1%Apn@Vn+dYJ&5*u-B{&>T8s1bwY6;i zn0`qbQGiAF%af8x#T>i1Lszr_EN=EiYnvgmB3@saK7_qj@PT`vrVO>}M+a!*#3IwDEZOkyrFE_h-+>XN>Kf???Soym6oy7!7;>sZi(<*Q~cX6B(oR{FA!i*|@eJ(+nKH<+%juEX2< zRYH{Qd`sn7(gP!3@^?Y|j5zs$QVxV32SXZ}3M$OEODMbvogi_7U|!;S3q7GS;B#vM z$b0e>T4LH=j6+C$y!1^13zx)U%np%Y{VSgdIEHUjE2A9q7`mo0P|#rI7}$7l*i_Y` zUC6YcFb;hfBFZHw*wS;RsOs*qA&oFXi@-xC4;N$)S83Ka!kX77(61~o^8sv7>a{Gm zb3nM*5(lfB*mN;Pcy70sAEIm2AS-fI^2m3#Sk@}#rds-P=>kI%`jtc6B9iN9Es|(i zhS$El8E=|DX6D0b8t_kWr)xTuk|ZXnxph}+iN~zI+>y?G;!hCy`Lm(+@`nH^eFEUY zfa1APNs=&xYfSpL8WqDA9rZ`wYSc?VMMOuN1HRH>k!=vYsUYvu<4RJB_Icc!SeIcS zixlT&{Pmu1tF8BbSb)>*J9yK^vH@h<;T0!hYZJobcK-G&82zw;gi+E0IMjq9Y>0-h z4-UW6sq?IhP}HHx_bRiq7;1Ur=SaAU2rnfto<Oc8hnvmqT9t5;3<9fFt14-i zl*wx6^2dJH671BV2H$;chH^B@Z$AyttaUr^Fo-VDF6)@`Is+q1%zD{JNa0Uo1iRye z$^mL_tje+O*+;*Fz3HQ)y30wq2o94}pma2hRr!?h4IL)-?l*6_murCm|A~eEnrm!| z3fJdmbWS6shJm~jnf7*rAE`c+^R20M?f&Ft4$?l$_-3%jRYKUB!OT-?slr^sjdQA* zn2#Cta=V*?RE;bja6N)=e&C6Eoma5meLlvimZM%~-zz?nhuUwu8#=luTe2m5}pIppf?eSKOnW`0@D}T zI9#b+LO9b7BzcDTX=!N-iq~}28-zkn9QL%2RzAyD$!{ z%(CRA?l`mNAewRjEdUrEr5g~$J1=puuX;3CYN1N^z|dP%gZTndwaVfSY#cp7U72OJ z+6RT~b7QHpr0YMk!Z`g;nn)!_yJGmj0P~5+M@TvuhH3AiHm#~<9Q1YX+kxczKDyv{Y+Rcb3mC+2 z1Ps~zk($d=^77oiY9_8lztsH8*-pOwRYGwF*%84BZVDjgR&g3Li5d%)M4JO1u@KnD z6$hoSm zvJw6D{BqOoo9h#mCG77tb2W=fV{^%2uMw^#KEGKwSF1;aVw<#(OBd0cpa9qDA=u|!pH^w*2 zlBZ_dH&4HIynEIM_m<7svHu?idZOxGuY>l0R3?;tt+;d3JC5JtYn+u5u#K_zdOBA> z-2(hV!mHx9?W#&@`!8cmvYT8tMjyHZoMNum#VPlCu4#AdaFMaBvy#kuMb?kEN_8r+awseQZ4IMGf=$1eO1A*0elKu zWfd)ROCaG~Qh+$^NXWs6@6a-J3=!Rd)lm|~i@6qOO_**?)q;<=XSuN;Ye zaj3G`&bv;p!2^Ad9qF^p>aFuYBjAAl^}+cUnrG_3c{3n)D?`RD1VDfa8m4xQ@9J;%?>Rdd9jjQ8{SZ75(!4X0*<) z!>oVJB0;2-bwd@y=Cwc0Duz!D6%$5OIa-&&rx5=~%kB;d-KGUmm%RwFn9}dAzw09< zC--rO*i*tqMSZi$Coqt84<_^=jg=F%wIPd z@Uc#S46Cor%iU$E1|e}$ahl(57O4j6_b)nIo^y?sw@uzAO8@~h4(ZvC>l75=!3;$> zbXr2SgOdx2yUS1kx&z9}%96q0zVlNT+ea1W4!O7JLH5Su zUq-KgUBt(R;74=wOJCmj`ruE0I*plBDzLwXh2hsz<;(U=-udIn{Z5BTaDv9r1G^~V z)&9S=0Dil!|M~+#5HJG-v^M^WM)pa+Svdb1-YgBkmTlA7izkB+rT1edF9LNQ4<{eX9L{8Y4hd`R!(r2`Mo`Xd*cO3y?S+>*2d_IiOQdE{e=B$ zh$o|!7Jq)94L&eOUodI7bz6S7g8#NIn+GU7`MyVQyy$-!Vycv-B?mCXeG~IP-}(tl zvcSw7;h4C6_208XDU5j$bn`bW`mHh(3k!=ogZ&sMGqXR2E;mZ}Z!hcfolIq0QSvmj zist>Ee(D=v1<Jn3Bn!B=0ruaP z^Hth&&CAx?pz{@x)TzrekeY^u2EH}GFNj%DC?<35Z#y#8fb_Q& zW1uFTk;KT$`|pmpZ!r9FTy1!4oO$N`DVvN%tN>063gE=cgz;a>C+fZ3{FL)=uZM?C z>9L1yCeec4#xTMAg<*Z3+)b8>N2sa_#vh66e240fFk#SGs)|gij$`QmMhp!&jTe%54Fw(gzq}Dah*Sl__%2rd%sK6> z(yxZ{zptf8Nv(G_FE@+HgwR_6h;LWV?R0+?ZZrF2+Wj|qg#X#NZJ{97NK7HXb2{kz zT_Y~eOmyp58_tV^NJ9Vj2EY8N4GIFB@C5LLLok3BGBpoY2baS=H-2HX|NHMulK9W= zV`LVGp}924luX#$efKo30?_~ap8px#-`4{E1qk7uL5PKMMkwC_p!`*a%%__Iyg18G z`se27Q9G@S%0fSZ@-%Cp0Pk@65aROG6-7$QuCHeG!uf6}@z5nOewx^hRm^6$Z*4?$h=*Pk+Iw8me zdlYmyH-<~MVmtgIv)XuPuGwj!jhs`js#E|x{xctOYH2l?@u~KBE+{fQJbZoDar69S zZ}Q{{N9F5PSD}EQIL6o05(vp`3eKPW&#iuCLAGNMIez_#S*hz|;o56f>%)13@Uzk* z&+P_WI8YcN6v8_n9x)6D>s-ZVp1bX=5Eu}E<^e%~db%8yPMue6cl5(1p#IqyV0GaD zfl%3115|Cc9;jvL*Lhv&QYVxvkJ;#BD2-X_59_plWky}1w=Q#m5oUZ}9xt;3FLlLS zdGl${_EhJsDfJRlMP#LYCYNSmJ+MhNMTbzT5ZFh&P|9ZI9aPC8%m8IIHx5eK!a&pUf5XX(D zM_XGU{hK;IC7nEV6JDpQtjHSIlFksTiHaie{l2Ggs}XLizGR;>SAgNTiav$`1WY3q zE$;4=p{iO4-54!W&(jv5`8ildWv!AijYTRpL$^L|Rp8hV0!5s(n5;8cx3w_mt^obdEY(4r8OqPq+l_>J-fLH-tUfcij2?ZF+g z_=uCx$$UN0tBTy^;wMxS>P%a}#U?q;o`=q)L4l{;H~y0^oS3 zY0sldDSr^Fk79$A0FIc$1*6w-!`jcR`OUk9JX_={?P?|th>JsHMty!Px(49ZdQD6? zgl~;bdF(0D7!D^50!7_ZUg#6E9PP5~^^7ioyu9zc`KdCWih&-3>R0(4+ciK)h7~Ax zgaF_%_w(gCRUbi|sP-T07FxCJx4>=)_;|Uz{L1e2|kvOG2b9qW9SIvZT%Q}Y?6E(IYd@3*f>LfM^rL-*RSF|yN{(5_EX!Y zYRLi3wDYv&us8x%QN15x}n!!)yCpZm;$_^xF?m3a9;HmvH#J z&&OPrwBY%^RO{ASldLmyR_`^nGf~6QmQnnfgEcFIDNgQ1cGmrAd7jz@{R3%)uLC|l z{$}*4#~G>|!ccun+^r%vd8_VnilK8OYB*dqN3R-A7;Awfe7zD4!tHpFtyPjic#DlJ zlPS4eCPL1{4f#l8Z{(&OP^y1b)S#)W8-JBYJ&c<^Zvi;m7Z^&c$6YQ|20XS!1)sl5?1qTagpq^T* z`6E?r)0o$gw!LKs$3ZObM)-%dX`pyz@p3pKSPlL?wpjXrRz*Z1BBSets#6jyS?4Il zam)PoG1G#KR8ib}(|QxZ7d&m$`AieQF{GOeFpsYg!9qB<0g`AjBmH_9K^MFG62QL{ zmGnv=aggYP0@xLEy0Si)t;VV0`-+*d_I_E{ru=#-``LGZY0R(l1rr3Us9_S<_9RJ8 zn-uEVcMD|jx^-A{s%8WOIKvDvlk7~4^Snp$(j!N|whE{UvcD2fG#E~#+1NYX{piiA zlCLYWK3bGodx<`SObX#d9%vDEMgLq@(fVfjG=OMF$+>{wHT!W+hILtHlGnIN8~d4J zj0NDpGdpb@8SJ*@lY|6tdTP9#0bDd;cBjOsbP9PlRP3u(lr@9ugQ?DEL-^szUDR|5 zzlB-6-+3{AV+9{9y0sL+I^>XDpeb|K~4Uy`M;^P1y|A5?I9qfip zvYXVqcI!~|xF_}JfC!Yu55fseUD9{1Fb0rTIE*Nd=Q9iGd<#I8I8NKFpU?98({ob| zQ76ku|vg3#)i@b_O+RP5tX<4>2GRx zvj$Gq4K6JSsqYtFHH%o6=s;_SfF1%%pbS}Z187`%a6y|W>YRV}Qsn1`AWl4$`S7EF zSY;*lBu*E>7TM-cH?y`^}1*A(Qr9?{lIZpT5>%QOTVz{37kKNC%%MLTY zIpa9Kb!?O9@)y#=?nKa;$3UP~@|k-qpsI4ExOD`#yE0B-r2z@NnztH6=}#QdT~#9T z=6ncJSmU2oYE@IMwIq*T=C`zkVi?E#Vs=#hjj-J|sY!jq9Y5%xF0h^Nc8N}Ov`Cjr z5Ey!8tPQnl3LsrQ<`Wtf(%1?0HGf8PwRq#7$dyx8Z7ujio_Ybx)q#D1qv1`j@wMy8u&WTb z#&P7=&ikX-%{`b(^@du#CV?N?>GQjUlSFPrR+hfukNKIT%WDl~Tw*1S6QN!gqB;d<8Xkbc7`%o>>cV((viF+}as$od)nK-6lv^(NuMN4qV3 z%eZe!!5Run2Il8zoBrM}PK02Y>NnzaNeMu|uK~>wc8~*|##}wl-~y-M-Cf z)YtXzK0G27AxX6PO=tT(|Lbdpg#76@-}B=>4F;>y2S%q{^Clj^;G+4zDxU@&@=`v6uc0`&T)puH)-q>G%pZ0CMB$h!vjMUF?`a(Ox#sBtpU; z6%wm>F7yj-k2)mQCy1(>um3ny)9HW}&Nc34ak)W*i7E$$V+wSSJ$mq!a8c-H7Mh0w zs1y^}i(F!2fi(IR?k~HxVagc!-UntrD|1i3h0kUfo=&Xir$U@C2GP*w`_9S~?B4LP z1IBar;Y03CxX017!SWk-f5w<(TCMVZe0lI0*8iNpH;}&z#~p?b&~e#i8!_9Xnu{P&_Hr#su#8`~? z^pt@&_$#4i+s2RPj3SqmR49%9yX*qyjlMgOPcQ?Xdul4A;KIi1<~)7+X8pqf1yuBD z|A27Mcii>!i=`tzckAflL%+G4A7j~?nj^p8YbIcYb6nLZ@-(%FAH()cI_Gp0xSTPv5zE%xLZq34aqiSQlvt6xVAYlFt32=>I|fNZ(~&E!6T#E%wL_JHREq zoc^G))tEmja@B9LLzF$}!j}eLS&svmitl0I8%;5(d?XGOvo!qGqjXd75Hl?~+1HM) z_uvoae7beN3G8_;etv#i$S!S7wbnB7n&p4#1LL(7E1{_Lm%wxvI%OetaiVO^+p_WH zg&sxUq=U>S$q#M!Pew)J2244#|s+w@mMgX4%EzLja(-KF{g+9ZhGk^VZMH&wThm?-gMV;uJD4tf{ zeZoDv+8f%%&~3ZLYf#6%`!pxRF5ZNc^G^a7ke4_(1Gk517-@BLbbD&z! z-aa$j;T>-|cH`5;{;+>o*sfI?{k{k4hLP2Id!V3I0wC_^+*2$z!IJys9v-Nh4&m(Vr_7JIQ(^~uE;_t1)8bm9`{k9 zrC;A3t8m7Eq{ex&WWG?;3>Bqa*NHT*&m@R$0or+FZtro&k8BakV_Efz+m)5P)H59b zE1dhDlve2VH}3bbgan;MsIA4~mi-+GHIh_!(O!f?hDSb1Xp#@L00ip#>13m_9cvoRWSJ~g^IopzK%enc91OC z$UoX6HISM;2_loDvu+Cm!3O2yusL#p%nkcMu|c4!pi$z3%zLc79e{WBOKXJM0)2bk zDD`zsT+0jTwdLt?klthiM4=muGZ_1FXTSJa{w;tHwWJg5tN;w1TV0wgnjK?nE&>F| zfRq6&hI5bGkj|AWFRC4vzWND!wl?1WumA{`nOU?~?p_(!2Bf8uIG1tdeX>E3^Y6U- zolz4yty;Hxrt9SmsF#jxZ4C z61(DjEmp_#%2raGzBgAXN6DBYSq+>t;Vmzl9)D<%6-yfnl@PYw8W^d*pqz7i+iv$F zx8LlLq!Ju)wms>lZ`@dl*T3xW&U68P?u?Fn^n4-wh~- zL&X%d><^m<^8di>U~g~_U%bQ0m`?m$tSGs`bx~dQp#ld z?%*;DhmHuA} z0==)CJ~tzF?HP{}rRC4UlY#!l3xnY)7lU!mA2`kOkBAe6`F5*V<`EMHzDgWKC-C1*kpgtS&r0>;Ng*Ddwk*xvXs@R`IKL3`nUwjA@~UaY5?388JikpR`;yIjdaQW*BZ-lFl` zPS4;ru={zKpUawoHthb>BH}Js9g19e(S#L~?SW@x9f$oF)(!<4w;JzXr|g1n zB0>O)l|Rs><+pc1>x4{>S=~He6zkkp@T%vwqsv3I3dAi2J3}dAbrDicJCA7ln{y?D zSI-$>rw)*e8Uq!dgDS;VBcI5mzHrwW;sDew_J`wyrC3>5GM(yxQONo%TombOy@rqT zy=_}EtLdzgK;;en(-ho98m2eD%xCcfK=Lu-2WLuw8Jw8Y7*w;& znT4Vh+nMbh&hr{E$@9B}%Gg3Y0K{_G)9e?taCG~|O-cjf8Pjd~#stl(vQ8a7P16md2OV$1V-yC%YRm||9hb?oaGW1=6x-eZs+iOGlI~|Q9kGG-v@*g``yQnd1-m1J>A0Fnr z2Wg3=BM2svO`t;S@@ZqB+udoDZgZ~)KAlPoeCeRm8?VSGYTP-P(rp4lF; zu|klUK8+Og`~VGyp~<<8S1aEcgo~Wn9{%56u2gR&cc zn1cuXpFTacS^vz-0{RWtGTOagTr|6b$&@lVV<$oct_7`q)$1`qzn zfBe1J|M}akQ-7l}&s`h44sZ05B>Wjz^qf1c;(Ry!)vDTbmk5L^lHif;;Y%B1QS@bi z5B@*s_6{@e-hFYi{sH%%E`g2TA|2h8Lw4J68;S7YalnJ*+LWp)bJd208kPJTL*u%H z;K5G>-%OkC!ub8vdeMI#<$?yD({9R5Y5`0fdH@F{L!C1cp8-ArP@hDCI_9r#@BdP` za50Z#+*nA2c)e&@Fa18H3h|X100wr6S@aig-nOg%5x-=@3cT7wXZgW98-5KW66@VW z?TjlHhlSWYL46WY)DLXc(HeV#a|GtP1uKst{RQUK|1YLckg;$JZ^(emKxw#IC%}cH znO?@ncT_#&9dCGkS~E#A#nW3tasPS`jr@@bNRGD%+PS~?{kwNJ9WjfmxUZac6i))v z*4J*MWp3>@0&)dlBjiYKl0xO@$9IiR@T>*Td&qnEMC zvD^1Zs?tpQZvYYZagc#fHCK`lBjyqF64UCMRFvzQekttTBW@J}9($yw83)Glr-A0c zn)}?CxphZ&29&eyN)p|sph*ySnb6-Yj#GlJbVZQg3v?w}ywJOx6f!s`J~ z+u|;AjCSNCX{746|FK`z3VOZl(4%Ik!!_tbQpD8B=(S}tiM6z{)#FVfUaY2@$jtQF zHr*W@o^!VWxR@Vn(X-8gWz-hHPZ*nRHOi~p@6)x;Sd)SHu6a)NUK{goa*M;U znx&p@w)7hiJs;;W3g_=#+Z++R60}6zs`xBEL;A8vhlzrrj)7n!8zdk*DPb_QJYAoM z5}R%&{R}c}uL7*G&}oRRq1sAO;;%Ah`sbv1Gu6`qZ3wck?95H6-duZR(+qesa@(`a z%%oUTKD#3QOH7n+N0L^Wb9=J82HL?)cBA#HN9$!wKvfihxb0ly3JfwSWbTRngZe4k z&rAD0ZB@dipIs~+d-6)$j!~I11)BE&gV36ivlct zXuiOHjfG46#oEsDZ~-dN4muuh>f@(kYRbsl7r1LvXak!r&z|+N2~#-r8q2ir)sZ-i z$>Ps7i1sM`VpbFKMM3R#u_+u7`VETQ^Tj;Jh}DTVqd=Jy6gk*h;9y25KczIZ*|MFJ zF#Y@hD5J1Xc$LCThaz2~PRuqF+IM-wITTOISCE%o{_(c`xO}95`YU7P{`;*PBMG$4 zL913+J_i=J@fUtLdvd;DTCr%SN#l<7&b8lvMqWP5oIw*2To6Adg2l`jrWsC)_Abx6 zRTgX6Zk_Z3Y;-VOp%-O}5Odsy_>$yxq@mqY$*cPeY9W0N>~O$=m!xoCYeT(^E4MnE zMIL^Fjy_L3$afo{qEp0Qt}81>Sx4h3I*rWqaVU9%MmzS4qt^-_mkTuEZOEtAa(Sli8x@cTU`g^a zF2*$JF1GJx4tTZnL(Ly1c10v*`pYf$M+K4%8YBJd9y^=J&}>A zx(#6KzgA3VW!rMv?vTnzz8@(K>gC!#nzjW4J3XdrC02?|^G{f{{e|FmLEgmKZ<(C| zUa#e*!OP{6z&C9J%^~jBe8rai`Qvb{7UcA0?=wk1@_EeAZzEa5GZIUtx6=6i@*1|W zMqL}wY}C(vE-qaqXO2ZB8+X8sXyBiWunXAIZAf6oakF6&r2yFLSaQN7Uitk6rxEj! z49-#KD(+yES~6a9wBB(EhBJfD@mtFsq!sM;UV9*wRy+_FZ1}z9cD02f_J>W`igVuW zpv~3aONzM4m_MGupX`T7*t*oBiQ`&48l2a_k9Ug4Y}x&MXq3-FwUPcv8vr?Tt(!uv z_%q9Zbk@3`31>R)?M#oyq|<=m=;x%N7ESd;E#ljK0LX%sH>B#?9_pMWxYf=aqe&3% z2&{^KkPFVZ_@=NE9=RixWr^Ux4(b#h$r)^hL>;9x#xs1b@iFZP5Ub*ib_}&zZJp^g zEOyN(0fk=bOb^`r;}T^H54~0whHRpJZ7ByY#dpEA)$e;1h;f$$LCac8=+~XR0?02F zi)%j2)O}$mt1|L^BaH5Sed6cGLpDnpmHUoP)>~?}e*c^l%tT+d{Igoe$*D5hZIBbO z4Q{j}VBZa|)99-&bMx79&6`SZf|z#3`y`f}D~-Nq6Q2|x@EVb|>5_E0KujRAX%hy+ zLCgk)jh*F(0*QpFBvjmhoG92SmgQ-?wXSp$v@$3fNKkDgGD(CTuUc?EaFMCSv2<}H z%Lc7k%G6>^yAEwKeO72cii z^(jNLW)z%Z*;8B~jUc#|JX<3U#fjFmXlZ$qVHu)1pcl;mri3^!!E?Bhr4Uot=Z zkh(+PQ+{gqd#h9J0rWdRiS->@fDcee3ZvBXm&fnNer*=F=^Caqn(KkmQz#)>GLrRv zys%)-WAEuEPWpZ=@_Ezpv(Fy3+$Q|x z1;{wgBmqvYPsdIS`r=Djz-VT40>|UGPEE{w3-hIhL^x*`!jKqrTiff}U($+PJCv=?5L^ zhr!?~QI&OG@PA)4FAVVP$@JxCmk3ujILT0}w{VV2Ts(+Iztl}vk?nuK#DBbFuJ71O zmoCAJt`M&HuD@vh-L#wogU;>$#}@?>^@!|uvno1h4O%f!wt|0rb0>rpa`W;Ix$jXv zLql$07?vwVI&{dAK)~ZEiN6j}K{?dvhnHM2c4||6A#|AM9I8L8M;hZ=AV$oxyD;Z{ z<@ELKC8CE8`a6&zumfrAmpq-nH{avZuVawfwBE-jzuFlCQX{#2Ja@`R82F}@D)Npy zeP{NRKA})|j_c(=3Ym?d5Ae!?z9fAgzsrsCG>m|>4%AIXF6rQvR^yJ@2B6YWE#vRJb2OtxZ}D zlW^lq$UCTUfp*Hi;E#7_n@R?9tZmZve>Z#D>1MZ&+DneZ<9)HhAR-5uK6c=~fHmiH zGc{TKS9(CB&v%xSiR{T2GlsGR z`jbK}OT%0=h2(nlH0Rk;DzlapUpTGSFs-**(W4Ipc<`3O)9cJ16EM0QLb?>?38?S+ zkw=;mEIgTu*q7(Yg9Z>Vu(kE3U%W^451tpx_*c;v0(x;x)HExnc*=L#9f&pMqWc+L z4o-mBD~$jE9K-e$4`Q)7X@G18sc|V!r-1?6MYpguqN@`wC)7YfYy8>Kgl$mbi!0Bq zb{K=Uu9ArS=Fo5Km9c($P~S=*HMoA46F7`#jGD}HwkWUA^e*z>ib~Y z5+C^a3QgKe@*v{7HeNCG3d%ztYE9N@vVocriZ4Rp6e9(6C<`B;8oDOYWp_G0kL{7! z(-ExH-F5B(w2Rod$e_9ag%-a4nchh*d4z>mYp#JU>LZ+OinO@}4ntA)b(7xn(jJQ= z3FFXECv4jz*#KnvRmg*=<6rR|R}Bgb9tS1HEn|Z&*;%z-W1%)Lx=s$)2}D+a>BPqh zFoe0ePTNkzVFEXkB|VY<7SK=jwi0}?5y**4AksCo=VE@;g?NquE-GtYmGm!nZOl+r z3YY&O@|bCvdHHs^{y2Sd^%Hvbz%P6rs zvHsa#g?ly$Jy|A7!DnG%=_;Vi5qM%hG&*h4lZ?L5mqS%;%4SM#^YwJ-+70C2kr>g3 zIysOe(2liWBV97Tfc452j*X`Y^feL~2+@Gnt=OAV+^B2ISnM`hx?pDFwt_Lb(Ji0u zwK~KflE9^FoOmIH)&*%;j9;|)?}n+afjEN>6pJe%Q33%2RoikPX&EJ7F(o4a0xVai z(?j*Y6BmeE1pZz-22iSa(MIlwv|-vbM9*w+ZP-9^IK zcg14OU_mlQ#w*Aj3T;s$Y&HfU$qJZK8*3@sF<4;jph_9*p~zeI^S@P^fA&eNSZp#t zsug(&FzTG{j5Iid!e;EkjN!mZc141C7o=fyiGhh?M9&4>g*vHgD7g*#c!@wZjSq&- zVMF`xn>~#j{j`P3>B>isS?m!n~3d< z$sGj7VSAh9eJ+2ONt212bF092r|AN6*$oK`t0&8$L@iiZjN=MvX=?fvYVek+Jn>yZ znAUv)xfbekU69-e%8mT7IvMOD_gjPDNy)8NuC9g;wm|iReH4b<@Mrb2D^c_}wehq^ z*SOUBe&|*zaWiYgXTZHWk**+=3c3*RX8~*#<}9H4^670UYk}xt;)@aewajPklSBhI z9qkT|)_^uo=rRr>)Pz&OF0|9~)!|_zTmiGE&BNm#Sr&P;8w8H3=MJ$(yhoXg*U3|C(pZ223`S~0^AD47H88S;X9c_{=PF^7%q zRGE4~&tc3lOtc~3dYo~%=oj^g#xnCVp9^PZx3xI#CkthHOfL!;$IZ4HdM^;sQPORL z{RJ@RK0=H_ySJ{75^WaPL_UF86e&j{C9l8P&7+qlP>bx8;hDYa-j^)PM^&EIW->7Mae`+##|kMQ zezu%pk}-s=7!K0U-smO|6e|PHF3VD^IQCFZZdl8eYJn`=5B-w;vYz>ZtqbLwwueL& zC}=Nk5&O)W*Va=drdg0uM$!jJTC=W||kv)RL0)@=}ZTr_CVi_Gvet2T6Yrakrx#l> zCcDM)-l%=6w;R_cQY)vedL&^!erwKyFp37cabw@M_oQ7HEmXEid#+dlorbB$ABL4R zzP8_4ADMkM2-l*r31VWHhNZy@QO^GZiHZrFVr$}Fi;jBvcF)&vxv&u)V8G>9Jxvo~ zqjJ(!m^p1;;KWE^gu2eV2VsNJYJNc;gn}y2^_U&2ebv4Hkr_*lMD;srl`b>mD|elD zo9q}%k-V|{HpYhHsn!%+0uFb3IVhQKbaGIPKn&oVcvGM?4pCeo)%yr;@+lQNw<-B- zH_bEesmSato_0F2YeH!>bzo>G#q4SH-VkXjE5zS`R_Flh6itL^Z!?c#i18{nZ;3Vb z%Rh>t2`|%*Gyn;ZxI-5pUe6V_8OYiTTLI~4sn?U9KLO{?0mcFnf+kyT-Jh8)*oTQC z`8F+9<8{my;sS+@da%t|VYc4IdC3#}x{uA~L(hyZhjLJPH_QUaBjUI1vvE78e7miH zbYhK!aNnR$6h$RfCu=aEF<{lpJ~JiL*5t#3>!Y}lFcgx7?#zi50Q$0KvEIt5!Ou#c z8#V~@w#EyCtq~5s>+M&_OR-ZWnH|UefAtH{ksl1F9SRhkrYLVN3I(d6h$h0 zqOR(i5gb_h{z{=IP(Cu~dkazlT$4!It^tanc|aa|aDW$%Hax#!u6ME=XrB%^U%ICN zwCg0%Zb}A>W#ShCfw*izZ#D7j|6-NG=b@r7xZQ~gK_qrW-Zd}>UjqusamckS;Q3L{ zUv`!q*@Kfu8F6!Dn+ z!(9U{nFQ4|po(l{Y7NLI0&^k#AO(r15lMz%H30gH=D~@V3&dDQQMQ4{cn*jS`%+`M zy4bb!FW1XVKbVx<4OyW03L$KSN|ek9rUm#7r_TE=@`e0owWihl<=c}B7>XC_rNSm^ zeGC`OrQtS^>y~z)7QT`x7}Iv?%qF{@cWmVpi-&nlwcbrRYWPQI#f)>U`nKDd5!1 zwQ3hdfouBy6CnI_UV_^E8Y7*6WI|>znmkrW(*^g#<+b`AFOrs8oX;K~23nS_{;B~7 zwV9dE($PAdJ^S))svrAVBDTBISDRVwvQWOUj!vhM>SdzP@RgJ42duGY>0TBve#ON! z?l6DOP4-`2iR>;=#?A9coG6(0h#Imt15EztG^cO!W= z+xc9LmZ_%~alc*YPlMf!<5u_um1!l35pJ9okl95a5To9Z0?80a`5G*ia6~FFc^OXS zZX)}Utu|qL`^B&m*;j)yzZYAH6W=A$#igb=^?@A(ZEtFS`^2j%Kr8uJtwPKTDHsZu zElc^Chln59dFT_$I}tS&-x+iAhW_So(RSnsc2P=c2hdlvmCe6t9$^w5^Aqu+0X@fU zcIkutpfzFXv%8Ij!hx-y+4`6IpQ-3qDFo&qeSp(&Lj~K%XIf4xWaom^e@C694%~gsp7HzDaeP*x>w<_--|=n7ES|8uXI74V`)iYJ z4`f?twsA#>euAhJog$zvx_8+KTuA)g;{gwCkQ+G{?J*&x6_g2b2ArP~kq1BpOM&f) zT-`A9ZnyNe2S>Yx;Iy?P7ZX%Yd}ga*Y7G$4%D8ftV+;YE)?N9ZQn=?qa(PqwmN%V( z0lp#@1alm&g0tcLgS%B#)reP0E@d#j8!;=4A?IJq5VQ!s;{8yOnnW$&0+|skF3Yyv z??hca3_}%&@7zun`1Ztm7Z?K1j?mib`_q)Oi$l{bHiW^)W&BM_L$6KaM8E%RV!Mfi zuP|yzLM}l>d@~(efKVNxHbwzndl%dWhmm0Pt|6-Kn4A_zS_44Y~WvA>br{kh%=SRo)6lk=? zg!`NeY;5Aq+quVe`onjQNXl)&^8_gcUt8>xZxK(~8hX_NVePtXlV7P39Nu@qk6oI` zHdM*YBK?8tGUSW7nJT)M#OkiL`OAwb?w^YEY?^q^V|2at0+iEhm8T6u?J}-%Bwkcj znxj@nwXyKAT9&`xDW&3vZ)Z_ukS2=1Kh@On*h05b1T=Bh5Q5X@b!YK3PUE#YLlKXD zb{l)nMZSxR)Lu`Um2aBUE@SSLt0A4jC9qfK92VBHXoalR6sBS~i9YOJ_6O2mZn7TH z=Teph0mz(k_0-4MHr@Xv>Wnkqi8jY(O;T=x!b)^%S3U!sf+3z$ zzaYi$7l>{*h3g)I@1ouR!09o7Wzxaa@xjrPnu0TVtnfrrD?#%&cA?u4Xg?y)_)Vl$rW@+GKxtvYOn02qmG$7isNTcdzs zT!QqKF(mazC5G2hWzYxb>jeQHve<^iv{G~fhi0BUsB&Q0y#DF|`!aY8)k~E3Z6+@> z^argBMvsJ;Jjy_qaVnCS5Nk`O>!h7Qqk?8C{02TjsN9V$R6ag(_%|m8{N5a7{d5Am zw?FSu2e77NlvQqIs{+@{}`NW*z(q5ns!?%X3@A3C`iI0bd4%3x)%uW<)EFX`U<$ z6SH(r$hVaO)_nYK>7KnOrI9TQB}7e>meiaemhs!=K`kMnD_gifFy9mU$H?cRtEY5u zU;#t1oE-F@PozR_EcOi8CDo8UNU=U<{+P#{RTA)i-F9!cwZQs}DO#Mgxn6j)*jQ>j z0w{bWqDFD7jHm8cJbA$2w9jk(TY`B7bN$oWU6})*5n)l~eD8Y z`|k`M32YMbY*p9PWTVs^XnAeq_c{e2Qyp^hN%XkJMnb*>RF87ukhK8A^R!ZBVlmel z?fEYv>V6IkXB{9K9|K1WqO4p_)aHS9M+X5j|n@SHaD zb^GR9c3SGz_vz?7knkmGB(>}y9%W}M0(_!d^~7V$<7n^2La5hl;!&f=4U8+MW>K1vVy> zK|2v5S{|^i<;7Vhj%1{@G?!+aEpye09F0DDP5`l6DK^=qdjp!7oy z@qv4TJD5LVisDS5p$MKFQKp}|Gf4QMq!cH>-oVrfvh}?as%@+N{r%~cKOJI_p!0}i z8XDM&%YP*s7(OG~?sy__P`$dgQP5^1r)dn*25io|vN6I8-;RtFX-s9I(I{8-$|DYJQ zQ9Vj+fa8!g#nc0`!Z12|aBlj|H0xLL|2b^C3b}Ch0jnaEdV316=3+j{oMBxhq<{I?$G7>HF4WL;pW6FfMwPe^=X_|B==C%SHgFEDwvvWG zv99Ab0^Er-4Zw8tiS|Jn8b?*LO%3!{-7esiQ6lrCY)cp<-YbYawff}g)2|e1cOtKb z^CzY^3kh=+!s61Y-`goRrzhZvIQS~H9uaiR81-M9n&k(D$59*vrd7lRe53#jk$9-Z zNU$70O16m*-!qiM5Bm4A9)u-p?6Wb-XZteMAFs8RVbF==`uA|Zz zq9&gEz>TAcr@F@wMzzQ%8Q*w2ug5(JQu->ll^_Q&;DNZm5|aO_bpsWUHek1>mDn>? z$h?7<_dw7&QxAVz03_w3=)5zQA-5uCf|XMS;;n?SohVHi99VTC>MN`E4=O@0_ZXib z*i~06S7aio&p!9z19?{?TdoljYk_n_ZkPQsk)8F`nb2L%Q3TWSRRBD5sCnKPffAKZ zN@7pt{GkuUqr-zK=lf0Xi_RVM_88!swOSp&=BXLQu--QZf*9VI-W^)M27)u%Z-w8^ z+JTSZ!cdL~{=05#yZB1IA5O`)`|9gy1J8j!@{YMdelg|f?J%u12~>eRx*V-Gkw{oZo_+BG&LgA( zXK3=bM(iur)a}F5inD1?E2!K)-VJD7v&aJ7mfmAHIcK=^oCbtX*Y1BUEOD{>vFw*H zis8AA`)|1$Esy@O{_|wMdzE&jE*v6A$}UH3Bg@dS4GY|-(cc%$Kl@gJRagI*41GPcU|0^- zhn;Y?`KmDAGNYWvx#ehQ`89J3OFUZtQPW^P9r;b-zF62cZAE{EPZe5&U%m=BTvRUde^clm2{B;H z1mZ^h$_6d-@3*sq5z?lg(CFXP+)9n(|JU2%QX#*+vz?VxO}Ix(<&*sL%g*pYv@rei zKQ~s=lkkTzaG8VB3iJNY|MHOzqQ!iV#pJ&|B2{F4`g&+j^9+2r`tC&p%WZ2-<~~pN(f6wu0}%+d|6WV54L@RKucDG_`};GO`#?JL zH(Lx$z&RY_o&y+1I@IHx5Q)$7Nowl`%E_Hr(W6H%=Ft&B{BzJ5#P443OgKSNfMs?4 zo|rHe;ZT>6Rd-ylk`BR>L{rl?C`+PZqoIXaKMDxe%13_H*PM1xyJjOZ>d@YS3>PTe z2St3}(sQZ)Ne0z{F0dz$=*v+UJ~Y-=Fqdr(;#~D$JlPoNO|y~4-U7xmg=sZM!saQsjkTutRf?XE^C;B=G<3KoA*cqT>0ZD=952Z z2z&;)H8nL~OJ$}Hsq$di{+!>^2MMvO^(kxdv)#?WS`U_%Ee!6K9t<%VgVxh|sb@cg z48pqYu#DtS@OztY2R0UHikOZI;8dB)^X#K_%Ko+Ih^boALl2LI~jZ zl#V=GThx7wYH_6l!mSBQ6cK7D?lu?FJImFPBnko#^q|X20qrQ{DD-wNO_te$4rpsc zFWgryt4O+iJF=w;;z3o7{}5;9yNH24&UrN7ZGHy8%XUt)h$pcsl_-;h(B}@C`W{sj z;kp8TgH*YOstq!CL~yDvxcT1d2>ucWh}iCq66f)=-2qd`JX%3E>{z{Of(`! zpM+wU^MD^Ibjt9aFX}D06hujFl7Y2{db4l^Ljmms(!OovpSYN<`G|OVrXp(&#pEj{ z$gOCcw<-_h&}n#!svD_2=X)kk&H#;dK7TMA z4j|!-cPs~6e33C2-E^DA0b6%4VbcfJ<$fnh-|Ws?Z794hU;s+3)h4iRjJG5$0X}1( z*#$CnOP5}+yh!)&iSaa13Yv}7Pjs}7Ug*Vu*d@-$_tF5*#TWhkpmQ|vAAc45h>@-d zWjV>M%slg`W_*DNETsF-1BJS!Zl?tVM5Jc-13aQXasu0oq*Br@6WI_rqzyU?MIBFp zlkFfO{~ZAW0YIrT(Y{{0cd5_PU7mR#`%3Avrftbc;KWX=`1&R8g3non3wz+s{Tq5-Vw=)*i%yHDfnt zZ0ze~p2cwQBSA37^-@))XR|?ROe?nO5Y-VQlozCT;R3cN`|5ui@iRxqCMW<-ACB`y zAa>@9zXC4y7NV9;|0mgXZ?4<8@IxeFN=B49mA$|wjBZiRmHJ0(}~dp zD(u#oU{VX|&?a1A55LL{Je~{Cec%^a+RT6=p#+h>ul!G)J25A8a zC;PO>&yp@}kZRdKOSXzk2p=5s=(^Px*}(Th|K?~V-WuT(3(#*RL5-9^XzgANX; ztI&vN0g;h!Gy|HDHtBa@^bdGJq9_YYD{cHz!!9IH3y((rJVxKo+#o!UynC^aH!9Eg zWNEqI0nR(MAbOPgsRLg%{sE*}a?|Gj=`kX+K^=yEa*Dy!%nU#*S;MPT3y$BH(OogH z9%RgYa9e-=!9U+P`3gJ4*ufkG?OUPLZ!@%HxCZI8H{k{irMCZ<@g@35vlM(}dgJB# z?j`=}RUU3Wlb^NI4S((s~n`t^#RI% z-XY&{S_Rdek=#eAQrmw&1>>oysbt#9-c1`F-f*AXSLlO;f4aW=jL+Qu`P%>aUH@0z z;txvCsri35m;cv){nx*e53)dz;z3_i*i-BO#J^tsuqx1zqJ6)THez$pH>bv^>YK?Y z7D`Rg{`YU;ipPSQ`XkA2q>NTZ1~YKR7}9>`An&^k_-ID2%GV`OWa%P*>p0}`HB5`p zQeQ6>utu?d>5uVj3=z&9=FY&GY?cN1ITy-Wo!(NfameV)^JoN{;Fd>AmF}I4+HNMd z|JWOrP#36WI5LPT=G{eb#p(~q4FnLnB)cu4Xtb|EqqdNm286sV-fK&}dcfl+18^Qs zifG*jWst-g$ZCpl^C=?kw>xc$gc{bM%Iho`in0VFY(_P%sHWEA?KTPKyG-=dZzWZ2 zGTeH8!efUcfUPC!#QT7M9xVch)B4MsTjX#pnq!YICOEZ2V5KH4NO z19(ln=McaAhOP)kMIbHq>K7`5uTklz$_01Q@~lKJHtfYUWp|J!+ABJup0>>l1i6ej zf0#Oh1mKSt0OYFR_2JP$JrrmUDI&NsX;9)6j_ePDdOF{@6)BOxqFpC4iArOL#*dg1b}U9C!Y5i=$(&OV?!-^3$h{La?0J(@~uo)7z;Hp2d;1 z}}--ZY<}9mqc8`(92&Qinb8m+2swN_)|=T+E_*_^uiKXP0+e$6>-e9s{rc zd*&Pvu9grM$58WpOpNHiAgAjhB=?)@12=e+_fltX;SmX92K&nu2PfOL^?e3ww15=Z7xK8GH`wc)t;!*`aOc%t`F~PyN|7x z>Ir&_MOE~C;!6)l7`bTojUvlnV;GIO-oxvv=sh2ZeM!1eYEJ*|DG6gq+0!IH8tq1Y zRR+=RQh%yHy40SqTAzRYoBe|->mihW0FlIPAUv=V&`UgmB7wqu^%ty>1z0)`w3-K0 zG^8ky0WkEjkD%Etl-G3yBrCc6Z-w&{_2V)}fLI)yu@P)rx+Ym1aUOWVYIX6KH6d#w=q2nvj}v=v=6jTfMB1q`{>)AoN&zLQ>lES9;=+0if|BI}_#i z5J*bEw9Vc%`Q8RHIP>v*Bwms9hV34bIe|2|O)GQ~SgsuL$V?h&&L?uqRu?Vv_H~0M z?Q~G;)~FlQNIf{N?OFMz#QKW+!d(c@L1HhmmiK|5)dt?)>V!-l#93K^*&?3i9SlUV zu8%sA+8-tAfI9Fz!vGG|YHQIH{wZY}c}b#Z%~&2c>BeW`~>A1&-$P-+i@JikQ7Q ztvx=b3$C7V7+skS?5b>KBJ$CaC9o9sJ#ZcOvi2+DV_pbK+TRbKNcO9WC0b?uc|_QFc@o5OS-K$|9Wh7gqpFk zQd#ySWaOivm)3&clr44m3Fj;5g#WYyv1r&M^PaxadbzbO?1;Lr%E=pkxWbRhl3(aJ zeof~}She+Kl99eQRS(iVP^_!Ib0ajDC=y8T18*~z)9x+oL1Hnjgnh7K`!ntL)~w zOigQqk735>#wVV+s4DRGj1o6bNqk>;YnrTWr(AptH=hs8&5^e2BMI)80=g^@7cdAg zVb19M#B;c+oB^!JZssamUMD4SC z0w{((;6<6(6F5j{NQE(ok%TZT=HGJ>!rZy<=tX9Hui2 z@5NY-Y)%V|*OUCM5~%gox|6=XdrUKNzkgr&7qV zvK6_l)XzDCZA$$5*E1ac^IHadhfFp=N6ur}SOq?TSXz-*+{dn#DSyFMx7}`aP&j%B zEa!ThNZXr1yJ@pFoWuH>4_b;F3)Lxqq0SQNCvd;wXQPY7V%hw`aRLD1FosEYO-bjp zuCzkjWV7#5c8mTi1Y9A&Y2+8cX!}1#NP_@nQ$-#cWaDvZL5^4r*^zh)ih83Hyr6tA zL;rR=R}(Pn%3&CplM=07&d)2#xm3t`?E1Fd@z$=`=s?t62c~UZElN}rHy8zjfRhfI z|JxL>LyQ==3P!FxaBTXl_>l-MJbX8k+hpE^{E;J1|W2 z=gh%oGz@4Dtz=r)0Gh)<`7!eA#aX}K_8%D^F8^Q>1$Wzm@}jxY5`pG!;0{uKf{6X0p-UQrGKSKefNwT911_N?-^U zi^q3>I9e%g1gfmB%F+-aVo)VN{>t&?{3qc-u@6R3!g-_p&WhZEpV@9oyZ$gXennYJ zKTq|(1P8$0L!mTTC znjSm5`!l$(k|o86FZr32l}6J`05zSM1}Siug8XK)GPlv90@l@e`B$=QQ+Az{y9B~L zS>ac9Ejnh{+oRqZI09`ULK)@`6}jF&okS*Y42}N1pqL22*>=!MX7Kgx z+~X+7mrq8x(=Fx2HD3(V;fIO{f3<{nO(8DMc@xY7d&PUPtP0DTl!`_PUQm1P&~K90-KD zfw5zdSW^8G2IPrha{|7=j35vIL@-%^AfP&BQ$kEPMA8_jDzf5WxZB)Bw)f^OU z0dq{ofO#>L=pE3<>-eK30OA3VaISj9tl^GbYlz_@*sZ@}MTBDLUID(&v7}aaWR#HAppNHh4I&)LwjKaii zd%Bcm7fk0bHX}KmXb4y~-5wDFpCoLjmU931a*VNJ0`fb^lLi-ZSduH@+w(==dcWIV zfe|23d?;q#_zpXd=8!>y$yt$m&9#8oXg3BJ8}`((qxtDFU5Hk-&H># zHF|Z;BOUsE>%uBzKK6!PcHD-(q6$K8N9cYEss10v-a0PobnPFP5fDKZ6$Akh5JlI01Ud@U;w40J6x2`QHD+dk(3(g-}Q0No@e*@$v)pd&Uu~nun3=- zx$o^gQ`f+s(-nj@n*V@$NfH*b0)Rjj-fS?T)&Yv+P5(BC6GrtBc;oraH`6qGC;i+jR zpSDAKX&DGQD@#kuDLgZ&`wS^R`&cFn;9i`3eBHpi$};8pryLkU*HCZ>w7feT9Me%WtJ42+ zBA+{v3BKG9SNX&EfP|@rl34ZTt2*ukn@z$0vge+M=lo1Qn2$t0%fHwA>F2gmq<5xt z3x?#o&>%2q2grG!2v-Ii4f(8kcbR`X@UXgpF#05Q-`xn%r>QT6rk?`&vjxyjZm9t^ z6|lA?pzIA+1H!)jrC>IIKig25JV^cli$fUvY{#0fTts#Aq|KKC0pPTn22|f;m!*;6 z+=_4PNK%f~ za&2_3T)EO@u0EX@yATr*ank9TxX?9Yx#E>C)Zg}I4}Q-X0AzChaIX9Fcm^~-M_71x z_)Ikt;7(L6?zU)yqyoe6Q|^52+3DY5yt@x3tR>e*goY{Ihb_Hq7cTxjNLH|(Yz!@B zmV10R^eiN1<^PWH6(TAprZ{T@8l*ySa1a%AAhS$ID$KZuwwq{(N&_neSyzoApkaf; zj$pW@TXGsVVCC)u$YmCseHBOxg(jeYZshxn;?q15{TO9>ExR82S3Qvxk7?qtckPo! z?Vr`KIIf18ANV>ll@P8;SOJ>7-T`$_D3nLE+9ywQM63!T?S7b^mwK;a@vwL;1sIVQ zWg(47!Zv+Ii27|CI?WtLbuw&0q#$hBa~@HxlAq?So9;}thkMqc5z!c^FWW)fc5jiT zI#QhRlZy6!JxwoQ_KYlu?d<~aZ-|!KEd_rawAe`S5O-$pJLButefck7zHZGen;^Bn z5n|7?+CHc8II-7LeXLy<-=4}m^PFt2qm|2Da{Is&?cD{?OMQR0-XzI|B5_A)Yio~~ zH7!9MUSEzIMoEAOi-;;JAqvVCC_7L@Yhe>?NbmZ9a~F__)jCg`0ec|hjXDquNN)Kw zU1=VFI9UCJO@A`rV3iyz=tpRMz#$6|2lRq8KzJH;2C$7 zttUOjUY4f6T&B8hAe zD<7i9ezqLe%u=Pdv9~H&=O#O~upwhc>@G$o0p+YiuT1ck(Ql`lXs)XD5=Xmr3kCoa z2c|9;Sm3k~3SQc`Sl6yy*L(p!B*zH5dazh7X6E|#EN4ne%3&&iKHan}piDP-7n0d2l-@c;{}s`W zt{D>6FzYdV++*mfHscZRP_OM!@B9rR%nky=A5wNLjch7Ai88>+;e>_@XKL#q5`v%` zYCF*o%&jWoJgtPZ3}+cGz?A7~%5sNJE#9f+38G6+1^}B|m%FqXYCmc3gJaIox{b{m z*~QT=Uv_5Lf8c=jjkU!iSyJfRww;Hs`2_ z=b8lwaN-BGt_=YOrEk>X@?|BkVDK>oBhnq631ZN8q8}A*ON+hsi6ZtRC(OT9z>Ho1 zCv!W}5rVyJz^*I?WASRiY=|gZrkr&-+Z?3Wn!ayOk&JBeu(HXVHl2PXe(moG z?d2Whm49Uc{I934z?rq&%9CFB0%mO29SoCUqQ6VQtWSzbAin0Os(XQk55!zYyr4LM-I1xI zOe0LcSTBO&V^8BOs6sh(-*{C#TqQKN=KZG`JgdbaCr#UJeEOgt6KKFKLXG+-$I8BX z4EogllEs7R0jH~uwm6+v>1*07YjzueZKdf8utJ7)M2iugcgVo9~LK9zmPC7 zp~2Xu55(zQeaRb&m0g!g1+(oZ>aRG%gz9#6T=9hGt{L+;n2 zZTRbDE&-kPSnUM``ZSg5V=Xws0frS~#XTbPor2AWd-+;_vk~paf!v)Fr7H0mg0i89 zqK|>6z0~*r*O0z6xU$dmZS#~n>`g}1rI~({l1v9IP(f0JLNKa3XFIf-0$|?ML_Op&VJVM0wTw>gfHb0P&4$_mU z)eQ;t7G;4Tf&n5b8(5BA(4`iBy*E;Bp&J2P$9KCEP5c~*xlc+S=(*$LlsUxx*bp)c z{SA$PrbGI^{b&YdQ7t5&F8*mA3NBJP0zkXHqWXj9O*|(1nJOR=@hMF&a=;@mkky%JyAj>Lo0i zrjNI;tyxD#<7A}tIwCT1*cJRpehkNpE>!+G8;6lq{`#Bw%VZDD-s}unB=^#tfORaEQF*rW^?Z7>x zj}q33bHJpJSkDBX_>l*0sUt01AF?u1tdM{NaJiN@!eC$u_?tI6<$AomL5VJ;2K9*v z{+xboXuo-UJ$U*}lF=JinX!T0D-T6WOY1SR1p;z}bL~Y!eW?dvOd1k{hMIh6x~i}> zeLUL-<+f8BwLd1vN^7Ap2{B&Tf-Y9rr0Muv_p88GBlX$@lnyBG;4xHa*ZS@%X?MAE#f0PwcUnPkxe!qy zghF$x=g)!a)Xh3=b{lBfQXn7(s4UbG)*F%RfdcyL1`X%ku^Nb&wu3)?2f3Qskg#r3 z=MO;ko_8Xj@nZ>JB!{{n1T@~IfG7Qw$pSr70{r&?M072pekV;r3Le(WBlqx);KQ+n z&fUdVMrj@8W!e&d|5D{aj$0-`4sci?i6e*K?)I)2M;GqVNsEwdcHEj&srqV56&(2 zlVxtsbd|K2PygTZbC`dG5}X(>XgXe<8>8vEe5l;ij;-tUJU>wz7}?b8-oU<$alWJJ z1Py+<=S_i~(wqH=SXJ4za_9Mnc5tL48@(D{paIYqQ)IYx>z3OTOSE>W{bYVll!%+9N(?j`d75X#|o+ko-z$9O-BS(9wAt;CHbbMAIu)6&nc23aQd? zBxA~D5>ck4NoYyO?^y#^AkT(x9&OJuNi zODPz5F9Zg(NzxKvj^T4H_V~@HA%=fQ&-3iR(C;UkIyy>7Yj;rM^l$-O@*prZKV^=D zkGRbCT$sNA^S=o|>*QWT60qkY$B}V}*vBU24t3AzgE6W zZ6N_>IX*ca1l*ew; zW3RirNAxcL!Hye%><5QGzW!Qch2}+V4Hx*f5B+S;!G*mZ`>la3^U0eghIaP0zPN4E!)m3!~h|(JNL~I;_Ik#vm zu>*D^bPnRzweK4=-;9293}f| z)d7?18@jJ`o*ncZ?#anSPfgsLyzQX?!J!w^PCfxe(D-jy#%nVF591Ai=w*^Yl7=__ zEtjS>&GS*Y%nW zV>td@Lx_N=pc%yv!zKR9DeHgjMvvW(b?t|h;tb7@b-Fk8cw+Kiq?uj2_ISHV7L7<%jt4!Rs-==VAfZeCIV3`4dPGZ-pdL{o zi5?)ced`eE;EE=Svdz4upzs8sx>N3FAI)XnC58r*@}%Kk;IAC?&Od(14=MfL z8=-Z3CKFC-(Js=WLd|B`*X+7DG#i36BtpT(dlvC5_ACv z>}aO=VFz;(s!AwxvU+FufGiHIO}$j^s|hq~8ly)qCK-*R96c}pA7gsj84{2~o~b%- zcLF_eQguUDM@Pp|*>G{XdKQy74{@=DASt=-z2vtu$p|yw@8uze756Q{h`nq?G%m=h zf!*%*dU@bo8O8XYtlGOTQW7*l4)rw7pQOyAR=g zAzFVy(ymD#Eo?GBdXc89@%zA3*|K`&|A@IbBkdh`2$C*tL(+veiG0K^IEkFVK9h>H zq~_LaSs^Bg!&>ezpUpqv$jygn#XgX9+Q65%H6k!l?5Pdl@3nz7S%}fKbFAlL*a2hQ zP|Y;<9pcJzMpTS&d5{ynMNAhPI0$YS@rHTd0ncYtcL)lTo)5cEKJR^XodWToD9u(` zk(waQjBtNUFAlbHBIysvYT&Ey1%1Q__2U+@_XDz)^p+Riyv;2jBBSnuxTM34_YEP< zun59!dy@Tt&_$|>hrfg6yA?)g>eF+hKz6~b4@yYKDb{K}cBy{YKUPuBbaOeBGtax- z&M!4zPFZ+)A`W6Kq(BFQS#sct;2ZBqKL8MVsmsJ&!5Eq$cL?nOh_~<%tX$*}V1}f$ z1c275*%dN1D(gNYdWf~J-LAmF_d|irOn^KH1_If-ilHEprlDu^8)D%?Bt(nCR1Vt^ zq_P0jy{`4yuov_rac45tu=Xu_lWTO{+=l(h?xz|mMEu(&C|M0-yOc3D2qC=-0X9e_ z4>0R3?Rj3^E|8gQ2cVq`K5ve#ZUb?={9H|X0oMfY z2f^(Npq+h(biWY)cIIpDIwU)kO?nCwosts*=v)GGOyp-uM(JYWWw{P|nw?~v7wOqf z-)_>jI6fAW<0|zc3RRc@@I@{2J*Q1mkP84Ltn<-(OKj>Y^@0drT~?>}x4eonU!q72wcBWMqHc(cS%AC?%UE3`CZp8`V&Vi$ zN7#ID9J`sv${~d^9`z7uO9EQH%^6;WrCtIz7*;}ad|dH zutyC-Gm;J9&7?yS0Y0U}u8D(yVN{Y``bN8gt+Ej%8vjJ)y}DK9QDj(Xrfd&$=X#3M zF|&*&!ZkN_Wm#dKuIcs72lg!`Ygct|VPkXa-Y`~ry7H`-JK zdXIp_;r3D*Y}pc@eH~den}BivorSBm^|wsToP=4%)5VZiTR9e6VqY=Kiil(vkSZk0 z6D_c7RM;}K=2%Me7-6<=%XC=7Y3M{C#S@oq?XFBv3sQ{ou`$Swv_(2{dg3bZh$9wZ zg<=`Bpj#Z2tb1l#n*Pe#&Rg(gN)o>nVO3T!_X)&_M&Fy(cpN@8-)gUZJY3V2u&OQr zk}xV)eWZ4cBVR0R6Zx67NG)a#vE{#TdMaKEzc{zC3Yv$A2$7RcH*?9H_GXR_(ZCErV2e`yA3w<#vQrLTkZZF5 zr?4BrJ5WdsLTJl;E|Q5d0O_^*9yd*=XJ17mwuG(b9sb!|qa;rB+nvX&5aQ;BBdG~= z*e3mreQX+9t-IQFDLed-3|;)xddiM*KlfRJUHv{( zru)Yz++}A`vG!miafgzlFJ*d&ILK@SQ9$BG(_|03EP3!RUt*VZbfe|&7^d8??0W|@Ed{>8n8o>~E9hWXX|7DNy+d~bkt2rg_LcXVj>V_ze9`rv|D+vwE&oG1tWi8_QMRfs8pE6n1)DA}(abrn=*(xOKyH>jD<2cX z_VnnnDdx~?#bMUxG|s8TJM3Zj6G+9aq$KR0i|>C|9Rx(DrZo%-eqwcLRO-uaq~2NO zLda~k7FC?;V?973t}qn9El8*Bz-;;Qr9fUP)`22EIeEcXQ{(I%zl#UZRbiJ2cF$_p z%JnW8LYzieZRG0doXsZx*2BSnkjTG9;xwk?TiB<2CnhK5h)3^-^lG&(@$8@ap*F3X ziQyFXAsu`_tnN_9a~ap|BLvM%mov)V{fxwwOPOEb^~uPhE)+NuNqy1|RU>QEnn9EAQ;nyN2aNGD{U^Fk z@;jQ&xRz1E8ca?QEvBTR!u?#)xt$l~$>_=RFMt2V<1{qa59RJu3oo8Vzob#k`eR6e znR@qBJYSOk$l>&hf6R6r7|Q2^K=cZ#F@J89{HT_)H{iH{h_YChafQc{jdvg&ANDEd z{QL*MzAQ+hB+$V79{1;Pvwu#1E$uWB02fpw3V_q3!kLrsa=qx~DOJ(}R0^2Q7v-Q3 z&?o*NlI9W@lHCFQsa)Qlg~WQ=-h4xp2kd8m zZb>qmRH{c?%2qt)hJ^n03&9g}i%CKPvR3-NxQ0&r>(|P7oKii+T~jq8i3f=0!^Qu- zQGM5c7Ei_ruh2~IH$^3-qK6L&OZCT^MGq`>OS?h1R;i0}xO%1eJc)due}FAGhq;%j z8E#Px-+EDJfoxP?Z=MQ*lrv&<*;n5MNrqqlfKi9Rzc)2@!SN|c)WbT^)Y*XB>4GC$ z?rkMh%iX`&jwiG1N(a&%VTu0yV-rsFb$r0)kfXs2bJF|iORXH4HzRBZy@G$*0*s;f*awEKTPyWr~&;%cxLT}<*X4Pqg@i4vEI%Iv+OAbJZ z`;ERo#*ZkfsJIRS%KV#z!BPp(2H8hxG=mNw^qKmOtU>O2@u$#E%wY)$I`{A%KbC*r z2J{;-FEESBzri-uY5&|5ykW$n8oO&GJ+_HOOU@9s01MAFny_KgtHTlv$ zTYcK*$)*72A{y&NwVVSEI3QafC~V;HY8467vLS(KEf8L(#2S$~8y18O5MSPfs}@m( zfM7ecAKXvMI1NPok5pZPzkT>zt?$iSnf#$ro@@DtlpSfX?*oNx#3Z#OveT;tI$IS@$p! zT8G?*NJZEN{6RnEEy?gvMZUAYzIKjZDn*1P*AC?l)kxc&LBBuxIwOO|E`Zk@w>0QMmHIjz9Q z1Yuh%Eq#|eHH=FAwg&NCQBhHKuFLXBXrhAJH+>q`L#0q?5!+t^up|r0e#A!-iqN6% zl`B!uGU7wyKK?RS#Wb`>{8Srx9T>XujwwxO3{WF7`vh}&riU`;`|Xje!Fg!^s&<+0 z>xPg?>lP<9_1c1=sQynmx*~{E!v@;W7W+WqpkH^hUkwEA=^XLytz4yd;d)0H3~qWQ z*>Y&uD07NxdF=@a7f5Mi0Z8)(<2G>&Zo~(#j|7y&8P^~g4j@`VXqFPl^{WJ~LZ5o@Foon;fEisC&=E+ZXEvm2 z{4DN3q~_DAstd3qI9{Okg8_K}(M8nmha&i>=_cFQrOQMwqh4&lzEKgl&30 zRXjhZmYzn>L6+$U<}j)Jo3%^@^@Bdh=({ID`Kf51XnwWs;#l;9HJ7PAjp<%cEf0Gd zKuQ)d7D67hM9d(Zfx8UD^cm9bW>K<;v@G6^GzDRyqEqyWt)l5+GTuJpZ2g?qv;m1H zMuzF1shQB*B24s(>Mk-=jfo=)t$Gfk&tMI|msh=!Ab}}}0jadWaH4{~l_9RTnoYgc zn)C()+Vb(H1{nR7&^VkIU=N{}B_PD9s+6 zCus63fw>y0kr>;Svo2;^or4$AX@-p{gXwpew`}1*NFJ?-j`f5OL~bGhhok+qcAh{* zhQ^L#n3AMhTvyGTkm$x$Puz%m^%0&Qv+J{!NaQ@iJB7B`5{pFB-WkgU=1T0h_|_0a7joE64idg$9{(O=ZUcQ4G~VPwd>nxZ7u!Fc`>5SBH#Gj z442`x9P_HhSXf6}zjT2G1nJNj2HU3;^q%f1mSqd^P%2N+yBM}`zs zO~^5UJ#kv}@=!=D`Al@Y#&Y}IYDafXZ}Mts)kv|Qjcf^iY1Dpen~BU%eL6KozwzMn zm*gw{<-UIAMa`7B8EDfAA}V@32(;g1L79g1Z8V3U`q&LLn-Gx>ouI{7LFJC|eZ6`{ zqQ)p9h(9c&%2q#m(^Q2mZA!?NBW}_(2Z{=r;t~P^*ex?LOama4n^HK(zWl0)E`yFW6G2E|UYwNwl3`UH6U9UA43d|Yv}msr zjlmnww8deyR||WZ2=fW9JgmzpLNCDU^s}xL6>CthAnqSptqPI6&C=?cs+v<@vFjO1 z@h5*+_~wXYn6DlAxHbfYGR2*9SY5m_?ITaK_8b`#;91_MEE+B;KY$H(ck0p~D$BKp z$4Ay-@kC%pGH)tQ_2sU)RR#34P;JU}E-5LgMpYt#^<3ywa#|}qXvCDS1Z>#C?+-~v z)bE5*3(>k*9n+TIu;K;)Hj!a?! zadl`wIy7sO%m?Z$wO`IF&8fZEaz!Iv*>6=L#wthpAOh_;2WpE!7^M7DwyQ}bghw-X zUZSkW2aujHkin%Oh7l@3wX_?uIY_!$w4DZ9<0CSf3Z|q22*dsMg};F+bF~z1o<4-N zZ@KzSkH`bji_-Id+3&BU_`&*i9X)v~0>!1f5hi8ALQnfL?PXdMlLNvL~lFjU6p@)u7K*4>5~42-O664^T5NR*L^Xs+kYf?+9p;_R?T>ivg)m_ zd34xr)r(IO)5wrM9 z!vR5m9IWBA(%$Z4VT;3+y1MPYt=tmI6unHZ&8jW)Z2@Im0d$XetLsRGlqfxq^~a?B zvH9LHEa&?%rvyl1&8QyrI2va;t4c0tm5*KejwA*!n~=9{HGVj#MU&|)Q4xPat9M;u z<)<+BC}{Ge^7r?9UU)-a=K*6QyS+Q;g0322zE9#St7SV52+Qzlz z!=*IH6d7YHsfnF0Ld;BubeAiv_nhx@iB_lNO2uL^jdK+?2@EmoTXgw7V_L5LhGON9 zL$tSX>vMu6v_pdQDtJZ?b)FwT#u`D;8II(%<&xowEU{zy%#5AjyCojGve?#_NerVJ zsYlRAZIY>U6E~qC)2sN~Ae!%Vm8QGD607p*fnZo2pEiK+&16!0aa`+KEyhFaQU65af7YJH$P0&q67~w1lEt z6W2V%U76yw_LaH9m7WWaqEo^;rC7x!V)`xjKn^L%B{j<<=8?@=HAE;Z)P%pK-0HlA zb8Yp^Hmd{Ch7yt=CO|86IQ$@A#QB$S=E+yhwb7!F5wt(+QR3sm#U*Nj>#>_hXOZZB zPUWN}-NRN(tXCpqzkU5=cebpb5wE*M=m9cBwET&#si#B1{MysOhfptw1QoMDi}k{- zK*)?}EhPd?U0525{Y=20m<+rAuk4o6Y;G+)u zC78nYQD;K~g~nD=lR3^EPevth-FbcE1VU`2^geppp?#q@6O1VGIPcb_g2>?XHk*Tf zdSm2A==GNa$!EleByw0^FR1V z`0=hcPK!70E2aMX*O8om>EAka?oe!yNQ&66AN*g4i~O~^$?wPgpC03?{&%_U6YG)&z zLpUMkS3S@Fb10!3PId{6MVz#5Y5Ubt#gvEYsL%+nKCrb=?)5isPMf^DtkL6muBMKcld!kg)mk)U&!!GOebE=R?n5 zq>0#*(rvjqOh?luQ^t-uLA|VOz|_$cD!;*avfr6$(y9tCAD6tis%tdP?igcxaw(4I zgUcw<#G;3jZ>ay=Psp(0g^ZfoNrE!bsywBC1XWMmF#4mU+Nsm8CdMVvwW;Rtw48$u zn#^KZy>`vFYNgv|)9It(%Chosx>Nr>B57e_f0;5u?^{019dqheH@??Ua;t9z-y*}f{rbe7P&)AvG$E|#R`!yoM?zm-$+5}5A}<8X)m#M{u5r7U9YdDXFCH*OpQc2LvBMuwE*OYvuae!5P| z`@<1b-mOIW_iuUcy3xwB+SibQ@J_(!8r|`(Z45s2u?#1vDCLZ*CqK3Q8RL*={xL5< z;Ryfp=p;$XhVA&rR>nTE{q50##!{XL)2^?wp)3E01y^oXKfC*i`FJF1Rjc7kKCK|? z1m&2}2|gWh%r;M;o&M#CSI`GgFJ6Se)fWBVVN^PSI+UWw$pUwYcg zmt;AiiNZ7&$p~ynm(kIoqjhlfFpsDD`|H%bm+n*vW^sve_m%NPFZM_ z7JQ{y_(ACDYQ>aMa)W~PjNz@M=Q7ahSC_cG;f%dVwUu&gDNzlrl6si6 z2Qs2WYwKBgf29hBA1^C1=O`aXMV<)nPT98zsS@$X-M38fEwRy4?wA{rIqN;1ShrbS zDHI*?;YpKj>h3wyTbEe>AWmYprPBzP3#?w*t%~NKuBM%KOpvPac<_e*Ye0$GtDd9$ zZr?pUeBXx_{NAeKHIhl+5mA}4tMK@@ubdLDjA2@rWWP{cN8~ z;h2|fkG=$MzWq|W)EV)B)>vZZ(?=)M&S^~Ty@Gpx{N}OvAk}1K$=YPCedfkf&*-Uq z9EWX!M)Vxx)}^k?6vt)8g8a=Yh*0)sh34FQGOIbGNO-30iw;CrYXM_p>l z*rTaOw=aWB96v$l5niU8KdfD&?ajg(#HqvLq-N@vSQjI(a4Y=Fm5G}CYSBS_&{7WV zxyiQJ+IYMqo}s$o^Gl5A;6~8u2b5#Vw!)V*kEK;S-#P`K{o@Hb_XJx1Amb6gq?uY_ z3Vw0Ec+16csSlrj$UE+hT&ju92=d-e9GX%J9TD>$V zJ$%cMSxt1ec6)efn6t(W^ECU`DcqxspzLARj=KWXes6WI4V5O@ZoOshczRwT&&nV8$RrM<$do=KdGZ!4d?^)+?Wv=y4%Q8AfFV%dIh zw)}D2T%4ak((F@gV^`*zvh}?rz2VcM6Wbo~qOUHW;9JULo-x|f56313ZDR@>T+~j5 z3+9PB$3C@>5;OlWy~ZK*mV42UHq|q_L1dE0v1U_wXjdY5sonBiq`;mOFTUMJ`=a9= z4(kp>=9Rhv3vbp^o3ee)>W;MRB0mnR+4*cCWRJn5?MJ`U z)M3UJ3Np=>S8^4K;qM=PdByhhOWtSQDcps`FV8!t`zJSH5xEX`xbk6}k*6u-o9VJ~ zV~2*jrFQ;^IbZm=<;RPO#X(II{Wqf1t+oo(6{Ebe#qXy>2b5$wq{e&Kc3FSCMhm(v z^-k>FFT3&XeTg%9Sh;vFO{X96lX;0t_eKPUh7c$9euq{5;gm&AS~s(oK0A>h*xl#Qd-OE`_Jd}>G~DRzG)UsE?2o$ z-}s<*?vdmKzbrpq&g@K}&qQ{=S;%U?@#m{~%%5Ae$#Ao?K7Gm4i8DtdFPI1Tc2q0~ zi%$D~Sj*COX07{5>sDCSX~9_&Iulnnpy3$rCnCv{;25QJ^K!M8_4jAh3VK4VgJYPt zI<(&f3+gkzGOZy&UkSwCHh&YIZvPAQK$np=I} zcEg#sUa0daE~vHb>fwl*{aw~vEM4$+Gr=s5P3wx}5}k;1XcEy|8~=8Jt$W);(chOM zTB|y}_I!eaHcRU^U5cefSNBxIThc?HMPL8(E|I@cXTHa9UWoNd+5M+7vSdKZXic}> z+*d3DtS9oQR+q@bIhHWyw0L_uZ`}fu;3CV&?~IFI|3piV|BiN@K0JJ*^a(48@GxPX zX52F}+gAq3?56zEWz{=FF)Ns6(oM5oL)Xzhe8~-Lg-`kec*lpDCMq58VDZZxIA`zc zc|LODy~Idy^j z4I8HnHTqfc2ufG^afIJ)>6mTDEk)b9#zm~L)1YVZNv&kkiR~Vf*x|JbDc!?%xtc;( z6MFN}yjon8r}^^?&HDyjYv-6FKf7vQchvtRn}n+^9c-w=xd$fhvU+t0&Fj%~Itwoz zH%!e?3j0X-!q>OY;NtQ7J%vg(t%Gxpi8p72`rT4W?T+JJh8ytBXB>z7h6x)Y8oWxI zuL)>Z3m^Z{?ckU?tXHM{Hp5W8HueU!ed7_El9aj2TEW{%k=Vj6Uu`>-GsYxi&=wVY zE3qw?YeIK*92cZKCipR=X34LZS1Qbkzf!;b zO}};dR*?WcJHPoyB6G*?#}9;?LrWtj0p2dyMzyLS+UpdKnh8qP2BA;R@X`je@_zLg zLpkaX-A*!RJ@T>bKe4K?$@1?DpYVVBkkjYfaU#?Og{XYSMI?vFw`)nrlmSUaSw`u& zstJq={=ocgYKgOLi6lBQTtsO4deB{cYpw>Z|2moNg)r8HZl}zsxb2gTFqZB!w|zms zq?)5lY#pzfQgacj3;O_sjwP*NBkF3hDd^%UdN2CgE0hrxdrTi5d&zb+pyZ+N-4`}D z*({}BFZ9ozpY>JPt&^p@Z*5dCI-Q^vIafL#gKyuuiJi`gN^M_l#(WvtI~<5=T!HcT z3R&4eoD%1Lg1u+Z(I%qU)KAJI;-z0H2JfeuHQ(fZ;E-#^dfX&Iz%pewYt)-Azr`YV zRd?{Z&R+NE8Om%*O8-dKSpxcf*JgJ^C#9~NiN*>&PqbfM-8g&F9IN6O|4fgMwrp=Y zPAzzkKZYN>bD}!|lQX*Z62n z6UxnJeh0gsz~s@I(pmcUC_ABkAB-60JouUV-3OG)f9JByO6QOgVB}3_(t5J`pnSS4 zo|T?25kIixM`Qk7ho0&Fu$_W;0;k+yTVQ-ivI*(|Moeb}EA7z0C)*%%NChV&w6uF~ zFYf)iePot-!dnxktC2Kx5(lI*B`&1 z`SavNj=;_HygR|2S8-@;Ph_6&mLk(C$55=>+>t`s1wR3r)+I#|g~piT*RM90UBlSA zH+o~qrqU^VqfAz1zuT#=*fyp-qcCn4Q}@+oR@BNcZ?&Y!Vr}KDjIjxDF;E}jFJmZo zTJB{DUFT6LKGnT)A7d+G+8?m(zMputz;e*8-yCI&3*EY_lSnZ;nCM7RTlC7EXO;;U zx~CzWK=FKa0NM}Zt5On{WgF>Ipd zDKi&-m`SvdB$?1>H(wI4?9pBr-$-P;W3BsrEltrV@TheEwBN1o2~;9}tM;pxMJ^{l zh(9bJa`aQVV5LZUe%TIMys$<$%@PxBnR~DKdVnN-XjQD}k${lT#e;%j7-I$TmE36o z+S-!%o-lNYi`$kQ!z%;1&Q@Ej%j{13{&nK5z+o0~IXqJL!poSbMAtgkfG z3XbEi$^Cc-8!AleU1B-g;b+l7OxeI`-Qs27RpBo5wtuJ62>ebw_%3D9wJ)RJ-v$JVLb>2;6vAhu%a zp*L|9*7$ge0HHpY$IaY{LVoE=ytLF?p9>X@%d7n^dMr$)nm*dhUyc1RH`U4*V)Cw7 zknuGpp?GYEOH_1vbb7zM#XC*6GyAaMjL7N>hileW1@tlBJg;LIgc%KJx+w|-5{QZ4 zMse7s+Vb|>hBVu4wh)rw64aar)>ixM7H;tql<2Ev=y=@Z(>`CQr1{KO3cttcVg2Qi z)3xo2m5vz?y!B`HhKd^--@5U(kQbv?Z1wTTLbpg&+}UoB=uCld_Sp-u?55$n${!QW z>K|EguhO*)tlgy-ZZ;)OiW3?xDyK`f5XhLH45t}%X{eGL6_ibRg?}mr-ILQB?g_1g z(+s=DhsnL;T^jXt6mIXw)$Mfq7v5LjS|y&=*%K2Il0An_;+$fX3#liZOe)SYvn}VJ zklZV8dXqC77{3zYYFi?;e`dMmlRa}JUq-#8x`Ec4Xl28>4wDq3gl1Lh$SLc@m_TqY7Y#`^<;h9mg=I_n(c=l31dlT75Hsqc;#`k z=3Dz#)r#x-=W7%{27TRnp`V!WtYm$nYW=~D_;v|`zAeS?Kazp=x`V2)*bcW-)kly;>eKC?gF7#ES4=*A!x z^54-4u-_8_>AL?wLetU+>Ei1?eKu<^923TOt`|U2<7yt7x{4)q>%+ zrCG1h+)!18x0F{oxr4rCug6;x>7?GYXyU4(1wrdK>G2e{mxZQSj--q#P;(kwq;E^O z>S7%gd;39W6aoIZNb5z0qe+{M5qht~^ae|-l|&dV=EPFtBEm!xiv4`~7|-PK*H@)v z2uF!ujQNdlOt48f&D`0;_}=aQch;$TpQ;~4UD9cAb?Y+0C?yoj+m(H#qFwKy>Yg!o zu{sZ%GQTO$ehl@r+?Fo4*awq(A9h_ex2KIJTCY7A`b(T^rS`)%c`KD>U%oUJAM4Eg zPO*0SGKkB(pHV#Qu8g*F4A|N zT(VO~^;F@?c=)aTyfm z*q%1{SRP44PbKmWy3TqzC%7wF%HvsLgO)BE4)#PXIww3e?sbaPetMPeNY3HdcBUj; z4Gk_xdcwD=_Q%KlOLWW56knriO5%xAoO%SAECLjBk8mrb=50P8>YR)XE_ArFwdG3d z8$gkc@f6*Jo<&oXGOG6Z!vzr&TvOk zo8@4>hpq__{PBR*LF;Z!Vx?LQmLgqwyE)t3ZB!_u)q`Q~=FD5eV{6SYM)LAS(I_2N zSe??|un`YDnh>Z%(`N6I?XzcmWlyhEzv{4|AUfA~f^UhXVljTH)$oId#}oZZWt{!0 z!EQj=^~7BYJ)_F5HvUmARtc2{zc+TDDHML4i=Xz`aH1Dni7&<`nP1GnTkz*qu}B~9 zw4He}GfUu_DBu5|3PSa!>sv5k7BJ6KF?`0S+D zx&)x#%!@U)AIy7$oYn9TC2{yI{Kq_d&rJnqhCJdw1S6LU4I5^$y@YQQg5!A1UmUgC zE0fntZmWn-_->xC`e|l@EX8XC{Wsd*W1TG*k4t;X^gj(y4n`o8Kv&pyOeL;9lTfh&N9$_ zTaqo8qoVKFmC?>tWy=n^3E_IBi?5%R7DId^F+9rNK zgQ5DmJ=wV0)Tq9*M z`({KUT;$p>Eehs`j0?D(y4hnm*aDSUTQ`!o&o^_L2o;I19eI|pa_!*g&hW*z1guZGO$Um9qk3XMq!L7>nqxFN3QnHTgOru0AbWF~* ze0?FGH!Urj_^ibgv}TAKk99nf?S;ig=6 zPIWGD&-3JS)>hP4>?9Oh6*R*-Gu54u)I9t1#cO!vA9Ih#pbAo#2_tXj?(Gbzr*fKs$r-cI?d+T)+G^I;(npsHup;3L&?gk@!1cDh)0Xwa_7u7^-mw(Iihs5 zStC-&v8CB%>Qm&~)~Ae^SoEslV(ON3R1E9ZsW%LQvJ${^f!A(`%wnvaxSt)ON8L!dRqfi#_-F5I68Cb@Y?1DI%J5;t2AcUyo&S*`+wg2%K;XGZ zhBL2rg9o`uzss6nU7UWa_!081`0V1a_Nc=9cv0;yPdX;%dNuT-72;QtF?#9EGEuaC zX3|$56nh*!ZMOKx{Ld}miw<1!<*btsRP$fP?PvCqfzVX>8pqke>-)M!{G&rFW9#NO z91E{?DXbDKw%<~Ud9JHOOvLq#2rS?iAwd4EpiQ5uLNtFrPk#|uM{Yn$Lt(!)`xb-v zd9X!AASF>=$0SIv$td3b7F+a&>n!E{)#tkdzconL^q7QxxHniO=WSebvN0+<{pt&U zw%hXywhF4>1{^Nlp*CM6EmRr4A0F)7K)m(Rq3PI9f{y7O;SeiJ>LI;*!_H>^P^LSw zFE{p(JBkbBL`*k%Ma~T5l_i^;QBite2O8pNPMx9~kcnfvIS-bU7U;C)fiaS-?FSg` z!$C&c0zR23aL+|SmIvVm(PhL%iD;)WD2GC`_6V@xH3@V|vOvgKIC#$_crWP2G(}y7 z-#A;(b2h!Fz_hilCeA0#x=w#BJ7_bD%SPLth~QptvUAFA0aI?9kTbhcFWUK|Z#4Fm z*q_~`Xzx=u1foThx=nwf-a0N#K#oKlT%A#;!kr|0}!4>C(upzDr^ z8z~5XLOc!GFp0Ly$`mQku6}=EK*+%$Te7KwdIH3LQ;>oa0fF0UNUQH`-@8|t^$- z5-BJzDKZ8PnJIXLCn$atS>sf|P&;*K7y@_%i1K~+HqCXZSKuFdK+v%XhUW!eE=ou@ zRE9i)Td0}Ef#O!PEX$IZ#mdPL3Dh=HHRb{NEK?9P5e@RQCUB2Vfo&`n5duQ7}_bSTG6(uLj0wN6G%{7 zK>5c7!tl_YHSDboUUDnMT|Km_$p+oM8uCT-FWx>~-#<2^+l z!=+co>yWKez5JexA2PHndCC~2z9OkO!U>yr_=N(Gy@_U1|EQCB9qJ8V!?&DR_q@8V zumv``cQI(Pgd=oypUP)_|K|mknI{3EMzYt38-EuDFv$`sP${c~#ebX;RHWjF;Gx8h zMG$v@^u=ab>98`=Izs;2>>==yo(GHYv1e0n^J)svY~>_|Ljk=18~ zJ;)y&kb&ZgDftIxI=`tW<_eVB^z+j%uj13|FOxsE#PY`>k*MUvR?9g~Q!OhHazPZ3 zaO?7J(Q6E*1^PM8fo>fAwG71aCY{NUH`qRpeJ0E$lvUp}#)Oe-^2V;Ni*E^eZP<9= zhuv(Ct1QJ5GYgGn^at!=8ZElGo1@-7b64c1`mX2z=3E7ToV?O7H!*TF$S2B3@6?uB zB=S%GGguND@a&$i@Z<5HyQOYii_MW4{x(~mWJ5uM{jI&1U8gA1!YD`e#g&qwSGgP* zZL^9c$Ofm#7Ax^ww@Q?C>A88aKkas^w9gj^&<+E?@kL%b{aED()&qz#`oR^t``GzH zP<^)N{;mnNt##iMqFq2^Iygf|h(s#c*0rSiVB(!#=s=#Sd}CJp|Hs%>$3?Y2ZCFA= z6fsbdFaVJh1SEt-uYsg=gVNod3$_S~bgOhqcNrk8fJjNBh_rOqJ4dhH>(7Pz{>Aw$ zyXTy5&pb2pJeuJ_iw#1PADxXB)RGeL&(vEXC86gh^2Mrwi{uJ0M2&C2M%sanAf~rJ zR|FjGwNP4l)G*TeHXE=JiEP4dW_~HEc%;~-m^iA!_>vk@o!A_}DA8Hj5Bsz}kX$Z& z+*}(H(k3)4t1UT=dZomT&>v5 z>9YH_QPe^4cui@L@bTc~Y@*}NEGw57m1#eOg!FziN&#K-_~9=@Oazt0jp%oVc8 zj`EDPBD3e?LRKdOLY_{NknEGQ}v$b>IIQv2J=cuAG7f5300@- z8w6p4m{2n<7KtDpI>4sFfY6(7Di}jE`m-v`;tW!Zxvu17LNab${aAq8LkRL)=bsGd4>nUzRz6rE@m zi5i27`x^y$FlDDye84YbPfo_aK%rxTC&{z1#=bO7${gppbCEf&j7w_NX zh)Tw1@UJ;`HG5<`DiQ#y&2+S zH`%K>M{s~5!I+D)P@gvBTeEFmT%0HS+4}P>RFQ{Iv?c&_`qemAu6+a%9k1-5qpY&b z=r#X~%=wrTQSl{)+l22-tIC;f7`@%OA4%k3C?*76sEpMrqu*u~KC6{x%)-ptlB!dO|hct6X3hRMtBJQGW;&iCPH*dcHfL=E7Q@gT!;1s$7+qpOA-kB_ekH%b0qPJyt z7*m=-5kDWz`wDHW!KeefeX`hT3M*rl%k>J)cu$G@funV^V_B~tKeB>zvk~g0nnYT7 z$InnEnnM)}9AXt$lI7y#K;+ipNLAR);(RUXa6buxXPsxziDDNh0!KqRfiySLZcnhT z#>15Rc4x))MkjAP3%Ne>M4IxhIR0$!(@dh(@Vm97TtcRZ##HJ*+4u00R}cd=yxyI>s@r5j2xn|QG* zAd0BN)`AB#J*B!3X;w8;HNjb+uOqeZyw(L|SI0o^Z*oWy&jvd1Nw`>^n%`O_aq-b`I@Kg z?FsSByh~wu#xJCDy5=MZd=I~EaZS`0UYA<^V9UFdq+8$kmCpY0mD9bBGtH5vU`;wx zd~83zZ`Z@4KT^GJdu?o90tVp$t<%6o^pDkh1&z7pQ(QZtTOM%hLHiR@>NktpJbmH( zNyl6&W^#;WM}VJqUp+U?IhR}tKVZPR1XWo*m0UnsqsNnSX83Dk=ZBr?z^;8iSmd2} z^Ui#z36e!9n6wPr^nsgOE1&2V#=pvw0`th|_>?S=UFQsw!bzu!$-l zTyp{J2}8%bJsJAEP}OG8eyv&V(cvS_=2f%Jye)_>01ai!lU#+wjL8K2+Am+e=q^n5 zCC|3uB^sbsa)yAlgB4NaI`<_lry&67>#&dvZx$=<6AWXPZ#%X4R5S)k9xCN2x{bef zgGiiNviRmM+DX&QGA2YCPStH=VtKk*F)OezrcBPQJ#4hb|B*IJUYwjA-h^OeY0oK?oW`SI#-oHy80XRFXLM%!*Ejq5W(0)S`Mb zqBR_(d6Jbx+$S10r+P%3HM*F?>LBZP_I^NnU3{ zlK4_t!MmNXp}tM7%+5>BHfZMPaGkP}mibi3MY@kw?n;Ux8;ia4J68w!(A($vtxs9C zYdYRDXhSMPGuo;5H=psu>yJ329ckrwFIUv&k%&_}!3bx})yS~ee zxw50bd-~=Zjeo7qf7abV$r=wEqs|D@{vy4n}(RWtB74pk}=atRMZLN?x4p?w&H zibu$RT1O6O$^|ypX7xcf?XykM_y!m(bx;>J2*id#z;j|K$gCj)d@jsDit`x$N`S32 zJ5l`Uu;}_9ul{$&8!W%QKT>I#<&OBEeaY>I+~m~t^vFt9Efm|k%CqMl=o`VxlM^V1M_7Z zywD7sid_Rp&8EQmT=PYquSl6K&sXm(q@Ge>z?UZu_+o9~Hg(|bm^k_$JbfNC(k;^jK7EMDdwy+~PY=TbM7sP1=mX~XYlLq`-!9eTuRzF0O3B+NFTGCYIwOl875Am7= z?qgMw3_oLhtYUMc#Fy@z8Vp68WEi)@Dik-4%S>EQpR&(`08usY!Q`}nOiOekzF<#6 zRTUNE)sF*t4jEeNtM5;5!dbx$tcpzx^jp^1f431DJ9~)EksubrGN){jjlOFWRF~+v zT2qvRV3{m9>(oVX@_%{A-}?wy8s5T4vVw;h2#0@D22G->O}KRGAYMt z7nVUz;skj9K?rq>Nc&DfiZ*@o?+$7?5N?T=gQQ@Xuyc0(>nx`T8MskT_np2fI zX?&Z_iu)4|xpk@)SeSbv_`}8*n%@AwU&|L+W<>0(? z=PS2U`5$&`P8d9yXWW_AK9=VXLiKS#lfyTh542b%>;eKE8}G3E?99nhtMmMux|t?CGTWL);H`%9s2A%J@>7jmA zu0&`oEeADtY%_TkkeZJ+{>60NiprXdh_L~Wel!8%cmeX#0vBPW9tC==0A&4T^zeDC zIYD(#Vo&1wYECO+A|k}eLqyt(K+X0D4Dt>;hn>zrEJKx8ehY zM{4#svh55!eax$JnsoTEu^N-y^KUT413$gGgN1tMN2$_af1653H-w(P0K}&3K(`kN z^w9T-7jsAQVUy@GNJr|9ZGe`ICJj;D?a4GPG0yLXxnv$&Z?SVFLDd3Cr1xlsNN3K2 z+1eG`1JWZbP;$6SFyPMCbNH`JFXDa;{klNwXO4G$cVd?ILxHt9aJIDTB7A88RvrJc zw3|-Y$j7#0qWweAVUjt+*SALXKyB6`y$Cjuo1OK1gj8DSP5gG74A+J6EX>5s9r!r= z0;53&WpmQGht;2+zcp^yRwTS0dPm-SCRBJFikoFohtFc6j=qMPMea-Js=a%mrwM2@ ztR-If_`Boq?~?`H0*cApCYBTgTX#S5E+7J4&{cofD$AkyAm`RA39+W;$l!#9G^gIK zS8unTb;$7Ekb*3Fl@eXw|2T<95&fZ2%RL0Rtcic-$z4W7s2pd1@a{~s4;yo zD)hSlKVI-E`0F=D9ZswK+mHC?iGN>VRFHIhwr<91Tq;`sY&Dd75rw0BU9TDb4GR7_ z)Hg0Nz`_W)q5r}cN*!!CUs!I6b-WNzbeQa5)~X3v-E|h^I$v`1TNlOuHT_Jk==>p; zaJduX>&<`X1OM-A-Iw;g98CE|fmiBrN&$R|^7S&e^p-Rw)x&M6N9p{TZK$6cg5j9u zwJ}iQJhp3fB~5$vFK;V>-~lwwhTpgh1NX5=*>@jCgVgA21aaRfWwwaa*|T6u9Ak~G zvf3Pz(@wMprmJzHku6FwG4121b)j|{`YJ}P7g|L z)XL?hZ%g4+Ibal5cx5bK>JYCW7Tt4K1mE2~n@@MHhx!suMmzLbFiLM-cr3F0pi^?8 zED9oap7q%KAKxQML>R|f;y7{k(a#@aZXUQIA#t35sPOhPQw8C)RgxCmel1?H zLyBvNHJ)u!ad~e4>?u-M-EP5iIQ!$Dv)Jpb-v#0$726!u-$&u51Ps|rMsd;qonZen zMhBsa?K0et^gq6LQ=)RvW{(#+HPbfM?)T->KkWo^UZ!yUc>hP(RyD1-vz4DZZr%82 z7b?f*sk|QesaKt(yi13HdT$9})bVpg$G9(q<(hTKh4Wi-AQjOqEeeN#8hgAmZ!FXu zq;539wsn4|J5y{62b5g=)%{_>I_Xz(5Kw_L=ACn)HA8k5HRCsTcJF7wZvhpQ6_UR@JR1RSR=&*fa|Un0!?D~|Z^8HNPR?Y+`x#Vi0* zSYGI|jE70^0R*$euU-uR_LGj9;$4v$I}+1@Woe-_?f4boE2#XWkVdPOH@D#|du4GZZY~T!q^Vtr2oiZ>5j1s|b!Hm+!YUdj@odDZ;0tCF5i3;+B+vp{j3a_Ig?~kQF9gRWsrQl(0HEBX-v$ z&B2QdxlHGLVu=f?KfR1DX(If*3t*LX>2#;5=EQ+F3H=|W+rTbwD11*9pW^fAWW2|+ zkJXB6GP~)Nlz%*K_Z)o#sR#~Tj8Z*rrGZkTvjOh0{JAe+V6$y-))O}OtD2aMTF=MK z>($FO^L{xqe4slwa1eqh6TChJhVE-X2HDZC-Zr6j%(lv#=mRkMPw(iP3zZ!_{a_yp z6-oKX>G0`qmYouThqW(idRo5 zBvIPLETl^1w0lQH%NJUG>X(u$b()rJoWDohZ+BEm?KA0;kJX0c*$OWa=GpqpD%{i* zRLW?cev(SYZ|jC%Nxq0z(!c-k?u_OtyD1RSYP3n!%xh$3*$y>4U-=0BX-~pfm45qq zqK37al}2p%8^re!up54~$>nj3QjXy)N#YJlH}cxomvC;#R7#amFL(?;19HKPPa$kH zr-hva;cu3yc+)&+zUEQvxvwMV4>7|^eixh<$#1EZU;#106QGQ8wEYRzPKQqhSzGO{ ztFRh*Xs3ZO)xf1;FJbmxl3gz!F9%0ab>Xz9u1Ml@_fyT5^OzF)vAh6E3UF9wuGIvx zsmdXHEfQAq1+W2<1>(V@^zVF>Cjr5pqbISVh9_P;ONRq4#6J?Uk=tkq+R$YRTJaTlVM418w%7A?m;m z9%TqHk*e9%7j)-FHn8)%+nAY=P&z$$xcd4e&X<2HgB+ryb!&t}Y9z1kSAuvR601IE zQV87?2*LGx#INDGOrQl80D+tb!M1RMrMi2xh##Vn792T&040bc&kdd!jSjiJ84|{f zcK!AkA&1f$@+RkafnN||Q7jJ3X3Scd?Zj^ZmSF`*+bW5}rgQ?x>Go;voCO+|kndoJ zat0f)K*j9@-e_WK+sR*{pt5Sg^P8=em$6RPVhk)-R1l+kRz(SH;pF2w*@gxQjdBEfY9P$?YxN%)g zb?KaNxh8U)?TJEux;}m3@MDu!jhIHq?~rr3G5T72 z7OQ4)ZX~D=7G?MAY|(PC<`p30W{nO!q4VUvM+D>$z50l0PzSV+s81BT$bMl&yPf?h zW@}pzok28b`!!A@<_*#ibR-D?0@_HD#cKRmFT!W^0R-~AEv;=l!-%)ACuJ#7TA3ku z8e)~UQsBJN;itdp0_Dlo{pX(FII*Q&k%g#(Ww|XL+=83`Z!-_A0DWZzi1=tQ*TJp3 z0SsV_%7G~-VZ#0!%f#Fc+kbNy;agbwMtb}|w8XA~A1Xzx;oB>8drK*l?g46DqFVq`#-Xn9|Z@4O5S)6-zDGQ%!B{2Y8fcg5%;Z4=vCoQ+aCAyzaa-S9gpy- zpE&x79fw`tAIq+l$-m>W4Q%`SZhDoA%-dhZZQ)~ya_=K{H{f-aM%Lnl8j` zJ^*?XfBFB%55E(1EEUJUBgPy@1+fM`4l?C?dt0jO?MQp=%&D9auo~rvaj2wtjG3^Ev=O-LAg26$yj}6-dQQ_m=(dT&R#cC z@UT%<)AkZ!6sRzwxmW9gyr&F%-|}oT2kwRf82F4(|Fzx7)_^q9;S8S$h3R(V7v3xx zll+fw-uST!{y(=oDHdi`5J~!b#rprwoWJBMt&ec`A0w6Q``>^4K3VQDLL?;X8-LEl zKfU{ZO`f|iei)!HtL?UiXplMiNC)+eFoLsr``e)zZXk2sH`ov7k@5?-pt|;h1?8U8 zKp4*!JID{z0}>%!cuA+lA;Q z1P}S|`J(d!4?*>LCiwk)ZJe`)(g0ELJQVm1caQvBt5oXWmpRp{vRB9U8^TR_1QAIV z*Zys!{&)vc&i+}THkVlHL*Z@z=$B0WHbsC^Nj>pHbTc784-0w$CqQSk?sDDK*1x^0 z34L%}JWI&!5_EyUo+Y5&|8DLX6?4BP(gx*C|s>ON8 zN+9<$7vT-Levriu!6zVo3*pIl!^UWC&=MPIOOHVEY!TZLOk4k2=hNS_RdAn&&lJHR zYH~3=NR7Hq3FvrwH}D%jd%OTKP|a%JL&rB)+AF>chG++aI*J52V)|V8L)m z1-okY>&^EY-4KNt=8I`GjIjiMS`kl(Tk0XPL3PNB)B`0rZs!?2?YXY_t$fvjMWgV=+_xT~=6NUYKPJu3DTOX2F~V!0 z<$Q9T4h{lU9l(eje2DvBwn8A0Nk(}3CdD^nV+nH}!-iZ@NYpl6&4JM9!VqW}7_>)1 zPA2H6gh0HnOT-g0XeT`Wt{(lh9RD>FDE+@9pM_4PS?@^k_n(wc@V)ikEI(}m)UclE zA0yv?YTsFovC~hZ9U%g^GB={@2%?BuXUL@q+z~m#Lzcw@!0!13;GT%nL(@)QkQ5=i z_(os@HQ||^>)evM9-r{@H2{>IN>kyn$7L%#+{a#6^ays8I@>Zmfz(SCX@S9gB_Q3m z%d@r?qJ4yxyM!|!S-3w-WcWk;gL;!yNZT}5Ley8{!83Y>FGBIsI6OT=Y-rP@r#O#Y z`OvA8p~Qa9UA;=#d1!}<;m=~^-W&AIqyTBvR{}3m> zFbAF98B@#=L5y89tr}Ueej0A@f~WwYdrpM>pMWE0tc1~bqk$}7akj=}x zyhLEL@&!WV`CUk2SQB0kC_tV75x+f~YSs?SeYn=ekI(M%fD`?B{5Cf&VK!N$eVghG ztUPg;)@JX`0T|(9O$Ys&8F0)cAU}DW1<|63i;H^;xefZDg$kQRWt!$Kx9GsY6Cg{M zy9!yGjI@GwBu5f41rnTP3QB1zM-jR6iNn_5g9ky-`Ha<0V5<4K760$m-OKn$Ax@-I zAOM`ZPtSu-v)vrZ5%>-&Kl|(6bB=VDET~AJ(i}oEdoNEkjuB5dg3Mw5k%UX@ z#v}|@YbZ5tMCb5X<`^Wi(RtpP`=811_crKFiUiy4Q*&B~?shEFxE@uu5VEh0ZUk~{ zo++XGu7F1#Jh5~pB3(L4u;W|f*scj>o~geRJ#o#n6W(5pL%I||8`bnXZPfp^5zXL< zob;ZNMPyq~5!fBjDAE@U@~9rOg!@}=9>_l^RpHdT)E3gaz4b#S2~nqey!}+;(L9>l z80z5Gk&ro+X`!t4%*C5-|3$)mz)V9}IwE-p_tEfo(;2uyL5=#5vdi}NEf|In_#mzO zf6x$~M_e^Zwx0hmMWVsB366Um0ag7B)`SZaq9h446Gm9&vtjjc<-NDcGIieKhEC&A3n(RiSHKHn`R1qj-N*? z?&U+o`aSHl?LDPmp7#F91@L3B!u{u6NGtze_Xl^wdw%FYUf7!Hd$h;eK_+ZVdEoC2 zK;x5v{0iEQ<3E5A&mX7!Qc3zd65PY{cUQ+U5OotdTW4&4KQFW}DWDxPj!%jhD48&D zE{g+=@fSa#Z*TK!KzX&7uDQZ<)6DjulK`d`MPZJ-4To#5pqz1@JJ98q#dz;w}ak|0kU@=Lpt#D zg)(^Q51>ww3<_v=;~%f(&fTy&pb4TbkcGm+ch?$XH&}7zs1u-gC7crYC2OUrvhC5) z3*&j2MPCmWfffb9Kqyq-7?Da%6hMC7vv_XaN5htxYoe}>C~alMZxl|v#)rsny(Buc zpOW&>McGQy{f91=?m8K3qfc4N&`(78*FJr!zz27|&dxk-|D?5l2b$wp?6YIBF*}dF z-ASZp5|+qU-@e%vHF-wr!;`raQZ~_poadNV$IBMf8tJ^p&P{e|uc~!GUKth&wlp2O zt!q-E)Gdni#bvXy)<$GJPYF$y+KMx9yPunHk*5)j9$J@^U{@4b?Pv^!|Kvn^tc(m6 zk7cletha^p=4zidUzb^qb6bIq8QBpis2K!NWjZdB5QyH?7L(zl2uUaz&|Y}|x|m>P zz-uqB(P|{!d2*`3u{@{F3#)M7UYu(T^4Q#+7fbpyIY*t(LG&|a=ZjV)^>nwdJTWZl z=6QvyhADog^&OB4$!q;u} zDjSqGfz|;+nfedd^OJfFS861QmLb1Lc>*+F&ecICJKa*>1no@=_x zQ+Gr)v{pOYgOB?QXV*y!!9TFCc8hAH+d>dA*s*kgRzFNXm?dVVB{9Y0L=s=cP~pUj zSGyEtC*ScjrbX{4_;8g~(LMdtd}ASHl8?zc(WlXVbwTw^Ar8Tey?^!yA<53GI*@+% zrqsyiSPnO4VUH6?iD-#T-;+`3xK3&oe`3`fEY~z|wV2iJAr~$>&$a1*O8M++5G;r^ zSzhg3EDzA>2EjKCshSYq+`x;}nz?Hrt&-6@6eC>2E>;$3Fe+Z6q~$y~8fIE+)urP@ zkz&4=!L{jumI7pNe&#m*5ND#AJx%pRf`JRaB5K zySaYIDXW0-3Z$!Bhn5Xn6_wdzpx_BBwu4q7V4_W1U@%a=E>(Md^weGsRX2Bi(uIQb z!ROixLslr$^B}CRMTI zL%$ZAs4TkD%FbnRV~db;@NYcp|X+bWiaDm^tdHPU^dqg7_zd3~jlV0VkK?*3)bdyTF#G|EUY zX(U8T-d0I)dY9AV5<=L46uds=hOMti1T>*qlI4fg+WI&zq#NGauauM`N#h#AhMnyW zXBpAH8ht&)n8h^&lMo{e%12kDPdkD~tSnZj*E85*QyV@uK0dA))K^o8)}`do+=KL} z0MEQ^U3$|FxE_uA|O|a^wjznX3;Ry(dz09i|kq^MW!wyYK`}Wg=?^7 zu+{GD0`1r#wO-`Sn8hk!*=MbNrmhH09TlFOxJ_ematumfG_b3yjQIJ&+8l>$Nvm5$ zNk6*}U!Xtbe{Mk~Cc>Z-5wy=72M><1F;Jp%8dOi&5LqfLIycGUoZkU5H;P1|E2Cc_g+rBS6v~j_&A?JC zAXCtU)iwx_QF?Y2vhK?HA`De?=GltA1hnRN)IcV&N||WC7eO7UBdUskI+b#m6RF1p zq$jin2HZJf^Hz9u)Upanc$H_mOJE+S3Pbv3t8M=-q45_1$*jt%`a#Zo*oSB44NdCx z#9A3wrP0%{FJ_L94nmF_8fEEiIWQJFO7X9K`|o`VqeMbdnRosgEn#&QIw4Zhp+iD@ z!P(Mu>5u{NTgo7#-uOBv{mcoy&I8GdR^Mf@gXX5Ug0`l?p{sSVThcVY! zxnQYXH}9giEX^x*n0Kyz}ai8pWH zc92}-Rn~L`2#~kDXi-O{?=74OAH%ZptGK@3@8IYd0)=tGNS03HyLV#BMg%xs#+=Ct z>H`#cV)WM>xx`P@bH$h6t4}kcg|D7rec#ccs$b4q;^8^Qe&rk_qqums?lN&s9X-?{ zoi2%EUcr*p)vDYVsJlTq`secf^+~1Cp5fu)lPIg_gGNR>{v6bwpHvo7QhL!)-?_&8 z$FcqQ@WVGR`BXw~h+gHx+XPQ3{`vTiUkqG;7aJHjJ4*EP_P^fe*G1*YJ;USUf%yDa z{caiHI1ql?4!#8V^0IzDK1t-;Jn;GD*pd=;;_hLABWmYycNX$%t~MfJ0*f?yu?gni zEt7xW`k*2z$FMONpFhR#ZuNFAW{d$Nk)-nB$nm7oHvAgxm{;Uo72d<;#w~KmMZS3uD~{f8n;- z5`IDwUk;2$Op)|{9Mg=jI>d|RWKH{Y3k}MaG%fdKi%omk?!X;%ZVF7@8r&X7Usw^% zOoV8J6p7eIox=6vzMPnE-@b8MyV;9dl29fQ1*^CO3LsDLEInvL$kA3XpE|ERtNg*ylk($q#-_|35F#LgXNSs7|aQ6oa=S z{qxR$KJ>jBexsLZdSd5x&wJ}-yX$8IaDC4x2={fv*YC1jG0JW0?H_UB9d z^GU2CN>fu4WtIL?VfS_;LSm=$KMUiZp+Md+mKPO`-+uUV-gd8@L?|dEq+cmOjjK&R zZ=Wj%yH9$H0`=CfgH|WEv$KOK@?T@m0b@THck0AH?fu_@F;d0=_Fd~`+U}Ft_j{9m z`)WEh7m@y$RO^-BKK<?~Xm0d<)_=i1LtVuO%Qzba+F-L1+hK`U9mGH+cKZ|iqQ$LgaXo6+gi z`o0@xOf;hKerx>iJmn>}B9`!U%p;s%YbFsbDVSi}y$uT?8!=oP$zbg1&tD4qmsD=-ZXc z(4(X;1`WZd8!U~{*5)W#@tY|SqCdq$=UUuZpN(ZxZBwvUnV(PO{&z~7yNY2x6z4yDXrAJR-*7GJH7uLKRLYK4l?{;neGjiSi)vede{LoyT} zHwJ^FG!{H3g9rN@I#nnw&@A*+2TRczH9rlLwCuW5KADvVaP)eoMn9l9tWTI_I3U5) z#FbZwX5ylZeYhgu~KjSE&bA)Hk(6Jm0Hd zwK&WDDg0Ru70wPK@x6|$5z@+sp9T^~2#Fb|KFT+Sm|7H9bURGWaj(}JN-RbipNL?w z|Jr4_VP{x0kZ~uXLodVLW1-QjN7ly*xJ>MdpsX_0EQY#b>5V>AW5uLyG%ZLZ3`K%0 zY?9@Wk!>^Xt<0AiBOINqTWWVnl!S6vyT7wSM7CkAig$1~f3G^7``So?kEJc$lG4m< zyAb^mxv6xEYu;8vT6UWAYlVb!;VjFyE3e<7RYIVSnT3LivQnV4=RjO!ELf}?NN9In zAqz_8LcpNU8(RG2He07$p6#|V8^X;<+|JkA--x#x8*`k8D6I zJ_JI`6^VPG2+?}~+^8zoc}pb_-<%0=FHJDCbUx6|U$I=DT|?&@2>!fs>x^4Dc&hVh z12|j)WVmzhRK8vqPSBz(OO?hJ&?QF1~rHuO^Gun$0PPsy?5pl$<$4mwzJj+hK zB6~Gz-C>fzf4BauNI}tBXIH9kv52UdO6Uml-ghoQPINeUV_*WZUA06Ovi#>;Y=JCB zNnWvy^&22P2w@T@NSG?{?$=B3S$j!cE(;~Lw`Q9BJ#x*8M-muSZJlG*PJZduJd=4s;Q|)KPMiDe!Jx zfmJLbqIOvaLu3)b=Z+bDYxkIkE=CBQ%do%KW$2aExg8eZR_g-+1!t|d&{QQsDuV(o zn?##>Lac^)nrAQ>>@qLtx$14V9u%acmK+J{gRM0n=U9>&+X?CVbx=Ienp2Co-UzM~ z-M6>D3m6_;5^^QI*gDMmma6$vD!V}m*tKn9-JoP5KNaN-Ua^0wms<8XpeFb^{(24U zS+A*DYoHj#KFE1-OnbAKEZ-eQk$uv+(X|c$WCizVkMiM*oTtFM4slT)a>PBg55}Vs zVvoe{xRgWUONKGoRrgd%TR?WTq_{Z6e~9|7x=C_;9iW8r9st#bKyi9jDF9^2+;t-J z$uf%aJR;ybuYY42Yh%O|gP3k{>Bf9!CbeQFNGxmcvg?;|u~fmhJFc6~uT<^!c!5N| z2cc9p+H@JpnkOth)umX&Buk+2`76#)Y*RhIM6jJLD9#X#*|YVA0U#bXsna$ibJK z^1*6m@738W)@BCFCfzOPpTufLV;ul-4*@;cyb}l8l~wf~U;L^jeEsFk4?;fd*uxSc z+7R@op$-Zu{leHf8TQpi3=3+`vKFP&)gmz?!N#X}^bM|-HM)30HBwlj=DeX6ZYMxz zj`YNyc-=W(i3b2v231NZM+wfQP{oA5zm^~wEfmT-M<$`Fo0lA)larJ3%n>gwt2btS z+)y9NH8P_^+6slc#$kQ8+WC7fW3>;9s`W2AUsC_zO(T(zE~#n|rhk}v_lW!G+nk|* zruNmL+-7N3(lZHPI&2;?nqN(b+`^mHy$t8U&zWAYU2ap`a{mJB-m4o0GxsGZ%x6XN zUDjEZ*a?VwN2T~a>iEn6kzKH`e7d9ffo@3iQAHr(UV8TTlg=Vtplr^qOuKw;qQ~kL z#gLjvpf;o$l-aljE}tNIC!CPB=gAP^wG;!?c^yAVsK zS2T4Y<#X{i2q0-*2(sgKSv7QB3h%Q!98~;qn|rsF8F;DyQkpuP8dZ6o`lDG-Pzs;M z%xc$A*bVF0YQ>`k0%FMx4dVJeyus}4ecbM`twXCF7DA<^UpmZY$;(^!>#J2G>oF1l zra=(9wJ9qsU3$aG0fh=K&Hya-<)rPm3|6^uZw9m5E%u00Kc+R%-}%Z{Q|;AG6YcaQ zPXtl}6wP57kWW>jk2VC-34Nr@k+KIg?V3!eNtfK`p3}3#0iqWsU~7=KcK5j7NhExr z)|aYlSPsVtOZPR%1}OYwO5v}HW#uIqdD8V?L3R11dn^*o?!%8wSIBU zH-Scm2!YuSn>UIDfvfak0&%y?4W8`Bj@+q8my8xJav|>4NqFN1e0Nu~-vDuzmXuH! zAfOW24M^O=<`o5h;82WbImBMhDaLZ*zxs3%r0TDTdu3J_Vi}hl;BX0o5S2TA0wt|q zaH?%C$(PP|&ND6YGK!6V9bupNunWhIsC$t?_r;-{=rhK0&G(MiFF>+-j$GSuu3d!O zw1eteyoM$<0;2W7r|=dVF&n!p+@IJCQ+sG-lutJq@MpHUwi0858tImT19s+g+lAMA zr8)*MsASh%N;yGe4xt~qwjN~jA!v)C@kof<>ijVyv-7PtFy+NZ9Y6X=N7aMsECBn1 zbwj&*%g0x~w$=qaaBOf~RAnABh%i&%(ML?<6*1|ThSTl`%j(i&1Z}&|a`JK$epBEA z)xi;#lhl9lhgpHY-P(w(VyUL3i3gdS?S0cu&PLVbgJDA(r=LtvJ&-!d%2slv!>FER z(7nr2LF#E?i=2dFRe>O00mH<>40FSGq1U+CzgQWTQz);tc6q=-VpgG>QS+y3kiP$5 z1Ix)Lce%6j>J@45Q3Kav8@#bO2>Xsm84hQ(R<1EYZqeS%{<3F`|vPC``;E8;&pqU>WoH5khI|EcAuSz%q486@|7^? z2IrIY7UA$=0*mEyU9DaDq(K%`IQ=~|4Y?lKRs2Fe5q!HSI8a7))M#a~XPovjH@MOy zi)L!?D4`$Qi}R;=n552O5Bt4#q`BA1EXmDZYB6c*P~!C4yI!GqhYp;ReEh0$8{ zwglbkVsd3h>+B>WDScSwlTiC-KO&!Sv7wyJm8&CbHd1NqF` z^r!>nBmDMVmsRO;TZG?V;?iFTe9YZ{>90;{-0sNtuXUbBAS&xn`+&+Ra#%niYbSqA zg5NJ5Vx2`R$~7G3X$LJGMHCE#7wqyv^g~p$({jF&4N661DvG>#b(_&RpY$c^ z>%1w3r2DwX;8GsCx``PPgfF#zzQSeP?X(}vgU--VHCi{J#h0QXITenIVIl*`_+1s< z*-x+=lRepMUD7n=kz^+h#p^~cNEIM;oB(qFqG@015s^e*>5 z^0u58p}Hxw_-rg?sD?4NUDCO|q*t*`!?xaLuIch($;xGsna3uO^xB!)XMbQ}$H;}r z7)bw3Ed6Gm$qKcz)v@6PKk=p!a|1ZZJ(gb42SXifwqZn7TT{+c2oK>pXx|5-4Q@Wc zZ{HUbMB=N7A?)MWm`J}ciaxJT8mj3zyMCdw-1<2JA$8c^c}RS2{~-7Ua(<#4OU?)?duYQCWJQj!;&ath^v*6wh0!JT9qpTVTQ3EOr&C*(6P2XvImV3 zMW;z>5TF(w8Tk`n*$)O~y6Gc~Ty<7N{a-sjiFzUc(~6qeA>6N(dj(c6ySD-brs>op z!*bqpJ$jkaEghE4bWWx1%ojH2XG|?Kxktm(uLJc!hPOi4r7TB;cP`cV2(`LdRxf?f zr03?UJ^%r%G-fZvoQH(^REdg4gBk?K(gGM-0Eg4&rX8r*7^_gC?*Z1!K}dPg>9A{- z!Xn20%pj*{QC;aOV&+?_(K&y4oQ(iQ=Fq0}Iv{{jLxh~78+Xha0D#ll66l$(;Y>GlZGne{R%49Clsfc<)zyOVc1wbVT#uwo}bIL#xdpcnIM~Yr)vV0 zD^^+=@t*?=Tt4JI97UkH%(i8~Wwn4fA^+4P2jFc8k*p16IXe5U2uWdcgWGVxztoqj z`OB{n<$Nn!>@_L*fjgZ_3dGYwX(xkEvKYy#(%SrJxfEf~NO3sPAHw}{1^7!kzB^=O zCp;iKB*ps{;0DPFkqEtcYQHW@pC!UPUv=>D;t1TJ`$>Uft?lqkWElWPc_%&|EI=UR zK`(+`Ny;qNf{+DM2TrKkF5#)l0IzOnfRePx@)wgRD7q+&qW~N}qYX(tOSR9D#NCf2 zqXY*!(5vs9K6ele>pRwNMtgcxCB~%DOn;1A_)^Ui8W7u_D7rp^kdH6u zrLGTO7Ci;T%z8oTK|@KcjE7VakSS$n0pWeQUPvbkM)n=IPvqJsE!_raQuUE^3;3!& zEiLUJ)YBxPbewxNP!25LJ!hz}{?Ob@m;%<7Vt{kOYbptau@eJq_%D%cqt=zymm(WQ z0Dz|SD~Y6dhw~T&>iIHq1?wo*>5R!lk^Ysindw|_nj$z8S>S{~53OT$teG0ziiRuN zq68Ug(=nSTWJA34x!bch1D(<#dNC-vIV*|<1X!8qWRBb8iwBaq$!A3v#Yzdq>wx@BA06}i@DUfQM!yTLKL^BLDpsdWTV(lMSJ|avJIBB@sxOsyw zoNLG@BO?Pzk2^)#`py=T^Idwzw3La;M@No%&OC_;MABByNcV-h1G?wQM!J-i*QPcI zL;(}VmlB>qG7k^dR{)b4IL_Z;T|}AyX=4>ENUo*K6gxJ%UP5aopftVC(sQ*d`&;o? zFyzN1Yblgvym(2bXvKwJvT-%LfoePj*)ug~o~(qZnIxM`>x>~;N7@?@g;4Bb;tCx( z2{65-QU$*OdE4sv*gSx98J&;uGLWm?`wCgc>zOS4{iT^%&jT@XlEMs+q-BDzdThDenYI|e9B z`|Q|HG;Rhqo(x0P^{=p{rSvT} z#qHm3dko+ejh~a{6BkMwBM48AnN{ltt0o}mEuvhOmJlnctqggj zCW4K6Ei5>XMc$AETl$v1dlN7<&rSa z$jQ&UjC$nkp`q&xPWIP260^0m%tZk@SkW@6Y_Ieh++j{|(r+%+U>|7BJqt({#F-L8 zlKTK{z`k0uW;*dfcm2eB15`wX+hDZufgOGGq>P*D&Tl*=AD>;C80z^vpQ(~J(l0yE z7S_upR!HbHBbkHj|I-7}i!|oB*IN682iU6$SB8L4FbQR7Kb2|d<5>=|8~YTrip72V zA~*`LNOIez8iFF4gx3@|lzw%#Mh=-g^Tzv9<>E0^kyp{D^Zma2N zQsT{bs|HsSLf-M8MlkszC4c4Ukpqneb&VbiIoS|bP)e;namcgw-2=w~h1mKQeI#@0 zPfN$w9^+d=R{3!LdD1!|F{O&Z%rpHi_V&X%4>6)wnOa>;HzZW;9>=|6R|(5*c{WHO zWTrvXA7207qo#ECWerq$iaCaeM(EHl6+yAbVw^%w8M}EhPR99!emNn zMoP5nB4o;@Fbhpq?Vx9aT%&Kby%K2lW`_k1XJSoM#89NI^z!89#^fn-uK6Qy>gzw` zZ99pi-nv?T7@A~F&y_2hsbj?g5xw?2b7KM4c4elt0&@9ZG@7&Fbh|5^4nT5O<7MSO zl6QL$z_M;3Q`Pl-m{se}KwFemRMeBYVpFz~%HWcTshNP}!>i=p^_DxrK69xWnx^L| zse%;6s^+;yMc39;Ek!U-{R5FRcn~5v9LO<&6phnP z3(6+U84ulfz`o!=)TSSUpqBGH3kjEz-0J|thA5EDW^B}^xhhWF1?n0aM4QCwp5C`f z-6kL&t(ee>1mUE_mmy=bFnnbiY$;b&B$ULeHGSV^Wh7C#@%>op!|AKq-(wnLb!Qou z7NREI5=V6NIt(Hd!<^u}*p%;J9(I6oQKe>G3y31RPqcC4o;Ri#Qjlzzpld(ZB7l0X zz|c5$xOV1Oq?*s%C2ab2j^gc>wS~T(9?jw*J-PbGWwWBsDe86W{_i6DnA|-!!ljhh z66rVc0}M0u)n3N+?Q*QO3jlUjZE>`T#~>tXv#E+UGmC(Ucn&|0zw{G@K3EMSwQQ+l z;%#aPkH*IYZ07~ZX9Po6Z0X*dsVWSl)^d7UwIz`4A2>LI*DYHtPt?C5@IGc_*Y5R~jo4Ox5y7Mst-LCm!IldSCy~fdx}?NWME&!rsip^onB;%vH7TX_?7~o`@$>9+ZL;b*0L_gAI5Z)s@VC z7x$Snp>^~KN71U>X;kRdwW_WVPWQ?7!IynK+R+~Qrj?i2iku8Pj8lWKm`Qi{JoC{j z?)Fgx7Q!#-J*Ir<)1t5ELso!tGp7h-d@CMMJ@;^sgumt+uBHQ=qHl|wsuCv!1T;}U+OxZ#&un$F2(~9vPvorkF%S?8aJJRCF*|`@AF!$3{->y^FQ2Mr?X+_m#N-vn*IG`7yb6$^Db{V~MQ}6dlLZQgT!9^h_?X~#xD6f84X+_-6&0#@SXKkA@OoI8e zKZ(Go$eImyS4Asxs9I4}Lg3ZZC`UUxg$|2(g(u5p9-^h`@<8g1Yip%ENQ)BJ+v7z; zTXUY3#FrE!7^<{mAmA2{_2<%p$1Afd_L6qe$Nzkh?*oE<02-hIme<~-97XG_k+op* zR(LUmfh&vCUAE`8O0FXy*K0eAUx<0=cGU~L7lpEvVo=>8eW&w0gTKwF(SzvG6I;AJ z0{vq406Ir=)TjifUHt=>IJk)ddzZVvdh>eQ1>7C+{Z^0nJ;LV?=v=t?3yJyTf-8dv ztYbz33k~|~QCl?rcqJu}(^X~Nc5#~)?x6yFZjX09%{JdzpbS0dnh&XFXIosv|LA~U^Ph|AM}8O$ zsnIHK4E}qQjCLc9#Q%TX?R(bYaeQ-noHO;uBPNaluDx)!VcQowkLu~^0U+;fC&a$C ze6>zA8ZU!lS2o9X2MBRzV2>oLT`JO&7opQ>&_Z0jC<&xZo(G;SjOt|R8O zjK4bikBfs2VA{TtHrugXM==4eJ>l?rKIG>V`SZCtDnx|+w}X@ix4X}YAGo$@B0CaWC>^AOb+gMqRc(t+2Wd^G0#Y{ud5#G+ zS_RvD@3vTMYN`aD#X$|lZRRl~X~M0lcWAe7b1OBZdFk^C5HZ`TvjFe}ha%iNyS@>> zXPb0%qbwv-cV~*=mUqI#EbNCIe}JsZ?`St}^H#SnpozP!l2V>*J;t%@KlZB(cK=f8 zZ2}WJAHZAPxuqDtRrBBx6Q`i4frTJLb9cGNHn++}su_|A_fKSRc(S+DCB7??gc7lvD8T_*Cx;?%UD#Hl-)ksq~w(I!=P{gp$%XCs6joX+0 zF?H2g-nX_g;rUrI!CSNjV|i{wP7l ze3}VPui?imF*3yXIeaTE-)00M#};bcluS~#NpFvxKqeLh;qpbv@wVP&NpdC8#2VO> zihupN|NQ!-pB|RrkmRpT0LR7uHFoClP_F$S&l1m^q@oRp3YC4ACCllQ>}B6M#|+9c zB4i!Zsh)~t-)SMjjD2S~6~5R9P3}dj@!2{7NixyNps-Bg}UBa@IWqfJ`t4@>jv*a75_z`@h| z`j$t5nVBAz=J0iKR|If_+Z!lq+ipyL%QG(jz_&XR{~Fed5dHcP<^B=U>E}+WJAz1* z|C%{9neC9g7Og@OKF+`=_&9fcwOGdf3T<$<5lNHZtf$m=0MLRg{dbSnE)G#8+#i1( zn>)JDGftbwgVTRo()!sjxG_%WsT@Zw000Fd@mdb6#SasdMG@J#GEF9597Z!x_a_WA zeVl9x!~a#|45{yk+1^Y!jNPx?XMX&$#nTWN_;$xUXHxJ15Q4?S1`T5D_cxZy*y-4H z1bWKJHiM+{<{;?GRslhkpc1o|OY|s&SpP!9-=h zUI$rD@*ou=K=b3kDLe!Q7u7k~%S-@N_F-H6HoT`IlLci6Ku_^u>Nw3Hpq1Ly68Y8ho44#M#`nds$oRX#UHu2PFB5_G< zv1dB0wQfUQOMuepx-+vJj_!d>%*XBmaW*D+`|Q(>aS>981hYQXZ zbqEo{qj4Glw2NL_48@LlnZ@&@yM#EF($PHUL zj~~B%<|(T=lifQ(X+()U@tST8UKDoPaGGutzT0gn{3<)q04)aOs2Rch zPd^!*o8{H})KNhTG6+fJ7>Dey22Uy$ws4|=k)qU{zNNHg{^4rdYAYhyO#OmG)Kiy1 zaB!3Z*a_iLY%(fs`mSCZkj8~19fUzAdPJvLcfR6j4`{ugBrM&RJ*xZNd~HrszU?8{2gGu}X2?9<BhVHd0CmViXUtJT?|!>IwXg>`! zfdUjhs`<_Er6owHLl z7VksS%y65>EVnQ~EukI^r3Y*~BD}0yZN5&qZP>h%PPhyukL*SJ01GnMhGcCqqOf9{j7Ld-cGDd1WL`1aY zM=s$g!u^e(6gHL=hQrc*4vk)Ew2%mJBw4&@a~5gbAtk6uJmng+{cTgPW`1yBzh`!X zTH34UGGx~xvtG?g2vO)CT@)dGIuo1|$ez*ai&_lIXF3OObf-%vwPuGPnrpPX^i~T} z;QVyU= zwJ=ki&+=^q_Lm+YdAjDRRF+^KA9xB5e#TO$pn}gPQB_xaUrVEcgiY%3^m|MfzT~xu zD|du9_7*BM`X~K7JUDoM5;!$%$q0`qm?b&y9yJOG1JpHd0~(2yi;9}i4vd~f{?uR3 z*iXhwt~sTL5K&INTA|D*^_UI;x@RGy{%zaMbK@7cJ=5sEBE7tUfq9D(K&^yA1|Ox z1$s@>^m)VtbIkU-8z&4~L@y;jVZ$|Ngp4oP@mx}g$?ssM@FjZf)WBCqQwWSoqjsgP zvQVS<^TC)17o4~k!^PtMzQ^1rl>>4+2sLsB*ACEfPQgwfVss>#IQMWDeN+vkma6&+gWn;G6*d#<+RnTiw{YKfM*veLSsr!h{o)V1_p$v=cY7F-Y<>$W}f_xeX zu1JGNKEfzx(4<8fs~@*rpA%L#U0iF#%ZJ2Yw$=_v7tbv++~Xx^1y_6*@pR)ykf^Qh8ps@;{hgDDoc6`1T> zhWUFnw=2b5`C)zZoS2JI@-ypZ1u`qgVY>^(V6I24E&d@shlFCf54w`?vgr29 z(E8JeKzaKbqEJp%=8T%B{Myz zTG7CBsDV}{k{)cV;I8@vMjPjh)NCUhBk` zvvQk9i!k9qv~E)I8G>g5k`_VLM~P5ZMlh`0dO9hd+d$B_jPUCuvxihMt`bpy(x7KO z*LX@t5@ix7imvPwrla>!d=^VYx7BV9`PN8Zac*mpK`KWXpEF{rku4S0W|PNP%eMuj zN!Iq*Q)v!%Fi!Y6#j74-pW1n$!Y%_Vy^Hk5*>ZYrAvzs zKC-B@b`dD+nF@~c!l6yti-E)DG>=X4))c<`eB++_I$U${l3WQTM@Ma8z7pqB3|4Cn zjTMPqAdXG0)Jfk=G#HK5HJMN{8TsXorJ*uV#HyO3XR{r$n^&M1lY>k{)KzPe=$>RoC;2EEWRK^+XyfLpGB@Tt*hrJm7jDkkfE|<+AFvN}m6}WU zQDXMd@A>FCIKwV`H6M6mZo_Ah%b?ucg+m~3|4+iLErKw58<3OV48u!(Ykhrn%T zJj(%*z3$S}n8-9A58Tm1A8^BkrOG~YzP&b94YFwMx{6%&40!HHg0;|CspeY<1iCyk)|A{}?=VMR#8~trT4vIu?{W zgS2UDxR<0b{}$;;QxF^)Q@BI?_co8mE8n)R71udoWsl(E=u6^CSYp0pkThw< z7bDRxJRP0_e;)~9FIA>Vu(Uh3!lnBrsYk(WTr%TcQU{6eta{qV<9I=dd0KT@KF4U& zGgX76CU`(pv?Q}zKVbR;ZVFX2Ni9`fRG<@!;+e+T$!eDAeCu9Q>LWq}eV1?H<^fD7 z@}%854hFM)d%#<^l@-|gq6_{>qFSk|sZ!!!_mUU_sy$)W>D(t@qSsP8wNzd?iqW{w z3KOb7YLgvQ3b9(*nqA3eDZa@ySov1M4Oy{nDXV8{JgI^b-m);xA;~W3jxuh40ltem zXwB=vxTVf$HMAY(B~y*;nS1Z`Cx?Xlgqz=c0!qO8tfyVo8xTspNEuDTz3Tm7(&_Ah)2(v#xS*p!p?Vik z0}Ga0MwzFk*~@KMtlXNc+eonqYL(7dUTd9YJTa^GUlfdAz16w!GM)Q@2!_mUXO(n1 zwMpQ8kESqnl6cX>CCp8XjC=C&+g$Pp{Sv?t-w@q^eCv1^+@lH6Z<9?;w5ti;MCr86Q+j_mf_e9#k`L`=k zC_*?Bu%niUHol5AXEDT1^{W9w4Q8`eE)=i$nN_}iDK7EY1b7I%LgD2R&-Z#(wUX}1 zN$&>2y^5{HdeN*sUxi7^um+z9Z$ovTUJ6{)zE@>J9P_pT$qka`4}8czS4hx9ZsUVM^3M~(oo{r zlU+xyuE!TD$pAKDcI_sF2?aYVZ%33Gb3Vdl7`c}!T}f#_RrH2R6}mgRx{e-%`Z+dJ zhY8-sQYIcK#>EKa-OlCeORTkp`b!fG7KDyUSF8AI5?^9;o?a?QS}s^sGHch6B$^gE zm*;n-AvE_1b2&S#xNXqghQl^WETW!hxhXHejTwns{5_01ZI$;^6%16PZLtK_JV>&O zOzlhF;)gD&QE{{F{#X`lG||WNfVWAWSBp^XqOy9U$&Bs;?)OV;`casAEDOWPf zZ)sJ+0N%~_ zHhfMT85UrE^L$;CsUPPTFG&dL2}%+0rn*%5$B8;TuKb98_@SEnk@A%YusL6aiy{0a*x`vNI@XQL1cNe(vRI8lGOJcosKt`p)bA~z+b+nHQ9R>T|A_=BA z*X`gC>y^wBVwPwMekzE^?B(N>@SSP~jgzqnd?~Y;Cxo8$7Pz|zsCKJ{3ssonVxoAK zmCx!9c+pp`M)h67#r$ksBx#Bp3OEkqhBrOe8S^Zb^h@wa@aTck7ZB1z23NT~l1DQ< zoVCd(pw#?f2JU9h@{6RwA$#SDTz&hASyO`%?Xz~6e1?3(R2+r)QH^ZAk}kkxXkOMl z1H09akr0%KGS*j>Bv=}2Go{~Ifn)i|^eRunha6rdQ=?WG<)b{;C|s}c%!?Ig3zm!F znr!CbcRq|nsyNwKCF+8&h4N#@4_Y%#y+f!*9!k&=3<*HLPsoH}@{gcU=z-g?!%5iw zLN z7Qyc|Xv;dKladB~rZx_|KFg9f!1JQ*FF6aA0$a?AoY4<6;JIzD)7_DrshTv3zGd6~ zdkPvuqm_+z5>s|wV|0HjH|&njn@+E0_Tmn^-nHL;;uLE&0h2KtL+qt-Im{p(ist)* z1@Jjn8xt7Pnq7hgCCntO)0(A0f|GGv7);%>XS8|N)XzCzFh01J!9F?AdW6~cfk3)e zQ>&D%lE3}(BEK~fNr+8Au2`jaM)*{tL~evnsxOvKqFljV5+As+Hd5ba!)omh(PV2X zP_g?Qynap@zY1@c;|pVcBb4>-I$I$pDTIWTAqLb5l{kHGT2in;arc)N<`W)aD?^k} zP{|R(Q`~OQ!wq#->&}QVEb{GHuC-JSo`(9j7kfJ4hY`jlT8csB+|V>AyQ8O~TlGAz z=;d>@W;B%Y!Z?(dw-u{gkRw~;X~M|%t%sLOCZ@Gd9E_MBoS(HBamp)6F&N_WJ$ghU zQwP_t1x|N=;(I+jKrObLuNTqN(^Lkw#WvXA6HE4}DCTLg{rZDmD&Ne{XX}di=;5;~U zfc}6I<_AH2xS#^%o4NXCJO~k!ywW}RoCu+7{na7L$DDU~YM`5;u1rwB=3_0CvSQwG z($ay36dk-=rYqnb6T(FK{Kwl?nWiRL1ruE$a^s1G&=vELvu=_LcaU)v@m6{(t{Ucj zIn!ZMPI2;$djq474p{QSDO$W$xV4iY%J0d-g`hmHkqG8cG9hq2sC>4?WT(`qDD0SO z_O7h(z=Jbdi!y=|U60C(hf)ne|DpnGC zpu;vi^cb4?epwU9)@autfAZ}gKpu+ko){BxZDQ17HY>FO1(^UA55dlKo-`QXUR4qG z&1=qJ2<$Y8!D7feW;3L?6gx(T(j%^L4rn3;-xFBJV{@Hvhq3KPaII2qS55%DKO(Pn z^i`*+MuwNo$m)b|a#J9tJ2-9Wd2l z)Tt$hQW^VwrEi$aDlP-fyr z{&8^`SA?&&wi1}@56kp9Mx6gm8n)LwV|n|pZCL553P%K%`}I%a8*d7s7Vt67wH@P; zw~t!M4(+&p(D-EC*D`MZiBqWE7^fd38fg;XQDDhS5hmiM#@CJ6+R7aNhL@`HOL;a& zrX42=l;JEqaHt5+g8Dq;iVxK-go1D$Y?4jtaiQ$KNUDp)%01HrZuoV_q+QBGEU)}a zXFJck1@nCzJvTNM(LArsvIr>MBS~tz`_7$bE{IR?%KqZ*u!D%5s#J;Qqg*+x^85mS zU20VS%$1|BUZvCZqb%ALuFkS@?{fK#k-V0*yQm8udiJ|psKM$nS7aizpd)3w~kqDOy2UTLCawJ^75}vAV&xrZvBm^ zF?KqPjBv84Dc@$+uef|sZ?B>o)ZBY^mmv%e-qqq0AThiW@Qv8-2ID?{6`Ha<;w=eOjs;O)t z5_PL&x#G78M4E_htJ?4tyTPXHpR4vAyY=Ep$6Axz@DyoF$Aj?JPNFAh$5vC6vF+z* zG5)PC38APFx>i)Hx^}}Pv5qC@vvNS}*e6TkM7kMIdoo8Y)NeJIG<})XJH$4gj=mnq z$1JVC6haSRytj_6yxzUCVMihDhe#sR8%QotsaSw5y{8|oH z_peiMjiXnhS6s1`ZQ-O-Q7!{bjAr?ld6NtNzR>b=){ELos&W};FvnilBV{HvUTav9 z^jW^1%t@;3oYDmyFLJ0``RW945oLrkwc6-yj0iK4tk!ge;`45P-5=XB^G;+YJ}iH0 z!UH(6vGap@b-REtsD8uI5LV+XPLfXohs@jV`I8*mdrLo;!;TgSZU+Yfv~XqV@8&2a zGSr1ETudp^kM5I~UIUUSY%^8M(M})_`L;f5{y97cLFw<{+17dcsrP~*TQ8)@tuG}f zv#tso^TOK?8&ZqWZ_aNHW4pz*1LN8AMDw^ey)pPtxqJ)HT*NnG6W1Pn*OigA zuRG&)09azTKHK5@#Q1$aPm$O`$;6Gb86eqg?qbTwSN-&_7yD;8g{(vXMcqVF>e;UP zM^MS!De$)xFa7>3aD%m)4g%NcmDm3+wSFF=k9)iA9Xp4V>(reMP6!3G4zs>=$vSW9 zvu}omgrrHP**}2xr!ye*SOy$$bl^xi(eyiC_=rqmW%Zr=2w6Fgi$YHL{bK|G!QmW{0LS2V*I(WXf`2G_2cSR-D2OX*Y77v~hQ)hA zzQftLf7~V^p7x>UPN5uxKr707_&nVeS*pf8brJT3R&>7u8!~8E=l!Xz(#orC7~@f< zon`XZWcYK20hyDc@8n8#cW(8y4>|GJ63p8q8->raXg^0RLvQy|{9h9LfBb&z+O{E& zi_307-ahc_(i{!`RFKmYRl_OG|gzaSt*9_}OrQHkeu$-n=mo&WiX zKL3r*J26|vGc>J*H(6+kueU01E!;Bht!MmtGdsQK=kU)h8lDMh*Z(YB-(eW;gnxmN z&ZAOP6aVYO{MQe=n?g{ssi=JXW}s>x;E^=4?k3cIQ?LGdGhnOh@9%%11cIKFhymL- zVME0gUqh60sZ?fMV2!BI!gt2?&+@ZBnbRmT;(Z6NNmq`tly<@3W}|#2TmKg%h~Dxu zJRQMyiwM2@Kym*yjw?4m>}Za0e{))vGyIzKt4B5Og&+K??s)64V1>&-d0G_DZ|R4P zJ|r$8JpcMGf34_0E;aj+en`8EeEPA#T{+*+gZ!gVO@(qcaxF&c8Y&&&Kl1X#CImW& z+6v_)kgEyfXrj;V+s_Z5c{mvAwzT)3D;~It_nJxkH%^VyR*$J?o5A-vJEVDU@~Qu| z^V2zTLyOsGI99Znjm_yw$MOegpF`s}5)W>|QHdvVj?RtL`I$a`nbGHY9{vMS)vSz2 z%`lZxI(T>C+A`*w(fN;$@X0`2d$MY2+`s-(^8{nDzQcVdA|XGAK#uxXy|es7YSW2> zcUh4=UDLZpmD5e8tCn0hnNmAHS#SzfAzahETxsj`m$jbz*Hg2{mAxV`kr5o8O&(YI T)H?^afPY4(&3{7w=pOffk3*jS literal 0 HcmV?d00001 From 223464a51a63e6ed4e4ba1664c682d379318339b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 5 Feb 2024 14:42:11 +0100 Subject: [PATCH 066/112] refactor(catalog-graph): apply review suggestions Signed-off-by: Camila Belo --- packages/app-next/app-config.yaml | 2 +- .../{EXPERIMENTAL.md => README-alpha.md} | 73 +++++++++---------- plugins/catalog-graph/README.md | 2 +- plugins/catalog-graph/src/alpha.tsx | 50 ++++--------- 4 files changed, 52 insertions(+), 75 deletions(-) rename plugins/catalog-graph/{EXPERIMENTAL.md => README-alpha.md} (65%) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index f43fe32faa..2007aaba58 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -16,7 +16,7 @@ app: config: filter: kind:component has:links - entity-card:linguist/languages - - entity-card:catalog-graph/entity-relations: + - entity-card:catalog-graph/relations: config: height: 300 # Entity page content diff --git a/plugins/catalog-graph/EXPERIMENTAL.md b/plugins/catalog-graph/README-alpha.md similarity index 65% rename from plugins/catalog-graph/EXPERIMENTAL.md rename to plugins/catalog-graph/README-alpha.md index 9af7dbea66..52c09c4b50 100644 --- a/plugins/catalog-graph/EXPERIMENTAL.md +++ b/plugins/catalog-graph/README-alpha.md @@ -2,7 +2,7 @@ > [!WARNING] > This documentation is made for those using the experimental new Frontend system. -> If you are not using the new Backstage frontend system, please go [here](./README.md). +> If you are not using the new frontend system, please go [here](./README.md). The Catalog graph plugin helps you to visualize the relations between entities, like ownership, grouping or API relationships. It comes with these features: @@ -44,7 +44,7 @@ app: packages: all extensions: # This is required because the card is not enable by default once you install the plugin - - entity-card:catalog-graph/entity-relations + - entity-card:catalog-graph/relations ``` 3. Then start the app, navigate to an entity's page and see the Relations graph there; @@ -128,9 +128,9 @@ The Catalog graphics plugin provides extensions for each of its features, see be An [entity card](https://backstage.io/docs/frontend-system/building-plugins/extension-types#entitycard---reference) extension that renders the relation graph for an entity on the Catalog entity page and has an action that redirects users to the more advanced [relations graph page](#catalog-entity-relations-graph-page). -| kind | namespace | name | id | Default enabled | -| ----------- | ------------- | ---------------- | -------------------------------------------- | --------------- | -| entity-card | catalog-graph | entity-relations | `entity-card:catalog-graph/entity-relations` | No | +| kind | namespace | name | id | Default enabled | +| ----------- | ------------- | ---------------- | ------------------------------------- | --------------- | +| entity-card | catalog-graph | entity-relations | `entity-card:catalog-graph/relations` | No | ##### Output @@ -142,7 +142,7 @@ There are no inputs available for this extension. ##### Config -The card configurations should be defined under the `app.extensions.entity-card:catalog-graph/entity-relations.config` key: +The card configurations should be defined under the `app.extensions.entity-card:catalog-graph/relations.config` key: ```yaml # app-config.yaml @@ -150,7 +150,7 @@ app: extensions: # this is the extension id and it follows the naming pattern bellow: # /: - - entity-card:catalog-graph/entity-relations: + - entity-card:catalog-graph/relations: config: # example configuring the card title # defaults to "Relations" @@ -159,22 +159,20 @@ app: See below the complete list of available configs: -| Key | Description | Type | Optional | Default value | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | -| `filter` | A [text-based query](<(https://github.com/backstage/backstage/pull/21480)>) used to filter whether the extension should be rendered or not. | `string` | yes | - | -| `title` | The card title. text. | `string` | yes | `'Relations'` | -| `variant` | The card layout. variants. | `flex` \| `fullHeight` \| `gridItem` | yes | `'gridItem'` | -| `height` | The card height fixed. size. | `number` | yes | - | -| `rootEntityNames` | A single our multiple compound root entity ref objects. | `{ kind: string, namespace: string, name: string }` \| `{ kind: string, namespace: string, name: string}[]` | yes | Defaults to the entity available in the entity page context. | -| `kinds` | Restricted list of entity [kinds](https://backstage.io/docs/features/software-catalog/descriptor-format/#contents) to display in the graph. | `string[]` | yes | - | -| `relations` | Restricted list of entity [relations](https://backstage.io/docs/features/software-catalog/descriptor-format/#common-to-all-kinds-relations) to display in the graph. | `string[]` | yes | - | -| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | -| `unidirectional` | Shows only relations that from the source to the target entity. | `boolean` | yes | `true` | -| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | -| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | -| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | -| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | -| `curve` | A factory name for curve generators addressing both lines and areas. | `curveStepBefore` \| `curveMonotoneX` | yes | `'enable-on-click' ` | +| Key | Description | Type | Optional | Default value | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | +| `filter` | A [text-based query](<(https://github.com/backstage/backstage/pull/21480)>) used to filter whether the extension should be rendered or not. | `string` | yes | - | +| `title` | The card title text. | `string` | yes | `'Relations'` | +| `height` | The card height fixed size. | `number` | yes | - | +| `kinds` | Restricted list of entity [kinds](https://backstage.io/docs/features/software-catalog/descriptor-format/#contents) to display in the graph. | `string[]` | yes | - | +| `relations` | Restricted list of entity [relations](https://backstage.io/docs/features/software-catalog/descriptor-format/#common-to-all-kinds-relations) to display in the graph. | `string[]` | yes | - | +| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | +| `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` | +| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | +| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | +| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | +| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | +| `curve` | A factory name for curve generators addressing both lines and areas. | `curveStepBefore` \| `curveMonotoneX` | yes | `'enable-on-click' ` | ##### Disable @@ -187,7 +185,7 @@ app: # this is the extension id and it follows the naming pattern bellow: # /: # example disbaling the graph card extension - - entity-card:catalog-graph/entity-relations: false + - entity-card:catalog-graph/relations: false ``` ##### Override @@ -256,20 +254,19 @@ app: See below the complete list of available configs: -| Key | Description | Type | Optional | Default value | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | -| `path` | The page route path. | `string` | true | `'/catalog-graph' ` | -| `initialState` | The page filters initial state. | `{ selectedRelations?: string[],selectedKinds?: string[],rootEntityRefs?: string[],maxDepth?: number,unidirectional?: boolean,mergeRelations?: boolean,direction?: Direction,showFilters?: boolean,curve?: 'curveStepBefore' \| 'curveMonotoneX' }` | true | `{}` | -| `rootEntityNames` | A single our multiple compound root entity ref objects. | `{ kind: string, namespace: string, name: string }` \| `{ kind: string, namespace: string, name: string}[]` | yes | Defaults to the entity available in the entity page .context | -| `kinds` | Restricted list of entity [kinds](https://backstage.io/docs/features/software-catalog/descriptor-format/#contents) to display in the graph. | `string[]` | yes | - | -| `relations` | Restricted list of entity [relations](https://backstage.io/docs/features/software-catalog/descriptor-format/#common-to-all-kinds-relations) to display in the graph. | `string[]` | yes | - | -| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | -| `unidirectional` | Shows only relations that from the source to the target entity. | `boolean` | yes | `true` | -| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | -| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | -| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | -| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | -| `curve` | A factory name for curve generators addressing both lines and areas. | `curveStepBefore` \| `curveMonotoneX` | yes | `'enable-on-click' ` | +| Key | Description | Type | Optional | Default value | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | +| `path` | The page route path. | `string` | true | `'/catalog-graph' ` | +| `initialState` | These are the initial filter values, as opposed to configuration of the available filter values. | `{ selectedRelations?: string[],selectedKinds?: string[],rootEntityRefs?: string[],maxDepth?: number,unidirectional?: boolean,mergeRelations?: boolean,direction?: Direction,showFilters?: boolean,curve?: 'curveStepBefore' \| 'curveMonotoneX' }` | true | `{}` | +| `kinds` | Restricted list of entity [kinds](https://backstage.io/docs/features/software-catalog/descriptor-format/#contents) to display in the graph. | `string[]` | yes | - | +| `relations` | Restricted list of entity [relations](https://backstage.io/docs/features/software-catalog/descriptor-format/#common-to-all-kinds-relations) to display in the graph. | `string[]` | yes | - | +| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | +| `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` | +| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | +| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | +| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | +| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | +| `curve` | A factory name for curve generators addressing both lines and areas. | `curveStepBefore` \| `curveMonotoneX` | yes | `'enable-on-click' ` | ##### Disable diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index 820ed1bdca..0862abbc96 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -1,7 +1,7 @@ # catalog-graph > Disclaimer: -> If you are looking for documentation on the experimental new frontend system support, please go [here](./EXPERIMENTAL.md). +> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). Welcome to the catalog graph plugin! The catalog graph visualizes the relations between entities, like ownership, grouping or API relationships. diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index 4f92b16b8b..6d8429332e 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -26,57 +26,37 @@ import { } from '@backstage/core-compat-api'; import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; -import { ALL_RELATION_PAIRS, Direction } from './components'; +import { Direction } from './components'; -type Zod = Parameters[0]>[0]; - -function getCompoundEntityRefConfigSchema(z: Zod) { - return z.object({ - kind: z.string(), - namespace: z.string(), - name: z.string(), - }); -} - -function getEntityGraphRelationsConfigSchema(z: Zod) { +function getEntityGraphRelationsConfigSchema( + z: Parameters[0]>[0], +) { // Mapping EntityRelationsGraphProps to config - // TODO: Define how className and render functions will be configured + // The classname and render functions are configurable only via extension overrides return z.object({ - rootEntityNames: getCompoundEntityRefConfigSchema(z) - .or(z.array(getCompoundEntityRefConfigSchema(z))) - .optional(), kinds: z.array(z.string()).optional(), relations: z.array(z.string()).optional(), - maxDepth: z.number().default(1), + maxDepth: z.number().optional(), unidirectional: z.boolean().optional(), mergeRelations: z.boolean().optional(), - direction: z.nativeEnum(Direction).default(Direction.LEFT_RIGHT), - relationPairs: z - .array(z.tuple([z.string(), z.string()])) - .optional() - .default(ALL_RELATION_PAIRS), - zoom: z - .enum(['enabled', 'disabled', 'enable-on-click']) - .default('enable-on-click'), - curve: z - .enum(['curveStepBefore', 'curveMonotoneX']) - .default('curveMonotoneX'), + direction: z.nativeEnum(Direction).optional(), + relationPairs: z.array(z.tuple([z.string(), z.string()])).optional(), + zoom: z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), + curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), }); } const CatalogGraphEntityCard = createEntityCardExtension({ - name: 'entity-relations', + name: 'relations', configSchema: createSchemaFromZod(z => z .object({ // Filter is a config required to all entity cards filter: z.string().optional(), - title: z.string().optional().default('Relations'), + title: z.string().optional(), height: z.number().optional(), - variant: z - .enum(['flex', 'fullHeight', 'gridItem']) - .optional() - .default('gridItem'), + // Skipping a "variant" config for now, defaulting to "gridItem" in the component + // For more details, see this comment: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 }) .merge(getEntityGraphRelationsConfigSchema(z)), ), @@ -93,7 +73,7 @@ const CatalogGraphPage = createPageExtension({ z.object({ // Path is a default config required to all pages path: z.string().default('/catalog-graph'), - // Mapping intialState prop to config + // Mapping intialState prop to config, these are the initial filter values, as opposed to configuration of the available filter values initialState: z .object({ selectedKinds: z.array(z.string()).optional(), From 501b750fa27f1874c652446dda7be4bbbfb81e8e Mon Sep 17 00:00:00 2001 From: Radu Ciopraga <1442639+raduciopraga@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:23:20 +0100 Subject: [PATCH 067/112] wait for GroupsExplorerContent test expectations to pass Signed-off-by: Radu Ciopraga <1442639+raduciopraga@users.noreply.github.com> --- .../GroupsExplorerContent.test.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 0e992c220d..00a6164aa9 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { screen } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; @@ -80,9 +80,11 @@ describe('', () => { mountedRoutes, ); - expect( - screen.getByRole('link', { name: 'group:my-namespace/group-a' }), - ).toBeInTheDocument(); + await waitFor(() => + expect( + screen.getByRole('link', { name: 'group:my-namespace/group-a' }), + ).toBeInTheDocument(), + ); }); it('renders a custom title', async () => { @@ -95,9 +97,11 @@ describe('', () => { mountedRoutes, ); - expect( - screen.getByText('Our Teams', { selector: 'h2' }), - ).toBeInTheDocument(); + await waitFor(() => + expect( + screen.getByText('Our Teams', { selector: 'h2' }), + ).toBeInTheDocument(), + ); }); it('renders a friendly error if it cannot collect domains', async () => { @@ -111,6 +115,8 @@ describe('', () => { mountedRoutes, ); - expect(screen.getAllByText(/Error: Network timeout/).length).not.toBe(0); + await waitFor(() => + expect(screen.getAllByText(/Error: Network timeout/).length).not.toBe(0), + ); }); }); From 10f0efdcb7a47a530a4571bb286f370567af3699 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 6 Feb 2024 08:14:00 +0200 Subject: [PATCH 068/112] test: fix failing template card test Signed-off-by: Heikki Hellgren --- .../components/TemplateCard/TemplateCard.test.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index 9ff1dd1b17..ec4e4e1b0a 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -358,12 +358,11 @@ describe('TemplateCard', () => { }, ); - expect( - getByRole('link', { name: 'group:default/my-test-user' }), - ).toBeInTheDocument(); - expect( - getByRole('link', { name: 'group:default/my-test-user' }), - ).toHaveAttribute('href', '/catalog/group/default/my-test-user'); + expect(getByRole('link', { name: /.*my-test-user$/ })).toBeInTheDocument(); + expect(getByRole('link', { name: /.*my-test-user$/ })).toHaveAttribute( + 'href', + '/catalog/group/default/my-test-user', + ); }); it('should call the onSelected handler when clicking the choose button', async () => { From d5b14a0ad52bf362904d1af9c36c0ac1b5b68cb4 Mon Sep 17 00:00:00 2001 From: AmbrishRamachandiran Date: Tue, 6 Feb 2024 12:19:15 +0530 Subject: [PATCH 069/112] conditionally rendering the user name and email in user settings page Signed-off-by: AmbrishRamachandiran --- .changeset/shiny-clocks-greet.md | 5 ++++ .../AuthProviders/ProviderSettingsItem.tsx | 24 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 .changeset/shiny-clocks-greet.md diff --git a/.changeset/shiny-clocks-greet.md b/.changeset/shiny-clocks-greet.md new file mode 100644 index 0000000000..0c112ff81f --- /dev/null +++ b/.changeset/shiny-clocks-greet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +conditionally rendering the user name and email in user settings page diff --git a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx index e3fb597a15..e9a710036f 100644 --- a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx +++ b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx @@ -101,16 +101,20 @@ export const ProviderSettingsItem = (props: { - - {profile.displayName} - - - {profile.email} - + {profile.displayName && ( + + {profile.displayName} + + )} + {profile.email && ( + + {profile.email} + + )} {description} From f24a0c1f6a784bc164f78b3d7bb5246f7b90d6ce Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Fri, 15 Dec 2023 14:05:35 +0200 Subject: [PATCH 070/112] feat: initial notifications support Signed-off-by: Heikki Hellgren --- packages/app/package.json | 1 + packages/app/src/App.tsx | 6 +- packages/app/src/components/Root/Root.tsx | 7 +- packages/backend/package.json | 2 + packages/backend/src/index.ts | 11 ++ packages/backend/src/plugins/notifications.ts | 39 +++++ packages/backend/src/types.ts | 6 +- plugins/notifications-backend/.eslintrc.js | 1 + plugins/notifications-backend/README.md | 14 ++ plugins/notifications-backend/api-report.md | 25 ++++ plugins/notifications-backend/package.json | 46 ++++++ plugins/notifications-backend/src/index.ts | 16 +++ plugins/notifications-backend/src/run.ts | 32 +++++ .../src/service/router.test.ts | 66 +++++++++ .../src/service/router.ts | 67 +++++++++ .../src/service/standaloneServer.ts | 99 +++++++++++++ .../notifications-backend/src/setupTests.ts | 16 +++ plugins/notifications-common/.eslintrc.js | 1 + plugins/notifications-common/README.md | 5 + plugins/notifications-common/api-report.md | 24 ++++ plugins/notifications-common/package.json | 32 +++++ plugins/notifications-common/src/index.ts | 23 +++ .../notifications-common/src/setupTests.ts | 16 +++ plugins/notifications-common/src/types.ts | 34 +++++ plugins/notifications-node/.eslintrc.js | 1 + plugins/notifications-node/README.md | 5 + plugins/notifications-node/api-report.md | 70 +++++++++ .../migrations/20231215_init.js | 34 +++++ plugins/notifications-node/package.json | 38 +++++ .../database/DatabaseNotificationsStore.ts | 97 +++++++++++++ .../src/database/NotificationsStore.ts | 36 +++++ .../notifications-node/src/database/index.ts | 17 +++ plugins/notifications-node/src/index.ts | 24 ++++ .../src/service/NotificationService.ts | 136 ++++++++++++++++++ .../notifications-node/src/service/index.ts | 16 +++ plugins/notifications-node/src/setupTests.ts | 16 +++ plugins/notifications/.eslintrc.js | 1 + plugins/notifications/README.md | 13 ++ plugins/notifications/api-report.md | 88 ++++++++++++ plugins/notifications/dev/index.tsx | 27 ++++ plugins/notifications/package.json | 53 +++++++ .../notifications/src/api/NotificationsApi.ts | 34 +++++ .../src/api/NotificationsClient.ts | 57 ++++++++ plugins/notifications/src/api/index.ts | 17 +++ .../NotificationsPage/NotificationsPage.tsx | 82 +++++++++++ .../src/components/NotificationsPage/index.ts | 16 +++ .../NotificationsSideBarItem.tsx | 49 +++++++ .../NotificationsSideBarItem/index.ts | 16 +++ .../NotificationsTable/NotificationsTable.tsx | 100 +++++++++++++ .../components/NotificationsTable/index.ts | 16 +++ plugins/notifications/src/components/index.ts | 17 +++ plugins/notifications/src/hooks/index.ts | 16 +++ .../src/hooks/useNotificationsApi.ts | 31 ++++ plugins/notifications/src/index.ts | 19 +++ plugins/notifications/src/plugin.test.ts | 22 +++ plugins/notifications/src/plugin.ts | 52 +++++++ plugins/notifications/src/routes.ts | 20 +++ plugins/notifications/src/setupTests.ts | 16 +++ yarn.lock | 130 ++++++++++++++++- 59 files changed, 1964 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/plugins/notifications.ts create mode 100644 plugins/notifications-backend/.eslintrc.js create mode 100644 plugins/notifications-backend/README.md create mode 100644 plugins/notifications-backend/api-report.md create mode 100644 plugins/notifications-backend/package.json create mode 100644 plugins/notifications-backend/src/index.ts create mode 100644 plugins/notifications-backend/src/run.ts create mode 100644 plugins/notifications-backend/src/service/router.test.ts create mode 100644 plugins/notifications-backend/src/service/router.ts create mode 100644 plugins/notifications-backend/src/service/standaloneServer.ts create mode 100644 plugins/notifications-backend/src/setupTests.ts create mode 100644 plugins/notifications-common/.eslintrc.js create mode 100644 plugins/notifications-common/README.md create mode 100644 plugins/notifications-common/api-report.md create mode 100644 plugins/notifications-common/package.json create mode 100644 plugins/notifications-common/src/index.ts create mode 100644 plugins/notifications-common/src/setupTests.ts create mode 100644 plugins/notifications-common/src/types.ts create mode 100644 plugins/notifications-node/.eslintrc.js create mode 100644 plugins/notifications-node/README.md create mode 100644 plugins/notifications-node/api-report.md create mode 100644 plugins/notifications-node/migrations/20231215_init.js create mode 100644 plugins/notifications-node/package.json create mode 100644 plugins/notifications-node/src/database/DatabaseNotificationsStore.ts create mode 100644 plugins/notifications-node/src/database/NotificationsStore.ts create mode 100644 plugins/notifications-node/src/database/index.ts create mode 100644 plugins/notifications-node/src/index.ts create mode 100644 plugins/notifications-node/src/service/NotificationService.ts create mode 100644 plugins/notifications-node/src/service/index.ts create mode 100644 plugins/notifications-node/src/setupTests.ts create mode 100644 plugins/notifications/.eslintrc.js create mode 100644 plugins/notifications/README.md create mode 100644 plugins/notifications/api-report.md create mode 100644 plugins/notifications/dev/index.tsx create mode 100644 plugins/notifications/package.json create mode 100644 plugins/notifications/src/api/NotificationsApi.ts create mode 100644 plugins/notifications/src/api/NotificationsClient.ts create mode 100644 plugins/notifications/src/api/index.ts create mode 100644 plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx create mode 100644 plugins/notifications/src/components/NotificationsPage/index.ts create mode 100644 plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx create mode 100644 plugins/notifications/src/components/NotificationsSideBarItem/index.ts create mode 100644 plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx create mode 100644 plugins/notifications/src/components/NotificationsTable/index.ts create mode 100644 plugins/notifications/src/components/index.ts create mode 100644 plugins/notifications/src/hooks/index.ts create mode 100644 plugins/notifications/src/hooks/useNotificationsApi.ts create mode 100644 plugins/notifications/src/index.ts create mode 100644 plugins/notifications/src/plugin.test.ts create mode 100644 plugins/notifications/src/plugin.ts create mode 100644 plugins/notifications/src/routes.ts create mode 100644 plugins/notifications/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 31be0e1610..b960712550 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -59,6 +59,7 @@ "@backstage/plugin-newrelic": "workspace:^", "@backstage/plugin-newrelic-dashboard": "workspace:^", "@backstage/plugin-nomad": "workspace:^", + "@backstage/plugin-notifications": "^0.0.0", "@backstage/plugin-octopus-deploy": "workspace:^", "@backstage/plugin-org": "workspace:^", "@backstage/plugin-pagerduty": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 90190b50be..3d8bd45e5a 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -67,15 +67,15 @@ import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechDocsIndexPage, - TechDocsReaderPage, techdocsPlugin, + TechDocsReaderPage, } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ExpandableNavigation, + LightBox, ReportIssue, TextSize, - LightBox, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { SettingsLayout, @@ -107,6 +107,7 @@ import { PuppetDbPage } from '@backstage/plugin-puppetdb'; import { DevToolsPage } from '@backstage/plugin-devtools'; import { customDevToolsPage } from './components/devtools/CustomDevToolsPage'; import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities'; +import { NotificationsPage } from '@backstage/plugin-notifications'; const app = createApp({ apis, @@ -272,6 +273,7 @@ const routes = ( }> {customDevToolsPage} + } /> ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 5f11218bba..6294aa7856 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -34,6 +34,7 @@ import { import { SidebarSearchModal } from '@backstage/plugin-search'; import { Shortcuts } from '@backstage/plugin-shortcuts'; import { + Link, Sidebar, sidebarConfig, SidebarDivider, @@ -42,16 +43,16 @@ import { SidebarPage, SidebarScrollWrapper, SidebarSpace, - Link, - useSidebarOpenState, SidebarSubmenu, SidebarSubmenuItem, + useSidebarOpenState, } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import { SearchModal } from '../search/SearchModal'; import Score from '@material-ui/icons/Score'; import { useApp } from '@backstage/core-plugin-api'; import BuildIcon from '@material-ui/icons/Build'; +import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; const useSidebarLogoStyles = makeStyles({ root: { @@ -166,6 +167,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( + + diff --git a/packages/backend/package.json b/packages/backend/package.json index 2386d799a9..1606158a7e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -55,6 +55,8 @@ "@backstage/plugin-lighthouse-backend": "workspace:^", "@backstage/plugin-linguist-backend": "workspace:^", "@backstage/plugin-nomad-backend": "workspace:^", + "@backstage/plugin-notifications-backend": "^0.0.0", + "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bade028597..fd29c4a8c7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -66,6 +66,7 @@ import linguist from './plugins/linguist'; import devTools from './plugins/devtools'; import nomad from './plugins/nomad'; import signals from './plugins/signals'; +import notifications from './plugins/notifications'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -74,6 +75,7 @@ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import { metrics } from '@opentelemetry/api'; import { DefaultSignalService } from '@backstage/plugin-signals-node'; +import { NotificationService } from '@backstage/plugin-notifications-node'; // Expose opentelemetry metrics using a Prometheus exporter on // http://localhost:9464/metrics . See prometheus.yml in packages/backend for @@ -103,6 +105,10 @@ function makeCreateEnv(config: Config) { const signalService = DefaultSignalService.create({ eventBroker, }); + const notificationService = NotificationService.create({ + database: databaseManager.forPlugin('notifications'), + discovery, + }); root.info(`Created UrlReader ${reader}`); @@ -125,6 +131,7 @@ function makeCreateEnv(config: Config) { scheduler, identity, signalService, + notificationService, }; }; } @@ -179,6 +186,9 @@ async function main() { const devToolsEnv = useHotMemoize(module, () => createEnv('devtools')); const nomadEnv = useHotMemoize(module, () => createEnv('nomad')); const signalsEnv = useHotMemoize(module, () => createEnv('signals')); + const notificationsEnv = useHotMemoize(module, () => + createEnv('notifications'), + ); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -206,6 +216,7 @@ async function main() { apiRouter.use('/devtools', await devTools(devToolsEnv)); apiRouter.use('/nomad', await nomad(nomadEnv)); apiRouter.use('/signals', await signals(signalsEnv)); + apiRouter.use('/notifications', await notifications(notificationsEnv)); apiRouter.use(notFoundHandler()); await lighthouse(lighthouseEnv); diff --git a/packages/backend/src/plugins/notifications.ts b/packages/backend/src/plugins/notifications.ts new file mode 100644 index 0000000000..91c01fb860 --- /dev/null +++ b/packages/backend/src/plugins/notifications.ts @@ -0,0 +1,39 @@ +/* + * 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 { createRouter } from '@backstage/plugin-notifications-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // TODO: Remove this test code + // setInterval(() => { + // env.notificationService.send( + // 'user:default/guest', + // 'Test', + // 'This is test notification', + // '/catalog', + // ); + // }, 60000); + + return await createRouter({ + logger: env.logger, + identity: env.identity, + notificationService: env.notificationService, + }); +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 3dad2f739b..6a48804d1c 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -27,7 +27,8 @@ import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { EventBroker } from '@backstage/plugin-events-node'; -import { DefaultSignalService } from '@backstage/plugin-signals-node'; +import { SignalService } from '@backstage/plugin-signals-node'; +import { NotificationService } from '@backstage/plugin-notifications-node'; export type PluginEnvironment = { logger: Logger; @@ -41,5 +42,6 @@ export type PluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; - signalService: DefaultSignalService; + signalService: SignalService; + notificationService: NotificationService; }; diff --git a/plugins/notifications-backend/.eslintrc.js b/plugins/notifications-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/notifications-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md new file mode 100644 index 0000000000..e90d287049 --- /dev/null +++ b/plugins/notifications-backend/README.md @@ -0,0 +1,14 @@ +# notifications + +Welcome to the notifications backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn +start` in the root directory, and then navigating to [/notifications](http://localhost:3000/notifications). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/notifications-backend/api-report.md b/plugins/notifications-backend/api-report.md new file mode 100644 index 0000000000..e571fd521a --- /dev/null +++ b/plugins/notifications-backend/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-notifications-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import express from 'express'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { Logger } from 'winston'; +import { NotificationService } from '@backstage/plugin-notifications-node'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + identity: IdentityApi; + // (undocumented) + logger: Logger; + // (undocumented) + notificationService: NotificationService; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json new file mode 100644 index 0000000000..ce67513669 --- /dev/null +++ b/plugins/notifications-backend/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-notifications-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-notifications-node": "workspace:^", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "knex": "^3.0.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^1.0.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/notifications-backend/src/index.ts b/plugins/notifications-backend/src/index.ts new file mode 100644 index 0000000000..d2e8d61bad --- /dev/null +++ b/plugins/notifications-backend/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export * from './service/router'; diff --git a/plugins/notifications-backend/src/run.ts b/plugins/notifications-backend/src/run.ts new file mode 100644 index 0000000000..d299ed23e9 --- /dev/null +++ b/plugins/notifications-backend/src/run.ts @@ -0,0 +1,32 @@ +/* + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts new file mode 100644 index 0000000000..c0f09b3ef9 --- /dev/null +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { NotificationService } from '@backstage/plugin-notifications-node'; + +describe('createRouter', () => { + let app: express.Express; + + const identityMock: IdentityApi = { + async getIdentity() { + return { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/guest', + }, + token: 'no-token', + }; + }, + }; + + const notificationServiceMock: jest.Mocked> = { + getStore: jest.fn(), + send: jest.fn(), + }; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + identity: identityMock, + notificationService: notificationServiceMock as NotificationService, + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts new file mode 100644 index 0000000000..8d9a7472bb --- /dev/null +++ b/plugins/notifications-backend/src/service/router.ts @@ -0,0 +1,67 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express, { Request } from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { NotificationService } from '@backstage/plugin-notifications-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; + +/** @public */ +export interface RouterOptions { + logger: Logger; + identity: IdentityApi; + notificationService: NotificationService; +} + +/** @public */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, notificationService, identity } = options; + + const store = await notificationService.getStore(); + + const getUser = async (req: Request) => { + const user = await identity.getIdentity({ request: req }); + return user ? user.identity.userEntityRef : 'user:default/guest'; + }; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.json({ status: 'ok' }); + }); + + router.get('/notifications', async (req, res) => { + const user = await getUser(req); + const notifications = await store.getNotifications({ user_ref: user }); + res.send(notifications); + }); + + router.get('/status', async (req, res) => { + const user = await getUser(req); + const status = await store.getStatus({ user_ref: user }); + res.send(status); + }); + + // TODO: Add endpoint to set read/unread by notification id(s) + + router.use(errorHandler()); + return router; +} diff --git a/plugins/notifications-backend/src/service/standaloneServer.ts b/plugins/notifications-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..c19ae01b74 --- /dev/null +++ b/plugins/notifications-backend/src/service/standaloneServer.ts @@ -0,0 +1,99 @@ +/* + * 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 { + createServiceBuilder, + loadBackendConfig, + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import Knex from 'knex'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { Request } from 'express'; +import { NotificationService } from '@backstage/plugin-notifications-node'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'notifications-backend' }); + logger.debug('Starting application server...'); + + const config = await loadBackendConfig({ logger, argv: process.argv }); + const db = Knex(config.get('backend.database')); + + const dbMock: PluginDatabaseManager = { + async getClient() { + return db; + }, + }; + + const discoveryMock: PluginEndpointDiscovery = { + async getBaseUrl(pluginId: string) { + return `http://localhost:7007/api/${pluginId}`; + }, + + async getExternalBaseUrl(pluginId: string) { + return `http://localhost:7007/api/${pluginId}`; + }, + }; + + const notificationService = NotificationService.create({ + database: dbMock, + discovery: discoveryMock, + }); + + const identityMock: IdentityApi = { + async getIdentity({ request }: { request: Request }) { + const token = request.headers.authorization?.split(' ')[1]; + return { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/guest', + }, + token: token || 'no-token', + }; + }, + }; + + const router = await createRouter({ + logger, + identity: identityMock, + notificationService, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/notifications', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/notifications-backend/src/setupTests.ts b/plugins/notifications-backend/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/notifications-backend/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export {}; diff --git a/plugins/notifications-common/.eslintrc.js b/plugins/notifications-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/notifications-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/notifications-common/README.md b/plugins/notifications-common/README.md new file mode 100644 index 0000000000..673e30288f --- /dev/null +++ b/plugins/notifications-common/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-notifications-common + +Welcome to the common package for the notifications plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md new file mode 100644 index 0000000000..6d7d74b64b --- /dev/null +++ b/plugins/notifications-common/api-report.md @@ -0,0 +1,24 @@ +## API Report File for "@backstage/plugin-notifications-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +type Notification_2 = { + id: string; + userRef: string; + title: string; + description: string; + link: string; + icon?: string; + created: Date; + read?: Date; +}; +export { Notification_2 as Notification }; + +// @public (undocumented) +export type NotificationStatus = { + unread: number; + read: number; +}; +``` diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json new file mode 100644 index 0000000000..91acf171f7 --- /dev/null +++ b/plugins/notifications-common/package.json @@ -0,0 +1,32 @@ +{ + "name": "@backstage/plugin-notifications-common", + "description": "Common functionalities for the notifications plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/notifications-common/src/index.ts b/plugins/notifications-common/src/index.ts new file mode 100644 index 0000000000..8d9f26f07a --- /dev/null +++ b/plugins/notifications-common/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Common functionalities for the notifications plugin. + * + * @packageDocumentation + */ + +export * from './types'; diff --git a/plugins/notifications-common/src/setupTests.ts b/plugins/notifications-common/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/notifications-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export {}; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts new file mode 100644 index 0000000000..c9df78e1ff --- /dev/null +++ b/plugins/notifications-common/src/types.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/** @public */ +export type Notification = { + id: string; + userRef: string; + title: string; + description: string; + link: string; + // TODO: Icon should be typed so that we know what to render + icon?: string; + created: Date; + read?: Date; +}; + +/** @public */ +export type NotificationStatus = { + unread: number; + read: number; +}; diff --git a/plugins/notifications-node/.eslintrc.js b/plugins/notifications-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/notifications-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/notifications-node/README.md b/plugins/notifications-node/README.md new file mode 100644 index 0000000000..4cf460c3fe --- /dev/null +++ b/plugins/notifications-node/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-notifications-node + +Welcome to the Node.js library package for the notifications plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md new file mode 100644 index 0000000000..68a53cd535 --- /dev/null +++ b/plugins/notifications-node/api-report.md @@ -0,0 +1,70 @@ +## API Report File for "@backstage/plugin-notifications-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; +import { NotificationStatus } from '@backstage/plugin-notifications-common'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public (undocumented) +export class DatabaseNotificationsStore implements NotificationsStore { + // (undocumented) + static create({ + database, + skipMigrations, + }: { + database: PluginDatabaseManager; + skipMigrations?: boolean; + }): Promise; + // (undocumented) + getNotifications(options: NotificationGetOptions): Promise; + // (undocumented) + getStatus(options: NotificationGetOptions): Promise<{ + unread: number; + read: number; + }>; + // (undocumented) + saveNotification(notification: Notification_2): Promise; +} + +// @public (undocumented) +export type NotificationGetOptions = { + user_ref: string; +}; + +// @public (undocumented) +export class NotificationService { + // (undocumented) + static create({ + database, + discovery, + }: NotificationServiceOptions): NotificationService; + // (undocumented) + getStore(): Promise; + // (undocumented) + send( + entityRef: string | string[], + title: string, + description: string, + link: string, + ): Promise; +} + +// @public (undocumented) +export type NotificationServiceOptions = { + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; +}; + +// @public (undocumented) +export interface NotificationsStore { + // (undocumented) + getNotifications(options: NotificationGetOptions): Promise; + // (undocumented) + getStatus(options: NotificationGetOptions): Promise; + // (undocumented) + saveNotification(notification: Notification_2): Promise; +} +``` diff --git a/plugins/notifications-node/migrations/20231215_init.js b/plugins/notifications-node/migrations/20231215_init.js new file mode 100644 index 0000000000..a76a8c3cbd --- /dev/null +++ b/plugins/notifications-node/migrations/20231215_init.js @@ -0,0 +1,34 @@ +/* + * 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. + */ + +exports.up = async function up(knex) { + await knex.schema.createTable('notifications', table => { + table.uuid('id').primary(); + table.string('userRef').notNullable(); + table.string('title').notNullable(); + table.text('description').notNullable(); + table.text('link').notNullable(); + table.datetime('created').notNullable(); + table.datetime('read').nullable(); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('notifications'); +}; diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json new file mode 100644 index 0000000000..4a9e9a2319 --- /dev/null +++ b/plugins/notifications-node/package.json @@ -0,0 +1,38 @@ +{ + "name": "@backstage/plugin-notifications-node", + "description": "Node.js library for the notifications plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ], + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-notifications-common": "workspace:^", + "knex": "^3.0.0", + "uuid": "^8.0.0" + } +} diff --git a/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts new file mode 100644 index 0000000000..450450d998 --- /dev/null +++ b/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts @@ -0,0 +1,97 @@ +/* + * 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 { + PluginDatabaseManager, + resolvePackagePath, +} from '@backstage/backend-common'; +import { + NotificationGetOptions, + NotificationsStore, +} from './NotificationsStore'; +import { Notification } from '@backstage/plugin-notifications-common'; +import { Knex } from 'knex'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-notifications-node', + 'migrations', +); + +/** @public */ +export class DatabaseNotificationsStore implements NotificationsStore { + private constructor(private readonly db: Knex) {} + + static async create({ + database, + skipMigrations, + }: { + database: PluginDatabaseManager; + skipMigrations?: boolean; + }): Promise { + const client = await database.getClient(); + + if (!database.migrations?.skip && !skipMigrations) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } + + return new DatabaseNotificationsStore(client); + } + + private mapToInteger = (val: string | number | undefined): number => { + return typeof val === 'string' ? Number.parseInt(val, 10) : val ?? 0; + }; + + async getNotifications(options: NotificationGetOptions) { + const { user_ref } = options; + const notificationQuery = this.db('notifications').where( + 'userRef', + user_ref, + ); + + const notifications = await notificationQuery.select('*'); + + return notifications; + } + + async saveNotification(notification: Notification) { + await this.db.insert(notification).into('notifications'); + } + + async getStatus(options: NotificationGetOptions) { + const { user_ref } = options; + const notificationQuery = this.db('notifications').where( + 'userRef', + user_ref, + ); + + const unreadQuery = await notificationQuery + .clone() + .whereNull('read') + .count('id as UNREAD') + .first(); + const readQuery = await notificationQuery + .clone() + .whereNotNull('read') + .count('id as READ') + .first(); + + return { + unread: this.mapToInteger((unreadQuery as any)?.UNREAD), + read: this.mapToInteger((readQuery as any)?.READ), + }; + } +} diff --git a/plugins/notifications-node/src/database/NotificationsStore.ts b/plugins/notifications-node/src/database/NotificationsStore.ts new file mode 100644 index 0000000000..349e2cd7fa --- /dev/null +++ b/plugins/notifications-node/src/database/NotificationsStore.ts @@ -0,0 +1,36 @@ +/* + * 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 { + Notification, + NotificationStatus, +} from '@backstage/plugin-notifications-common'; + +/** @public */ +export type NotificationGetOptions = { + user_ref: string; +}; + +/** @public */ +export interface NotificationsStore { + getNotifications(options: NotificationGetOptions): Promise; + + saveNotification(notification: Notification): Promise; + + getStatus(options: NotificationGetOptions): Promise; + + // TODO: Mark as read/unread by notification id(s) +} diff --git a/plugins/notifications-node/src/database/index.ts b/plugins/notifications-node/src/database/index.ts new file mode 100644 index 0000000000..6d2f549f38 --- /dev/null +++ b/plugins/notifications-node/src/database/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +export * from './DatabaseNotificationsStore'; +export * from './NotificationsStore'; diff --git a/plugins/notifications-node/src/index.ts b/plugins/notifications-node/src/index.ts new file mode 100644 index 0000000000..c686e95d3c --- /dev/null +++ b/plugins/notifications-node/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Node.js library for the notifications plugin. + * + * @packageDocumentation + */ + +export * from './database'; +export * from './service'; diff --git a/plugins/notifications-node/src/service/NotificationService.ts b/plugins/notifications-node/src/service/NotificationService.ts new file mode 100644 index 0000000000..55dcff546f --- /dev/null +++ b/plugins/notifications-node/src/service/NotificationService.ts @@ -0,0 +1,136 @@ +/* + * 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 { Notification } from '@backstage/plugin-notifications-common'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { NotificationsStore } from '../database/NotificationsStore'; +import { v4 as uuid } from 'uuid'; +import { + Entity, + isGroupEntity, + isUserEntity, + RELATION_HAS_MEMBER, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { DatabaseNotificationsStore } from '../database'; + +/** @public */ +export type NotificationServiceOptions = { + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; +}; + +/** @public */ +export class NotificationService { + private store: NotificationsStore | null = null; + + private constructor( + private readonly database: PluginDatabaseManager, + private readonly catalog: CatalogApi, + ) {} + + static create({ + database, + discovery, + }: NotificationServiceOptions): NotificationService { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + + return new NotificationService(database, catalogClient); + } + + async send( + entityRef: string | string[], + title: string, + description: string, + link: string, + ): Promise { + const users = await this.getUsersForEntityRef(entityRef); + const notifications = []; + const store = await this.getStore(); + for (const user of users) { + const notification = { + id: uuid(), + userRef: user, + title, + description, + link, + created: new Date(), + }; + + await store.saveNotification(notification); + notifications.push(notification); + } + + // TODO: Signal service + // TODO: Other senders + + return notifications; + } + + async getStore(): Promise { + if (!this.store) { + this.store = await DatabaseNotificationsStore.create({ + database: this.database, + }); + } + return this.store; + } + + private async getUsersForEntityRef( + entityRef: string | string[], + ): Promise { + const refs = Array.isArray(entityRef) ? entityRef : [entityRef]; + const entities = await this.catalog.getEntitiesByRefs({ entityRefs: refs }); + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + if (isUserEntity(entity)) { + return [stringifyEntityRef(entity)]; + } else if (isGroupEntity(entity) && entity.relations) { + return entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && relation.targetRef, + ) + .map(r => r.targetRef); + } else if (!isGroupEntity(entity) && entity.spec?.owner) { + const owner = await this.catalog.getEntityByRef( + entity.spec.owner as string, + ); + if (owner) { + return mapEntity(owner); + } + } + + return []; + }; + + const users: string[] = []; + for (const entity of entities.items) { + const u = await mapEntity(entity); + users.push(...u); + } + return users; + } +} diff --git a/plugins/notifications-node/src/service/index.ts b/plugins/notifications-node/src/service/index.ts new file mode 100644 index 0000000000..450f4f7dcc --- /dev/null +++ b/plugins/notifications-node/src/service/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export * from './NotificationService'; diff --git a/plugins/notifications-node/src/setupTests.ts b/plugins/notifications-node/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/notifications-node/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export {}; diff --git a/plugins/notifications/.eslintrc.js b/plugins/notifications/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/notifications/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md new file mode 100644 index 0000000000..3eeac7a647 --- /dev/null +++ b/plugins/notifications/README.md @@ -0,0 +1,13 @@ +# notifications + +Welcome to the notifications plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/notifications](http://localhost:3000/notifications). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md new file mode 100644 index 0000000000..00ff5f4d4c --- /dev/null +++ b/plugins/notifications/api-report.md @@ -0,0 +1,88 @@ +## API Report File for "@backstage/plugin-notifications" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; +import { NotificationStatus } from '@backstage/plugin-notifications-common'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export interface NotificationsApi { + // (undocumented) + getNotifications(): Promise; + // (undocumented) + getStatus(): Promise; +} + +// @public (undocumented) +export const notificationsApiRef: ApiRef; + +// @public (undocumented) +export class NotificationsClient implements NotificationsApi { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }); + // (undocumented) + getNotifications(): Promise; + // (undocumented) + getStatus(): Promise; +} + +// @public (undocumented) +export const NotificationsPage: () => JSX_2.Element; + +// @public (undocumented) +export const notificationsPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// @public (undocumented) +export const NotificationsSidebarItem: () => React_2.JSX.Element; + +// @public (undocumented) +export const NotificationsTable: (props: { + notifications?: Notification_2[]; +}) => React_2.JSX.Element; + +// @public (undocumented) +export function useNotificationsApi( + f: (api: NotificationsApi) => Promise, + deps?: any[], +): + | { + retry: () => void; + loading: boolean; + error?: undefined; + value?: undefined; + } + | { + retry: () => void; + loading: false; + error: Error; + value?: undefined; + } + | { + retry: () => void; + loading: true; + error?: Error | undefined; + value?: T | undefined; + } + | { + retry: () => void; + loading: false; + error?: undefined; + value: T; + }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/notifications/dev/index.tsx b/plugins/notifications/dev/index.tsx new file mode 100644 index 0000000000..f4ea31f4ab --- /dev/null +++ b/plugins/notifications/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { notificationsPlugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(notificationsPlugin) + .addPage({ + element: