Merge branch 'backstage:master' into feat/techdocs-azurite-support

This commit is contained in:
Aswath S
2023-08-24 20:51:59 +05:30
committed by GitHub
77 changed files with 1823 additions and 591 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-playlist': patch
---
change color of delete icon to secondary in playlist table
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-radar': patch
---
Fixed the width of the tech radar dialog so that the content fits
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Update OpenAPI schema to relax the encoding validation of all request parameters.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-react': patch
---
Optionally initializes the search context with default settings for search queries only when the config is defined, rather than always overriding it.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed validation of the `fullTextFilterFields` query parameter.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-module-addons-contrib': minor
---
Enable zoom icon for techdocs images inside lightbox
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add examples for `github:actions:dispatch` scaffolder actions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fix issue with `catalogFileName` not being a required property for `/analyze-location`
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-techdocs-backend': minor
'@backstage/plugin-techdocs-node': minor
'@techdocs/cli': minor
---
Add optional config and cli option for techdocs to specify default mkdocs plugins.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Allow specifying static widgets to custom home page
+2 -2
View File
@@ -58,8 +58,8 @@ yarn.lock @backstage/maintainers @backst
/plugins/home-* @backstage/discoverability-maintainers
/plugins/kafka @backstage/maintainers @backstage/reviewers @andrewthauer
/plugins/kafka-backend @backstage/maintainers @backstage/reviewers @andrewthauer
/plugins/kubernetes @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers
/plugins/kubernetes-* @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers
/plugins/kubernetes @backstage/kubernetes-maintainers
/plugins/kubernetes-* @backstage/kubernetes-maintainers
/plugins/linguist @backstage/maintainers @backstage/reviewers @awanlin
/plugins/linguist-backend @backstage/maintainers @backstage/reviewers @awanlin
/plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- name: checkout
+2
View File
@@ -94,11 +94,13 @@ Scope: The TechDocs plugin and related tooling
| ------------------------ | ------------ | ------- | ------------------------------------------------- | ------------------- |
| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | `alexlorenzi` |
| Bond Yan | Spotify | ProTean | [byan1197](https://github.com/byan1197) | `bondify` |
| Daniel Lochrie | Spotify | ProTean | [dlochrie](https://github.com/dlochrie) | - |
| Jackson Chen | Spotify | ProTean | [PeaWarrior](https://github.com/PeaWarrior) | `jacksonc#3322` |
| John Philip | Spotify | ProTean | [johnphilip283](https://github.com/johnphilip283) | `john_philip#2399` |
| Morgan Bentell | Spotify | ProTean | [agentbellnorm](http://github.com/agentbellnorm) | `morganbentell` |
| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | `raghunandanb#1114` |
| Sydney Achinger | Spotify | ProTean | [squid-ney](https://github.com/squid-ney) | - |
| Vojta Burian | Spotify | ProTean | [sankaritan](https://github.com/sankaritan) | `vojtabu` |
## Incubating Project Areas
+1 -1
View File
@@ -6,7 +6,7 @@
"features": {
"ghcr.io/devcontainers/features/common-utils:1": {},
"ghcr.io/devcontainers/features/docker-from-docker:1": {},
"ghcr.io/devcontainers-contrib/features/mkdocs:1": {}
"ghcr.io/devcontainers-contrib/features/mkdocs:2": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
+4 -1
View File
@@ -1,3 +1,6 @@
#!/bin/bash
yarn install
pip install mkdocs-techdocs-core
export VIRTUAL_ENV=$HOME/venv
python3 -m venv $VIRTUAL_ENV
export PATH="$VIRTUAL_ENV/bin:$PATH"
python3 -m pip install mkdocs-techdocs-core
@@ -123,3 +123,14 @@ source that should be mirrored into Backstage. To make Backstage a mirror of
this remote source, users cannot also register new entities with e.g. the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
plugin.
## Clean up orphaned entities
In short entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning).
However, if you do with to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted.
```
catalog:
orphanStrategy: delete
```
@@ -222,7 +222,8 @@ either, it becomes _orphaned_. The end result is as follows:
- The stitching process injects a `backstage.io/orphan: 'true'` annotation on
the child entity.
- The child entity is _not_ removed from the catalog, but stays around until
explicitly deleted via the catalog API, or "reclaimed" by the original parent
explicitly deleted via the catalog API, implicitly if `orphanStrategy: delete`
configuration is set, or until it is "reclaimed" by the original parent
or another parent starting to reference it.
- The catalog page in Backstage for the child entity detects the new annotation
and informs users about the orphan status.
@@ -259,9 +260,13 @@ entities without explicit owner consent. The catalog therefore takes the stance
that entities that often were added by direct user action should also be deleted
only by direct user action.
It is possible to use the catalog API to build automated "reaper" systems that
finally delete entities that are orphaned. This is however not something that's
provided out of the box.
However, if you want to delete orphaned entities automatically anyway, you can
enable the automated clean up with the following app-config option.
```
catalog:
orphanStrategy: delete
```
## Implicit Deletion
+6 -6
View File
@@ -122,12 +122,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered.
Addons can, in principle, be provided by any plugin! To make it easier to
discover available Addons, we've compiled a list of them here:
| Addon | Package/Plugin | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. |
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
| [`<LightBox />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. |
| Addon | Package/Plugin | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. |
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
| [`<LightBox />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). |
Got an Addon to contribute? Feel free to add a row above!
+1
View File
@@ -141,6 +141,7 @@ Options:
if not found.
--etag <ETAG> A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored
in techdocs_metadata.json.
--defaultPlugin <PLUGIN_NAME> Plugins which should be added automatically to the mkdocs.yaml file. (default: [])
--omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files.
Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
--legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md
+9
View File
@@ -49,6 +49,15 @@ techdocs:
# will be broken in these scenarios.
legacyCopyReadmeMdToIndexMd: false
# (Optional) Configures the default plugins which should be added
# automatically to every mkdocs.yaml file. This simplifies the usage as
# e.g. styling plugins can be added once for all.
# Make sure that the defined plugins are installed locally / in the Docker
# image.
# By default, only the techdocs-core plugin will be added (except if
# omitTechdocsCorePlugin: true).
defaultPlugins: ['techdocs-core']
# techdocs.builder can be either 'local' or 'external'.
# Using the default build strategy, if builder is set to 'local' and you open a TechDocs page,
# techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwards.
+28
View File
@@ -13,6 +13,34 @@ npm.
Releases are handled by changesets and trigger whenever the "Version Packages"
PR is merged. This is typically done every Tuesday around noon CET.
## Next Line Release Process
- PR Checks: Ensure there are no outstanding PRs pending to be merged for this version. If there are any, reach out to maintainers and relevant owners of the affected code reminding them of the deadline for the release.
- [optional] Lock main branch
- Lock the main branch to prevent any new merges.
- Note: Admin rights are required to lock the branch. If you lack the necessary permissions, contact a core maintainer to perform this action on your behalf.
- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22)
- Verify the version we are shipping is correct, by looking at the version packages PR title. It should be "Version Packages (next)"
- Check [`.changeset/pre.json`](https://github.com/backstage/backstage/blob/master/.changeset/pre.json) if the `mode` is set to `pre`. If you encounter `mode: "exit"` or if it's not defined, it indicates a mainline release.
- Verify that there are no active/unfinished `sync_version-packages` actions running (https://github.com/backstage/backstage/actions/workflows/sync_version-packages.yml)
- Locking the main branch will prevent new ones to be created, but be sure to check for running actions again after unlocking, since it may cause pending auto-merge PRs to be merged.
- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22) for sufficient approval to be merged
- Check generated `changelog` in the changed files of the pull request under `docs/releases` to see if there are any unexpected major bumps e.g. by searching the file
- Review & approve changes
- Reach out to core maintainer to merge the pull request
- Heads-up: The microsite building step can be skipped as long as the `prettier` task passes & everything else looks green
Merging the `Version Packages (next)` Pull Request will trigger the deployment workflows. Follow along the [deployment workflow](https://github.com/backstage/backstage/actions/workflows/deploy_packages.yml). If you notice flakiness (e.g. if the build is flaky or if the release step runs into an error with releasing to npm) just restart the workflow.
Congratulations on the release! There should be now a post in the [`#announcements` channel](https://discord.com/channels/687207715902193673/705123584468582400) in Discord linking to the release tag - check if links & tag look as expected. Finally unlock the main branch again. Merging PRs in master directly after release should be done with caution as it potential complicates fixing issues introduced in the release.
## Switching Release Modes
- To enter pre-release mode: `yarn changeset pre enter next` & create PR + merge changes
- To exit pre-release mode: `yarn changeset pre exit` & create PR + merge changes
- Has to be done before the mainline release
- It's not time critical; Affects the next release happening
## Emergency Release Process
**This emergency release process is intended only for the Backstage
+10
View File
@@ -0,0 +1,10 @@
---
title: Flux Plugins
author: Weaveworks
authorUrl: https://weave.works
category: GitOps
description: View the status of fluxcd.io resources
documentation: https://github.com/weaveworks/weaveworks-backstage/tree/main/plugins/backstage-plugin-flux#readme
iconUrl: https://raw.githubusercontent.com/fluxcd/website/main/assets/icons/logo.svg
npmPackageName: '@weaveworksoss/backstage-plugin-flux'
addedDate: '2023-08-23'
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import React from 'react';
import {
createExtension,
createPlugin,
coreExtensionData,
createSchemaFromZod,
} from '@backstage/frontend-plugin-api';
import { Router as GraphiQLPage } from '@backstage/plugin-graphiql';
@@ -27,12 +27,12 @@ export const GraphiqlPageExtension = createExtension({
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
},
configSchema: createSchemaFromZod(z =>
z.object({ path: z.string().default('/graphiql') }),
),
factory({ bind, config }) {
bind.component(() => {
return <GraphiQLPage />;
});
// TODO: In need of schemas and type safety
bind.path((config as { path: string }).path);
bind.component(GraphiQLPage);
bind.path(config.path);
},
});
@@ -43,7 +43,6 @@ export const graphiqlPlugin = createPlugin({
id: 'graphiql.page',
at: 'core.router/routes',
extension: GraphiqlPageExtension,
config: { path: '/graphiql' },
},
],
});
@@ -15,14 +15,14 @@
*/
import {
HomePageRandomJoke,
WelcomeTitle,
HeaderWorldClock,
ClockConfig,
HomePageStarredEntities,
CustomHomepageGrid,
HomePageToolkit,
HeaderWorldClock,
HomePageCompanyLogo,
HomePageRandomJoke,
HomePageStarredEntities,
HomePageToolkit,
WelcomeTitle,
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
@@ -64,6 +64,9 @@ const defaultConfig = [
y: 0,
width: 12,
height: 1,
movable: false,
resizable: false,
deletable: false,
},
{
component: 'WelcomeTitle',
@@ -77,7 +80,7 @@ const defaultConfig = [
x: 0,
y: 2,
width: 12,
height: 1,
height: 2,
},
];
+4 -7
View File
@@ -1,12 +1,9 @@
# @backstage/frontend-app-api
_This package was created through the Backstage CLI_.
**This package is EXPERIMENTAL, we recommend against using it for production deployments**
## Installation
This package provides the core API used by Backstage frontend apps. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372).
Install the package via Yarn:
## Documentation
```sh
cd <package-dir> # if within a monorepo
yarn add @backstage/frontend-app-api
```
- [Backstage Documentation](https://backstage.io/docs)
+28 -103
View File
@@ -15,9 +15,9 @@
*/
import React from 'react';
import { Config, ConfigReader } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import {
ExtensionInstanceConfig,
ExtensionInstanceParameters,
BackstagePlugin,
coreExtensionData,
} from '@backstage/frontend-plugin-api';
@@ -26,65 +26,10 @@ import {
createExtensionInstance,
ExtensionInstance,
} from './createExtensionInstance';
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
function readAppExtensionConfigs(
rootConfig: Config,
): Partial<ExtensionInstanceConfig>[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
return arr.map((value, index) => {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${index}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
if (typeof value === 'string') {
return { id: value };
} else if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(value);
if (keys.length !== 1) {
const joinedKeys = `"${keys.join('", "')}"`;
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const key = keys[0];
const obj = value[key];
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error(errorMsg('must be an object', key));
}
const at = obj.at;
if (at !== undefined && typeof at !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
}
const extension = obj.extension;
if (extension !== undefined && typeof extension !== 'string') {
throw new Error(errorMsg('must be a string', key, 'extension'));
}
if (extension) {
throw new Error('TODO: implement extension resolution');
}
return { id: key, at, config: obj.config /* validate later */ };
});
}
import {
mergeExtensionParameters,
readAppExtensionParameters,
} from './wiring/parameters';
/** @public */
export function createApp(options: { plugins: BackstagePlugin[] }): {
@@ -92,10 +37,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
} {
const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionInstanceConfigs = [
...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
const builtinExtensionInstanceParams = [
{
id: 'core.router',
at: 'root/default',
@@ -104,40 +46,23 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
},
];
const appExtensionConfigs = readAppExtensionConfigs(appConfig);
for (const appExtensionConfig of appExtensionConfigs) {
const existingConfig = extensionInstanceConfigs.find(
e => e.id === appExtensionConfig.id,
);
if (existingConfig) {
if (appExtensionConfig.at) {
existingConfig.at = appExtensionConfig.at;
}
if (appExtensionConfig.extension) {
// TODO: do we want to reset config here? it might be completely
// unrelated to the previous one
existingConfig.extension = appExtensionConfig.extension;
}
if (appExtensionConfig.config) {
// TODO: merge config?
existingConfig.config = appExtensionConfig.config;
}
} else if (appExtensionConfig.id) {
const { id, at, extension, config } = appExtensionConfig;
if (!at || !extension) {
throw new Error(`Extension ${appExtensionConfig.id} is incomplete`);
}
extensionInstanceConfigs.push({ id, at, extension, config });
}
}
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionInstanceParams = mergeExtensionParameters(
[
...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
...builtinExtensionInstanceParams,
],
readAppExtensionParameters(appConfig),
);
// Create attachment map so that we can look attachments up during instance creation
const attachmentMap = new Map<
string,
Map<string, ExtensionInstanceConfig[]>
Map<string, ExtensionInstanceParameters[]>
>();
for (const instanceConfig of extensionInstanceConfigs) {
const [extensionId, pointId = 'default'] = instanceConfig.at.split('/');
for (const instanceParams of extensionInstanceParams) {
const [extensionId, pointId = 'default'] = instanceParams.at.split('/');
let pointMap = attachmentMap.get(extensionId);
if (!pointMap) {
@@ -151,21 +76,21 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
pointMap.set(pointId, instances);
}
instances.push(instanceConfig);
instances.push(instanceParams);
}
const instances = new Map<string, ExtensionInstance>();
function createInstance(
instanceConfig: ExtensionInstanceConfig,
instanceParams: ExtensionInstanceParameters,
): ExtensionInstance {
const existingInstance = instances.get(instanceConfig.id);
const existingInstance = instances.get(instanceParams.id);
if (existingInstance) {
return existingInstance;
}
const attachments = Object.fromEntries(
Array.from(attachmentMap.get(instanceConfig.id)?.entries() ?? []).map(
Array.from(attachmentMap.get(instanceParams.id)?.entries() ?? []).map(
([inputName, attachmentConfigs]) => [
inputName,
attachmentConfigs.map(createInstance),
@@ -174,16 +99,16 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
);
return createExtensionInstance({
id: instanceConfig.id,
config: instanceConfig.config,
extension: instanceConfig.extension,
id: instanceParams.id,
config: instanceParams.config,
extension: instanceParams.extension,
attachments,
});
}
const rootConfigs = attachmentMap.get('root')?.get('default') ?? [];
const rootInstances = rootConfigs.map(instanceConfig =>
createInstance(instanceConfig),
const rootInstances = rootConfigs.map(instanceParams =>
createInstance(instanceParams),
);
return {
@@ -27,26 +27,45 @@ export interface ExtensionInstance {
/** @internal */
export function createExtensionInstance(options: {
id: string;
extension: Extension;
extension: Extension<unknown>;
config: unknown;
attachments: Record<string, ExtensionInstance[]>;
}): ExtensionInstance {
const { extension, config, attachments } = options;
const { id, extension, config, attachments } = options;
const extensionData = new Map<string, unknown>();
extension.factory({
config,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
}),
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
// TODO: validation
return (attachments[inputName] ?? []).map(attachment =>
mapValues(pointData, ref => attachment.data.get(ref.id)),
);
},
),
});
return { id: options.id, data: extensionData, $$type: 'extension-instance' };
let parsedConfig: unknown;
try {
parsedConfig = extension.configSchema?.parse(config ?? {});
} catch (e) {
throw new Error(
`Invalid configuration for extension instance '${id}', ${e}`,
);
}
try {
extension.factory({
config: parsedConfig,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
}),
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
// TODO: validation
return (attachments[inputName] ?? []).map(attachment =>
mapValues(pointData, ref => attachment.data.get(ref.id)),
);
},
),
});
} catch (e) {
throw new Error(`Failed to instantiate extension instance '${id}', ${e}`);
}
return {
id: options.id,
data: extensionData,
$$type: 'extension-instance',
};
}
@@ -0,0 +1,259 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Extension } from '@backstage/frontend-plugin-api';
import {
mergeExtensionParameters,
readAppExtensionParameters,
} from './parameters';
function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
return {
id,
at: 'foo/bar',
extension: {} as Extension<unknown>,
disabled: status === 'disabled',
};
}
describe('mergeExtensionParameters', () => {
it('should filter out disabled extension instances', () => {
expect(mergeExtensionParameters([makeExt('a', 'disabled')], [])).toEqual(
[],
);
});
it('should pass through extension instances', () => {
expect(mergeExtensionParameters([makeExt('a'), makeExt('b')], [])).toEqual([
makeExt('a'),
makeExt('b'),
]);
});
it('should override extension instances', () => {
expect(
mergeExtensionParameters(
[makeExt('a'), makeExt('b')],
[
{
id: 'b',
extension: { ext: 'other' } as unknown as Extension<unknown>,
},
],
),
).toEqual([makeExt('a'), { ...makeExt('b'), extension: { ext: 'other' } }]);
});
it('should override attachment points', () => {
expect(
mergeExtensionParameters(
[makeExt('a'), makeExt('b')],
[
{
id: 'b',
at: 'derp',
},
],
),
).toEqual([makeExt('a'), { ...makeExt('b'), at: 'derp' }]);
});
it('should fully override configuration', () => {
expect(
mergeExtensionParameters(
[
{ ...makeExt('a'), config: { foo: { bar: 1 } } },
{ ...makeExt('b'), config: { foo: { bar: 2 } } },
],
[
{
id: 'b',
config: { foo: { qux: 3 } },
},
],
),
).toEqual([
{ ...makeExt('a'), config: { foo: { bar: 1 } } },
{ ...makeExt('b'), config: { foo: { qux: 3 } } },
]);
});
it('should place enabled instances in the order that they were enabled', () => {
expect(
mergeExtensionParameters(
[makeExt('a', 'disabled'), makeExt('b', 'disabled')],
[
{
id: 'b',
disabled: false,
},
{
id: 'a',
disabled: false,
},
],
),
).toEqual([makeExt('b'), makeExt('a')]);
});
});
describe('readAppExtensionParameters', () => {
it('should disable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: true,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: true } }] },
}),
),
).toEqual([
{
at: undefined,
config: undefined,
disabled: true,
id: 'core.router',
},
]);
});
it('should enable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: ['core.router'] } }),
),
).toEqual([
{
id: 'core.router',
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: false } }] },
}),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
});
it('should support extension implementation shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router': 'example-package#CustomRouter' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'core.router',
extension: { ref: 'example-package#CustomRouter' },
},
]);
});
it('should support attachment shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
},
],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
config: { foo: 'bar' },
},
]);
});
it('should support attachment with extension shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router/routes': 'example-package#MyPage' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
},
]);
});
it('should reject attachment shorthand with explicit attachment', () => {
expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
at: 'other/input',
},
},
],
},
}),
),
).toThrow(
`Invalid extension configuration at app.extensions[0][core.router/routes], must not specify 'at' when using attachment shorthand form`,
);
});
});
@@ -0,0 +1,175 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import {
Extension,
ExtensionInstanceParameters,
} from '@backstage/frontend-plugin-api';
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
/** @internal */
export function readAppExtensionParameters(
rootConfig: Config,
resolveExtensionRef: (ref: string) => Extension<unknown> = () => {
throw new Error('Not Implemented');
},
): Partial<ExtensionInstanceParameters>[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
const generateIndex = (() => {
let generatedIndex = 1;
return () => `generated.${generatedIndex++}`;
})();
return arr.map((arrayEntry, index) => {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${index}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
// Example YAML:
// - entity.card.about
if (typeof arrayEntry === 'string') {
return { id: arrayEntry };
}
// All other forms are single-key objects
if (
typeof arrayEntry !== 'object' ||
arrayEntry === null ||
Array.isArray(arrayEntry)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(arrayEntry);
if (keys.length !== 1) {
const joinedKeys = `"${keys.join('", "')}"`;
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const key = keys[0];
let value = arrayEntry[key];
let at = key.includes('/') ? key : undefined;
const id = at ? generateIndex() : key;
if (typeof value === 'boolean') {
if (at) {
throw new Error(
errorMsg('cannot be applied to an instance input', key),
);
}
value = { disabled: !value };
} else if (typeof value === 'string') {
value = { extension: value };
} else if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value)
) {
throw new Error(errorMsg('must be an object', key));
}
const atRef = value.at;
if (atRef !== undefined && typeof atRef !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
} else if (atRef !== undefined) {
if (at) {
throw new Error(
errorMsg(
`must not specify 'at' when using attachment shorthand form`,
key,
),
);
}
at = atRef;
}
const disabled = value.disabled;
if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', key, 'disabled'));
}
const extensionRef = value.extension;
if (extensionRef !== undefined && typeof extensionRef !== 'string') {
throw new Error(errorMsg('must be a string', key, 'extension'));
}
const extension = extensionRef
? resolveExtensionRef(extensionRef)
: undefined;
return {
id,
at,
disabled,
extension,
config: value.config /* validate later */,
};
});
}
/** @internal */
export function mergeExtensionParameters(
base: ExtensionInstanceParameters[],
overrides: Array<Partial<ExtensionInstanceParameters>>,
): ExtensionInstanceParameters[] {
const extensionInstanceParams = base.slice();
for (const overrideParam of overrides) {
const existingParamIndex = extensionInstanceParams.findIndex(
e => e.id === overrideParam.id,
);
if (existingParamIndex !== -1) {
const existingParam = extensionInstanceParams[existingParamIndex];
if (overrideParam.at) {
existingParam.at = overrideParam.at;
}
if (overrideParam.extension) {
// TODO: do we want to reset config here? it might be completely
// unrelated to the previous one
existingParam.extension = overrideParam.extension;
}
if (overrideParam.config) {
// TODO: merge config?
existingParam.config = overrideParam.config;
}
if (Boolean(existingParam.disabled) !== Boolean(overrideParam.disabled)) {
existingParam.disabled = Boolean(overrideParam.disabled);
if (!existingParam.disabled) {
// bump
extensionInstanceParams.splice(existingParamIndex, 1);
extensionInstanceParams.push(existingParam);
}
}
} else if (overrideParam.id) {
const { id, at, extension, config } = overrideParam;
if (!at || !extension) {
throw new Error(`Extension ${overrideParam.id} is incomplete`);
}
extensionInstanceParams.push({ id, at, extension, config });
}
}
return extensionInstanceParams.filter(param => !param.disabled);
}
+4 -7
View File
@@ -1,12 +1,9 @@
# @backstage/frontend-plugin-api
_This package was created through the Backstage CLI_.
**This package is EXPERIMENTAL, we recommend against using it for production deployments**
## Installation
This package provides the core API used by Backstage frontend plugins. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372).
Install the package via Yarn:
## Documentation
```sh
cd <package-dir> # if within a monorepo
yarn add @backstage/frontend-plugin-api
```
- [Backstage Documentation](https://backstage.io/docs)
+32 -9
View File
@@ -4,6 +4,10 @@
```ts
import { ComponentType } from 'react';
import { JsonObject } from '@backstage/types';
import { z } from 'zod';
import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
// @public (undocumented)
export type AnyExtensionDataMap = Record<string, ExtensionDataRef<any>>;
@@ -13,7 +17,7 @@ export interface BackstagePlugin {
// (undocumented)
$$type: 'backstage-plugin';
// (undocumented)
defaultExtensionInstances: ExtensionInstanceConfig[];
defaultExtensionInstances: ExtensionInstanceParameters[];
// (undocumented)
id: string;
}
@@ -21,7 +25,7 @@ export interface BackstagePlugin {
// @public (undocumented)
export interface BackstagePluginOptions {
// (undocumented)
defaultExtensionInstances?: ExtensionInstanceConfig[];
defaultExtensionInstances?: ExtensionInstanceParameters[];
// (undocumented)
id: string;
}
@@ -41,7 +45,8 @@ export function createExtension<
extensionData: AnyExtensionDataMap;
}
>,
>(options: CreateExtensionOptions<TData, TPoint>): Extension;
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig>;
// @public (undocumented)
export interface CreateExtensionOptions<
@@ -52,11 +57,14 @@ export interface CreateExtensionOptions<
extensionData: AnyExtensionDataMap;
}
>,
TConfig,
> {
// (undocumented)
configSchema?: PortableSchema<TConfig>;
// (undocumented)
factory(options: {
bind: ExtensionDataBind<TData>;
config?: unknown;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
@@ -73,13 +81,20 @@ export interface CreateExtensionOptions<
export function createPlugin(options: BackstagePluginOptions): BackstagePlugin;
// @public (undocumented)
export interface Extension {
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput>;
// @public (undocumented)
export interface Extension<TConfig> {
// (undocumented)
$$type: 'extension';
// (undocumented)
configSchema?: PortableSchema<TConfig>;
// (undocumented)
factory(options: {
bind: ExtensionDataBind<AnyExtensionDataMap>;
config?: unknown;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
// (undocumented)
@@ -111,14 +126,22 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
};
// @public (undocumented)
export interface ExtensionInstanceConfig {
export interface ExtensionInstanceParameters {
// (undocumented)
at: string;
// (undocumented)
config: unknown;
config?: unknown;
// (undocumented)
extension: Extension;
disabled?: boolean;
// (undocumented)
extension: Extension<unknown>;
// (undocumented)
id: string;
}
// @public (undocumented)
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
```
+4 -1
View File
@@ -33,6 +33,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"dependencies": {
"lodash": "^4.17.21"
"@backstage/types": "workspace:^",
"lodash": "^4.17.21",
"zod": "^3.21.4",
"zod-to-json-schema": "^3.21.4"
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSchemaFromZod } from './createSchemaFromZod';
describe('createSchemaFromZod', () => {
it('should provide nice parse errors', () => {
const { parse } = createSchemaFromZod(z =>
z.object({
foo: z.union([z.string(), z.number()]),
derp: z.object({ bar: z.number() }),
}),
);
expect(() => parse({ derp: { bar: 'derp' } })).toThrow(
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
);
expect(() => parse(undefined)).toThrow(`Missing required value`);
expect(() => parse('derp')).toThrow(`Expected object, received string`);
});
});
@@ -0,0 +1,59 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { z, ZodSchema, ZodTypeDef } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
/** @public */
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
/** @public */
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput> {
const schema = schemaCreator(z);
return {
// TODO: Types allow z.array etc here but it will break stuff
parse: input => {
const result = schema.safeParse(input);
if (result.success) {
return result.data;
}
throw new Error(result.error.issues.map(formatIssue).join('; '));
},
// TODO: Verify why we are not compatible with the latest zodToJsonSchema.
schema: zodToJsonSchema(schema) as JsonObject,
};
}
function formatIssue(issue: z.ZodIssue): string {
if (issue.code === 'invalid_union') {
return formatIssue(issue.unionErrors[0].issues[0]);
}
let message = issue.message;
if (message === 'Required') {
message = `Missing required value`;
}
if (issue.path.length) {
message += ` at '${issue.path.join('.')}'`;
}
return message;
}
+5 -1
View File
@@ -24,7 +24,7 @@ export {
createExtension,
coreExtensionData,
createPlugin,
type ExtensionInstanceConfig,
type ExtensionInstanceParameters,
type BackstagePlugin,
type Extension,
type AnyExtensionDataMap,
@@ -34,3 +34,7 @@ export {
type ExtensionDataRef,
type ExtensionDataValue,
} from './types';
export {
createSchemaFromZod,
type PortableSchema,
} from './createSchemaFromZod';
+16 -9
View File
@@ -15,6 +15,7 @@
*/
import { ComponentType } from 'react';
import { PortableSchema } from './createSchemaFromZod';
/** @public */
export type ExtensionDataRef<T> = {
@@ -51,12 +52,14 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
export interface CreateExtensionOptions<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig,
> {
inputs?: TPoint;
output: TData;
configSchema?: PortableSchema<TConfig>;
factory(options: {
bind: ExtensionDataBind<TData>;
config?: unknown;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
@@ -66,13 +69,15 @@ export interface CreateExtensionOptions<
}
/** @public */
export interface Extension {
export interface Extension<TConfig> {
$$type: 'extension';
// TODO: will extensions have a default "at" as part of their contract, making it optional in the instance config?
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
bind: ExtensionDataBind<AnyExtensionDataMap>;
config?: unknown;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
}
@@ -81,29 +86,31 @@ export interface Extension {
export function createExtension<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
>(options: CreateExtensionOptions<TData, TPoint>): Extension {
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig> {
return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
}
/** @public */
export interface ExtensionInstanceConfig {
export interface ExtensionInstanceParameters {
id: string;
at: string;
extension: Extension;
config: unknown;
extension: Extension<unknown>;
config?: unknown;
disabled?: boolean;
}
/** @public */
export interface BackstagePluginOptions {
id: string;
defaultExtensionInstances?: ExtensionInstanceConfig[];
defaultExtensionInstances?: ExtensionInstanceParameters[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
defaultExtensionInstances: ExtensionInstanceConfig[];
defaultExtensionInstances: ExtensionInstanceParameters[];
}
/** @public */
+1
View File
@@ -37,6 +37,7 @@ Options:
-v --verbose
--omitTechdocsCoreMkdocsPlugin
--legacyCopyReadmeMdToIndexMd
--defaultPlugin [defaultPlugins...]
-h, --help
```
@@ -47,6 +47,7 @@ export default async function generate(opts: OptionValues) {
const dockerImage = opts.dockerImage;
const pullImage = opts.pull;
const legacyCopyReadmeMdToIndexMd = opts.legacyCopyReadmeMdToIndexMd;
const defaultPlugins = opts.defaultPlugin;
logger.info(`Using source dir ${sourceDir}`);
logger.info(`Will output generated files in ${outputDir}`);
@@ -68,6 +69,7 @@ export default async function generate(opts: OptionValues) {
mkdocs: {
legacyCopyReadmeMdToIndexMd,
omitTechdocsCorePlugin,
defaultPlugins,
},
},
},
@@ -70,6 +70,11 @@ export function registerCommands(program: Command) {
'Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md in case a default <docs-dir>/index.md is not provided.',
false,
)
.option(
'--defaultPlugin [defaultPlugins...]',
'Plugins which should be added automatically to the mkdocs.yaml file',
[],
)
.alias('build')
.action(lazy(() => import('./generate/generate').then(m => m.default)));
@@ -45,6 +45,7 @@ export const spec = {
name: 'kind',
in: 'path',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -53,6 +54,7 @@ export const spec = {
name: 'namespace',
in: 'path',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -61,6 +63,7 @@ export const spec = {
name: 'name',
in: 'path',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -69,6 +72,7 @@ export const spec = {
name: 'uid',
in: 'path',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -78,6 +82,7 @@ export const spec = {
in: 'query',
description: 'Cursor to a set page of results.',
required: false,
allowReserved: true,
schema: {
type: 'string',
minLength: 1,
@@ -88,6 +93,7 @@ export const spec = {
in: 'query',
description: 'Pointer to the previous page of results.',
required: false,
allowReserved: true,
schema: {
type: 'string',
minLength: 1,
@@ -111,6 +117,7 @@ export const spec = {
in: 'query',
description: 'Filter for just the entities defined by this filter.',
required: false,
allowReserved: true,
schema: {
type: 'array',
items: {
@@ -123,6 +130,7 @@ export const spec = {
in: 'query',
description: 'Number of records to skip in the query page.',
required: false,
allowReserved: true,
schema: {
type: 'integer',
minimum: 0,
@@ -133,6 +141,7 @@ export const spec = {
in: 'query',
description: 'Number of records to return in the response.',
required: false,
allowReserved: true,
schema: {
type: 'integer',
minimum: 0,
@@ -1037,6 +1046,7 @@ export const spec = {
in: 'query',
description: 'Text search term.',
required: false,
allowReserved: true,
schema: {
type: 'string',
},
@@ -1047,6 +1057,7 @@ export const spec = {
description:
'A comma separated list of fields to sort returned results by.',
required: false,
allowReserved: true,
schema: {
type: 'array',
items: {
@@ -1086,6 +1097,7 @@ export const spec = {
in: 'query',
name: 'facet',
required: true,
allowReserved: true,
schema: {
type: 'array',
items: {
@@ -1141,6 +1153,7 @@ export const spec = {
in: 'query',
name: 'dryRun',
required: false,
allowReserved: true,
schema: {
type: 'string',
},
@@ -1226,6 +1239,7 @@ export const spec = {
in: 'path',
name: 'id',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -1251,6 +1265,7 @@ export const spec = {
in: 'path',
name: 'id',
required: true,
allowReserved: true,
schema: {
type: 'string',
},
@@ -1295,7 +1310,7 @@ export const spec = {
$ref: '#/components/schemas/LocationInput',
},
},
required: ['catalogFileName', 'location'],
required: ['location'],
},
},
},
@@ -20,24 +20,28 @@ components:
name: kind
in: path
required: true
allowReserved: true
schema:
type: string
namespace:
name: namespace
in: path
required: true
allowReserved: true
schema:
type: string
name:
name: name
in: path
required: true
allowReserved: true
schema:
type: string
uid:
name: uid
in: path
required: true
allowReserved: true
schema:
type: string
cursor:
@@ -45,6 +49,7 @@ components:
in: query
description: Cursor to a set page of results.
required: false
allowReserved: true
schema:
type: string
minLength: 1
@@ -53,6 +58,7 @@ components:
in: query
description: Pointer to the previous page of results.
required: false
allowReserved: true
schema:
type: string
minLength: 1
@@ -71,6 +77,7 @@ components:
in: query
description: Filter for just the entities defined by this filter.
required: false
allowReserved: true
schema:
type: array
items:
@@ -80,6 +87,7 @@ components:
in: query
description: Number of records to skip in the query page.
required: false
allowReserved: true
schema:
type: integer
minimum: 0
@@ -88,6 +96,7 @@ components:
in: query
description: Number of records to return in the response.
required: false
allowReserved: true
schema:
type: integer
minimum: 0
@@ -788,12 +797,14 @@ paths:
in: query
description: Text search term.
required: false
allowReserved: true
schema:
type: string
- name: fullTextFilterFields
in: query
description: A comma separated list of fields to sort returned results by.
required: false
allowReserved: true
schema:
type: array
items:
@@ -818,6 +829,7 @@ paths:
- in: query
name: facet
required: true
allowReserved: true
schema:
type: array
items:
@@ -853,6 +865,7 @@ paths:
- in: query
name: dryRun
required: false
allowReserved: true
schema:
type: string
requestBody:
@@ -908,6 +921,7 @@ paths:
- in: path
name: id
required: true
allowReserved: true
schema:
type: string
delete:
@@ -923,6 +937,7 @@ paths:
- in: path
name: id
required: true
allowReserved: true
schema:
type: string
/analyze-location:
@@ -952,7 +967,6 @@ paths:
location:
$ref: '#/components/schemas/LocationInput'
required:
- catalogFileName
- location
/validate-entity:
post:
@@ -28,11 +28,11 @@ describe('parseQueryEntitiesParams', () => {
const validRequest = {
authorizationToken: 'to_not_be_returned',
fields: ['kind'],
limit: '3',
limit: 3,
filter: ['a=1', 'b=2'],
orderField: ['metadata.name,desc'],
fullTextFilterTerm: 'query',
fullTextFilterFields: 'metadata.name,metadata.namespace',
fullTextFilterFields: ['metadata.name', 'metadata.namespace'],
};
const parsedObj = parseQueryEntitiesParams(
validRequest,
@@ -65,9 +65,7 @@ describe('parseQueryEntitiesParams', () => {
{ filter: 3 },
{ orderField: ['metadata.uid,diagonal'] },
{ fields: [4] },
{ fullTextFilterTerm: [] },
{ fullTextFilterFields: 3 },
])('should throw if some parameter is not valid %p', params => {
])('should throw if some parameter is not valid %p', (params: any) => {
expect(() => parseQueryEntitiesParams(params)).toThrow();
});
});
@@ -83,7 +81,7 @@ describe('parseQueryEntitiesParams', () => {
const validRequest = {
authorizationToken: 'to_not_be_returned',
fields: ['kind'],
limit: '3',
limit: 3,
cursor: encodeCursor(cursor),
};
const parsedObj = parseQueryEntitiesParams(
@@ -103,10 +101,10 @@ describe('parseQueryEntitiesParams', () => {
const validRequest = {
authorizationToken: 'to_not_be_returned',
fields: ['kind'],
limit: '3',
limit: 3,
cursor: encodeCursor(cursor),
filter: ['a=1', 'b=2'],
orderField: 'orderField,desc',
orderField: ['orderField,desc'],
query: 'query',
};
const parsedObj = parseQueryEntitiesParams(
@@ -128,7 +126,7 @@ describe('parseQueryEntitiesParams', () => {
it.each([{ cursor: [] }, { fields: [4] }])(
'should throw if some parameter is not valid %p',
params => {
(params: any) => {
expect(() => parseQueryEntitiesParams(params)).toThrow();
},
);
@@ -20,19 +20,19 @@ import {
QueryEntitiesRequest,
} from '../../catalog/types';
import { decodeCursor } from '../util';
import { parseStringParam } from './common';
import { parseEntityFilterParams } from './parseEntityFilterParams';
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
import { parseEntityTransformParams } from './parseEntityTransformParams';
import { parseFullTextFilterFields } from './parseFullTextFilterFields';
import { spec } from '../../schema/openapi.generated';
import { internal } from '@backstage/backend-openapi-utils';
export function parseQueryEntitiesParams(
params: Record<string, unknown>,
params: internal.QuerySchema<typeof spec, '/entities/by-query', 'get'>,
): Omit<QueryEntitiesRequest, 'authorizationToken' | 'limit'> {
const fields = parseEntityTransformParams(params);
const cursor = parseStringParam(params.cursor, 'cursor');
if (cursor) {
const decodedCursor = decodeCursor(cursor);
if (params.cursor) {
const decodedCursor = decodeCursor(params.cursor);
const response: Omit<QueryEntitiesCursorRequest, 'authorizationToken'> = {
cursor: decodedCursor,
fields,
@@ -41,12 +41,6 @@ export function parseQueryEntitiesParams(
}
const filter = parseEntityFilterParams(params);
const fullTextFilterTerm = parseStringParam(
params.fullTextFilterTerm,
'fullTextFilterTerm',
);
const fullTextFilterFields = parseFullTextFilterFields(params);
const orderFields = parseEntityOrderFieldParams(params);
const response: Omit<QueryEntitiesInitialRequest, 'authorizationToken'> = {
@@ -54,8 +48,8 @@ export function parseQueryEntitiesParams(
filter,
orderFields,
fullTextFilter: {
term: fullTextFilterTerm || '',
fields: fullTextFilterFields,
term: params.fullTextFilterTerm || '',
fields: params.fullTextFilterFields,
},
};
+3
View File
@@ -233,6 +233,9 @@ const defaultConfig = [
y: 0,
width: 12,
height: 1,
movable: true,
resizable: false,
deletable: false,
},
];
+3
View File
@@ -148,6 +148,9 @@ export type LayoutConfiguration = {
y: number;
width: number;
height: number;
movable?: boolean;
deletable?: boolean;
resizable?: boolean;
};
// @public @deprecated (undocumented)
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ReactNode, useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import {
ElementCollection,
@@ -40,6 +40,7 @@ import { WidgetSettingsOverlay } from './WidgetSettingsOverlay';
import { AddWidgetDialog } from './AddWidgetDialog';
import { CustomHomepageButtons } from './CustomHomepageButtons';
import {
CustomHomepageGridProps,
CustomHomepageGridStateV1,
CustomHomepageGridStateV1Schema,
GridWidget,
@@ -158,6 +159,9 @@ const convertConfigToDefaultWidgets = (
isResizable: false,
},
settings: {},
movable: conf.movable,
deletable: conf.deletable,
resizable: conf.resizable,
};
});
return compact(ret);
@@ -190,82 +194,6 @@ const availableWidgetsFilter = (elements: ElementCollection) => {
});
};
/**
* Breakpoint options for <CustomHomepageGridProps/>
*
* @public
*/
export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* Props customizing the <CustomHomepageGrid/> component.
*
* @public
*/
export type CustomHomepageGridProps = {
/**
* Children contain all widgets user can configure on their own homepage.
*/
children?: ReactNode;
/**
* Default layout for the homepage before users have modified it.
*/
config?: LayoutConfiguration[];
/**
* Height of grid row in pixels.
* @defaultValue 60
*/
rowHeight?: number;
/**
* Screen width in pixels for different breakpoints.
* @defaultValue theme breakpoints
*/
breakpoints?: Record<Breakpoint, number>;
/**
* Number of grid columns for different breakpoints.
* @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \}
*/
cols?: Record<Breakpoint, number>;
/**
* Grid container padding (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerPadding?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Grid container margin (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerMargin?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Maximum number of rows user can have in the grid.
* @defaultValue unlimited
*/
maxRows?: number;
/**
* Custom style for grid.
*/
style?: React.CSSProperties;
/**
* Compaction type of widgets in the grid. This controls where widgets are moved in case
* they are overlapping in the grid.
*/
compactType?: 'vertical' | 'horizontal' | null;
/**
* Controls if widgets can overlap in the grid. If true, grid can be placed one over the other.
* @defaultValue false
*/
allowOverlap?: boolean;
/**
* Controls if widgets can collide with each other. If true, grid items won't change position when being dragged over.
* @defaultValue false
*/
preventCollision?: boolean;
};
/**
* A component that allows customizing components in home grid layout.
*
@@ -318,6 +246,9 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
isDraggable: editMode,
},
settings: {},
movable: widget.movable,
deletable: widget.deletable,
resizable: widget.resizable,
},
]);
setAddWidgetDialogOpen(false);
@@ -348,9 +279,11 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
setEditMode(mode);
setWidgets(
widgets.map(w => {
const resizable = w.resizable === false ? false : mode;
const movable = w.movable === false ? false : mode;
return {
...w,
layout: { ...w.layout, isDraggable: mode, isResizable: mode },
layout: { ...w.layout, isDraggable: movable, isResizable: resizable },
};
}),
);
@@ -370,7 +303,20 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
};
const handleRestoreDefaultConfig = () => {
setWidgets(defaultLayout);
setWidgets(
defaultLayout.map(w => {
const resizable = w.resizable === false ? false : editMode;
const movable = w.movable === false ? false : editMode;
return {
...w,
layout: {
...w.layout,
isDraggable: movable,
isResizable: resizable,
},
};
}),
);
};
return (
@@ -404,7 +350,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
style={props.style}
allowOverlap={props.allowOverlap}
preventCollision={props.preventCollision}
draggableCancel=".overlayGridItem,.widgetSettingsDialog"
draggableCancel=".overlayGridItem,.widgetSettingsDialog,.disabled"
containerPadding={props.containerPadding}
margin={props.containerMargin}
breakpoints={
@@ -435,7 +381,9 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
return (
<div
key={l.i}
className={`${styles.widgetWrapper} ${editMode && 'edit'}`}
className={`${styles.widgetWrapper} ${editMode && 'edit'} ${
w.movable === false && 'disabled'
}`}
>
<ErrorBoundary>
<widget.component.type {...widgetProps} />
@@ -447,6 +395,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
handleRemove={handleRemove}
handleSettingsSave={handleSettingsSave}
settings={w.settings}
deletable={w.deletable}
/>
)}
</div>
@@ -58,10 +58,12 @@ interface WidgetSettingsOverlayProps {
handleRemove: (id: string) => void;
handleSettingsSave: (id: string, settings: Record<string, any>) => void;
settings?: Record<string, any>;
deletable?: boolean;
}
export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
const { id, widget, settings, handleRemove, handleSettingsSave } = props;
const { id, widget, settings, handleRemove, handleSettingsSave, deletable } =
props;
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
const styles = useStyles();
@@ -110,13 +112,15 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
</Tooltip>
</Grid>
)}
<Grid item className="overlayGridItem">
<Tooltip title="Delete widget">
<IconButton color="secondary" onClick={() => handleRemove(id)}>
<DeleteIcon fontSize="large" />
</IconButton>
</Tooltip>
</Grid>
{deletable !== false && (
<Grid item className="overlayGridItem">
<Tooltip title="Delete widget">
<IconButton color="secondary" onClick={() => handleRemove(id)}>
<DeleteIcon fontSize="large" />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
</div>
);
@@ -14,5 +14,8 @@
* limitations under the License.
*/
export { CustomHomepageGrid } from './CustomHomepageGrid';
export type { CustomHomepageGridProps, Breakpoint } from './CustomHomepageGrid';
export type { LayoutConfiguration } from './types';
export type {
CustomHomepageGridProps,
Breakpoint,
LayoutConfiguration,
} from './types';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ReactElement } from 'react';
import React, { ReactElement, ReactNode } from 'react';
import { Layout } from 'react-grid-layout';
import { z } from 'zod';
import { RJSFSchema, UiSchema } from '@rjsf/utils';
@@ -24,12 +24,91 @@ const RSJFTypeUiSchema: z.ZodType<UiSchema> = z.any();
const ReactElementSchema: z.ZodType<ReactElement> = z.any();
const LayoutSchema: z.ZodType<Layout> = z.any();
/**
* Breakpoint options for <CustomHomepageGridProps/>
*
* @public
*/
export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* Props customizing the <CustomHomepageGrid/> component.
*
* @public
*/
export type CustomHomepageGridProps = {
/**
* Children contain all widgets user can configure on their own homepage.
*/
children?: ReactNode;
/**
* Default layout for the homepage before users have modified it.
*/
config?: LayoutConfiguration[];
/**
* Height of grid row in pixels.
* @defaultValue 60
*/
rowHeight?: number;
/**
* Screen width in pixels for different breakpoints.
* @defaultValue theme breakpoints
*/
breakpoints?: Record<Breakpoint, number>;
/**
* Number of grid columns for different breakpoints.
* @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \}
*/
cols?: Record<Breakpoint, number>;
/**
* Grid container padding (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerPadding?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Grid container margin (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerMargin?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Maximum number of rows user can have in the grid.
* @defaultValue unlimited
*/
maxRows?: number;
/**
* Custom style for grid.
*/
style?: React.CSSProperties;
/**
* Compaction type of widgets in the grid. This controls where widgets are moved in case
* they are overlapping in the grid.
*/
compactType?: 'vertical' | 'horizontal' | null;
/**
* Controls if widgets can overlap in the grid. If true, grid can be placed one over the other.
* @defaultValue false
*/
allowOverlap?: boolean;
/**
* Controls if widgets can collide with each other. If true, grid items won't change position when being dragged over.
* @defaultValue false
*/
preventCollision?: boolean;
};
export const LayoutConfigurationSchema = z.object({
component: ReactElementSchema,
x: z.number().nonnegative('x must be positive number'),
y: z.number().nonnegative('y must be positive number'),
width: z.number().positive('width must be positive number'),
height: z.number().positive('height must be positive number'),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
/**
@@ -43,6 +122,9 @@ export type LayoutConfiguration = {
y: number;
width: number;
height: number;
movable?: boolean;
deletable?: boolean;
resizable?: boolean;
};
export const WidgetSchema = z.object({
@@ -64,6 +146,9 @@ export const WidgetSchema = z.object({
.optional(),
settingsSchema: RSJFTypeSchema.optional(),
uiSchema: RSJFTypeUiSchema.optional(),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
export type Widget = z.infer<typeof WidgetSchema>;
@@ -72,6 +157,9 @@ const GridWidgetSchema = z.object({
id: z.string(),
layout: LayoutSchema,
settings: z.record(z.string(), z.any()),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
export type GridWidget = z.infer<typeof GridWidgetSchema>;
@@ -16,7 +16,6 @@
import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import { endOfDay, startOfDay } from '@date-io/luxon';
import {
MuiPickersUtilsProvider,
KeyboardDatePicker,
@@ -67,14 +66,14 @@ const SelectWindow = ({ windowOptions, window, setWindow }) => {
};
const handleStartDateChange = date => {
if (isValid(date)) {
setStartDate(startOfDay(date));
if (isValid(date.toJSDate())) {
setStartDate(date.startOf('day').toJSDate());
}
};
const handleEndDateChange = date => {
if (isValid(date)) {
setEndDate(endOfDay(date));
if (isValid(date.toJSDate())) {
setEndDate(date.endOf('day').toJSDate());
}
};
+5 -2
View File
@@ -30,10 +30,13 @@ export const EntityOwnershipCard: (props: {
variant?: InfoCardVariants | undefined;
entityFilterKind?: string[] | undefined;
hideRelationsToggle?: boolean | undefined;
relationsType?: string | undefined;
relationsType?: EntityRelationAggregation | undefined;
entityLimit?: number | undefined;
}) => JSX_2.Element;
// @public (undocumented)
export type EntityRelationAggregation = 'direct' | 'aggregated';
// @public (undocumented)
export const EntityUserProfileCard: (props: {
variant?: InfoCardVariants | undefined;
@@ -77,7 +80,7 @@ export const OwnershipCard: (props: {
variant?: InfoCardVariants;
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
relationsType?: string;
relationsType?: EntityRelationAggregation;
entityLimit?: number;
}) => React_2.JSX.Element;
+1
View File
@@ -60,6 +60,7 @@
"@testing-library/dom": "^8.0.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"msw": "^1.0.0"
@@ -34,6 +34,7 @@ import React from 'react';
import pluralize from 'pluralize';
import { catalogIndexRouteRef } from '../../../routes';
import { useGetEntities } from './useGetEntities';
import { EntityRelationAggregation } from './types';
const useStyles = makeStyles((theme: BackstageTheme) =>
createStyles({
@@ -109,13 +110,11 @@ const EntityCountTile = ({
export const ComponentsGrid = ({
entity,
relationsType,
isGroup,
entityFilterKind,
entityLimit = 6,
}: {
entity: Entity;
relationsType: string;
isGroup: boolean;
relationsType: EntityRelationAggregation;
entityFilterKind?: string[];
entityLimit?: number;
}) => {
@@ -123,7 +122,6 @@ export const ComponentsGrid = ({
const { componentsWithCounters, loading, error } = useGetEntities(
entity,
relationsType,
isGroup,
entityFilterKind,
entityLimit,
);
@@ -263,10 +263,11 @@ describe('OwnershipCard', () => {
},
);
expect(getByText('OPENAPI').closest('a')).toHaveAttribute(
'href',
'/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all',
);
const href = getByText('OPENAPI').closest('a')?.href ?? '';
// This env does not support URLSearchParams
const queryParams = decodeURIComponent(href);
expect(queryParams).toContain('filters[owners]=my-team');
});
it('links to the catalog with the user and groups filters from an user profile', async () => {
@@ -299,7 +300,7 @@ describe('OwnershipCard', () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={userEntity}>
<OwnershipCard />
<OwnershipCard relationsType="aggregated" />
</EntityProvider>
</TestApiProvider>,
{
@@ -309,9 +310,11 @@ describe('OwnershipCard', () => {
},
);
expect(getByText('OPENAPI').closest('a')).toHaveAttribute(
'href',
'/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all',
const href = getByText('OPENAPI').closest('a')?.href ?? '';
// This env does not support URLSearchParams
const queryParams = decodeURIComponent(href);
expect(queryParams).toMatch(
/filters\[owners\]=custom\/some\-team.*filters\[owners\]=user:the-user/,
);
});
@@ -27,6 +27,7 @@ import {
} from '@material-ui/core';
import React, { useState } from 'react';
import { ComponentsGrid } from './ComponentsGrid';
import { EntityRelationAggregation } from './types';
const useStyles = makeStyles(theme => ({
list: {
@@ -56,7 +57,7 @@ export const OwnershipCard = (props: {
variant?: InfoCardVariants;
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
relationsType?: string;
relationsType?: EntityRelationAggregation;
entityLimit?: number;
}) => {
const {
@@ -70,7 +71,6 @@ export const OwnershipCard = (props: {
hideRelationsToggle === undefined ? false : hideRelationsToggle;
const classes = useStyles();
const { entity } = useEntity();
const isGroup = entity.kind === 'Group';
const [getRelationsType, setRelationsType] = useState(
relationsType || 'direct',
);
@@ -102,7 +102,6 @@ export const OwnershipCard = (props: {
}
name="pin"
inputProps={{ 'aria-label': 'Ownership Type Switch' }}
disabled={!isGroup}
/>
</Tooltip>
Aggregated Relations
@@ -114,7 +113,6 @@ export const OwnershipCard = (props: {
entity={entity}
entityLimit={entityLimit}
relationsType={getRelationsType}
isGroup={isGroup}
entityFilterKind={entityFilterKind}
/>
</InfoCard>
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './OwnershipCard';
export * from './types';
@@ -1,5 +1,3 @@
import { parseStringParam } from './common';
/*
* Copyright 2023 The Backstage Authors
*
@@ -15,14 +13,6 @@ import { parseStringParam } from './common';
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function parseFullTextFilterFields(params: Record<string, unknown>) {
const fullTextFilterFields = parseStringParam(
params.fullTextFilterFields,
'fullTextFilterFields',
);
if (!fullTextFilterFields) {
return undefined;
}
return fullTextFilterFields.split(',');
}
/** @public */
export type EntityRelationAggregation = 'direct' | 'aggregated';
@@ -0,0 +1,150 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
import { useGetEntities } from './useGetEntities';
import { CatalogApi } from '@backstage/catalog-client';
import { renderHook } from '@testing-library/react-hooks';
import { getEntityRelations } from '@backstage/plugin-catalog-react';
const givenParentGroup = 'team.squad1';
const givenLeafGroup = 'team.squad2';
const givenUser = 'user.john';
const givenParentGroupEntity = {
kind: 'Group',
metadata: {
name: givenParentGroup,
},
} as Partial<Entity> as Entity;
const givenLeafGroupEntity = {
kind: 'Group',
metadata: {
name: givenLeafGroup,
},
} as Partial<Entity> as Entity;
const givenUserEntity = {
kind: 'User',
metadata: {
name: givenUser,
},
} as Partial<Entity> as Entity;
const catalogApiMock: Pick<CatalogApi, 'getEntities' | 'getEntitiesByRefs'> = {
getEntities: jest.fn(async () => Promise.resolve({ items: [] })),
getEntitiesByRefs: jest.fn(async ({ entityRefs: [ref] }) =>
ref.includes(givenParentGroup)
? { items: [givenParentGroupEntity] }
: { items: [givenLeafGroupEntity] },
),
};
jest.mock('@backstage/core-plugin-api', () => ({
useApi: jest.fn(() => catalogApiMock),
}));
jest.mock('@backstage/plugin-catalog-react', () => ({
catalogApiRef: {},
getEntityRelations: jest.fn(entity => {
if (entity?.metadata.name === givenParentGroup) {
return [
{
kind: 'Group',
namespace: 'default',
name: givenLeafGroup,
} as CompoundEntityRef,
];
} else if (entity?.kind === 'User') {
return [
{
kind: 'Group',
namespace: 'default',
name: givenLeafGroup,
} as CompoundEntityRef,
];
}
return [];
}) as typeof getEntityRelations,
}));
describe('useGetEntities', () => {
const ownersFilter = (...owners: string[]) =>
expect.objectContaining({
filter: expect.arrayContaining([
expect.objectContaining({
'relations.ownedBy': owners,
}),
]),
});
describe('given aggregated relationsType', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const hook = renderHook(
({ entity }) => useGetEntities(entity, 'aggregated'),
{
initialProps: { entity: _entity },
},
);
await hook.waitForNextUpdate();
};
it('given group entity should aggregate child ownership', async () => {
await whenHookIsCalledWith(givenParentGroupEntity);
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
ownersFilter(
`group:default/${givenParentGroup}`,
`group:default/${givenLeafGroup}`,
),
);
});
it('given user entity should aggregate parent ownership and direct', async () => {
await whenHookIsCalledWith(givenUserEntity);
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
ownersFilter(
`group:default/${givenLeafGroup}`,
`user:default/${givenUser}`,
),
);
});
});
describe('given direct relationsType', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const hook = renderHook(
({ entity }) => useGetEntities(entity, 'direct'),
{
initialProps: { entity: _entity },
},
);
await hook.waitForNextUpdate();
};
it('given group entity should return directly owned entities', async () => {
await whenHookIsCalledWith(givenLeafGroupEntity);
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
ownersFilter(`group:default/${givenLeafGroup}`),
);
});
it('given user entity should return directly owned entities', async () => {
await whenHookIsCalledWith(givenUserEntity);
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
ownersFilter(`user:default/${givenUser}`),
);
});
});
});
@@ -31,6 +31,7 @@ import limiterFactory from 'p-limit';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import qs from 'qs';
import { EntityRelationAggregation as EntityRelationsAggregation } from './types';
const limiter = limiterFactory(10);
@@ -57,78 +58,109 @@ const getQueryParams = (
return qs.stringify({ filters }, { arrayFormat: 'repeat' });
};
const getOwnersEntityRef = (owner: Entity): string[] => {
let owners = [stringifyEntityRef(owner)];
if (owner.kind === 'User') {
const ownerGroups = getEntityRelations(owner, RELATION_MEMBER_OF, {
kind: 'Group',
const getMemberOfEntityRefs = (owner: Entity): string[] => {
const parentGroups = getEntityRelations(owner, RELATION_MEMBER_OF, {
kind: 'Group',
});
const ownerGroupsNames = parentGroups.map(({ kind, namespace, name }) =>
stringifyEntityRef({
kind,
namespace,
name,
}),
);
return [...ownerGroupsNames, stringifyEntityRef(owner)];
};
const isEntity = (entity: Entity | undefined): entity is Entity =>
entity !== undefined;
const getChildOwnershipEntityRefs = async (
entity: Entity,
catalogApi: CatalogApi,
): Promise<string[]> => {
const childGroups = getEntityRelations(entity, RELATION_PARENT_OF, {
kind: 'Group',
});
const hasChildGroups = childGroups.length > 0;
if (hasChildGroups) {
const entityRefs = childGroups.map(r => stringifyEntityRef(r));
const childGroupResponse = await catalogApi.getEntitiesByRefs({
fields: ['kind', 'metadata.namespace', 'metadata.name'],
entityRefs,
});
const ownerGroupsName = ownerGroups.map(ownerGroup =>
stringifyEntityRef({
kind: ownerGroup.kind,
namespace: ownerGroup.namespace,
name: ownerGroup.name,
}),
);
owners = [...owners, ...ownerGroupsName];
const childGroupEntities = childGroupResponse.items.filter(isEntity);
return (
await Promise.all(
childGroupEntities.map(childGroupEntity =>
limiter(() =>
getChildOwnershipEntityRefs(childGroupEntity, catalogApi),
),
),
)
).flatMap(aggregated => aggregated);
}
return [stringifyEntityRef(entity)];
};
const getOwners = async (
entity: Entity,
relations: EntityRelationsAggregation,
catalogApi: CatalogApi,
): Promise<string[]> => {
const isGroup = entity.kind === 'Group';
const isAggregated = relations === 'aggregated';
const isUserEntity = entity.kind === 'User';
const owners: string[] = [];
if (isAggregated && isGroup) {
const childEntityRefs = await getChildOwnershipEntityRefs(
entity,
catalogApi,
);
owners.push(stringifyEntityRef(entity));
owners.push.apply(owners, childEntityRefs);
} else if (isAggregated && isUserEntity) {
const parentEntityRefs = getMemberOfEntityRefs(entity);
owners.push.apply(owners, parentEntityRefs);
} else {
owners.push(stringifyEntityRef(entity));
}
return owners;
};
const getAggregatedOwnersEntityRef = async (
parentGroup: Entity,
const getOwnedEntitiesByOwners = (
owners: string[],
kinds: string[],
catalogApi: CatalogApi,
): Promise<string[]> => {
const requestedEntities: Entity[] = [];
const outstandingEntities = new Map<string, Promise<Entity | undefined>>();
const processedEntities = new Set<string>();
requestedEntities.push(parentGroup);
let currentEntity = parentGroup;
while (requestedEntities.length > 0) {
const childRelations = getEntityRelations(
currentEntity,
RELATION_PARENT_OF,
) =>
catalogApi.getEntities({
filter: [
{
kind: 'Group',
kind: kinds,
'relations.ownedBy': owners,
},
);
await Promise.all(
childRelations.map(childGroup =>
limiter(async () => {
const promise = catalogApi.getEntityByRef(childGroup);
outstandingEntities.set(childGroup.name, promise);
try {
const processedEntity = await promise;
if (processedEntity) {
requestedEntities.push(processedEntity);
}
} finally {
outstandingEntities.delete(childGroup.name);
}
}),
),
);
requestedEntities.shift();
processedEntities.add(
stringifyEntityRef({
kind: currentEntity.kind,
namespace: currentEntity.metadata.namespace,
name: currentEntity.metadata.name,
}),
);
// always set currentEntity to the first element of array requestedEntities
currentEntity = requestedEntities[0];
}
return Array.from(processedEntities);
};
],
fields: [
'kind',
'metadata.name',
'metadata.namespace',
'spec.type',
'relations',
],
});
export function useGetEntities(
entity: Entity,
relationsType: string,
isGroup: boolean,
relations: EntityRelationsAggregation,
entityFilterKind?: string[],
entityLimit = 6,
): {
@@ -151,25 +183,13 @@ export function useGetEntities(
error,
value: componentsWithCounters,
} = useAsync(async () => {
const owners =
relationsType === 'aggregated' && isGroup
? await getAggregatedOwnersEntityRef(entity, catalogApi)
: getOwnersEntityRef(entity);
const ownedEntitiesList = await catalogApi.getEntities({
filter: [
{
kind: kinds,
'relations.ownedBy': owners,
},
],
fields: [
'kind',
'metadata.name',
'metadata.namespace',
'spec.type',
'relations',
],
});
const owners = await getOwners(entity, relations, catalogApi);
const ownedEntitiesList = await getOwnedEntitiesByOwners(
owners,
kinds,
catalogApi,
);
const counts = ownedEntitiesList.items.reduce(
(acc: EntityTypeProps[], ownedEntity) => {
@@ -204,7 +224,7 @@ export function useGetEntities(
kind: string;
queryParams: string;
}>;
}, [catalogApi, entity, relationsType]);
}, [catalogApi, entity, relations]);
return {
componentsWithCounters,
+4
View File
@@ -14,3 +14,7 @@
* limitations under the License.
*/
import '@testing-library/jest-dom';
beforeEach(() => {
jest.clearAllMocks();
});
@@ -108,7 +108,7 @@ export const PlaylistEntitiesTable = ({
const actions = editAllowed
? [
{
icon: DeleteIcon,
icon: () => <DeleteIcon color="secondary" />,
tooltip: `Remove from ${singularTitleLowerCase}`,
onClick: removeEntity,
},
@@ -166,7 +166,7 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => {
},
{
label: `Delete ${singularTitle}`,
icon: <DeleteIcon />,
icon: <DeleteIcon color="secondary" />,
disabled: !deleteAllowed,
onClick: () => setOpenDeleteDialog(true),
},
@@ -0,0 +1,133 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ScmIntegrations,
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
} from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { PassThrough } from 'stream';
import { createGithubActionsDispatchAction } from './githubActionsDispatch';
import yaml from 'yaml';
import { examples } from './githubActionsDispatch.examples';
const mockOctokit = {
rest: {
actions: {
createWorkflowDispatch: jest.fn(),
},
},
};
jest.mock('octokit', () => ({
Octokit: class {
constructor() {
return mockOctokit;
}
},
}));
describe('github:actions:dispatch', () => {
const config = new ConfigReader({
integrations: {
github: [
{ host: 'github.com', token: 'tokenlols' },
{ host: 'ghe.github.com' },
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
let githubCredentialsProvider: GithubCredentialsProvider;
let action: TemplateAction<any>;
const mockContext = {
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
workflowId: 'a-workflow-id',
branchOrTagName: 'main',
},
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
action = createGithubActionsDispatchAction({
integrations,
githubCredentialsProvider,
});
});
it('should call the githubApis for creating WorkflowDispatch without an input object', async () => {
mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({
data: {
foo: 'bar',
},
});
const repoUrl = 'github.com?repo=repo&owner=owner';
const workflowId = 'dispatch_workflow';
const branchOrTagName = 'main';
const ctx = Object.assign({}, mockContext, {
input: { repoUrl, workflowId, branchOrTagName },
});
await action.handler(ctx);
expect(
mockOctokit.rest.actions.createWorkflowDispatch,
).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
workflow_id: workflowId,
ref: branchOrTagName,
});
});
it('should call the githubApis for creating WorkflowDispatch with an input object', async () => {
mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({
data: {
foo: 'bar',
},
});
const repoUrl = 'github.com?repo=repo&owner=owner';
const workflowId = 'dispatch_workflow';
const branchOrTagName = 'main';
const workflowInputs = yaml.parse(examples[1].example).steps[0].input
.workflowInputs;
const ctx = Object.assign({}, mockContext, {
input: { repoUrl, workflowId, branchOrTagName, workflowInputs },
});
await action.handler(ctx);
expect(
mockOctokit.rest.actions.createWorkflowDispatch,
).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
workflow_id: workflowId,
ref: branchOrTagName,
inputs: workflowInputs,
});
});
});
@@ -0,0 +1,74 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
import yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description: 'GitHub Action Workflow Without Inputs.',
example: yaml.stringify({
steps: [
{
action: 'github:actions:dispatch',
name: 'Dispatch Github Action Workflow',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
workflowId: 'WORKFLOW_ID',
branchOrTagName: 'main',
},
},
],
}),
},
{
description: 'GitHub Action Workflow With Inputs',
example: yaml.stringify({
steps: [
{
action: 'github:actions:dispatch',
name: 'Dispatch Github Action Workflow with inputs',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
workflowId: 'WORKFLOW_ID',
branchOrTagName: 'main',
workflowInputs: {
input1: 'value1',
input2: 'value2',
},
},
},
],
}),
},
{
description: 'GitHub Action Workflow With Custom Token',
example: yaml.stringify({
steps: [
{
action: 'github:actions:dispatch',
name: 'Dispatch GitHub Action Workflow (custom token)',
input: {
repoUrl: 'github.com?repo=reponame&owner=owner',
workflowId: 'WORKFLOW_ID',
branchOrTagName: 'release-1.0',
token: '${{ secrets.MY_CUSTOM_TOKEN }}',
},
},
],
}),
},
];
@@ -23,6 +23,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { Octokit } from 'octokit';
import { parseRepoUrl } from '../publish/util';
import { getOctokitOptions } from './helpers';
import { examples } from './githubActionsDispatch.examples';
/**
* Creates a new action that dispatches a GitHub Action workflow for a given branch or tag.
@@ -44,6 +45,7 @@ export function createGithubActionsDispatchAction(options: {
id: 'github:actions:dispatch',
description:
'Dispatches a GitHub Action workflow for a given branch or tag',
examples,
schema: {
input: {
type: 'object',
@@ -102,7 +102,7 @@ export const useSearchContextCheck = () => {
* The initial state of `SearchContextProvider`.
*
*/
const searchInitialState: SearchContextState = {
const defaultInitialSearchState: SearchContextState = {
term: '',
types: [],
filters: {},
@@ -111,7 +111,7 @@ const searchInitialState: SearchContextState = {
};
const useSearchContextValue = (
initialValue: SearchContextState = searchInitialState,
initialValue: SearchContextState = defaultInitialSearchState,
) => {
const searchApi = useApi(searchApiRef);
@@ -245,12 +245,16 @@ export const SearchContextProvider = (props: SearchContextProviderProps) => {
const configApi = useApi(configApiRef);
const propsInitialSearchState = initialState ?? {};
const configInitialSearchState = configApi.has('search.query.pageLimit')
? { pageLimit: configApi.getNumber('search.query.pageLimit') }
: {};
const searchContextInitialState = {
...searchInitialState,
...(initialState || {}),
pageLimit:
configApi.getOptionalNumber('search.query.pageLimit') ||
initialState?.pageLimit,
...defaultInitialSearchState,
...propsInitialSearchState,
...configInitialSearchState,
};
return hasParentContext && inheritParentContextIfAvailable ? (
@@ -45,7 +45,13 @@ const RadarDescription = (props: Props): JSX.Element => {
const { open, onClose, title, description, timeline, url, links } = props;
return (
<Dialog data-testid="radar-description" open={open} onClose={onClose}>
<Dialog
data-testid="radar-description"
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
>
<DialogTitle data-testid="radar-description-dialog-title">
{title}
</DialogTitle>
+5
View File
@@ -57,6 +57,11 @@ export interface Config {
* will be broken in these scenarios.
*/
legacyCopyReadmeMdToIndexMd?: boolean;
/**
* List of mkdocs plugins which should be added as default to all mkdocs.yml files.
*/
defaultPlugins?: string[];
};
};
@@ -17,7 +17,7 @@
import { useEffect } from 'react';
import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
// @ts-ignore
import PhotoSwipeLightbox, { DataSource } from 'photoswipe/lightbox';
import PhotoSwipeLightbox, { DataSource, ZoomLevel } from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
import 'photoswipe/style.css';
import './lightbox.css';
@@ -30,6 +30,21 @@ export const LightBoxAddon = () => {
let lightbox = new PhotoSwipeLightbox({
pswpModule: PhotoSwipe,
initialZoomLevel: 1,
secondaryZoomLevel: (zoomLevelObject: ZoomLevel) => {
// photoswipe/lightbox won't zoom the image further then the given width and height.
// therefore we need to calculate the zoom factor needed to fit the complete image in the viewport manually.
const imageWidth = zoomLevelObject.elementSize.x;
const imageHeight = zoomLevelObject.elementSize.y;
const viewportWidth = zoomLevelObject.panAreaSize.x;
const viewportHeight = zoomLevelObject.panAreaSize.y;
const widthScale = viewportWidth / imageWidth;
const heightScale = viewportHeight / imageHeight;
const scaleFactor = Math.min(widthScale, heightScale);
return scaleFactor;
},
wheelToZoom: true,
arrowPrevSVG:
'<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowBackIosIcon" aria-label="fontSize large"><path d="M11.67 3.87 9.9 2.1 0 12l9.9 9.9 1.77-1.77L3.54 12z"></path></svg>',
@@ -37,8 +52,9 @@ export const LightBoxAddon = () => {
'<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowForwardIosIcon" aria-label="fontSize large"><path d="M6.23 20.23 8 22l10-10L8 2 6.23 3.77 14.46 12z"></path></svg>',
closeSVG:
'<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="CloseIcon" aria-label="fontSize large"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>',
zoomSVG:
'<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ZoomInIcon" aria-label="fontSize large"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path><path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"></path></svg>',
zoomSVG: `<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ZoomIcon" aria-label="fontSize large">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path><path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z" id="photoswipe-zoom-icon-zoomin-path"></path><path d="M12 10H 7 V 9 H 12 Z" id="photoswipe-zoom-icon-zoomout-path">
</svg>`,
});
images.forEach((image, index) => {
@@ -9,3 +9,13 @@
.pswp__img {
cursor: pointer !important;
}
/* hide "-" icon path */
.pswp--zoomed-allowed #photoswipe-zoom-icon-zoomout-path {
display: none;
}
/* hide "+" path and show "-" */
.pswp--zoomed-in #photoswipe-zoom-icon-zoomin-path {
display: none;
}
@@ -33,7 +33,7 @@ import {
} from './helpers';
import {
patchMkdocsYmlPreBuild,
pathMkdocsYmlWithTechdocsPlugin,
patchMkdocsYmlWithPlugins,
} from './mkdocsPatchers';
import yaml from 'js-yaml';
@@ -310,7 +310,7 @@ describe('helpers', () => {
});
});
describe('pathMkdocsYmlWithTechdocsPlugin', () => {
describe('patchMkdocsYmlWithPlugins', () => {
beforeEach(() => {
mockFs({
'/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
@@ -319,7 +319,7 @@ describe('helpers', () => {
});
});
it('should not add additional plugins if techdocs exists already in mkdocs file', async () => {
await pathMkdocsYmlWithTechdocsPlugin(
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_techdocs_plugin.yml',
mockLogger,
);
@@ -334,7 +334,7 @@ describe('helpers', () => {
expect(parsedYml.plugins).toContain('techdocs-core');
});
it("should add the needed plugin if it doesn't exist in mkdocs file", async () => {
await pathMkdocsYmlWithTechdocsPlugin(
await patchMkdocsYmlWithPlugins(
'/mkdocs_without_plugins.yml',
mockLogger,
);
@@ -347,7 +347,7 @@ describe('helpers', () => {
expect(parsedYml.plugins).toContain('techdocs-core');
});
it('should not override existing plugins', async () => {
await pathMkdocsYmlWithTechdocsPlugin(
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_additional_plugins.yml',
mockLogger,
);
@@ -362,6 +362,23 @@ describe('helpers', () => {
expect(parsedYml.plugins).toContain('not-techdocs-core');
expect(parsedYml.plugins).toContain('also-not-techdocs-core');
});
it('should add all provided default plugins', async () => {
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_additional_plugins.yml',
mockLogger,
['techdocs-core', 'custom-plugin'],
);
const updatedMkdocsYml = await fs.readFile(
'/mkdocs_with_additional_plugins.yml',
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
plugins: string[];
};
expect(parsedYml.plugins).toHaveLength(4);
expect(parsedYml.plugins).toContain('techdocs-core');
expect(parsedYml.plugins).toContain('custom-plugin');
});
});
describe('patchIndexPreBuild', () => {
@@ -138,27 +138,34 @@ export const patchMkdocsYmlPreBuild = async (
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
*
* List of tasks:
* - Add techdocs-core plugin to mkdocs file if it doesn't exist
* - Add all provided default plugins
*
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
*
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
* @param logger - A logger instance
* @param defaultPlugins - List of default mkdocs plugins
*/
export const pathMkdocsYmlWithTechdocsPlugin = async (
export const patchMkdocsYmlWithPlugins = async (
mkdocsYmlPath: string,
logger: Logger,
defaultPlugins: string[] = ['techdocs-core'],
) => {
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
// Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there
// Modify mkdocs.yaml to contain the required default plugins
if (!('plugins' in mkdocsYml)) {
mkdocsYml.plugins = ['techdocs-core'];
mkdocsYml.plugins = defaultPlugins;
return true;
}
if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) {
mkdocsYml.plugins.push('techdocs-core');
if (
mkdocsYml.plugins &&
!defaultPlugins.every(plugin => mkdocsYml.plugins!.includes(plugin))
) {
mkdocsYml.plugins = [
...new Set([...mkdocsYml.plugins, ...defaultPlugins]),
];
return true;
}
return false;
@@ -157,4 +157,24 @@ describe('readGeneratorConfig', () => {
legacyCopyReadmeMdToIndexMd: true,
});
});
it('should read the default plugins config', () => {
const config = new ConfigReader({
techdocs: {
generator: {
runIn: 'docker',
dockerImage: 'my-org/techdocs',
pullImage: false,
mkdocs: { defaultPlugins: ['mkdocs-custom-plugin'] },
},
},
});
expect(readGeneratorConfig(config, logger)).toEqual({
runIn: 'docker',
dockerImage: 'my-org/techdocs',
pullImage: false,
defaultPlugins: ['mkdocs-custom-plugin'],
});
});
});
@@ -33,7 +33,7 @@ import {
import {
patchMkdocsYmlPreBuild,
pathMkdocsYmlWithTechdocsPlugin,
patchMkdocsYmlWithPlugins,
} from './mkdocsPatchers';
import {
GeneratorBase,
@@ -121,10 +121,18 @@ export class TechdocsGenerator implements GeneratorBase {
await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
}
if (!this.options.omitTechdocsCoreMkdocsPlugin) {
await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger);
// patch the list of mkdocs plugins
const defaultPlugins = this.options.defaultPlugins ?? [];
if (
!this.options.omitTechdocsCoreMkdocsPlugin &&
!defaultPlugins.includes('techdocs-core')
) {
defaultPlugins.push('techdocs-core');
}
await patchMkdocsYmlWithPlugins(mkdocsYmlPath, childLogger, defaultPlugins);
// Directories to bind on container
const mountDirs = {
[inputDir]: '/input',
@@ -233,5 +241,8 @@ export function readGeneratorConfig(
legacyCopyReadmeMdToIndexMd: config.getOptionalBoolean(
'techdocs.generator.mkdocs.legacyCopyReadmeMdToIndexMd',
),
defaultPlugins: config.getOptionalStringArray(
'techdocs.generator.mkdocs.defaultPlugins',
),
};
}
@@ -41,6 +41,7 @@ export type GeneratorConfig = {
pullImage?: boolean;
omitTechdocsCoreMkdocsPlugin?: boolean;
legacyCopyReadmeMdToIndexMd?: boolean;
defaultPlugins?: string[];
};
/**
+198 -148
View File
@@ -3197,12 +3197,12 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.21.0
resolution: "@babel/runtime@npm:7.21.0"
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.22.11
resolution: "@babel/runtime@npm:7.22.11"
dependencies:
regenerator-runtime: ^0.13.11
checksum: 7b33e25bfa9e0e1b9e8828bb61b2d32bdd46b41b07ba7cb43319ad08efc6fda8eb89445193e67d6541814627df0ca59122c0ea795e412b99c5183a0540d338ab
regenerator-runtime: ^0.14.0
checksum: a5cd6683a8fcdb8065cb1677f221e22f6c67ec8f15ad1d273b180b93ab3bd86c66da2c48f500d4e72d8d2cfa85ff4872a3f350e5aa3855630036af5da765c001
languageName: node
linkType: hard
@@ -4153,8 +4153,11 @@ __metadata:
resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^5.10.1
lodash: ^4.17.21
zod: ^3.21.4
zod-to-json-schema: ^3.21.4
peerDependencies:
react: ^16.13.1 || ^17.0.0
languageName: unknown
@@ -7918,6 +7921,7 @@ __metadata:
"@testing-library/dom": ^8.0.0
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/react-hooks": ^8.0.1
"@testing-library/user-event": ^14.0.0
"@types/node": ^16.11.26
"@types/react": ^16.13.1 || ^17.0.0
@@ -10427,16 +10431,16 @@ __metadata:
languageName: node
linkType: hard
"@emotion/cache@npm:^11.10.5, @emotion/cache@npm:^11.10.7":
version: 11.10.7
resolution: "@emotion/cache@npm:11.10.7"
"@emotion/cache@npm:^11.10.5, @emotion/cache@npm:^11.11.0":
version: 11.11.0
resolution: "@emotion/cache@npm:11.11.0"
dependencies:
"@emotion/memoize": ^0.8.0
"@emotion/sheet": ^1.2.1
"@emotion/utils": ^1.2.0
"@emotion/weak-memoize": ^0.3.0
stylis: 4.1.3
checksum: 6b1efed2dffc93dac419409d91f6d57a200d858ec5ffa4b7c30080fdbd93db431ff86bb779c5b8830b8373f3c5dd754d9beb386604ed2667c7d55608ff653dfc
"@emotion/memoize": ^0.8.1
"@emotion/sheet": ^1.2.2
"@emotion/utils": ^1.2.1
"@emotion/weak-memoize": ^0.3.1
stylis: 4.2.0
checksum: 8eb1dc22beaa20c21a2e04c284d5a2630a018a9d51fb190e52de348c8d27f4e8ca4bbab003d68b4f6cd9cc1c569ca747a997797e0f76d6c734a660dc29decf08
languageName: node
linkType: hard
@@ -10454,19 +10458,19 @@ __metadata:
languageName: node
linkType: hard
"@emotion/is-prop-valid@npm:^1.1.0, @emotion/is-prop-valid@npm:^1.2.0":
version: 1.2.0
resolution: "@emotion/is-prop-valid@npm:1.2.0"
"@emotion/is-prop-valid@npm:^1.1.0, @emotion/is-prop-valid@npm:^1.2.0, @emotion/is-prop-valid@npm:^1.2.1":
version: 1.2.1
resolution: "@emotion/is-prop-valid@npm:1.2.1"
dependencies:
"@emotion/memoize": ^0.8.0
checksum: cc7a19850a4c5b24f1514665289442c8c641709e6f7711067ad550e05df331da0692a16148e85eda6f47e31b3261b64d74c5e25194d053223be16231f969d633
"@emotion/memoize": ^0.8.1
checksum: 8f42dc573a3fad79b021479becb639b8fe3b60bdd1081a775d32388bca418ee53074c7602a4c845c5f75fa6831eb1cbdc4d208cc0299f57014ed3a02abcad16a
languageName: node
linkType: hard
"@emotion/memoize@npm:^0.8.0":
version: 0.8.0
resolution: "@emotion/memoize@npm:0.8.0"
checksum: c87bb110b829edd8e1c13b90a6bc37cebc39af29c7599a1e66a48e06f9bec43e8e53495ba86278cc52e7589549492c8dfdc81d19f4fdec0cee6ba13d2ad2c928
"@emotion/memoize@npm:^0.8.0, @emotion/memoize@npm:^0.8.1":
version: 0.8.1
resolution: "@emotion/memoize@npm:0.8.1"
checksum: a19cc01a29fcc97514948eaab4dc34d8272e934466ed87c07f157887406bc318000c69ae6f813a9001c6a225364df04249842a50e692ef7a9873335fbcc141b0
languageName: node
linkType: hard
@@ -10507,10 +10511,10 @@ __metadata:
languageName: node
linkType: hard
"@emotion/sheet@npm:^1.2.1":
version: 1.2.1
resolution: "@emotion/sheet@npm:1.2.1"
checksum: ce78763588ea522438156344d9f592203e2da582d8d67b32e1b0b98eaba26994c6c270f8c7ad46442fc9c0a9f048685d819cd73ca87e544520fd06f0e24a1562
"@emotion/sheet@npm:^1.2.2":
version: 1.2.2
resolution: "@emotion/sheet@npm:1.2.2"
checksum: d973273c9c15f1c291ca2269728bf044bd3e92a67bca87943fa9ec6c3cd2b034f9a6bfe95ef1b5d983351d128c75b547b43ff196a00a3875f7e1d269793cecfe
languageName: node
linkType: hard
@@ -10567,17 +10571,17 @@ __metadata:
languageName: node
linkType: hard
"@emotion/utils@npm:^1.2.0":
version: 1.2.0
resolution: "@emotion/utils@npm:1.2.0"
checksum: 55457a49ddd4db6a014ea0454dc09eaa23eedfb837095c8ff90470cb26a303f7ceb5fcc1e2190ef64683e64cfd33d3ba3ca3109cd87d12bc9e379e4195c9a4dd
"@emotion/utils@npm:^1.2.0, @emotion/utils@npm:^1.2.1":
version: 1.2.1
resolution: "@emotion/utils@npm:1.2.1"
checksum: e0b44be0705b56b079c55faff93952150be69e79b660ae70ddd5b6e09fc40eb1319654315a9f34bb479d7f4ec94be6068c061abbb9e18b9778ae180ad5d97c73
languageName: node
linkType: hard
"@emotion/weak-memoize@npm:^0.3.0":
version: 0.3.0
resolution: "@emotion/weak-memoize@npm:0.3.0"
checksum: f43ef4c8b7de70d9fa5eb3105921724651e4188e895beb71f0c5919dc899a7b8743e1fdd99d38b9092dd5722c7be2312ebb47fbdad0c4e38bea58f6df5885cc0
"@emotion/weak-memoize@npm:^0.3.0, @emotion/weak-memoize@npm:^0.3.1":
version: 0.3.1
resolution: "@emotion/weak-memoize@npm:0.3.1"
checksum: b2be47caa24a8122622ea18cd2d650dbb4f8ad37b636dc41ed420c2e082f7f1e564ecdea68122b546df7f305b159bf5ab9ffee872abd0f052e687428459af594
languageName: node
linkType: hard
@@ -11015,6 +11019,44 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.4.1":
version: 1.4.1
resolution: "@floating-ui/core@npm:1.4.1"
dependencies:
"@floating-ui/utils": ^0.1.1
checksum: be4ab864fe17eeba5e205bd554c264b9a4895a57c573661bbf638357fa3108677fed7ba3269ec15b4da90e29274c9b626d5a15414e8d1fe691e210d02a03695c
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.3.0":
version: 1.5.1
resolution: "@floating-ui/dom@npm:1.5.1"
dependencies:
"@floating-ui/core": ^1.4.1
"@floating-ui/utils": ^0.1.1
checksum: ddb509030978536ba7b321cf8c764ae9d0142a3b1fefb7e6bc050a5de7e825e12131fa5089009edabf7c125fb274886da211a5220fe17a71d875a7a96eb1386c
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^2.0.1":
version: 2.0.1
resolution: "@floating-ui/react-dom@npm:2.0.1"
dependencies:
"@floating-ui/dom": ^1.3.0
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 00fef2cf69ac2b15952e47505fd9e23f6cc5c20a26adc707862932826d1682f3c30f83c9887abfc93574fdca2d34dd2fc00271527318b1db403549cd6bc9eb00
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.1.1":
version: 0.1.1
resolution: "@floating-ui/utils@npm:0.1.1"
checksum: 548acdda7902f45b0afbe34e2e7f4cbff0696b95bad8c039f80936519de24ef2ec20e79902825b7815294b37f51a7c52ee86288b0688869a57cc229a164d86b4
languageName: node
linkType: hard
"@fmvilas/pseudo-yaml-ast@npm:^0.3.1":
version: 0.3.1
resolution: "@fmvilas/pseudo-yaml-ast@npm:0.3.1"
@@ -13167,16 +13209,17 @@ __metadata:
languageName: node
linkType: hard
"@mui/base@npm:5.0.0-alpha.127":
version: 5.0.0-alpha.127
resolution: "@mui/base@npm:5.0.0-alpha.127"
"@mui/base@npm:5.0.0-beta.12":
version: 5.0.0-beta.12
resolution: "@mui/base@npm:5.0.0-beta.12"
dependencies:
"@babel/runtime": ^7.21.0
"@emotion/is-prop-valid": ^1.2.0
"@babel/runtime": ^7.22.10
"@emotion/is-prop-valid": ^1.2.1
"@floating-ui/react-dom": ^2.0.1
"@mui/types": ^7.2.4
"@mui/utils": ^5.12.0
"@popperjs/core": ^2.11.7
clsx: ^1.2.1
"@mui/utils": ^5.14.6
"@popperjs/core": ^2.11.8
clsx: ^2.0.0
prop-types: ^15.8.1
react-is: ^18.2.0
peerDependencies:
@@ -13186,29 +13229,29 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 330baeef1fdaa513707f295e07a3436af24d0831a18109ed877f76b86cf1f392309833d33a2c7ca250db79fdf53f5e88c2618f876f50dbbbf9323bbf08e367e7
checksum: 3ed9b43323595588ccea7325a79bbaab60bf5a99a970821555c449f53ca4c8f7f298a4d5c8a280c5ea0b2b47273a0b1a03e33ef6cbbbd428b14bcc33222b5f75
languageName: node
linkType: hard
"@mui/core-downloads-tracker@npm:^5.12.2":
version: 5.12.2
resolution: "@mui/core-downloads-tracker@npm:5.12.2"
checksum: d748bdc56df6fdfe6712550a94263cf094c53ddcb70f6a8f633caf23927edf5ac88b1f888429d895fe2a5045b27eb7aa9826ccee5b917e31973667d604ed87fe
"@mui/core-downloads-tracker@npm:^5.14.6":
version: 5.14.6
resolution: "@mui/core-downloads-tracker@npm:5.14.6"
checksum: 500b6a0e709f75fa8798caf4ad993d1024c046f25f0624e7d36c454ca53aebf119eb8645bb6cf762a2e27520deb843e546c95874e32183f1255044fe73184c79
languageName: node
linkType: hard
"@mui/material@npm:^5.12.2":
version: 5.12.2
resolution: "@mui/material@npm:5.12.2"
version: 5.14.6
resolution: "@mui/material@npm:5.14.6"
dependencies:
"@babel/runtime": ^7.21.0
"@mui/base": 5.0.0-alpha.127
"@mui/core-downloads-tracker": ^5.12.2
"@mui/system": ^5.12.1
"@babel/runtime": ^7.22.10
"@mui/base": 5.0.0-beta.12
"@mui/core-downloads-tracker": ^5.14.6
"@mui/system": ^5.14.6
"@mui/types": ^7.2.4
"@mui/utils": ^5.12.0
"@types/react-transition-group": ^4.4.5
clsx: ^1.2.1
"@mui/utils": ^5.14.6
"@types/react-transition-group": ^4.4.6
clsx: ^2.0.0
csstype: ^3.1.2
prop-types: ^15.8.1
react-is: ^18.2.0
@@ -13226,16 +13269,16 @@ __metadata:
optional: true
"@types/react":
optional: true
checksum: a3464dfbcc496164967e134d6f1d40e060ee02fc2ddb5fcf648ca85580efdbd5eff5aab6a73486d0a7f329f7299127b52501a13993bb131cbd4e310c7f2e633f
checksum: e22977cb010b54ba43e7b13105c7dbe3e5361f960fd4eecc3ed28480a8abc531b44abdb093bafe9ce291c6a23c25039e0677d5d965210d7fc9ac5e25bb68a876
languageName: node
linkType: hard
"@mui/private-theming@npm:^5.12.0":
version: 5.12.0
resolution: "@mui/private-theming@npm:5.12.0"
"@mui/private-theming@npm:^5.14.6":
version: 5.14.6
resolution: "@mui/private-theming@npm:5.14.6"
dependencies:
"@babel/runtime": ^7.21.0
"@mui/utils": ^5.12.0
"@babel/runtime": ^7.22.10
"@mui/utils": ^5.14.6
prop-types: ^15.8.1
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
@@ -13243,16 +13286,16 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 761bc7a57e1643c2c4c327886882fa5efc7bacae1c6fffe6be4197f49d337261c916a883d96996445efcdedc0672251725c1e5b264b6250d6c9527fd0cafcc62
checksum: 2d477657c5c10bbc2628b741c85d5bc66f36f379eaafc97f534500584e4ab2f098e8cf29eec0f079b9a34a414027235ab433f3f41771ea4b9cacdcd783111726
languageName: node
linkType: hard
"@mui/styled-engine@npm:^5.12.0":
version: 5.12.0
resolution: "@mui/styled-engine@npm:5.12.0"
"@mui/styled-engine@npm:^5.14.6":
version: 5.14.6
resolution: "@mui/styled-engine@npm:5.14.6"
dependencies:
"@babel/runtime": ^7.21.0
"@emotion/cache": ^11.10.7
"@babel/runtime": ^7.22.10
"@emotion/cache": ^11.11.0
csstype: ^3.1.2
prop-types: ^15.8.1
peerDependencies:
@@ -13264,20 +13307,20 @@ __metadata:
optional: true
"@emotion/styled":
optional: true
checksum: 4a415473cf62aa05012f667dd2e9b1dc2fb175be5b0c4d0b8df541e2dac3d7db410e920e0d5910c8e2b8996a4fb51f74d79483e2878a9b5c0d334498f5537d74
checksum: 7bbef4574195d70945041a645f5d0e372762cad28d873794e750bbd570daf901392205d69c3593ad55c43088361e1b90c7b9798a5d013c4a1783553d42c758dd
languageName: node
linkType: hard
"@mui/system@npm:^5.12.1":
version: 5.12.1
resolution: "@mui/system@npm:5.12.1"
"@mui/system@npm:^5.14.6":
version: 5.14.6
resolution: "@mui/system@npm:5.14.6"
dependencies:
"@babel/runtime": ^7.21.0
"@mui/private-theming": ^5.12.0
"@mui/styled-engine": ^5.12.0
"@babel/runtime": ^7.22.10
"@mui/private-theming": ^5.14.6
"@mui/styled-engine": ^5.14.6
"@mui/types": ^7.2.4
"@mui/utils": ^5.12.0
clsx: ^1.2.1
"@mui/utils": ^5.14.6
clsx: ^2.0.0
csstype: ^3.1.2
prop-types: ^15.8.1
peerDependencies:
@@ -13292,7 +13335,7 @@ __metadata:
optional: true
"@types/react":
optional: true
checksum: b951959bf5e5af581319354c044f441d97849ff120cfec111d2ed5e7fa05efd63721acff96f48093b8e2bdeb5ccbe35c48613fb925be2b58c596447d10dbed3e
checksum: 5090761f86b858df6109c57c7c1d6912462781b1280d43d672b64e095564eefe4f64f5bf8ebf5ad9cefdc27efba8964e59bc2c234005ca33da4831aff4fed8db
languageName: node
linkType: hard
@@ -13308,18 +13351,18 @@ __metadata:
languageName: node
linkType: hard
"@mui/utils@npm:^5.12.0":
version: 5.12.0
resolution: "@mui/utils@npm:5.12.0"
"@mui/utils@npm:^5.14.6":
version: 5.14.6
resolution: "@mui/utils@npm:5.14.6"
dependencies:
"@babel/runtime": ^7.21.0
"@babel/runtime": ^7.22.10
"@types/prop-types": ^15.7.5
"@types/react-is": ^16.7.1 || ^17.0.0
"@types/react-is": ^18.2.1
prop-types: ^15.8.1
react-is: ^18.2.0
peerDependencies:
react: ^17.0.0 || ^18.0.0
checksum: 87b2c7468803b083f50af28d7c215c45291e73fef16570848b596d0f1cde1fc613c20e8951f431217b31451de254744abd50eda5013dedec4982420b5bf1c6b6
checksum: c5d2f37dec7984b9cf4da10a2fd9eabe23e7481ad0f3674cc9cdc2b7a3938eca28e92068f6d58b42cc807192dbb5e40d4885f8d896a4a01bab32a05c5ac3920b
languageName: node
linkType: hard
@@ -14288,10 +14331,10 @@ __metadata:
languageName: node
linkType: hard
"@popperjs/core@npm:^2.11.7":
version: 2.11.7
resolution: "@popperjs/core@npm:2.11.7"
checksum: 5b6553747899683452a1d28898c1b39173a4efd780e74360bfcda8eb42f1c5e819602769c81a10920fc68c881d07fb40429604517d499567eac079cfa6470f19
"@popperjs/core@npm:^2.11.8":
version: 2.11.8
resolution: "@popperjs/core@npm:2.11.8"
checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0
languageName: node
linkType: hard
@@ -16834,11 +16877,11 @@ __metadata:
linkType: hard
"@types/codemirror@npm:^5.0.0":
version: 5.60.8
resolution: "@types/codemirror@npm:5.60.8"
version: 5.60.9
resolution: "@types/codemirror@npm:5.60.9"
dependencies:
"@types/tern": "*"
checksum: bc3a63eab0308b3ef5ed2ca22afb7f1c0b8acde6477e1d569df7e93f3fe4f78754d28a3e081c72931259fe1bf906a319b54bcdeba74600b989e5558d32478496
checksum: 89da80a8d5c904f573289769098ad7721a4b58e056c1d08168b967bcfb63e30433d9d0bee0c43ff4b6679b0b7b87cfe942cc3f250ca0365ee244c9fb68ac9a70
languageName: node
linkType: hard
@@ -17007,9 +17050,9 @@ __metadata:
linkType: hard
"@types/d3-force@npm:^3.0.0":
version: 3.0.4
resolution: "@types/d3-force@npm:3.0.4"
checksum: 779fb597fb41e7bc6a5e1b8969d500deb95c4a73428c7c268bf0ca6f3ed668dd2ed6aa652de7af14d2f9c192dad4f6e7badf2c5bc330624bd8405ac88440b278
version: 3.0.5
resolution: "@types/d3-force@npm:3.0.5"
checksum: 09aa0e999841f69b34296fa92273b5b251c50f74c1b7ec2bdef7a3a7fd2d756467e4ede7cc0dcfc4151c04bde0f3feaa48d9b669c38620fb51cee2d7faa01e24
languageName: node
linkType: hard
@@ -17046,9 +17089,9 @@ __metadata:
linkType: hard
"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.1":
version: 3.0.5
resolution: "@types/d3-selection@npm:3.0.5"
checksum: 77ea35c92273cd2d736355ab5a4cf6477086b064a1f608489a6fef4198ee6bc1d5440e5dfd880099265ac96c4f4b4fda659d89fbaaef37ba03708663196ba8ad
version: 3.0.6
resolution: "@types/d3-selection@npm:3.0.6"
checksum: 01f9f3a41b98280947109911bcc6ebffff5c8c3e617695979c04ef9beef445a8cdc48a9fbdb3c9cff45a7345e83e8a5d6b346d7ceedca6ebc11dfd9717dbff6b
languageName: node
linkType: hard
@@ -17062,11 +17105,11 @@ __metadata:
linkType: hard
"@types/d3-shape@npm:^3.0.1, @types/d3-shape@npm:^3.1.0":
version: 3.1.1
resolution: "@types/d3-shape@npm:3.1.1"
version: 3.1.2
resolution: "@types/d3-shape@npm:3.1.2"
dependencies:
"@types/d3-path": "*"
checksum: 8f1762ecdeb4833a3802be1c65363cbc7cca753d0b836a3855fde4ba12f8e6fc142dba3c5f6d669a9e89374cc6dc414464e4f2d04e72fafd4bc64819ce30bb63
checksum: 17399a78872aa89cab600cb7977405136a72798dbbb59a4b3684012c2a2ffb9b62720cceaaedc412088b7da086518a6722c909e03cb81e3e238569722feb60f9
languageName: node
linkType: hard
@@ -17085,12 +17128,12 @@ __metadata:
linkType: hard
"@types/d3-zoom@npm:^3.0.1":
version: 3.0.3
resolution: "@types/d3-zoom@npm:3.0.3"
version: 3.0.4
resolution: "@types/d3-zoom@npm:3.0.4"
dependencies:
"@types/d3-interpolate": "*"
"@types/d3-selection": "*"
checksum: 1c828538e2b04c47f659ead552824bed1c545dd1934f38abfec95f4a3c1cb4eaf7e107959ebd05a3dfec0166fa98677037ede6f633383c6502b1a05eaeb1d418
checksum: 6330b8411822b979ab0fd5a7a7755295377b4dfc61fd8f925d97e5b388e3dd604b455c0009b0eda3320c14c4ea367f740a04991a71d35befdca5f75218492a68
languageName: node
linkType: hard
@@ -17204,14 +17247,14 @@ __metadata:
linkType: hard
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.5":
version: 4.17.35
resolution: "@types/express-serve-static-core@npm:4.17.35"
version: 4.17.36
resolution: "@types/express-serve-static-core@npm:4.17.36"
dependencies:
"@types/node": "*"
"@types/qs": "*"
"@types/range-parser": "*"
"@types/send": "*"
checksum: cc8995d10c6feda475ec1b3a0e69eb0f35f21ab6b49129ad5c6f279e0bc5de8175bc04ec51304cb79a43eec3ed2f5a1e01472eb6d5f827b8c35c6ca8ad24eb6e
checksum: 410b13cbd663f18c0f8729e7f2ff54d960d96de76ebbae7cadb612972f85cc66c54051e00d32f11aa230c0a683d81a6d6fc7f7e4e383a95c0801494c517f36e1
languageName: node
linkType: hard
@@ -17461,11 +17504,11 @@ __metadata:
linkType: hard
"@types/jquery@npm:^3.3.34":
version: 3.5.16
resolution: "@types/jquery@npm:3.5.16"
version: 3.5.17
resolution: "@types/jquery@npm:3.5.17"
dependencies:
"@types/sizzle": "*"
checksum: 13c995f15d1c2f1d322103dc1cb0a22b95eecc3e7546f00279b8731aea21d7ec04550af40e609ee48e755d4e11bf61c25b4aa9f53df3bcbec4b8fe8e81471732
checksum: be289003382a8d4a1c6eab9c68845e919e467926557493033e78f6793ed8acd482b01b7b47955e4bf51f65f9b05607beccb32d31d649a2b42f3b07f1444d1f1a
languageName: node
linkType: hard
@@ -17716,11 +17759,11 @@ __metadata:
linkType: hard
"@types/morgan@npm:^1.9.0":
version: 1.9.4
resolution: "@types/morgan@npm:1.9.4"
version: 1.9.5
resolution: "@types/morgan@npm:1.9.5"
dependencies:
"@types/node": "*"
checksum: d1e99c66a43501dcdf6e94e013dfff4e6c152cbb5f782d954bb722d230a9c1c0fe06fab6df3f3dfa3547735a7598f9471633cc6813d794e9562fd022e217c6ae
checksum: f98deb4c7f2ad6049ad34ed7b0f0d427546bdf2358011070af9d597de1b0a03b38cc10cfe65ef2e7673e569c384303d949e76df701acefe288d547f614142973
languageName: node
linkType: hard
@@ -17770,9 +17813,9 @@ __metadata:
linkType: hard
"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0":
version: 20.5.0
resolution: "@types/node@npm:20.5.0"
checksum: 659bc5fc93b5c02bd88ca4bfae4f6b9dc307d45884d1dd9d69df85819a9943cdc00cd3c87eec3048866df6a67f52297f74d170e47a44f61edb3e8f770d94e85e
version: 20.5.4
resolution: "@types/node@npm:20.5.4"
checksum: 18de76c990e1c298183661cfc7cf16c9384531538c8090cc27bc3cab801e7c46b9f6943591ab0433955dd57961601684cc0fb19ba2deee9609e137c91afd2e25
languageName: node
linkType: hard
@@ -17784,9 +17827,9 @@ __metadata:
linkType: hard
"@types/node@npm:^14.14.31":
version: 14.18.54
resolution: "@types/node@npm:14.18.54"
checksum: 9fd66f91fcd8e9b25067f784a9c60bd710ef86a89c838c131ab2b1921398adc53b1c70d741bceed48bb2403b75c434b1bbbb255240773819cde36295c4b6abf1
version: 14.18.56
resolution: "@types/node@npm:14.18.56"
checksum: 990cd0e853caa264586aa703ed923767fb445ad240e54cfe7c877c8db2e0b377f85c1bc4b80575c7f07dc9f4c56bdfbc6b5c2704a689cf7c8f9ac4ec2ed84975
languageName: node
linkType: hard
@@ -17798,16 +17841,16 @@ __metadata:
linkType: hard
"@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2":
version: 16.18.41
resolution: "@types/node@npm:16.18.41"
checksum: b12650d8e4289edafcf0453c8a66c00d6397d465a48b1c683babba4f16f92a6418f678e98a85751e3fef78d23c2b07f641df8da14bf5428ad8282b57b2695243
version: 16.18.44
resolution: "@types/node@npm:16.18.44"
checksum: 5600765a303e2ab8913e8dbc6871f2740678bf6420f0b009a0ad059d4cbf7cd93a9679dc304c9d382bee57fa9cf802ea8b8b70839d11903e98bd9e09c98a7e32
languageName: node
linkType: hard
"@types/node@npm:^18.11.17":
version: 18.17.5
resolution: "@types/node@npm:18.17.5"
checksum: b8c658a99234b99425243c324b641ed7b9ceb6bee6b06421fdc9bb7c58f9a5552e353225cc549e6982462ac384abe1985022ed76e2e4728797f59b21f659ca2b
version: 18.17.9
resolution: "@types/node@npm:18.17.9"
checksum: 0d5835710e49a654a1ca34167e7cd504cc709c006006ebe9b23c360dac5637c6c4f1b4f3aff33df1b7f9debfb9ad8bd81da2fc4540970d2ad56eb479cda77613
languageName: node
linkType: hard
@@ -18034,12 +18077,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react-is@npm:^16.7.1 || ^17.0.0":
version: 17.0.3
resolution: "@types/react-is@npm:17.0.3"
"@types/react-is@npm:^18.2.1":
version: 18.2.1
resolution: "@types/react-is@npm:18.2.1"
dependencies:
"@types/react": "*"
checksum: 6abb7c47d54f012272650df8a962a28bd82f219291e5ef8b4dfa7fe0bb98ae243b060bf9fbe8ceff6213141794855a006db194b490b00ffd15842ae19d0ce1f0
checksum: b44c3262efa2c68fa6fe2beb9ef86170b18305469461a3f97aa14943cc033cb21a26944f718bdb6434eea6e8f7fcba251c4f45b65b897a3fcf751b5a6003cf82
languageName: node
linkType: hard
@@ -18091,12 +18134,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react-transition-group@npm:^4.2.0, @types/react-transition-group@npm:^4.4.5":
version: 4.4.5
resolution: "@types/react-transition-group@npm:4.4.5"
"@types/react-transition-group@npm:^4.2.0, @types/react-transition-group@npm:^4.4.6":
version: 4.4.6
resolution: "@types/react-transition-group@npm:4.4.6"
dependencies:
"@types/react": "*"
checksum: 265f1c74061556708ffe8d15559e35c60d6c11478c9950d3735575d2c116ca69f461d85effa06d73a613eb8b73c84fd32682feb57cf7c5f9e4284021dbca25b0
checksum: 0872143821d7ee20a1d81e965f8b1e837837f11cd2206973f1f98655751992d9390304d58bac192c9cd923eca95bff107d8c9e3364a180240d5c2a6fd70fd7c3
languageName: node
linkType: hard
@@ -18119,13 +18162,13 @@ __metadata:
linkType: hard
"@types/react@npm:^17":
version: 17.0.64
resolution: "@types/react@npm:17.0.64"
version: 17.0.65
resolution: "@types/react@npm:17.0.65"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: 71a7b402e8849ff31ed542d1d1a2ff6adb0e6f0e17321d126e7382a2fcb671ec1e1d1bf7045306922fa90b0319c57a8a45e6215bbf00fb6f73c3860d3f869870
checksum: bb862f0f97a49e690cd5c62743307d17398ee62b87bbb2c85c1b01cb992244c0aef4ecdedaab0e8d33c6315d5a8f067cf90e7f1c366b8819678c59204e202932
languageName: node
linkType: hard
@@ -21660,13 +21703,20 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.1, clsx@npm:^1.2.1":
"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.1":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12
languageName: node
linkType: hard
"clsx@npm:^2.0.0":
version: 2.0.0
resolution: "clsx@npm:2.0.0"
checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e
languageName: node
linkType: hard
"cluster-key-slot@npm:^1.1.0":
version: 1.1.0
resolution: "cluster-key-slot@npm:1.1.0"
@@ -22158,8 +22208,8 @@ __metadata:
linkType: hard
"concurrently@npm:^8.0.0":
version: 8.2.0
resolution: "concurrently@npm:8.2.0"
version: 8.2.1
resolution: "concurrently@npm:8.2.1"
dependencies:
chalk: ^4.1.2
date-fns: ^2.30.0
@@ -22173,7 +22223,7 @@ __metadata:
bin:
conc: dist/bin/concurrently.js
concurrently: dist/bin/concurrently.js
checksum: eafe6a4d9b7fda87f55ea285cfc6acd937a5286ceec8991ab48e6cc27c45fce6a5c6f45e18d7555defa15dc7d7e8941bc5a9d1ceaf182e31441d420e00333434
checksum: 216cb16d5b301cbd9c657b19430836d1686fe8fa9b9ef35ef7ac601e1a5cf6535166a3e57de446696dbd5e7e3f45d78fc70f33c5fd4bb565342cd5e752c5b069
languageName: node
linkType: hard
@@ -37744,13 +37794,6 @@ __metadata:
languageName: node
linkType: hard
"regenerator-runtime@npm:^0.13.11":
version: 0.13.11
resolution: "regenerator-runtime@npm:0.13.11"
checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4
languageName: node
linkType: hard
"regenerator-runtime@npm:^0.14.0":
version: 0.14.0
resolution: "regenerator-runtime@npm:0.14.0"
@@ -40233,13 +40276,20 @@ __metadata:
languageName: node
linkType: hard
"stylis@npm:4.1.3, stylis@npm:^4.0.6":
"stylis@npm:4.1.3":
version: 4.1.3
resolution: "stylis@npm:4.1.3"
checksum: d04dbffcb9bf2c5ca8d8dc09534203c75df3bf711d33973ea22038a99cc475412a350b661ebd99cbc01daa50d7eedcf0d130d121800eb7318759a197023442a6
languageName: node
linkType: hard
"stylis@npm:4.2.0, stylis@npm:^4.0.6":
version: 4.2.0
resolution: "stylis@npm:4.2.0"
checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584
languageName: node
linkType: hard
"subscriptions-transport-ws@npm:^0.11.0":
version: 0.11.0
resolution: "subscriptions-transport-ws@npm:0.11.0"