Merge remote-tracking branch 'upstream/master' into airbrake-backend
# Conflicts: # ADOPTERS.md
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The `plugin:diff` command no longer validates the existence of any of the files within `dev/` or `src/`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-todo': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `EntityTodoContent` is now a routable extension. This means it must be rendered within a route, but that's most likely already the case for most apps. The mount point `RouteRef` is available via `todoPlugin.routes.entityContent`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
chore(deps-dev): bump `@types/concat-stream` from 1.6.1 to 2.0.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Tweaked frontend bundling configuration to avoid leaking declarations into global scope.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Updated `useEntityListProvider` and catalog pickers to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
fix for the `gitlab:publish` action to use the `oauthToken` key when creating a
|
||||
`Gitlab` client. This only happens if `ctx.input.token` is provided else the key `token` will be used.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
The `ErrorPage` now falls back to using the default support configuration if the `ConfigApi` is not available.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Minor API cleanups
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Adds new StarredEntities component responsible for rendering a list of starred entities on the home page
|
||||
+40
-33
@@ -5,42 +5,49 @@
|
||||
# https://help.github.com/articles/about-codeowners/
|
||||
|
||||
* @backstage/reviewers
|
||||
/docs/features/techdocs @backstage/techdocs-core
|
||||
/docs/features/search @backstage/techdocs-core
|
||||
/docs/assets/search @backstage/techdocs-core
|
||||
/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps
|
||||
/plugins/circleci @backstage/reviewers @adamdmharvey
|
||||
/plugins/code-coverage @backstage/reviewers @alde @nissayeva
|
||||
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
|
||||
/plugins/cost-insights @backstage/silver-lining
|
||||
/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios
|
||||
/plugins/search @backstage/techdocs-core
|
||||
/plugins/search-* @backstage/techdocs-core
|
||||
/plugins/techdocs @backstage/techdocs-core
|
||||
/plugins/techdocs-backend @backstage/techdocs-core
|
||||
/plugins/ilert @backstage/reviewers @yacut
|
||||
/plugins/home @backstage/techdocs-core
|
||||
/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/jenkins @backstage/reviewers @timja
|
||||
/plugins/jenkins-backend @backstage/reviewers @timja
|
||||
/plugins/kafka @backstage/reviewers @nirga
|
||||
/plugins/kafka-backend @backstage/reviewers @nirga
|
||||
/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka
|
||||
/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski
|
||||
/plugins/git-release-manager @backstage/reviewers @erikengervall
|
||||
/tech-insights-backend @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b
|
||||
/packages/search-common @backstage/techdocs-core
|
||||
/packages/techdocs-cli @backstage/techdocs-core
|
||||
/packages/techdocs-cli-embedded-app @backstage/techdocs-core
|
||||
/packages/techdocs-common @backstage/techdocs-core
|
||||
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
|
||||
/.changeset/search-* @backstage/techdocs-core
|
||||
/.changeset/techdocs-* @backstage/techdocs-core
|
||||
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
|
||||
/docs/assets/search @backstage/techdocs-core
|
||||
/docs/features/search @backstage/techdocs-core
|
||||
/docs/features/techdocs @backstage/techdocs-core
|
||||
/packages/search-common @backstage/techdocs-core
|
||||
/packages/techdocs-cli @backstage/techdocs-core
|
||||
/packages/techdocs-cli-embedded-app @backstage/techdocs-core
|
||||
/packages/techdocs-common @backstage/techdocs-core
|
||||
/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps
|
||||
/plugins/apache-airflow @backstage/reviewers @cmpadden
|
||||
/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin
|
||||
/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/catalog-graph @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/circleci @backstage/reviewers @adamdmharvey
|
||||
/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios
|
||||
/plugins/code-coverage @backstage/reviewers @alde @nissayeva
|
||||
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
|
||||
/plugins/cost-insights @backstage/silver-lining
|
||||
/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/git-release-manager @backstage/reviewers @erikengervall
|
||||
/plugins/home @backstage/techdocs-core
|
||||
/plugins/ilert @backstage/reviewers @yacut
|
||||
/plugins/jenkins @backstage/reviewers @timja
|
||||
/plugins/jenkins-backend @backstage/reviewers @timja
|
||||
/plugins/kafka @backstage/reviewers @nirga
|
||||
/plugins/kafka-backend @backstage/reviewers @nirga
|
||||
/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7
|
||||
/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski
|
||||
/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka
|
||||
/plugins/search @backstage/techdocs-core
|
||||
/plugins/search-* @backstage/techdocs-core
|
||||
/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers
|
||||
/plugins/techdocs @backstage/techdocs-core
|
||||
/plugins/techdocs-backend @backstage/techdocs-core
|
||||
/tech-insights-backend @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b
|
||||
/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b
|
||||
|
||||
@@ -89,3 +89,4 @@
|
||||
| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. |
|
||||
| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
|
||||
| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
|
||||
| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. |
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
"concurrently": "^7.0.0",
|
||||
"eslint-plugin-notice": "^0.9.10",
|
||||
"fs-extra": "9.1.0",
|
||||
"husky": "^6.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"lerna": "^4.0.0",
|
||||
"lint-staged": "^12.2.0",
|
||||
"minimist": "^1.2.5",
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"@backstage/test-utils": "^0.2.4",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
"@types/concat-stream": "^2.0.0",
|
||||
"@types/fs-extra": "^9.0.3",
|
||||
"@types/http-errors": "^1.6.3",
|
||||
"@types/minimist": "^1.2.0",
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"yaml": "^1.10.0",
|
||||
"yml-loader": "^2.1.0",
|
||||
"yn": "^4.0.0"
|
||||
"yn": "^4.0.0",
|
||||
"zod": "^3.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.10.6",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { buildBundle } from '../../lib/bundler';
|
||||
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
|
||||
import { loadCliConfig } from '../../lib/config';
|
||||
import { paths } from '../../lib/paths';
|
||||
|
||||
interface BuildAppOptions {
|
||||
writeStats: boolean;
|
||||
configPaths: string[];
|
||||
}
|
||||
|
||||
export async function buildApp(options: BuildAppOptions) {
|
||||
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
|
||||
await buildBundle({
|
||||
entry: 'src/index',
|
||||
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
|
||||
statsJsonEnabled: options.writeStats,
|
||||
...(await loadCliConfig({
|
||||
args: options.configPaths,
|
||||
fromPackage: name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import tar, { CreateOptions } from 'tar';
|
||||
import { createDistWorkspace } from '../../lib/packager';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
|
||||
import { buildPackage, Output } from '../../lib/builder';
|
||||
|
||||
const BUNDLE_FILE = 'bundle.tar.gz';
|
||||
const SKELETON_FILE = 'skeleton.tar.gz';
|
||||
|
||||
interface BuildBackendOptions {
|
||||
skipBuildDependencies: boolean;
|
||||
}
|
||||
|
||||
export async function buildBackend(options: BuildBackendOptions) {
|
||||
const targetDir = paths.resolveTarget('dist');
|
||||
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
|
||||
|
||||
// We build the target package without generating type declarations.
|
||||
await buildPackage({ outputs: new Set([Output.cjs]) });
|
||||
|
||||
const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle'));
|
||||
try {
|
||||
await createDistWorkspace([pkg.name], {
|
||||
targetDir: tmpDir,
|
||||
buildDependencies: !options.skipBuildDependencies,
|
||||
buildExcludes: [pkg.name],
|
||||
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
|
||||
skeleton: SKELETON_FILE,
|
||||
});
|
||||
|
||||
// We built the target backend package using the regular build process, but the result of
|
||||
// that has now been packed into the dist workspace, so clean up the dist dir.
|
||||
await fs.remove(targetDir);
|
||||
await fs.mkdir(targetDir);
|
||||
|
||||
// Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice.
|
||||
await fs.move(
|
||||
resolvePath(tmpDir, SKELETON_FILE),
|
||||
resolvePath(targetDir, SKELETON_FILE),
|
||||
);
|
||||
|
||||
// Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache.
|
||||
await tar.create(
|
||||
{
|
||||
file: resolvePath(targetDir, BUNDLE_FILE),
|
||||
cwd: tmpDir,
|
||||
portable: true,
|
||||
noMtime: true,
|
||||
gzip: true,
|
||||
} as CreateOptions & { noMtime: boolean },
|
||||
[''],
|
||||
);
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { buildPackage, Output } from '../../lib/builder';
|
||||
import { findRoleFromCommand, getRoleInfo } from '../../lib/role';
|
||||
import { buildApp } from './buildApp';
|
||||
import { buildBackend } from './buildBackend';
|
||||
|
||||
export async function command(cmd: Command): Promise<void> {
|
||||
const role = await findRoleFromCommand(cmd);
|
||||
|
||||
if (role === 'app') {
|
||||
return buildApp({
|
||||
configPaths: cmd.config as string[],
|
||||
writeStats: Boolean(cmd.stats),
|
||||
});
|
||||
}
|
||||
if (role === 'backend') {
|
||||
return buildBackend({
|
||||
skipBuildDependencies: Boolean(cmd.skipBuildDependencies),
|
||||
});
|
||||
}
|
||||
|
||||
const roleInfo = getRoleInfo(role);
|
||||
|
||||
const outputs = new Set<Output>();
|
||||
|
||||
if (roleInfo.output.includes('cjs')) {
|
||||
outputs.add(Output.cjs);
|
||||
}
|
||||
if (roleInfo.output.includes('esm')) {
|
||||
outputs.add(Output.esm);
|
||||
}
|
||||
if (roleInfo.output.includes('types')) {
|
||||
outputs.add(Output.types);
|
||||
}
|
||||
|
||||
return buildPackage({
|
||||
outputs,
|
||||
minify: Boolean(cmd.minify),
|
||||
useApiExtractor: Boolean(cmd.experimentalTypeBuild),
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
* 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.
|
||||
@@ -13,10 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { todoPlugin } from './plugin';
|
||||
|
||||
describe('todo', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(todoPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
export { command } from './command';
|
||||
@@ -18,14 +18,112 @@ import { assertError } from '@backstage/errors';
|
||||
import { CommanderStatic } from 'commander';
|
||||
import { exitWithError } from '../lib/errors';
|
||||
|
||||
export function registerCommands(program: CommanderStatic) {
|
||||
const configOption = [
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml',
|
||||
(opt: string, opts: string[]) => [...opts, opt],
|
||||
Array<string>(),
|
||||
] as const;
|
||||
const configOption = [
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml',
|
||||
(opt: string, opts: string[]) => [...opts, opt],
|
||||
Array<string>(),
|
||||
] as const;
|
||||
|
||||
export function registerScriptCommand(program: CommanderStatic) {
|
||||
const command = program
|
||||
.command('script [command]', { hidden: true })
|
||||
.description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]');
|
||||
|
||||
command
|
||||
.command('start')
|
||||
.description('Start a package for local development')
|
||||
.option(...configOption)
|
||||
.option('--role <name>', 'Run the command with an explicit package role')
|
||||
.option('--check', 'Enable type checking and linting if available')
|
||||
.option('--inspect', 'Enable debugger in Node.js environments')
|
||||
.option(
|
||||
'--inspect-brk',
|
||||
'Enable debugger in Node.js environments, breaking before code starts',
|
||||
)
|
||||
.action(lazy(() => import('./start').then(m => m.command)));
|
||||
|
||||
command
|
||||
.command('build')
|
||||
.description('Build a package for production deployment or publishing')
|
||||
.option(
|
||||
'--minify',
|
||||
'Minify the generated code. Does not apply to app or backend packages.',
|
||||
)
|
||||
.option(
|
||||
'--experimental-type-build',
|
||||
'Enable experimental type build. Does not apply to app or backend packages.',
|
||||
)
|
||||
.option(
|
||||
'--skip-build-dependencies',
|
||||
'Skip the automatic building of local dependencies. Applies to backend packages only.',
|
||||
)
|
||||
.option(
|
||||
'--stats',
|
||||
'If bundle stats are available, write them to the output directory. Applies to app packages only.',
|
||||
)
|
||||
.option(
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml. Applies to app packages only.',
|
||||
(opt: string, opts: string[]) => [...opts, opt],
|
||||
Array<string>(),
|
||||
)
|
||||
.action(lazy(() => import('./build').then(m => m.command)));
|
||||
|
||||
program
|
||||
.command('lint')
|
||||
.option(
|
||||
'--format <format>',
|
||||
'Lint report output format',
|
||||
'eslint-formatter-friendly',
|
||||
)
|
||||
.option('--fix', 'Attempt to automatically fix violations')
|
||||
.description('Lint a package')
|
||||
.action(lazy(() => import('./lint').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('test')
|
||||
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
|
||||
.helpOption(', --backstage-cli-help') // Let Jest handle help
|
||||
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
|
||||
.action(lazy(() => import('./testCommand').then(m => m.default)));
|
||||
|
||||
command
|
||||
.command('clean')
|
||||
.description('Delete cache directories')
|
||||
.action(lazy(() => import('./clean/clean').then(m => m.default)));
|
||||
|
||||
command
|
||||
.command('prepack')
|
||||
.description('Prepares a package for packaging before publishing')
|
||||
.action(lazy(() => import('./pack').then(m => m.pre)));
|
||||
|
||||
command
|
||||
.command('postpack')
|
||||
.description('Restores the changes made by the prepack command')
|
||||
.action(lazy(() => import('./pack').then(m => m.post)));
|
||||
}
|
||||
|
||||
export function registerMigrateCommand(program: CommanderStatic) {
|
||||
const command = program
|
||||
.command('migrate [command]', { hidden: true })
|
||||
.description('Migration utilities [EXPERIMENTAL]');
|
||||
|
||||
command
|
||||
.command('package-role')
|
||||
.description(`Add package role field to packages that don't have it`)
|
||||
.action(lazy(() => import('./migrate/packageRole').then(m => m.default)));
|
||||
|
||||
command
|
||||
.command('package-scripts')
|
||||
.description('Set package scripts according to each package role')
|
||||
.action(
|
||||
lazy(() => import('./migrate/packageScripts').then(m => m.command)),
|
||||
);
|
||||
}
|
||||
|
||||
export function registerCommands(program: CommanderStatic) {
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('app:build')
|
||||
.description('Build an app for a production release')
|
||||
@@ -33,6 +131,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.option(...configOption)
|
||||
.action(lazy(() => import('./app/build').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('app:serve')
|
||||
.description('Serve an app for local development')
|
||||
@@ -40,6 +139,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.option(...configOption)
|
||||
.action(lazy(() => import('./app/serve').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('backend:build')
|
||||
.description('Build a backend plugin')
|
||||
@@ -47,6 +147,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.option('--experimental-type-build', 'Enable experimental type build')
|
||||
.action(lazy(() => import('./backend/build').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('backend:bundle')
|
||||
.description('Bundle the backend into a deployment archive')
|
||||
@@ -56,6 +157,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
)
|
||||
.action(lazy(() => import('./backend/bundle').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('backend:dev')
|
||||
.description('Start local development server with HMR for the backend')
|
||||
@@ -104,6 +206,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
|
||||
);
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('plugin:build')
|
||||
.description('Build a plugin')
|
||||
@@ -111,6 +214,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.option('--experimental-type-build', 'Enable experimental type build')
|
||||
.action(lazy(() => import('./plugin/build').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('plugin:serve')
|
||||
.description('Serves the dev/ folder of a plugin')
|
||||
@@ -125,14 +229,16 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.description('Diff an existing plugin with the creation template')
|
||||
.action(lazy(() => import('./plugin/diff').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('build')
|
||||
.description('Build a package for publishing')
|
||||
.option('--outputs <formats>', 'List of formats to output [types,cjs,esm]')
|
||||
.option('--minify', 'Minify the generated code')
|
||||
.option('--experimental-type-build', 'Enable experimental type build')
|
||||
.action(lazy(() => import('./build').then(m => m.default)));
|
||||
.action(lazy(() => import('./oldBuild').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('lint')
|
||||
.option(
|
||||
@@ -144,6 +250,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.description('Lint a package')
|
||||
.action(lazy(() => import('./lint').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('test')
|
||||
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
|
||||
@@ -205,6 +312,9 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.description('Print configuration schema')
|
||||
.action(lazy(() => import('./config/schema').then(m => m.default)));
|
||||
|
||||
registerScriptCommand(program);
|
||||
registerMigrateCommand(program);
|
||||
|
||||
program
|
||||
.command('versions:bump')
|
||||
.option(
|
||||
@@ -220,16 +330,19 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.description('Check Backstage package versioning')
|
||||
.action(lazy(() => import('./versions/lint').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('prepack')
|
||||
.description('Prepares a package for packaging before publishing')
|
||||
.action(lazy(() => import('./pack').then(m => m.pre)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('postpack')
|
||||
.description('Restores the changes made by the prepack command')
|
||||
.action(lazy(() => import('./pack').then(m => m.post)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of script variant
|
||||
program
|
||||
.command('clean')
|
||||
.description('Delete cache directories')
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role';
|
||||
|
||||
export default async () => {
|
||||
const { packages } = await getPackages(paths.targetDir);
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson: pkg }) => {
|
||||
const { name } = pkg;
|
||||
const existingRole = getRoleFromPackage(pkg);
|
||||
if (existingRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedRole = detectRoleFromPackage(pkg);
|
||||
if (!detectedRole) {
|
||||
console.error(`No role detected for package ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Detected package role of ${name} as ${detectedRole}`);
|
||||
|
||||
let newPkg = pkg as any;
|
||||
|
||||
const pkgKeys = Object.keys(pkg);
|
||||
if (pkgKeys.includes('backstage')) {
|
||||
newPkg.backstage = {
|
||||
...newPkg.backstage,
|
||||
role: detectedRole,
|
||||
};
|
||||
} else {
|
||||
// We insert the backstage field after one of these fields, otherwise at the end
|
||||
const index =
|
||||
Math.max(
|
||||
pkgKeys.indexOf('version'),
|
||||
pkgKeys.indexOf('private'),
|
||||
pkgKeys.indexOf('publishConfig'),
|
||||
) + 1 || pkgKeys.length;
|
||||
|
||||
const pkgEntries = Object.entries(pkg);
|
||||
pkgEntries.splice(index, 0, ['backstage', { role: detectedRole }]);
|
||||
newPkg = Object.fromEntries(pkgEntries);
|
||||
}
|
||||
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), newPkg, {
|
||||
spaces: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { PackageGraph } from '../../lib/monorepo';
|
||||
import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role';
|
||||
|
||||
const noStartRoles: PackageRole[] = ['cli', 'common-library'];
|
||||
|
||||
export async function command() {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson }) => {
|
||||
const role = getRoleFromPackage(packageJson);
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleInfo = getRoleInfo(role);
|
||||
const hasStart = !noStartRoles.includes(role);
|
||||
const isBundled = roleInfo.output.includes('bundle');
|
||||
|
||||
const expectedScripts = {
|
||||
...(hasStart && { start: 'backstage-cli script start' }),
|
||||
build: 'backstage-cli script build',
|
||||
lint: 'backstage-cli script lint',
|
||||
test: 'backstage-cli script test',
|
||||
clean: 'backstage-cli script clean',
|
||||
...(!isBundled && {
|
||||
postpack: 'backstage-cli script postpack',
|
||||
prepack: 'backstage-cli script prepack',
|
||||
}),
|
||||
};
|
||||
|
||||
let changed = false;
|
||||
const currentScripts: Record<string, string | undefined> =
|
||||
(packageJson.scripts = packageJson.scripts || {});
|
||||
|
||||
for (const [name, value] of Object.entries(expectedScripts)) {
|
||||
if (currentScripts[name] !== value) {
|
||||
changed = true;
|
||||
currentScripts[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
console.log(`Updating scripts for ${packageJson.name}`);
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -39,18 +39,13 @@ const fileHandlers = [
|
||||
patterns: ['package.json'],
|
||||
handler: handlers.packageJson,
|
||||
},
|
||||
{
|
||||
// Not all plugins have routes
|
||||
patterns: ['src/routes.ts'],
|
||||
handler: handlers.skip,
|
||||
},
|
||||
{
|
||||
// make sure files in 1st level of src/ and dev/ exist
|
||||
patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/],
|
||||
patterns: ['.eslintrc.js'],
|
||||
handler: handlers.exists,
|
||||
},
|
||||
{
|
||||
patterns: ['README.md', 'tsconfig.json', /^src\//],
|
||||
patterns: ['README.md', 'tsconfig.json', /^src\//, /^dev\//],
|
||||
handler: handlers.skip,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { startBackend } from './startBackend';
|
||||
import { startFrontend } from './startFrontend';
|
||||
import { findRoleFromCommand } from '../../lib/role';
|
||||
|
||||
export async function command(cmd: Command): Promise<void> {
|
||||
const role = await findRoleFromCommand(cmd);
|
||||
|
||||
const options = {
|
||||
configPaths: cmd.config as string[],
|
||||
checksEnabled: Boolean(cmd.check),
|
||||
inspectEnabled: Boolean(cmd.inspect),
|
||||
inspectBrkEnabled: Boolean(cmd.inspectBrk),
|
||||
};
|
||||
|
||||
switch (role) {
|
||||
case 'backend':
|
||||
case 'plugin-backend':
|
||||
case 'plugin-backend-module':
|
||||
case 'node-library':
|
||||
return startBackend(options);
|
||||
case 'app':
|
||||
return startFrontend({
|
||||
...options,
|
||||
entry: 'src/index',
|
||||
verifyVersions: true,
|
||||
});
|
||||
case 'web-library':
|
||||
case 'plugin-frontend':
|
||||
case 'plugin-frontend-module':
|
||||
return startFrontend({ entry: 'dev/index', ...options });
|
||||
default:
|
||||
throw new Error(
|
||||
`Start command is not supported for package role '${role}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { command } from './command';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { serveBackend } from '../../lib/bundler';
|
||||
|
||||
interface StartBackendOptions {
|
||||
checksEnabled: boolean;
|
||||
inspectEnabled: boolean;
|
||||
inspectBrkEnabled: boolean;
|
||||
}
|
||||
|
||||
export async function startBackend(options: StartBackendOptions) {
|
||||
// Cleaning dist/ before we start the dev process helps work around an issue
|
||||
// where we end up with the entrypoint executing multiple times, causing
|
||||
// a port bind conflict among other things.
|
||||
await fs.remove(paths.resolveTarget('dist'));
|
||||
|
||||
const waitForExit = await serveBackend({
|
||||
entry: 'src/index',
|
||||
checksEnabled: options.checksEnabled,
|
||||
inspectEnabled: options.inspectEnabled,
|
||||
inspectBrkEnabled: options.inspectBrkEnabled,
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import uniq from 'lodash/uniq';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { loadCliConfig } from '../../lib/config';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { Lockfile } from '../../lib/versioning';
|
||||
import { includedFilter } from '../versions/lint';
|
||||
|
||||
interface StartAppOptions {
|
||||
verifyVersions?: boolean;
|
||||
entry: string;
|
||||
|
||||
checksEnabled: boolean;
|
||||
configPaths: string[];
|
||||
}
|
||||
|
||||
export async function startFrontend(options: StartAppOptions) {
|
||||
if (options.verifyVersions) {
|
||||
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
|
||||
const result = lockfile.analyze({
|
||||
filter: includedFilter,
|
||||
});
|
||||
const problemPackages = [...result.newVersions, ...result.newRanges].map(
|
||||
({ name }) => name,
|
||||
);
|
||||
|
||||
if (problemPackages.length > 1) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Some of the following packages may be outdated or have duplicate installations:
|
||||
|
||||
${uniq(problemPackages).join(', ')}
|
||||
`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ This can be resolved using the following command:
|
||||
|
||||
yarn backstage-cli versions:check --fix
|
||||
`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
|
||||
const waitForExit = await serveBundle({
|
||||
entry: options.entry,
|
||||
checksEnabled: options.checksEnabled,
|
||||
...(await loadCliConfig({
|
||||
args: options.configPaths,
|
||||
fromPackage: name,
|
||||
withFilteredKeys: true,
|
||||
})),
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export const optimization = (
|
||||
minimizer: [
|
||||
new ESBuildMinifyPlugin({
|
||||
target: 'es2019',
|
||||
format: 'iife',
|
||||
}),
|
||||
],
|
||||
runtimeChunk: 'single',
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Package } from '@manypkg/get-packages';
|
||||
import { getPackages, Package } from '@manypkg/get-packages';
|
||||
import { paths } from '../paths';
|
||||
import { PackageRole } from '../role';
|
||||
|
||||
type PackageJSON = Package['packageJson'];
|
||||
|
||||
@@ -25,8 +27,17 @@ export interface ExtendedPackageJSON extends PackageJSON {
|
||||
// The `bundled` field is a field known within Backstage, it means
|
||||
// that the package bundles all of its dependencies in its build output.
|
||||
bundled?: boolean;
|
||||
|
||||
backstage?: {
|
||||
role?: PackageRole;
|
||||
};
|
||||
}
|
||||
|
||||
export type ExtendedPackage = {
|
||||
dir: string;
|
||||
packageJson: ExtendedPackageJSON;
|
||||
};
|
||||
|
||||
export type PackageGraphNode = {
|
||||
/** The name of the package */
|
||||
name: string;
|
||||
@@ -47,6 +58,11 @@ export type PackageGraphNode = {
|
||||
};
|
||||
|
||||
export class PackageGraph extends Map<string, PackageGraphNode> {
|
||||
static async listTargetPackages(): Promise<ExtendedPackage[]> {
|
||||
const { packages } = await getPackages(paths.targetDir);
|
||||
return packages as ExtendedPackage[];
|
||||
}
|
||||
|
||||
static fromPackages(packages: Package[]): PackageGraph {
|
||||
const graph = new PackageGraph();
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
PackageRoleInfo,
|
||||
PackagePlatform,
|
||||
PackageOutputType,
|
||||
PackageRole,
|
||||
} from './types';
|
||||
export {
|
||||
getRoleInfo,
|
||||
getRoleFromPackage,
|
||||
findRoleFromCommand,
|
||||
detectRoleFromPackage,
|
||||
} from './packageRoles';
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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 mockFs from 'mock-fs';
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
getRoleInfo,
|
||||
getRoleFromPackage,
|
||||
findRoleFromCommand,
|
||||
detectRoleFromPackage,
|
||||
} from './packageRoles';
|
||||
|
||||
describe('getRoleInfo', () => {
|
||||
it('provides role info by role', () => {
|
||||
expect(getRoleInfo('web-library')).toEqual({
|
||||
role: 'web-library',
|
||||
platform: 'web',
|
||||
output: ['types', 'esm'],
|
||||
});
|
||||
|
||||
expect(getRoleInfo('app')).toEqual({
|
||||
role: 'app',
|
||||
platform: 'web',
|
||||
output: ['bundle'],
|
||||
});
|
||||
|
||||
expect(() => getRoleInfo('invalid')).toThrow(
|
||||
`Unknown package role 'invalid'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoleFromPackage', () => {
|
||||
it('reads explicit package roles', () => {
|
||||
expect(
|
||||
getRoleFromPackage({
|
||||
backstage: {
|
||||
role: 'web-library',
|
||||
},
|
||||
}),
|
||||
).toEqual('web-library');
|
||||
|
||||
expect(
|
||||
getRoleFromPackage({
|
||||
backstage: {
|
||||
role: 'app',
|
||||
},
|
||||
}),
|
||||
).toEqual('app');
|
||||
|
||||
expect(() =>
|
||||
getRoleFromPackage({
|
||||
name: 'test',
|
||||
backstage: {},
|
||||
}),
|
||||
).toThrow('Package test must specify a role in the "backstage" field');
|
||||
|
||||
expect(() =>
|
||||
getRoleFromPackage({
|
||||
name: 'test',
|
||||
backstage: { role: 'invalid' },
|
||||
}),
|
||||
).toThrow(`Unknown package role 'invalid'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRoleFromCommand', () => {
|
||||
function mkCommand(args: string) {
|
||||
return new Command()
|
||||
.option('--role <role>', 'test role')
|
||||
.parse(['node', 'entry.js', ...args.split(' ')]) as Command;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test',
|
||||
backstage: {
|
||||
role: 'web-library',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('provides role info by role', async () => {
|
||||
await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual(
|
||||
'web-library',
|
||||
);
|
||||
|
||||
await expect(
|
||||
findRoleFromCommand(mkCommand('--role node-library')),
|
||||
).resolves.toEqual('node-library');
|
||||
|
||||
await expect(
|
||||
findRoleFromCommand(mkCommand('--role invalid')),
|
||||
).rejects.toThrow(`Unknown package role 'invalid'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectRoleFromPackage', () => {
|
||||
it('detects the role of example-app', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: 'example-app',
|
||||
private: true,
|
||||
bundled: true,
|
||||
scripts: {
|
||||
start: 'backstage-cli app:serve',
|
||||
build: 'backstage-cli app:build',
|
||||
clean: 'backstage-cli clean',
|
||||
test: 'backstage-cli test',
|
||||
'test:e2e':
|
||||
'start-server-and-test start http://localhost:3000 cy:dev',
|
||||
'test:e2e:ci':
|
||||
'start-server-and-test start http://localhost:3000 cy:run',
|
||||
lint: 'backstage-cli lint',
|
||||
'cy:dev': 'cypress open',
|
||||
'cy:run': 'cypress run',
|
||||
},
|
||||
}),
|
||||
).toEqual('app');
|
||||
});
|
||||
|
||||
it('detects the role of example-backend', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: 'example-backend',
|
||||
main: 'dist/index.cjs.js',
|
||||
types: 'src/index.ts',
|
||||
scripts: {
|
||||
build: 'backstage-cli backend:bundle',
|
||||
'build-image':
|
||||
'docker build ../.. -f Dockerfile --tag example-backend',
|
||||
start: 'backstage-cli backend:dev',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
clean: 'backstage-cli clean',
|
||||
'migrate:create': 'knex migrate:make -x ts',
|
||||
},
|
||||
}),
|
||||
).toEqual('backend');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-catalog', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-catalog',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.esm.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli plugin:build',
|
||||
start: 'backstage-cli plugin:serve',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
diff: 'backstage-cli plugin:diff',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('plugin-frontend');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-catalog-backend', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-catalog-backend',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.cjs.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
start: 'backstage-cli backend:dev',
|
||||
build: 'backstage-cli backend:build',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('plugin-backend');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-catalog-react', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-catalog-react',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.esm.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli build',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('web-library');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-catalog-common', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-catalog-common',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.cjs.js',
|
||||
module: 'dist/index.esm.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli build',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test --passWithNoTests',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('common-library');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-catalog-backend-module-ldap',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.cjs.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli backend:build',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('plugin-backend-module');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-permission-node', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-permission-node',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
homepage: 'https://backstage.io',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.cjs.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli backend:build',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('node-library');
|
||||
});
|
||||
|
||||
it('detects the role of @backstage/plugin-analytics-module-ga', () => {
|
||||
expect(
|
||||
detectRoleFromPackage({
|
||||
name: '@backstage/plugin-analytics-module-ga',
|
||||
main: 'src/index.ts',
|
||||
types: 'src/index.ts',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
main: 'dist/index.esm.js',
|
||||
types: 'dist/index.d.ts',
|
||||
},
|
||||
scripts: {
|
||||
build: 'backstage-cli plugin:build',
|
||||
start: 'backstage-cli plugin:serve',
|
||||
lint: 'backstage-cli lint',
|
||||
test: 'backstage-cli test',
|
||||
diff: 'backstage-cli plugin:diff',
|
||||
prepack: 'backstage-cli prepack',
|
||||
postpack: 'backstage-cli postpack',
|
||||
clean: 'backstage-cli clean',
|
||||
},
|
||||
}),
|
||||
).toEqual('plugin-frontend-module');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import fs from 'fs-extra';
|
||||
import { Command } from 'commander';
|
||||
import { paths } from '../paths';
|
||||
import { PackageRole, PackageRoleInfo } from './types';
|
||||
|
||||
const packageRoleInfos: PackageRoleInfo[] = [
|
||||
{
|
||||
role: 'app',
|
||||
platform: 'web',
|
||||
output: ['bundle'],
|
||||
},
|
||||
{
|
||||
role: 'backend',
|
||||
platform: 'node',
|
||||
output: ['bundle'],
|
||||
},
|
||||
{
|
||||
role: 'cli',
|
||||
platform: 'node',
|
||||
output: ['cjs'],
|
||||
},
|
||||
{
|
||||
role: 'web-library',
|
||||
platform: 'web',
|
||||
output: ['types', 'esm'],
|
||||
},
|
||||
{
|
||||
role: 'node-library',
|
||||
platform: 'node',
|
||||
output: ['types', 'cjs'],
|
||||
},
|
||||
{
|
||||
role: 'common-library',
|
||||
platform: 'common',
|
||||
output: ['types', 'esm', 'cjs'],
|
||||
},
|
||||
{
|
||||
role: 'plugin-frontend',
|
||||
platform: 'web',
|
||||
output: ['types', 'esm'],
|
||||
},
|
||||
{
|
||||
role: 'plugin-frontend-module',
|
||||
platform: 'web',
|
||||
output: ['types', 'esm'],
|
||||
},
|
||||
{
|
||||
role: 'plugin-backend',
|
||||
platform: 'node',
|
||||
output: ['types', 'cjs'],
|
||||
},
|
||||
{
|
||||
role: 'plugin-backend-module',
|
||||
platform: 'node',
|
||||
output: ['types', 'cjs'],
|
||||
},
|
||||
];
|
||||
|
||||
export function getRoleInfo(role: string): PackageRoleInfo {
|
||||
const roleInfo = packageRoleInfos.find(r => r.role === role);
|
||||
if (!roleInfo) {
|
||||
throw new Error(`Unknown package role '${role}'`);
|
||||
}
|
||||
return roleInfo;
|
||||
}
|
||||
|
||||
const readSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
backstage: z
|
||||
.object({
|
||||
role: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
|
||||
const pkg = readSchema.parse(pkgJson);
|
||||
|
||||
// If there's an explicit role, use that.
|
||||
if (pkg.backstage) {
|
||||
const { role } = pkg.backstage;
|
||||
if (!role) {
|
||||
throw new Error(
|
||||
`Package ${pkg.name} must specify a role in the "backstage" field`,
|
||||
);
|
||||
}
|
||||
|
||||
return getRoleInfo(role).role;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function findRoleFromCommand(cmd: Command): Promise<PackageRole> {
|
||||
if (cmd.role) {
|
||||
return getRoleInfo(cmd.role)?.role;
|
||||
}
|
||||
|
||||
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
|
||||
const info = getRoleFromPackage(pkg);
|
||||
if (!info) {
|
||||
throw new Error(`Target package must have 'backstage.role' set`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
const detectionSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
scripts: z
|
||||
.object({
|
||||
start: z.string().optional(),
|
||||
build: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
publishConfig: z
|
||||
.object({
|
||||
main: z.string().optional(),
|
||||
types: z.string().optional(),
|
||||
module: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
main: z.string().optional(),
|
||||
types: z.string().optional(),
|
||||
module: z.string().optional(),
|
||||
});
|
||||
|
||||
export function detectRoleFromPackage(
|
||||
pkgJson: unknown,
|
||||
): PackageRole | undefined {
|
||||
const pkg = detectionSchema.parse(pkgJson);
|
||||
|
||||
if (pkg.scripts?.start?.includes('app:serve')) {
|
||||
return 'app';
|
||||
}
|
||||
if (pkg.scripts?.build?.includes('backend:bundle')) {
|
||||
return 'backend';
|
||||
}
|
||||
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) {
|
||||
return 'plugin-backend-module';
|
||||
}
|
||||
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {
|
||||
return 'plugin-frontend-module';
|
||||
}
|
||||
if (pkg.scripts?.start?.includes('plugin:serve')) {
|
||||
return 'plugin-frontend';
|
||||
}
|
||||
if (pkg.scripts?.start?.includes('backend:dev')) {
|
||||
return 'plugin-backend';
|
||||
}
|
||||
|
||||
const mainEntry = pkg.publishConfig?.main || pkg.main;
|
||||
const moduleEntry = pkg.publishConfig?.module || pkg.module;
|
||||
const typesEntry = pkg.publishConfig?.types || pkg.types;
|
||||
if (typesEntry) {
|
||||
if (mainEntry && moduleEntry) {
|
||||
return 'common-library';
|
||||
}
|
||||
if (moduleEntry || mainEntry?.endsWith('.esm.js')) {
|
||||
return 'web-library';
|
||||
}
|
||||
if (mainEntry) {
|
||||
return 'node-library';
|
||||
}
|
||||
} else if (mainEntry) {
|
||||
return 'cli';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type PackageRole =
|
||||
| 'app'
|
||||
| 'backend'
|
||||
| 'cli'
|
||||
| 'web-library'
|
||||
| 'node-library'
|
||||
| 'common-library'
|
||||
| 'plugin-frontend'
|
||||
| 'plugin-frontend-module'
|
||||
| 'plugin-backend'
|
||||
| 'plugin-backend-module';
|
||||
|
||||
export type PackagePlatform = 'node' | 'web' | 'common';
|
||||
export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs';
|
||||
|
||||
export interface PackageRoleInfo {
|
||||
role: PackageRole;
|
||||
platform: PackagePlatform;
|
||||
output: PackageOutputType[];
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { useApiHolder, configApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export type SupportItemLink = {
|
||||
url: string;
|
||||
@@ -50,8 +50,9 @@ const DEFAULT_SUPPORT_CONFIG: SupportConfig = {
|
||||
};
|
||||
|
||||
export function useSupportConfig(): SupportConfig {
|
||||
const config = useApi(configApiRef);
|
||||
const supportConfig = config.getOptionalConfig('app.support');
|
||||
const apiHolder = useApiHolder();
|
||||
const config = apiHolder.get(configApiRef);
|
||||
const supportConfig = config?.getOptionalConfig('app.support');
|
||||
|
||||
if (!supportConfig) {
|
||||
return DEFAULT_SUPPORT_CONFIG;
|
||||
|
||||
@@ -33,6 +33,15 @@ export function defaultUserTransformer(
|
||||
userPhoto?: string,
|
||||
): Promise<UserEntity | undefined>;
|
||||
|
||||
// @public
|
||||
export type GroupMember =
|
||||
| (MicrosoftGraph.Group & {
|
||||
'@odata.type': '#microsoft.graph.user';
|
||||
})
|
||||
| (MicrosoftGraph.User & {
|
||||
'@odata.type': '#microsoft.graph.group';
|
||||
});
|
||||
|
||||
// @public
|
||||
export type GroupTransformer = (
|
||||
group: MicrosoftGraph.Group,
|
||||
@@ -54,7 +63,6 @@ export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id';
|
||||
export class MicrosoftGraphClient {
|
||||
constructor(baseUrl: string, pca: msal.ConfidentialClientApplication);
|
||||
static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient;
|
||||
// Warning: (ae-forgotten-export) The symbol "GroupMember" needs to be exported by the entry point index.d.ts
|
||||
getGroupMembers(groupId: string): AsyncIterable<GroupMember>;
|
||||
// (undocumented)
|
||||
getGroupPhoto(groupId: string, sizeId?: string): Promise<string | undefined>;
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { MicrosoftGraphClient } from './client';
|
||||
export type { ODataQuery } from './client';
|
||||
export type { GroupMember, ODataQuery } from './client';
|
||||
export { readMicrosoftGraphConfig } from './config';
|
||||
export type { MicrosoftGraphProviderConfig } from './config';
|
||||
export {
|
||||
|
||||
@@ -95,8 +95,6 @@ export const EntityCatalogGraphCard: ({
|
||||
// @public
|
||||
export type EntityEdge = DependencyGraphTypes.DependencyEdge<EntityEdgeData>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type EntityEdgeData = {
|
||||
relations: string[];
|
||||
@@ -106,8 +104,6 @@ export type EntityEdgeData = {
|
||||
// @public
|
||||
export type EntityNode = DependencyGraphTypes.DependencyNode<EntityNodeData>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type EntityNodeData = {
|
||||
name: string;
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DependencyGraphTypes } from '@backstage/core-components';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
/**
|
||||
* Additional Data for entities
|
||||
* Additional Data for entities.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityEdgeData = {
|
||||
/**
|
||||
@@ -40,6 +43,8 @@ export type EntityEdge = DependencyGraphTypes.DependencyEdge<EntityEdgeData>;
|
||||
|
||||
/**
|
||||
* Additional data for Entity Node
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityNodeData = {
|
||||
/**
|
||||
|
||||
@@ -26,9 +26,7 @@ import { UnpackNestedValue } from 'react-hook-form';
|
||||
import { UseFormProps } from 'react-hook-form';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AnalyzeResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type AnalyzeResult =
|
||||
| {
|
||||
type: 'locations';
|
||||
@@ -45,26 +43,36 @@ export type AnalyzeResult =
|
||||
generatedEntities: PartialEntity[];
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "AutocompleteTextField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const AutocompleteTextField: <TFieldValue extends string>({
|
||||
name,
|
||||
options,
|
||||
required,
|
||||
errors,
|
||||
rules,
|
||||
loading,
|
||||
loadingText,
|
||||
helperText,
|
||||
errorHelperText,
|
||||
textFieldProps,
|
||||
}: Props_5<TFieldValue>) => JSX.Element;
|
||||
// @public
|
||||
export const AutocompleteTextField: <TFieldValue extends string>(
|
||||
props: AutocompleteTextFieldProps<TFieldValue>,
|
||||
) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CatalogImportApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface AutocompleteTextFieldProps<TFieldValue extends string> {
|
||||
// (undocumented)
|
||||
errorHelperText?: string;
|
||||
// (undocumented)
|
||||
errors?: FieldErrors;
|
||||
// (undocumented)
|
||||
helperText?: React_2.ReactNode;
|
||||
// (undocumented)
|
||||
loading?: boolean;
|
||||
// (undocumented)
|
||||
loadingText?: string;
|
||||
// (undocumented)
|
||||
name: TFieldValue;
|
||||
// (undocumented)
|
||||
options: string[];
|
||||
// (undocumented)
|
||||
required?: boolean;
|
||||
// (undocumented)
|
||||
rules?: React_2.ComponentProps<typeof Controller>['rules'];
|
||||
// (undocumented)
|
||||
textFieldProps?: Omit<TextFieldProps, 'required' | 'fullWidth'>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface CatalogImportApi {
|
||||
// (undocumented)
|
||||
analyzeUrl(url: string): Promise<AnalyzeResult>;
|
||||
@@ -85,14 +93,10 @@ export interface CatalogImportApi {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "catalogImportApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const catalogImportApiRef: ApiRef<CatalogImportApi>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CatalogImportClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class CatalogImportClient implements CatalogImportApi {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
@@ -110,12 +114,7 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
body: string;
|
||||
}>;
|
||||
// (undocumented)
|
||||
submitPullRequest({
|
||||
repositoryUrl,
|
||||
fileContent,
|
||||
title,
|
||||
body,
|
||||
}: {
|
||||
submitPullRequest(options: {
|
||||
repositoryUrl: string;
|
||||
fileContent: string;
|
||||
title: string;
|
||||
@@ -126,14 +125,10 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CatalogImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const CatalogImportPage: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "catalogImportPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
const catalogImportPlugin: BackstagePlugin<
|
||||
{
|
||||
importPage: RouteRef<undefined>;
|
||||
@@ -143,9 +138,7 @@ const catalogImportPlugin: BackstagePlugin<
|
||||
export { catalogImportPlugin };
|
||||
export { catalogImportPlugin as plugin };
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ImportFlows" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "StepperProvider" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "defaultGenerateStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function defaultGenerateStepper(
|
||||
@@ -153,98 +146,221 @@ export function defaultGenerateStepper(
|
||||
defaults: StepperProvider,
|
||||
): StepperProvider;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DefaultImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const DefaultImportPage: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "EntityListComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityListComponent: ({
|
||||
locations,
|
||||
collapsed,
|
||||
locationListItemIcon,
|
||||
onItemClick,
|
||||
firstListItem,
|
||||
withLinks,
|
||||
}: Props) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ImportInfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const ImportInfoCard: ({
|
||||
exampleLocationUrl,
|
||||
exampleRepositoryUrl,
|
||||
}: Props_2) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ImportStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const ImportStepper: ({
|
||||
initialUrl,
|
||||
generateStepper,
|
||||
variant,
|
||||
}: Props_3) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "PreparePullRequestForm" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const PreparePullRequestForm: <
|
||||
export const EntityListComponent: (
|
||||
props: EntityListComponentProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface EntityListComponentProps {
|
||||
// (undocumented)
|
||||
collapsed?: boolean;
|
||||
// (undocumented)
|
||||
firstListItem?: React_2.ReactElement;
|
||||
// (undocumented)
|
||||
locationListItemIcon: (target: string) => React_2.ReactElement;
|
||||
// (undocumented)
|
||||
locations: Array<{
|
||||
target: string;
|
||||
entities: (Entity | EntityName)[];
|
||||
}>;
|
||||
// (undocumented)
|
||||
onItemClick?: (target: string) => void;
|
||||
// (undocumented)
|
||||
withLinks?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ImportFlows =
|
||||
| 'unknown'
|
||||
| 'single-location'
|
||||
| 'multiple-locations'
|
||||
| 'no-location';
|
||||
|
||||
// @public
|
||||
export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface ImportInfoCardProps {
|
||||
// (undocumented)
|
||||
exampleLocationUrl?: string;
|
||||
// (undocumented)
|
||||
exampleRepositoryUrl?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ImportState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ImportState = State & {
|
||||
activeFlow: ImportFlows;
|
||||
activeStepNumber: number;
|
||||
analysisUrl?: string;
|
||||
onGoBack?: () => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const ImportStepper: (props: ImportStepperProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface ImportStepperProps {
|
||||
// (undocumented)
|
||||
generateStepper?: (
|
||||
flow: ImportFlows,
|
||||
defaults: StepperProvider,
|
||||
) => StepperProvider;
|
||||
// (undocumented)
|
||||
initialUrl?: string;
|
||||
// (undocumented)
|
||||
variant?: InfoCardVariants;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const PreparePullRequestForm: <TFieldValues extends Record<string, any>>(
|
||||
props: PreparePullRequestFormProps<TFieldValues>,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type PreparePullRequestFormProps<
|
||||
TFieldValues extends Record<string, any>,
|
||||
>({
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
render,
|
||||
}: Props_6<TFieldValues>) => JSX.Element;
|
||||
> = Pick<UseFormProps<TFieldValues>, 'defaultValues'> & {
|
||||
onSubmit: SubmitHandler<TFieldValues>;
|
||||
render: (
|
||||
props: Pick<
|
||||
UseFormReturn<TFieldValues>,
|
||||
'formState' | 'register' | 'control' | 'setValue'
|
||||
> & {
|
||||
values: UnpackNestedValue<TFieldValues>;
|
||||
},
|
||||
) => React_2.ReactNode;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "PreviewCatalogInfoComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const PreviewCatalogInfoComponent: ({
|
||||
repositoryUrl,
|
||||
entities,
|
||||
classes,
|
||||
}: Props_7) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "PreviewPullRequestComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const PreviewPullRequestComponent: ({
|
||||
title,
|
||||
description,
|
||||
classes,
|
||||
}: Props_8) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "StepInitAnalyzeUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const StepInitAnalyzeUrl: ({
|
||||
onAnalysis,
|
||||
analysisUrl,
|
||||
disablePullRequest,
|
||||
exampleLocationUrl,
|
||||
}: Props_4) => JSX.Element;
|
||||
export type PrepareResult =
|
||||
| {
|
||||
type: 'locations';
|
||||
locations: Array<{
|
||||
exists?: boolean;
|
||||
target: string;
|
||||
entities: EntityName[];
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
type: 'repository';
|
||||
url: string;
|
||||
integrationType: string;
|
||||
pullRequest: {
|
||||
url: string;
|
||||
};
|
||||
locations: Array<{
|
||||
target: string;
|
||||
entities: EntityName[];
|
||||
}>;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "StepPrepareCreatePullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const StepPrepareCreatePullRequest: ({
|
||||
analyzeResult,
|
||||
onPrepare,
|
||||
onGoBack,
|
||||
renderFormFields,
|
||||
}: Props_9) => JSX.Element;
|
||||
// @public
|
||||
export const PreviewCatalogInfoComponent: (
|
||||
props: PreviewCatalogInfoComponentProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface PreviewCatalogInfoComponentProps {
|
||||
// (undocumented)
|
||||
classes?: {
|
||||
card?: string;
|
||||
cardContent?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
entities: Entity[];
|
||||
// (undocumented)
|
||||
repositoryUrl: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const PreviewPullRequestComponent: (
|
||||
props: PreviewPullRequestComponentProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface PreviewPullRequestComponentProps {
|
||||
// (undocumented)
|
||||
classes?: {
|
||||
card?: string;
|
||||
cardContent?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
description: string;
|
||||
// (undocumented)
|
||||
title: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const StepInitAnalyzeUrl: (
|
||||
props: StepInitAnalyzeUrlProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface StepInitAnalyzeUrlProps {
|
||||
// (undocumented)
|
||||
analysisUrl?: string;
|
||||
// (undocumented)
|
||||
disablePullRequest?: boolean;
|
||||
// (undocumented)
|
||||
exampleLocationUrl?: string;
|
||||
// (undocumented)
|
||||
onAnalysis: (
|
||||
flow: ImportFlows,
|
||||
url: string,
|
||||
result: AnalyzeResult,
|
||||
opts?: {
|
||||
prepareResult?: PrepareResult;
|
||||
},
|
||||
) => void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const StepPrepareCreatePullRequest: (
|
||||
props: StepPrepareCreatePullRequestProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export interface StepPrepareCreatePullRequestProps {
|
||||
// (undocumented)
|
||||
analyzeResult: Extract<
|
||||
AnalyzeResult,
|
||||
{
|
||||
type: 'repository';
|
||||
}
|
||||
>;
|
||||
// (undocumented)
|
||||
onGoBack?: () => void;
|
||||
// (undocumented)
|
||||
onPrepare: (
|
||||
result: PrepareResult,
|
||||
opts?: {
|
||||
notRepeatable?: boolean;
|
||||
},
|
||||
) => void;
|
||||
// Warning: (ae-forgotten-export) The symbol "FormData" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
renderFormFields: (
|
||||
props: Pick<
|
||||
UseFormReturn<FormData_2>,
|
||||
'register' | 'setValue' | 'formState'
|
||||
> & {
|
||||
values: UnpackNestedValue<FormData_2>;
|
||||
groups: string[];
|
||||
groupsLoading: boolean;
|
||||
},
|
||||
) => React_2.ReactNode;
|
||||
}
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/api/CatalogImportApi.d.ts:15:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts
|
||||
// src/api/CatalogImportApi.d.ts:25:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts
|
||||
```
|
||||
|
||||
@@ -18,11 +18,20 @@ import { EntityName } from '@backstage/catalog-model';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { PartialEntity } from '../types';
|
||||
|
||||
/**
|
||||
* Utility API reference for the {@link CatalogImportApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const catalogImportApiRef = createApiRef<CatalogImportApi>({
|
||||
id: 'plugin.catalog-import.service',
|
||||
});
|
||||
|
||||
// result of the analyze state
|
||||
/**
|
||||
* Result of the analysis.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AnalyzeResult =
|
||||
| {
|
||||
type: 'locations';
|
||||
@@ -39,6 +48,11 @@ export type AnalyzeResult =
|
||||
generatedEntities: PartialEntity[];
|
||||
};
|
||||
|
||||
/**
|
||||
* API for driving catalog imports.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CatalogImportApi {
|
||||
analyzeUrl(url: string): Promise<AnalyzeResult>;
|
||||
|
||||
@@ -46,6 +60,7 @@ export interface CatalogImportApi {
|
||||
title: string;
|
||||
body: string;
|
||||
}>;
|
||||
|
||||
submitPullRequest(options: {
|
||||
repositoryUrl: string;
|
||||
fileContent: string;
|
||||
|
||||
@@ -34,6 +34,11 @@ import { getGithubIntegrationConfig } from './GitHub';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { getBranchName, getCatalogFilename } from '../components/helpers';
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link CatalogImportApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class CatalogImportClient implements CatalogImportApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
@@ -142,17 +147,14 @@ the component will become available.\n\nFor more information, read an \
|
||||
};
|
||||
}
|
||||
|
||||
async submitPullRequest({
|
||||
repositoryUrl,
|
||||
fileContent,
|
||||
title,
|
||||
body,
|
||||
}: {
|
||||
async submitPullRequest(options: {
|
||||
repositoryUrl: string;
|
||||
fileContent: string;
|
||||
title: string;
|
||||
body: string;
|
||||
}): Promise<{ link: string; location: string }> {
|
||||
const { repositoryUrl, fileContent, title, body } = options;
|
||||
|
||||
const ghConfig = getGithubIntegrationConfig(
|
||||
this.scmIntegrationsApi,
|
||||
repositoryUrl,
|
||||
@@ -172,9 +174,7 @@ the component will become available.\n\nFor more information, read an \
|
||||
}
|
||||
|
||||
// TODO: this could be part of the catalog api
|
||||
private async generateEntityDefinitions({
|
||||
repo,
|
||||
}: {
|
||||
private async generateEntityDefinitions(options: {
|
||||
repo: string;
|
||||
}): Promise<PartialEntity[]> {
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
@@ -187,7 +187,7 @@ the component will become available.\n\nFor more information, read an \
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
location: { type: 'url', target: repo },
|
||||
location: { type: 'url', target: options.repo },
|
||||
}),
|
||||
},
|
||||
).catch(e => {
|
||||
@@ -204,12 +204,7 @@ the component will become available.\n\nFor more information, read an \
|
||||
}
|
||||
|
||||
// TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm
|
||||
private async checkGitHubForExistingCatalogInfo({
|
||||
url,
|
||||
owner,
|
||||
repo,
|
||||
githubIntegrationConfig,
|
||||
}: {
|
||||
private async checkGitHubForExistingCatalogInfo(options: {
|
||||
url: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
@@ -220,6 +215,8 @@ the component will become available.\n\nFor more information, read an \
|
||||
entities: EntityName[];
|
||||
}>
|
||||
> {
|
||||
const { url, owner, repo, githubIntegrationConfig } = options;
|
||||
|
||||
const { token } = await this.scmAuthApi.getCredentials({ url });
|
||||
const octo = new Octokit({
|
||||
auth: token,
|
||||
@@ -269,15 +266,7 @@ the component will become available.\n\nFor more information, read an \
|
||||
}
|
||||
|
||||
// TODO: extract this function and implement for non-github
|
||||
private async submitGitHubPrToRepo({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
fileContent,
|
||||
repositoryUrl,
|
||||
githubIntegrationConfig,
|
||||
}: {
|
||||
private async submitGitHubPrToRepo(options: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
title: string;
|
||||
@@ -286,6 +275,16 @@ the component will become available.\n\nFor more information, read an \
|
||||
repositoryUrl: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ link: string; location: string }> {
|
||||
const {
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
fileContent,
|
||||
repositoryUrl,
|
||||
githubIntegrationConfig,
|
||||
} = options;
|
||||
|
||||
const { token } = await this.scmAuthApi.getCredentials({
|
||||
url: repositoryUrl,
|
||||
additionalScope: {
|
||||
|
||||
@@ -27,6 +27,11 @@ import React from 'react';
|
||||
import { ImportInfoCard } from '../ImportInfoCard';
|
||||
import { ImportStepper } from '../ImportStepper';
|
||||
|
||||
/**
|
||||
* The default catalog import page.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const DefaultImportPage = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const appTitle = configApi.getOptional('app.title') || 'Backstage';
|
||||
|
||||
@@ -47,23 +47,35 @@ function sortEntities(entities: Array<EntityName | Entity>) {
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link EntityListComponent}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EntityListComponentProps {
|
||||
locations: Array<{ target: string; entities: (Entity | EntityName)[] }>;
|
||||
locationListItemIcon: (target: string) => React.ReactElement;
|
||||
collapsed?: boolean;
|
||||
firstListItem?: React.ReactElement;
|
||||
onItemClick?: (target: string) => void;
|
||||
withLinks?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a result list of entities.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityListComponent = (props: EntityListComponentProps) => {
|
||||
const {
|
||||
locations,
|
||||
collapsed = false,
|
||||
locationListItemIcon,
|
||||
onItemClick,
|
||||
firstListItem,
|
||||
withLinks = false,
|
||||
} = props;
|
||||
|
||||
export const EntityListComponent = ({
|
||||
locations,
|
||||
collapsed = false,
|
||||
locationListItemIcon,
|
||||
onItemClick,
|
||||
firstListItem,
|
||||
withLinks = false,
|
||||
}: Props) => {
|
||||
const app = useApp();
|
||||
const classes = useStyles();
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { EntityListComponent } from './EntityListComponent';
|
||||
export type { EntityListComponentProps } from './EntityListComponent';
|
||||
|
||||
@@ -21,15 +21,27 @@ import React from 'react';
|
||||
import { catalogImportApiRef } from '../../api';
|
||||
import { useCatalogFilename } from '../../hooks';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link ImportInfoCard}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ImportInfoCardProps {
|
||||
exampleLocationUrl?: string;
|
||||
exampleRepositoryUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows information about the import process.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ImportInfoCard = (props: ImportInfoCardProps) => {
|
||||
const {
|
||||
exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
exampleRepositoryUrl = 'https://github.com/backstage/backstage',
|
||||
} = props;
|
||||
|
||||
export const ImportInfoCard = ({
|
||||
exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
exampleRepositoryUrl = 'https://github.com/backstage/backstage',
|
||||
}: Props) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const appTitle = configApi.getOptional('app.title') || 'Backstage';
|
||||
const catalogImportApi = useApi(catalogImportApiRef);
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { ImportInfoCard } from './ImportInfoCard';
|
||||
export type { ImportInfoCardProps } from './ImportInfoCard';
|
||||
|
||||
@@ -18,6 +18,11 @@ import React from 'react';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { DefaultImportPage } from '../DefaultImportPage';
|
||||
|
||||
/**
|
||||
* The whole catalog import page.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ImportPage = () => {
|
||||
const outlet = useOutlet();
|
||||
|
||||
|
||||
@@ -34,20 +34,32 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link ImportStepper}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ImportStepperProps {
|
||||
initialUrl?: string;
|
||||
generateStepper?: (
|
||||
flow: ImportFlows,
|
||||
defaults: StepperProvider,
|
||||
) => StepperProvider;
|
||||
variant?: InfoCardVariants;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The stepper that holds the different import stages.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ImportStepper = (props: ImportStepperProps) => {
|
||||
const {
|
||||
initialUrl,
|
||||
generateStepper = defaultGenerateStepper,
|
||||
variant,
|
||||
} = props;
|
||||
|
||||
export const ImportStepper = ({
|
||||
initialUrl,
|
||||
generateStepper = defaultGenerateStepper,
|
||||
variant,
|
||||
}: Props) => {
|
||||
const catalogImportApi = useApi(catalogImportApiRef);
|
||||
const classes = useStyles();
|
||||
const state = useImportState({ initialUrl });
|
||||
|
||||
@@ -42,7 +42,12 @@ export type StepConfiguration = {
|
||||
content: React.ReactElement;
|
||||
};
|
||||
|
||||
export type StepperProvider = {
|
||||
/**
|
||||
* Defines the details of the stepper.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface StepperProvider {
|
||||
analyze: (
|
||||
s: Extract<ImportState, { activeState: 'analyze' }>,
|
||||
opts: { apis: StepperApis },
|
||||
@@ -59,7 +64,7 @@ export type StepperProvider = {
|
||||
s: Extract<ImportState, { activeState: 'finish' }>,
|
||||
opts: { apis: StepperApis },
|
||||
) => StepConfiguration;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The default stepper generation function.
|
||||
@@ -69,6 +74,7 @@ export type StepperProvider = {
|
||||
*
|
||||
* @param flow - the name of the active flow
|
||||
* @param defaults - the default steps
|
||||
* @public
|
||||
*/
|
||||
export function defaultGenerateStepper(
|
||||
flow: ImportFlows,
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export { ImportStepper } from './ImportStepper';
|
||||
export type { ImportStepperProps } from './ImportStepper';
|
||||
export { defaultGenerateStepper } from './defaults';
|
||||
|
||||
@@ -27,7 +27,12 @@ type FormData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link StepInitAnalyzeUrl}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface StepInitAnalyzeUrlProps {
|
||||
onAnalysis: (
|
||||
flow: ImportFlows,
|
||||
url: string,
|
||||
@@ -37,7 +42,7 @@ type Props = {
|
||||
disablePullRequest?: boolean;
|
||||
analysisUrl?: string;
|
||||
exampleLocationUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A form that lets the user input a url and analyze it for existing locations or potential entities.
|
||||
@@ -45,13 +50,16 @@ type Props = {
|
||||
* @param onAnalysis - is called when the analysis was successful
|
||||
* @param analysisUrl - a url that can be used as a default value
|
||||
* @param disablePullRequest - if true, repositories without entities will abort the wizard
|
||||
* @public
|
||||
*/
|
||||
export const StepInitAnalyzeUrl = ({
|
||||
onAnalysis,
|
||||
analysisUrl = '',
|
||||
disablePullRequest = false,
|
||||
exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
}: Props) => {
|
||||
export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => {
|
||||
const {
|
||||
onAnalysis,
|
||||
analysisUrl = '',
|
||||
disablePullRequest = false,
|
||||
exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
} = props;
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const catalogImportApi = useApi(catalogImportApiRef);
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl';
|
||||
export type { StepInitAnalyzeUrlProps } from './StepInitAnalyzeUrl';
|
||||
|
||||
+28
-14
@@ -20,7 +20,12 @@ import { Autocomplete } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { Controller, FieldErrors } from 'react-hook-form';
|
||||
|
||||
type Props<TFieldValue extends string> = {
|
||||
/**
|
||||
* Props for {@link AutocompleteTextField}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AutocompleteTextFieldProps<TFieldValue extends string> {
|
||||
name: TFieldValue;
|
||||
options: string[];
|
||||
required?: boolean;
|
||||
@@ -35,20 +40,29 @@ type Props<TFieldValue extends string> = {
|
||||
errorHelperText?: string;
|
||||
|
||||
textFieldProps?: Omit<TextFieldProps, 'required' | 'fullWidth'>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An autocompletion text field for the catalog import flows.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const AutocompleteTextField = <TFieldValue extends string>(
|
||||
props: AutocompleteTextFieldProps<TFieldValue>,
|
||||
) => {
|
||||
const {
|
||||
name,
|
||||
options,
|
||||
required,
|
||||
errors,
|
||||
rules,
|
||||
loading = false,
|
||||
loadingText,
|
||||
helperText,
|
||||
errorHelperText,
|
||||
textFieldProps = {},
|
||||
} = props;
|
||||
|
||||
export const AutocompleteTextField = <TFieldValue extends string>({
|
||||
name,
|
||||
options,
|
||||
required,
|
||||
errors,
|
||||
rules,
|
||||
loading = false,
|
||||
loadingText,
|
||||
helperText,
|
||||
errorHelperText,
|
||||
textFieldProps = {},
|
||||
}: Props<TFieldValue>) => {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
|
||||
+14
-9
@@ -24,10 +24,14 @@ import {
|
||||
UseFormReturn,
|
||||
} from 'react-hook-form';
|
||||
|
||||
type Props<TFieldValues extends Record<string, any>> = Pick<
|
||||
UseFormProps<TFieldValues>,
|
||||
'defaultValues'
|
||||
> & {
|
||||
/**
|
||||
* Props for {@link PreparePullRequestForm}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PreparePullRequestFormProps<
|
||||
TFieldValues extends Record<string, any>,
|
||||
> = Pick<UseFormProps<TFieldValues>, 'defaultValues'> & {
|
||||
onSubmit: SubmitHandler<TFieldValues>;
|
||||
|
||||
render: (
|
||||
@@ -48,14 +52,15 @@ type Props<TFieldValues extends Record<string, any>> = Pick<
|
||||
* @param onSubmit - a callback that is executed when the form is submitted
|
||||
* (initiated by a button of type="submit")
|
||||
* @param render - render the form elements
|
||||
* @public
|
||||
*/
|
||||
export const PreparePullRequestForm = <
|
||||
TFieldValues extends Record<string, any>,
|
||||
>({
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
render,
|
||||
}: Props<TFieldValues>) => {
|
||||
>(
|
||||
props: PreparePullRequestFormProps<TFieldValues>,
|
||||
) => {
|
||||
const { defaultValues, onSubmit, render } = props;
|
||||
|
||||
const methods = useForm<TFieldValues>({ mode: 'onTouched', defaultValues });
|
||||
const { handleSubmit, watch, control, register, formState, setValue } =
|
||||
methods;
|
||||
|
||||
+16
-7
@@ -22,17 +22,26 @@ import { CodeSnippet } from '@backstage/core-components';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { useCatalogFilename } from '../../hooks';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link PreviewCatalogInfoComponent}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PreviewCatalogInfoComponentProps {
|
||||
repositoryUrl: string;
|
||||
entities: Entity[];
|
||||
classes?: { card?: string; cardContent?: string };
|
||||
};
|
||||
}
|
||||
|
||||
export const PreviewCatalogInfoComponent = ({
|
||||
repositoryUrl,
|
||||
entities,
|
||||
classes,
|
||||
}: Props) => {
|
||||
/**
|
||||
* Previews information about an entity to create.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PreviewCatalogInfoComponent = (
|
||||
props: PreviewCatalogInfoComponentProps,
|
||||
) => {
|
||||
const { repositoryUrl, entities, classes } = props;
|
||||
const catalogFilename = useCatalogFilename();
|
||||
|
||||
return (
|
||||
|
||||
+16
-7
@@ -18,17 +18,26 @@ import { Card, CardContent, CardHeader } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { MarkdownContent } from '@backstage/core-components';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link PreviewPullRequestComponent}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PreviewPullRequestComponentProps {
|
||||
title: string;
|
||||
description: string;
|
||||
classes?: { card?: string; cardContent?: string };
|
||||
};
|
||||
}
|
||||
|
||||
export const PreviewPullRequestComponent = ({
|
||||
title,
|
||||
description,
|
||||
classes,
|
||||
}: Props) => {
|
||||
/**
|
||||
* Previews a pull request.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PreviewPullRequestComponent = (
|
||||
props: PreviewPullRequestComponentProps,
|
||||
) => {
|
||||
const { title, description, classes } = props;
|
||||
return (
|
||||
<Card variant="outlined" className={classes?.card}>
|
||||
<CardHeader title={title} subheader="Create a new Pull Request" />
|
||||
|
||||
+17
-8
@@ -53,7 +53,12 @@ type FormData = {
|
||||
useCodeowners: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Props for {@link StepPrepareCreatePullRequest}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface StepPrepareCreatePullRequestProps {
|
||||
analyzeResult: Extract<AnalyzeResult, { type: 'repository' }>;
|
||||
onPrepare: (
|
||||
result: PrepareResult,
|
||||
@@ -71,7 +76,7 @@ type Props = {
|
||||
groupsLoading: boolean;
|
||||
},
|
||||
) => React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export function generateEntities(
|
||||
entities: PartialEntity[],
|
||||
@@ -93,12 +98,16 @@ export function generateEntities(
|
||||
}));
|
||||
}
|
||||
|
||||
export const StepPrepareCreatePullRequest = ({
|
||||
analyzeResult,
|
||||
onPrepare,
|
||||
onGoBack,
|
||||
renderFormFields,
|
||||
}: Props) => {
|
||||
/**
|
||||
* Prepares a pull request.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const StepPrepareCreatePullRequest = (
|
||||
props: StepPrepareCreatePullRequestProps,
|
||||
) => {
|
||||
const { analyzeResult, onPrepare, onGoBack, renderFormFields } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const catalogImportApi = useApi(catalogImportApiRef);
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
export { AutocompleteTextField } from './AutocompleteTextField';
|
||||
export type { AutocompleteTextFieldProps } from './AutocompleteTextField';
|
||||
export { PreparePullRequestForm } from './PreparePullRequestForm';
|
||||
export type { PreparePullRequestFormProps } from './PreparePullRequestForm';
|
||||
export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent';
|
||||
export type { PreviewCatalogInfoComponentProps } from './PreviewCatalogInfoComponent';
|
||||
export { PreviewPullRequestComponent } from './PreviewPullRequestComponent';
|
||||
export type { PreviewPullRequestComponentProps } from './PreviewPullRequestComponent';
|
||||
export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest';
|
||||
export type { StepPrepareCreatePullRequestProps } from './StepPrepareCreatePullRequest';
|
||||
|
||||
@@ -20,3 +20,4 @@ export * from './ImportInfoCard';
|
||||
export * from './ImportStepper';
|
||||
export * from './StepInitAnalyzeUrl';
|
||||
export * from './StepPrepareCreatePullRequest';
|
||||
export type { ImportFlows, ImportState, PrepareResult } from './useImportState';
|
||||
|
||||
@@ -18,7 +18,11 @@ import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { useReducer } from 'react';
|
||||
import { AnalyzeResult } from '../api';
|
||||
|
||||
// the configuration of the stepper
|
||||
/**
|
||||
* The configuration of the stepper.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ImportFlows =
|
||||
| 'unknown'
|
||||
| 'single-location'
|
||||
@@ -28,7 +32,11 @@ export type ImportFlows =
|
||||
// the available states of the stepper
|
||||
type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish';
|
||||
|
||||
// result of the prepare state
|
||||
/**
|
||||
* Result of the prepare state.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PrepareResult =
|
||||
| {
|
||||
type: 'locations';
|
||||
|
||||
@@ -34,6 +34,12 @@ export const rootRouteRef = createRouteRef({
|
||||
id: 'catalog-import',
|
||||
});
|
||||
|
||||
/**
|
||||
* A plugin that helps the user in importing projects and YAML files into the
|
||||
* catalog.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const catalogImportPlugin = createPlugin({
|
||||
id: 'catalog-import',
|
||||
apis: [
|
||||
@@ -70,6 +76,11 @@ export const catalogImportPlugin = createPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The page for importing projects and YAML files into the catalog.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const CatalogImportPage = catalogImportPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'CatalogImportPage',
|
||||
|
||||
@@ -105,22 +105,14 @@ export type CatalogReactUserListPickerClassKey =
|
||||
// @public @deprecated (undocumented)
|
||||
export const catalogRouteRef: RouteRef<undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createDomainColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createDomainColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createEntityRefColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createEntityRefColumn<T extends Entity>({
|
||||
defaultKind,
|
||||
}: {
|
||||
function createEntityRefColumn<T extends Entity>(options: {
|
||||
defaultKind?: string;
|
||||
}): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createEntityRelationColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createEntityRelationColumn<T extends Entity>({
|
||||
title,
|
||||
@@ -136,28 +128,18 @@ function createEntityRelationColumn<T extends Entity>({
|
||||
};
|
||||
}): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createMetadataDescriptionColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createMetadataDescriptionColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createOwnerColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createOwnerColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createSpecLifecycleColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createSpecLifecycleColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createSpecTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createSpecTypeColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createSystemColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function createSystemColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
@@ -215,14 +197,18 @@ export class EntityKindFilter implements EntityFilter {
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "EntityKindFilterProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "EntityKindPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityKindPicker: ({
|
||||
initialFilter,
|
||||
hidden,
|
||||
}: EntityKindFilterProps) => JSX.Element | null;
|
||||
export const EntityKindPicker: (
|
||||
props: EntityKindPickerProps,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export interface EntityKindPickerProps {
|
||||
// (undocumented)
|
||||
hidden: boolean;
|
||||
// (undocumented)
|
||||
initialFilter?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityLifecycleFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -237,8 +223,6 @@ export class EntityLifecycleFilter implements EntityFilter {
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityLifecyclePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityLifecyclePicker: () => JSX.Element | null;
|
||||
|
||||
@@ -270,8 +254,6 @@ export class EntityOwnerFilter implements EntityFilter {
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityOwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityOwnerPicker: () => JSX.Element | null;
|
||||
|
||||
@@ -289,10 +271,7 @@ export interface EntityProviderProps {
|
||||
entity?: Entity;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "EntityRefLinkProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "EntityRefLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const EntityRefLink: React_2.ForwardRefExoticComponent<
|
||||
Pick<
|
||||
EntityRefLinkProps,
|
||||
@@ -581,16 +560,27 @@ export const EntityRefLink: React_2.ForwardRefExoticComponent<
|
||||
React_2.RefAttributes<any>
|
||||
>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "EntityRefLinksProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "EntityRefLinks" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type EntityRefLinkProps = {
|
||||
entityRef: Entity | EntityName;
|
||||
defaultKind?: string;
|
||||
title?: string;
|
||||
children?: React_2.ReactNode;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
// @public
|
||||
export const EntityRefLinks: ({
|
||||
entityRefs,
|
||||
defaultKind,
|
||||
...linkProps
|
||||
}: EntityRefLinksProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityRefLinksProps = {
|
||||
entityRefs: (Entity | EntityName)[];
|
||||
defaultKind?: string;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "entityRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public @deprecated (undocumented)
|
||||
@@ -618,8 +608,6 @@ export const entityRouteRef: RouteRef<{
|
||||
namespace: string;
|
||||
}>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntitySearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntitySearchBar: () => JSX.Element;
|
||||
|
||||
@@ -631,18 +619,12 @@ export type EntitySourceLocation = {
|
||||
integrationType?: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
// Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function EntityTable<T extends Entity>({
|
||||
entities,
|
||||
title,
|
||||
emptyContent,
|
||||
variant,
|
||||
columns,
|
||||
}: Props<T>): JSX.Element;
|
||||
// @public
|
||||
export function EntityTable<T extends Entity>(
|
||||
props: EntityTableProps<T>,
|
||||
): JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace EntityTable {
|
||||
@@ -656,6 +638,22 @@ export namespace EntityTable {
|
||||
componentEntityColumns: TableColumn<ComponentEntity>[];
|
||||
}
|
||||
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "EntityTable" has more than one declaration; you need to add a TSDoc member reference selector
|
||||
//
|
||||
// @public
|
||||
export interface EntityTableProps<T extends Entity> {
|
||||
// (undocumented)
|
||||
columns: TableColumn<T>[];
|
||||
// (undocumented)
|
||||
emptyContent?: ReactNode;
|
||||
// (undocumented)
|
||||
entities: T[];
|
||||
// (undocumented)
|
||||
title: string;
|
||||
// (undocumented)
|
||||
variant?: 'gridItem';
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityTagFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -669,8 +667,6 @@ export class EntityTagFilter implements EntityFilter {
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityTagPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityTagPicker: () => JSX.Element | null;
|
||||
|
||||
@@ -700,26 +696,24 @@ export class EntityTypeFilter implements EntityFilter {
|
||||
readonly value: string | string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type EntityTypeFilterProps = {
|
||||
initialFilter?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityTypePicker: (
|
||||
props: EntityTypeFilterProps,
|
||||
props: EntityTypePickerProps,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export interface EntityTypePickerProps {
|
||||
// (undocumented)
|
||||
hidden?: boolean;
|
||||
// (undocumented)
|
||||
initialFilter?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "FavoriteEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const FavoriteEntity: (props: Props_2) => JSX.Element;
|
||||
export const FavoriteEntity: (props: Props) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "favoriteEntityIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -835,7 +829,7 @@ export const UnregisterEntityDialog: ({
|
||||
onConfirm,
|
||||
onClose,
|
||||
entity,
|
||||
}: Props_3) => JSX.Element;
|
||||
}: Props_2) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export function useEntity<T extends Entity = Entity>(): {
|
||||
|
||||
@@ -19,15 +19,20 @@ import { Alert } from '@material-ui/lab';
|
||||
import { useEntityListProvider } from '../../hooks';
|
||||
import { EntityKindFilter } from '../../filters';
|
||||
|
||||
type EntityKindFilterProps = {
|
||||
/**
|
||||
* Props for {@link EntityKindPicker}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EntityKindPickerProps {
|
||||
initialFilter?: string;
|
||||
hidden: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const EntityKindPicker = (props: EntityKindPickerProps) => {
|
||||
const { initialFilter, hidden } = props;
|
||||
|
||||
export const EntityKindPicker = ({
|
||||
initialFilter,
|
||||
hidden,
|
||||
}: EntityKindFilterProps) => {
|
||||
const { updateFilters, queryParameters } = useEntityListProvider();
|
||||
const [selectedKind] = useState(
|
||||
[queryParameters.kind].flat()[0] ?? initialFilter,
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { EntityKindPicker } from './EntityKindPicker';
|
||||
export type { EntityKindPickerProps } from './EntityKindPicker';
|
||||
|
||||
+30
@@ -161,4 +161,34 @@ describe('<EntityLifecyclePicker/>', () => {
|
||||
lifecycles: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['experimental'] },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['experimental']),
|
||||
});
|
||||
rendered.rerender(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['production'] },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+14
-3
@@ -46,20 +46,31 @@ const useStyles = makeStyles(
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/** @public */
|
||||
export const EntityLifecyclePicker = () => {
|
||||
const classes = useStyles();
|
||||
const { updateFilters, backendEntities, filters, queryParameters } =
|
||||
useEntityListProvider();
|
||||
|
||||
const queryParamLifecycles = [queryParameters.lifecycles]
|
||||
.flat()
|
||||
.filter(Boolean) as string[];
|
||||
const queryParamLifecycles = useMemo(
|
||||
() => [queryParameters.lifecycles].flat().filter(Boolean) as string[],
|
||||
[queryParameters],
|
||||
);
|
||||
|
||||
const [selectedLifecycles, setSelectedLifecycles] = useState(
|
||||
queryParamLifecycles.length
|
||||
? queryParamLifecycles
|
||||
: filters.lifecycles?.values ?? [],
|
||||
);
|
||||
|
||||
// Set selected lifecycles on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamLifecycles.length) {
|
||||
setSelectedLifecycles(queryParamLifecycles);
|
||||
}
|
||||
}, [queryParamLifecycles]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
lifecycles: selectedLifecycles.length
|
||||
|
||||
@@ -191,4 +191,34 @@ describe('<EntityOwnerPicker/>', () => {
|
||||
owner: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { owners: ['team-a'] },
|
||||
}}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
owners: new EntityOwnerFilter(['team-a']),
|
||||
});
|
||||
rendered.rerender(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { owners: ['team-b'] },
|
||||
}}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
owners: new EntityOwnerFilter(['team-b']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,18 +48,29 @@ const useStyles = makeStyles(
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/** @public */
|
||||
export const EntityOwnerPicker = () => {
|
||||
const classes = useStyles();
|
||||
const { updateFilters, backendEntities, filters, queryParameters } =
|
||||
useEntityListProvider();
|
||||
|
||||
const queryParamOwners = [queryParameters.owners]
|
||||
.flat()
|
||||
.filter(Boolean) as string[];
|
||||
const queryParamOwners = useMemo(
|
||||
() => [queryParameters.owners].flat().filter(Boolean) as string[],
|
||||
[queryParameters],
|
||||
);
|
||||
|
||||
const [selectedOwners, setSelectedOwners] = useState(
|
||||
queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [],
|
||||
);
|
||||
|
||||
// Set selected owners on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamOwners.length) {
|
||||
setSelectedOwners(queryParamOwners);
|
||||
}
|
||||
}, [queryParamOwners]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
owners: selectedOwners.length
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
@@ -25,6 +26,11 @@ import { Link, LinkProps } from '@backstage/core-components';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityRefLink}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityRefLinkProps = {
|
||||
entityRef: Entity | EntityName;
|
||||
defaultKind?: string;
|
||||
@@ -32,6 +38,11 @@ export type EntityRefLinkProps = {
|
||||
children?: React.ReactNode;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
/**
|
||||
* Shows a clickable link to an entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityRefLink = forwardRef<any, EntityRefLinkProps>(
|
||||
(props, ref) => {
|
||||
const { entityRef, defaultKind, title, children, ...linkProps } = props;
|
||||
|
||||
@@ -13,16 +13,27 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { EntityRefLink } from './EntityRefLink';
|
||||
import { LinkProps } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityRefLink}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityRefLinksProps = {
|
||||
entityRefs: (Entity | EntityName)[];
|
||||
defaultKind?: string;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
/**
|
||||
* Shows a list of clickable links to entities.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityRefLinks = ({
|
||||
entityRefs,
|
||||
defaultKind,
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { EntityRefLink } from './EntityRefLink';
|
||||
export type { EntityRefLinkProps } from './EntityRefLink';
|
||||
export { EntityRefLinks } from './EntityRefLinks';
|
||||
export type { EntityRefLinksProps } from './EntityRefLinks';
|
||||
export { formatEntityRefTitle } from './format';
|
||||
|
||||
@@ -45,6 +45,7 @@ const useStyles = makeStyles(
|
||||
},
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const EntitySearchBar = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
|
||||
@@ -21,13 +21,18 @@ import * as columnFactories from './columns';
|
||||
import { componentEntityColumns, systemEntityColumns } from './presets';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
|
||||
type Props<T extends Entity> = {
|
||||
/**
|
||||
* Props for {@link EntityTable}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EntityTableProps<T extends Entity> {
|
||||
title: string;
|
||||
variant?: 'gridItem';
|
||||
entities: T[];
|
||||
emptyContent?: ReactNode;
|
||||
columns: TableColumn<T>[];
|
||||
};
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
empty: {
|
||||
@@ -37,13 +42,21 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export function EntityTable<T extends Entity>({
|
||||
entities,
|
||||
title,
|
||||
emptyContent,
|
||||
variant = 'gridItem',
|
||||
columns,
|
||||
}: Props<T>) {
|
||||
/**
|
||||
* A general entity table component, that can be used for composing more
|
||||
* specific entity tables.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function EntityTable<T extends Entity>(props: EntityTableProps<T>) {
|
||||
const {
|
||||
entities,
|
||||
title,
|
||||
emptyContent,
|
||||
variant = 'gridItem',
|
||||
columns,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
const tableStyle: React.CSSProperties = {
|
||||
minWidth: '0',
|
||||
|
||||
@@ -29,11 +29,11 @@ import {
|
||||
formatEntityRefTitle,
|
||||
} from '../EntityRefLink';
|
||||
|
||||
export function createEntityRefColumn<T extends Entity>({
|
||||
defaultKind,
|
||||
}: {
|
||||
/** @public */
|
||||
export function createEntityRefColumn<T extends Entity>(options: {
|
||||
defaultKind?: string;
|
||||
}): TableColumn<T> {
|
||||
const { defaultKind } = options;
|
||||
function formatContent(entity: T): string {
|
||||
return (
|
||||
entity.metadata?.title ||
|
||||
@@ -49,7 +49,7 @@ export function createEntityRefColumn<T extends Entity>({
|
||||
customFilterAndSearch(filter, entity) {
|
||||
// TODO: We could implement this more efficiently, like searching over
|
||||
// each field that is displayed individually (kind, namespace, name).
|
||||
// but that migth confuse the user as it will behave different than a
|
||||
// but that might confuse the user as it will behave different than a
|
||||
// simple text search.
|
||||
// Another alternative would be to cache the values. But writing them
|
||||
// into the entity feels bad too.
|
||||
@@ -70,6 +70,7 @@ export function createEntityRefColumn<T extends Entity>({
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createEntityRelationColumn<T extends Entity>({
|
||||
title,
|
||||
relation,
|
||||
@@ -110,6 +111,7 @@ export function createEntityRelationColumn<T extends Entity>({
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createOwnerColumn<T extends Entity>(): TableColumn<T> {
|
||||
return createEntityRelationColumn({
|
||||
title: 'Owner',
|
||||
@@ -118,6 +120,7 @@ export function createOwnerColumn<T extends Entity>(): TableColumn<T> {
|
||||
});
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createDomainColumn<T extends Entity>(): TableColumn<T> {
|
||||
return createEntityRelationColumn({
|
||||
title: 'Domain',
|
||||
@@ -129,6 +132,7 @@ export function createDomainColumn<T extends Entity>(): TableColumn<T> {
|
||||
});
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createSystemColumn<T extends Entity>(): TableColumn<T> {
|
||||
return createEntityRelationColumn({
|
||||
title: 'System',
|
||||
@@ -140,6 +144,7 @@ export function createSystemColumn<T extends Entity>(): TableColumn<T> {
|
||||
});
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createMetadataDescriptionColumn<
|
||||
T extends Entity,
|
||||
>(): TableColumn<T> {
|
||||
@@ -157,6 +162,7 @@ export function createMetadataDescriptionColumn<
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createSpecLifecycleColumn<T extends Entity>(): TableColumn<T> {
|
||||
return {
|
||||
title: 'Lifecycle',
|
||||
@@ -164,6 +170,7 @@ export function createSpecLifecycleColumn<T extends Entity>(): TableColumn<T> {
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createSpecTypeColumn<T extends Entity>(): TableColumn<T> {
|
||||
return {
|
||||
title: 'Type',
|
||||
|
||||
@@ -13,4 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { EntityTable } from './EntityTable';
|
||||
export type { EntityTableProps } from './EntityTable';
|
||||
|
||||
@@ -149,4 +149,34 @@ describe('<EntityTagPicker/>', () => {
|
||||
tags: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { tags: ['tag1'] },
|
||||
}}
|
||||
>
|
||||
<EntityTagPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
tags: new EntityTagFilter(['tag1']),
|
||||
});
|
||||
rendered.rerender(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { tags: ['tag2'] },
|
||||
}}
|
||||
>
|
||||
<EntityTagPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
tags: new EntityTagFilter(['tag2']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,18 +46,29 @@ const useStyles = makeStyles(
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/** @public */
|
||||
export const EntityTagPicker = () => {
|
||||
const classes = useStyles();
|
||||
const { updateFilters, backendEntities, filters, queryParameters } =
|
||||
useEntityListProvider();
|
||||
|
||||
const queryParamTags = [queryParameters.tags]
|
||||
.flat()
|
||||
.filter(Boolean) as string[];
|
||||
const queryParamTags = useMemo(
|
||||
() => [queryParameters.tags].flat().filter(Boolean) as string[],
|
||||
[queryParameters],
|
||||
);
|
||||
|
||||
const [selectedTags, setSelectedTags] = useState(
|
||||
queryParamTags.length ? queryParamTags : filters.tags?.values ?? [],
|
||||
);
|
||||
|
||||
// Set selected tags on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamTags.length) {
|
||||
setSelectedTags(queryParamTags);
|
||||
}
|
||||
}, [queryParamTags]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined,
|
||||
|
||||
@@ -151,4 +151,38 @@ describe('<EntityTypePicker/>', () => {
|
||||
type: new EntityTypeFilter(['tool']),
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = await renderWithEffects(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { type: 'service' },
|
||||
}}
|
||||
>
|
||||
<EntityTypePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
type: new EntityTypeFilter(['service']),
|
||||
});
|
||||
rendered.rerender(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { type: 'tool' },
|
||||
}}
|
||||
>
|
||||
<EntityTypePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
type: new EntityTypeFilter(['tool']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,12 +22,18 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter';
|
||||
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { Select } from '@backstage/core-components';
|
||||
|
||||
export type EntityTypeFilterProps = {
|
||||
/**
|
||||
* Props for {@link EntityTypePicker}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EntityTypePickerProps {
|
||||
initialFilter?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const EntityTypePicker = (props: EntityTypeFilterProps) => {
|
||||
/** @public */
|
||||
export const EntityTypePicker = (props: EntityTypePickerProps) => {
|
||||
const { hidden, initialFilter } = props;
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { error, availableTypes, selectedTypes, setSelectedTypes } =
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { EntityTypePicker } from './EntityTypePicker';
|
||||
export type { EntityTypeFilterProps } from './EntityTypePicker';
|
||||
export type { EntityTypePickerProps } from './EntityTypePicker';
|
||||
|
||||
@@ -253,6 +253,42 @@ describe('<UserListPicker />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: { user: ['all'] },
|
||||
}}
|
||||
>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter('all', mockIsOwnedEntity, mockIsStarredEntity),
|
||||
});
|
||||
rendered.rerender(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: { user: ['owned'] },
|
||||
}}
|
||||
>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity),
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
type | filterFn
|
||||
${'owned'} | ${mockIsOwnedEntity}
|
||||
|
||||
@@ -161,8 +161,13 @@ export const UserListPicker = ({
|
||||
[isOwnedEntity, isStarredEntity],
|
||||
);
|
||||
|
||||
const queryParamUserFilter = useMemo(
|
||||
() => [queryParameters.user].flat()[0],
|
||||
[queryParameters],
|
||||
);
|
||||
|
||||
const [selectedUserFilter, setSelectedUserFilter] = useState(
|
||||
[queryParameters.user].flat()[0] ?? initialFilter,
|
||||
queryParamUserFilter ?? initialFilter,
|
||||
);
|
||||
|
||||
// To show proper counts for each section, apply all other frontend filters _except_ the user
|
||||
@@ -190,6 +195,14 @@ export const UserListPicker = ({
|
||||
[entitiesWithoutUserFilter, starredFilter, ownedFilter],
|
||||
);
|
||||
|
||||
// Set selected user filter on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamUserFilter) {
|
||||
setSelectedUserFilter(queryParamUserFilter as UserListFilterKind);
|
||||
}
|
||||
}, [queryParamUserFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!loading &&
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './EntityKindPicker';
|
||||
export * from './EntityLifecyclePicker';
|
||||
export * from './EntityOwnerPicker';
|
||||
|
||||
@@ -27,6 +27,7 @@ import { MockStorageApi, TestApiProvider } from '@backstage/test-utils';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import qs from 'qs';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { catalogApiRef } from '../api';
|
||||
import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis';
|
||||
import { EntityKindPicker, UserListPicker } from '../components';
|
||||
@@ -82,31 +83,35 @@ const mockCatalogApi: Partial<CatalogApi> = {
|
||||
|
||||
const wrapper = ({
|
||||
userFilter,
|
||||
location,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
userFilter?: UserListFilterKind;
|
||||
location?: string;
|
||||
}>) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, mockConfigApi],
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[identityApiRef, mockIdentityApi],
|
||||
[storageApiRef, MockStorageApi.create()],
|
||||
[
|
||||
starredEntitiesApiRef,
|
||||
new DefaultStarredEntitiesApi({
|
||||
storageApi: MockStorageApi.create(),
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<EntityListProvider>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<UserListPicker initialFilter={userFilter} />
|
||||
{children}
|
||||
</EntityListProvider>
|
||||
</TestApiProvider>
|
||||
<MemoryRouter initialEntries={[location ?? '']}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, mockConfigApi],
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[identityApiRef, mockIdentityApi],
|
||||
[storageApiRef, MockStorageApi.create()],
|
||||
[
|
||||
starredEntitiesApiRef,
|
||||
new DefaultStarredEntitiesApi({
|
||||
storageApi: MockStorageApi.create(),
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<EntityListProvider>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<UserListPicker initialFilter={userFilter} />
|
||||
{children}
|
||||
</EntityListProvider>
|
||||
</TestApiProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -168,10 +173,11 @@ describe('<EntityListProvider />', () => {
|
||||
const query = qs.stringify({
|
||||
filters: { kind: 'component', type: 'service' },
|
||||
});
|
||||
delete (window as any).location;
|
||||
(window as any).location = new URL(`http://localhost/catalog?${query}`);
|
||||
const { result, waitFor } = renderHook(() => useEntityListProvider(), {
|
||||
wrapper,
|
||||
initialProps: {
|
||||
location: `/catalog?${query}`,
|
||||
},
|
||||
});
|
||||
await waitFor(() => !!result.current.queryParameters);
|
||||
expect(result.current.queryParameters).toEqual({
|
||||
|
||||
@@ -25,6 +25,7 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
@@ -98,7 +99,6 @@ type OutputState<EntityFilters extends DefaultEntityFilters> = {
|
||||
appliedFilters: EntityFilters;
|
||||
entities: Entity[];
|
||||
backendEntities: Entity[];
|
||||
queryParameters: Record<string, string | string[]>;
|
||||
};
|
||||
|
||||
export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
@@ -109,19 +109,26 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
const [requestedFilters, setRequestedFilters] = useState<EntityFilters>(
|
||||
{} as EntityFilters,
|
||||
);
|
||||
|
||||
// We use react-router's useLocation hook so updates from external sources trigger an update to
|
||||
// the queryParameters in outputState. Updates from this hook use replaceState below and won't
|
||||
// trigger a useLocation change; this would instead come from an external source, such as a manual
|
||||
// update of the URL or two catalog sidebar links with different catalog filters.
|
||||
const location = useLocation();
|
||||
const queryParameters = useMemo(
|
||||
() =>
|
||||
(qs.parse(location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
}).filters ?? {}) as Record<string, string | string[]>,
|
||||
[location],
|
||||
);
|
||||
|
||||
const [outputState, setOutputState] = useState<OutputState<EntityFilters>>(
|
||||
() => {
|
||||
const query = qs.parse(window.location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
return {
|
||||
appliedFilters: {} as EntityFilters,
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
queryParameters: (query.filters ?? {}) as Record<
|
||||
string,
|
||||
string | string[]
|
||||
>,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -163,19 +170,17 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
appliedFilters: requestedFilters,
|
||||
backendEntities: response.items,
|
||||
entities: response.items.filter(entityFilter),
|
||||
queryParameters: queryParams,
|
||||
});
|
||||
} else {
|
||||
setOutputState({
|
||||
appliedFilters: requestedFilters,
|
||||
backendEntities: outputState.backendEntities,
|
||||
entities: outputState.backendEntities.filter(entityFilter),
|
||||
queryParameters: queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (isMounted()) {
|
||||
const oldParams = qs.parse(window.location.search, {
|
||||
const oldParams = qs.parse(location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
const newParams = qs.stringify(
|
||||
@@ -191,7 +196,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
window.history?.replaceState(null, document.title, newUrl);
|
||||
}
|
||||
},
|
||||
[catalogApi, requestedFilters, outputState],
|
||||
[catalogApi, queryParameters, requestedFilters, outputState],
|
||||
{ loading: true },
|
||||
);
|
||||
|
||||
@@ -220,11 +225,11 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
entities: outputState.entities,
|
||||
backendEntities: outputState.backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: outputState.queryParameters,
|
||||
queryParameters,
|
||||
loading,
|
||||
error,
|
||||
}),
|
||||
[outputState, updateFilters, loading, error],
|
||||
[outputState, updateFilters, queryParameters, loading, error],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,13 +42,23 @@ export function useEntityTypeFilter(): EntityTypeReturn {
|
||||
updateFilters,
|
||||
} = useEntityListProvider();
|
||||
|
||||
const queryParamTypes = [queryParameters.type]
|
||||
.flat()
|
||||
.filter(Boolean) as string[];
|
||||
const queryParamTypes = useMemo(
|
||||
() => [queryParameters.type].flat().filter(Boolean) as string[],
|
||||
[queryParameters],
|
||||
);
|
||||
|
||||
const [selectedTypes, setSelectedTypes] = useState(
|
||||
queryParamTypes.length ? queryParamTypes : typeFilter?.getTypes() ?? [],
|
||||
);
|
||||
|
||||
// Set selected types on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamTypes.length) {
|
||||
setSelectedTypes(queryParamTypes);
|
||||
}
|
||||
}, [queryParamTypes]);
|
||||
|
||||
const [availableTypes, setAvailableTypes] = useState<string[]>([]);
|
||||
const kind = useMemo(() => kindFilter?.value, [kindFilter]);
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, useCallback, useState } from 'react';
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
DefaultEntityFilters,
|
||||
EntityListContext,
|
||||
@@ -32,6 +37,7 @@ export const MockEntityListContextProvider = ({
|
||||
const [filters, setFilters] = useState<DefaultEntityFilters>(
|
||||
value?.filters ?? {},
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(
|
||||
update:
|
||||
@@ -49,23 +55,32 @@ export const MockEntityListContextProvider = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const defaultContext: EntityListContextProps = {
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
updateFilters,
|
||||
filters,
|
||||
loading: false,
|
||||
queryParameters: {},
|
||||
};
|
||||
// Memoize the default values since pickers have useEffect triggers on these; naively defaulting
|
||||
// below with `?? <X>` breaks referential equality on subsequent updates.
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
queryParameters: {},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Extract value.filters to avoid overwriting it; some tests exercise filter updates. The value
|
||||
// provided is used as the initial seed in useState above.
|
||||
const { filters: _, ...otherContextFields } = value ?? {};
|
||||
const resolvedValue: EntityListContextProps = useMemo(
|
||||
() => ({
|
||||
entities: value?.entities ?? defaultValues.entities,
|
||||
backendEntities: value?.backendEntities ?? defaultValues.backendEntities,
|
||||
updateFilters: value?.updateFilters ?? updateFilters,
|
||||
filters,
|
||||
loading: value?.loading ?? false,
|
||||
queryParameters: value?.queryParameters ?? defaultValues.queryParameters,
|
||||
error: value?.error,
|
||||
}),
|
||||
[value, defaultValues, filters, updateFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityListContext.Provider
|
||||
value={{ ...defaultContext, ...otherContextFields }}
|
||||
>
|
||||
<EntityListContext.Provider value={resolvedValue}>
|
||||
{children}
|
||||
</EntityListContext.Provider>
|
||||
);
|
||||
|
||||
@@ -113,6 +113,13 @@ export const HomePageRandomJoke: (
|
||||
},
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const HomePageStarredEntities: (
|
||||
props: ComponentRenderer & {
|
||||
title?: string | undefined;
|
||||
},
|
||||
) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ToolkitContentProps" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.9.10",
|
||||
"@backstage/core-components": "^0.8.7",
|
||||
"@backstage/core-plugin-api": "^0.6.0",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@backstage/plugin-catalog-react": "^0.6.13",
|
||||
"@backstage/plugin-search": "^0.6.1",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 {
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
MockStorageApi,
|
||||
} from '@backstage/test-utils';
|
||||
import {
|
||||
starredEntitiesApiRef,
|
||||
entityRouteRef,
|
||||
DefaultStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { Content } from './Content';
|
||||
|
||||
describe('StarredEntitiesContent', () => {
|
||||
it('should render list of tools', async () => {
|
||||
const mockStorageApi = MockStorageApi.create();
|
||||
await mockStorageApi
|
||||
.forBucket('starredEntities')
|
||||
.set('entityRefs', [
|
||||
'component:default/mock-starred-entity',
|
||||
'component:default/mock-starred-entity-2',
|
||||
]);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
starredEntitiesApiRef,
|
||||
new DefaultStarredEntitiesApi({
|
||||
storageApi: mockStorageApi,
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<Content />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(getByText('mock-starred-entity')).toBeInTheDocument();
|
||||
expect(getByText('mock-starred-entity-2')).toBeInTheDocument();
|
||||
expect(getByText('mock-starred-entity').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/mock-starred-entity',
|
||||
);
|
||||
expect(getByText('mock-starred-entity-2').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/mock-starred-entity-2',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 {
|
||||
useStarredEntities,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* A component to display a list of starred entities for the user.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
export const Content = () => {
|
||||
const catalogEntityRoute = useRouteRef(entityRouteRef);
|
||||
const { starredEntities, toggleStarredEntity } = useStarredEntities();
|
||||
|
||||
if (starredEntities.size === 0)
|
||||
return (
|
||||
<Typography variant="body1">
|
||||
You do not have any starred entities yet!
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<List>
|
||||
{Array.from(starredEntities).map(entity => (
|
||||
<ListItem key={entity}>
|
||||
<Link to={catalogEntityRoute(parseEntityRef(entity))}>
|
||||
<ListItemText primary={parseEntityRef(entity).name} />
|
||||
</Link>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Remove from starred">
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="unstar"
|
||||
onClick={() => toggleStarredEntity(entity)}
|
||||
>
|
||||
<StarIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { HomePageStarredEntities } from '../../plugin';
|
||||
import {
|
||||
wrapInTestApp,
|
||||
TestApiProvider,
|
||||
MockStorageApi,
|
||||
} from '@backstage/test-utils';
|
||||
import {
|
||||
starredEntitiesApiRef,
|
||||
entityRouteRef,
|
||||
DefaultStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
const mockStorageApi = MockStorageApi.create();
|
||||
mockStorageApi
|
||||
.forBucket('starredEntities')
|
||||
.set('entityRefs', [
|
||||
'component:default/example-starred-entity',
|
||||
'component:default/example-starred-entity-2',
|
||||
'component:default/example-starred-entity-3',
|
||||
'component:default/example-starred-entity-4',
|
||||
]);
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/StarredEntities',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
starredEntitiesApiRef,
|
||||
new DefaultStarredEntitiesApi({
|
||||
storageApi: mockStorageApi,
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<Story />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageStarredEntities />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { Content } from './Content';
|
||||
@@ -26,6 +26,7 @@ export {
|
||||
HomePageRandomJoke,
|
||||
HomePageToolkit,
|
||||
HomePageCompanyLogo,
|
||||
HomePageStarredEntities,
|
||||
ComponentAccordion,
|
||||
ComponentTabs,
|
||||
ComponentTab,
|
||||
|
||||
@@ -115,3 +115,16 @@ export const HomePageToolkit = homePlugin.provide(
|
||||
components: () => import('./homePageComponents/Toolkit'),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* A component to display a list of starred entities for the user.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HomePageStarredEntities = homePlugin.provide(
|
||||
createCardExtension({
|
||||
name: 'HomePageStarredEntities',
|
||||
title: 'Your Starred Entities',
|
||||
components: () => import('./homePageComponents/StarredEntities'),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -14,20 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateBackstageLogo} from './TemplateBackstageLogo';
|
||||
import {TemplateBackstageLogoIcon} from './TemplateBackstageLogoIcon';
|
||||
import { HomePageToolkit, HomePageCompanyLogo } from '../plugin';
|
||||
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { TemplateBackstageLogo } from './TemplateBackstageLogo';
|
||||
import { TemplateBackstageLogoIcon } from './TemplateBackstageLogoIcon';
|
||||
import {
|
||||
HomePageToolkit,
|
||||
HomePageCompanyLogo,
|
||||
HomePageStarredEntities,
|
||||
} from '../plugin';
|
||||
import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-utils';
|
||||
import { Content, Page, InfoCard } from '@backstage/core-components';
|
||||
import {
|
||||
starredEntitiesApiRef,
|
||||
entityRouteRef,
|
||||
DefaultStarredEntitiesApi
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
HomePageSearchBar,
|
||||
SearchContextProvider,
|
||||
searchApiRef,
|
||||
searchPlugin
|
||||
searchPlugin,
|
||||
} from '@backstage/plugin-search';
|
||||
import { Grid, makeStyles } from '@material-ui/core';
|
||||
import React, { ComponentType} from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
const mockStorageApi = MockStorageApi.create();
|
||||
mockStorageApi
|
||||
.forBucket('starredEntities')
|
||||
.set('entityRefs', [
|
||||
'component:default/example-starred-entity',
|
||||
'component:default/example-starred-entity-2',
|
||||
'component:default/example-starred-entity-3',
|
||||
'component:default/example-starred-entity-4'
|
||||
]);
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Templates',
|
||||
@@ -35,13 +53,26 @@ export default {
|
||||
(Story: ComponentType<{}>) =>
|
||||
wrapInTestApp(
|
||||
<>
|
||||
<TestApiProvider apis={[[searchApiRef, {query: () => Promise.resolve({results: []})}]]}>
|
||||
<Story />
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
starredEntitiesApiRef,
|
||||
new DefaultStarredEntitiesApi({
|
||||
storageApi: mockStorageApi,
|
||||
}),
|
||||
],
|
||||
[searchApiRef, { query: () => Promise.resolve({ results: [] }) }],
|
||||
]}
|
||||
>
|
||||
<Story />
|
||||
</TestApiProvider>
|
||||
</>,
|
||||
{
|
||||
mountedRoutes: {'/hello-company': searchPlugin.routes.root }
|
||||
}
|
||||
mountedRoutes: {
|
||||
'/hello-company': searchPlugin.routes.root,
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -82,20 +113,17 @@ export const DefaultTemplate = () => {
|
||||
<Grid container justifyContent="center" spacing={6}>
|
||||
<HomePageCompanyLogo
|
||||
className={container}
|
||||
logo={<TemplateBackstageLogo classes={{svg, path}} />}
|
||||
logo={<TemplateBackstageLogo classes={{ svg, path }} />}
|
||||
/>
|
||||
<Grid container item xs={12} alignItems="center" direction="row">
|
||||
<HomePageSearchBar
|
||||
classes={{root: classes.searchBar}}
|
||||
classes={{ root: classes.searchBar }}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<InfoCard title="Composable Section">
|
||||
{/* placeholder for content */}
|
||||
<div style={{ height: 210 }} />
|
||||
</InfoCard>
|
||||
<HomePageStarredEntities />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageToolkit
|
||||
|
||||
@@ -112,10 +112,11 @@ export function createPublishGitlabAction(options: {
|
||||
}
|
||||
|
||||
const token = ctx.input.token || integrationConfig.config.token!;
|
||||
const tokenType = ctx.input.token ? 'oauthToken' : 'token';
|
||||
|
||||
const client = new Gitlab({
|
||||
host: integrationConfig.config.baseUrl,
|
||||
token,
|
||||
[tokenType]: token,
|
||||
});
|
||||
|
||||
let { id: targetNamespace } = (await client.Namespaces.show(owner)) as {
|
||||
|
||||
+2
-1
@@ -118,10 +118,11 @@ export const createPublishGitlabMergeRequestAction = (options: {
|
||||
}
|
||||
|
||||
const token = ctx.input.token ?? integrationConfig.config.token!;
|
||||
const tokenType = ctx.input.token ? 'oauthToken' : 'token';
|
||||
|
||||
const api = new Gitlab({
|
||||
host: integrationConfig.config.baseUrl,
|
||||
token,
|
||||
[tokenType]: token,
|
||||
});
|
||||
|
||||
const fileRoot = ctx.workspacePath;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export const EntityTodoContent: () => JSX.Element;
|
||||
@@ -79,5 +80,10 @@ export type TodoListResult = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export const todoPlugin: BackstagePlugin<{}, {}>;
|
||||
export const todoPlugin: BackstagePlugin<
|
||||
{
|
||||
entityContent: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user