Merge branch 'backstage:master' into feat/techdocs-azurite-support
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-playlist': patch
|
||||
---
|
||||
|
||||
change color of delete icon to secondary in playlist table
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
---
|
||||
|
||||
Fixed the width of the tech radar dialog so that the content fits
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Update OpenAPI schema to relax the encoding validation of all request parameters.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Fixed validation of the `fullTextFilterFields` query parameter.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-module-addons-contrib': minor
|
||||
---
|
||||
|
||||
Enable zoom icon for techdocs images inside lightbox
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add examples for `github:actions:dispatch` scaffolder actions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Fix issue with `catalogFileName` not being a required property for `/analyze-location`
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Allow specifying static widgets to custom home page
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -233,6 +233,9 @@ const defaultConfig = [
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 1,
|
||||
movable: true,
|
||||
resizable: false,
|
||||
deletable: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
+2
-12
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
+133
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+74
@@ -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 }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
+2
@@ -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>
|
||||
|
||||
Vendored
+5
@@ -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[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user