Merge branch 'master' into docs-theming

This commit is contained in:
Charles de Dreuille
2025-09-15 12:39:26 +01:00
230 changed files with 2196 additions and 779 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Updated dependency `dompurify` to `^3.2.4`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
TechDocs page titles have been improved, especially for deeply nested pages.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications': patch
---
Fixed missing app context when rendering the notifications view
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-gitlab': patch
---
When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-react': patch
---
Update to documentation regarding TechDocs redirects.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/cli': patch
---
Modify the `backstage.json` also for custom patterns if it extends the default pattern.
Examples:
- `@backstage/*` (default pattern)
- `@{backstage,backstage-community}/*`
- `@{extra1,backstage,extra2}/*`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': minor
---
Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/catalog-client': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-events-backend': patch
'@backstage/plugin-search-backend': patch
'@backstage/plugin-events-node': patch
---
Updated generated schemas
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Ensure that msgraph parent group stays same in case the group has multiple parents
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': minor
---
Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog': patch
---
Catalog table columns support i18n
+1 -1
View File
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "next",
"initialVersions": {
"example-app": "0.2.112",
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Add block creations field in github branch protection scaffolder actions
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-catalog-backend': minor
---
Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration.
For example, this enables allowing all `Template` entities with the type `website`:
```diff
catalog:
rules:
- allow:
- Component
- API
- Resource
- System
- Domain
- Location
+ - allow:
+ - kind: Template
+ spec.type: website
locations:
- type: url
pattern: https://github.com/org/*\/blob/master/*.yaml
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead.
+35
View File
@@ -0,0 +1,35 @@
---
'@backstage/plugin-scaffolder-backend-module-github': minor
---
Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility
It can be used like this
```
steps:
- id: create-simple-issue
name: Create Simple Issue
action: github:issues:create
input:
repoUrl: ${{ parameters.repoUrl }}
title: "[${{ parameters.projectName }}] Simple Bug Report"
body: |
## Bug Description
This is a simple bug report created by the scaffolder template.
### Steps to Reproduce
1. Run the application
2. Navigate to the main page
3. Click on the problematic button
### Expected Behavior
The button should work correctly.
### Actual Behavior
The button does not respond to clicks.
output:
links:
- title: Simple Issue
url: ${{ steps['create-simple-issue'].output.issueUrl }}
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
feat: support no en languages
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-addons-test-utils': minor
---
Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Prevent the MultiEntityPicker from removing options present in form state when new options are selected
@@ -38,6 +38,7 @@ Bitrise
Blackbox
bool
boolean
breakpoint
Brex
broadcasted
bugfixes
@@ -146,6 +147,7 @@ Expedia
facto
failover
Fargate
faqs
featureful
Figma
firehydrant
@@ -460,6 +462,7 @@ subfolders
subheader
subheaders
subkey
subpage
subpath
subroutes
substring
+16 -22
View File
@@ -217,18 +217,18 @@ export const cellPropDefs: Record<string, PropDef> = {
};
export const tablePaginationPropDefs: Record<string, PropDef> = {
pageIndex: {
offset: {
type: 'number',
description: 'The current page index.',
description: 'The current offset (starting index) for pagination.',
},
pageSize: {
type: 'number',
description: 'The number of items per page.',
},
setPageIndex: {
setOffset: {
type: 'enum',
values: ['(pageIndex: number) => void'],
description: 'Handler that is called when the page index changes.',
values: ['(offset: number) => void'],
description: 'Handler that is called when the offset changes.',
},
setPageSize: {
type: 'enum',
@@ -277,10 +277,7 @@ export const tableUsageSnippet = `import { Cell, ..., TableHeader, TablePaginati
</Table>
<TablePagination />`;
export const tableBasicSnippet = `import { Table, TablePagination } from '@backstage/ui';
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(5);
export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, Cell, CellProfile, TablePagination, useTable } from '@backstage/ui';
const data = [
{
@@ -293,10 +290,13 @@ const data = [
// ... more data
];
const newData = data4.slice(
pageIndex * pageSize,
(pageIndex + 1) * pageSize,
);
// Uncontrolled pagination (easiest)
const { data: paginatedData, paginationProps } = useTable({
data,
pagination: {
defaultPageSize: 5,
},
});
<Table>
<TableHeader>
@@ -306,9 +306,9 @@ const newData = data4.slice(
<Column>Albums</Column>
</TableHeader>
<TableBody>
{newData.map(item => (
{paginatedData?.map(item => (
<Row key={item.name}>
<CellProfileBUI
<CellProfile
name={item.name}
src={item.image}
href={item.website}
@@ -320,10 +320,4 @@ const newData = data4.slice(
))}
</TableBody>
</Table>
<TablePagination
pageIndex={pageIndex}
pageSize={pageSize}
rowCount={data4.length}
setPageIndex={setPageIndex}
setPageSize={setPageSize}
/>`;
<TablePagination {...paginationProps} />`;
+4 -4
View File
@@ -53,7 +53,7 @@ Once the host build is complete, we are ready to build our image. The following
`Dockerfile` is included when creating a new app with `@backstage/create-app`:
```dockerfile
FROM node:20-bookworm-slim
FROM node:22-bookworm-slim
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
@@ -178,7 +178,7 @@ the repo root:
```dockerfile
# Stage 1 - Create yarn install skeleton layer
FROM node:20-bookworm-slim AS packages
FROM node:22-bookworm-slim AS packages
WORKDIR /app
COPY backstage.json package.json yarn.lock ./
@@ -193,7 +193,7 @@ COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+
# Stage 2 - Install dependencies and build packages
FROM node:20-bookworm-slim AS build
FROM node:22-bookworm-slim AS build
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
@@ -231,7 +231,7 @@ RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle
# Stage 3 - Build the actual backend image and install production dependencies
FROM node:20-bookworm-slim
FROM node:22-bookworm-slim
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
@@ -126,7 +126,7 @@ metadata:
The value of this annotation informs of the path to this component's TechDocs within an external entity that owns the TechDocs.
In conjunction with [backstage.io/techdocs-entity](#backstageiotechdocs-entity) this allows for deep linking into the TechDocs of
another entity, not just linking to the root of another entities TechDocs.
another entity, not just linking to the root of another entity's TechDocs.
### backstage.io/view-url, backstage.io/edit-url
+6
View File
@@ -12,6 +12,8 @@ This page answers frequently asked questions about [TechDocs](README.md).
- [What static site generator is TechDocs using?](#what-static-site-generator-is-techdocs-using)
- [What is the mkdocs-techdocs-core plugin?](#what-is-the-mkdocs-techdocs-core-plugin)
- [Does TechDocs support file formats other than Markdown (e.g. RST, AsciiDoc)?](#does-techdocs-support-file-formats-other-than-markdown-eg-rst-asciidoc-)
- [What should be the value of `backstage.io/techdocs-ref` when using external build and storage?](#what-should-be-the-value-of-backstageiotechdocs-ref-when-using-external-build-and-storage)
- [Is it possible for users to suggest changes or provide feedback on a TechDocs page?](#is-it-possible-for-users-to-suggest-changes-or-provide-feedback-on-a-techdocs-page)
#### What static site generator is TechDocs using?
@@ -46,6 +48,10 @@ annotation should still be present in entity descriptor file (e.g.
`catalog-info.yaml`) for Backstage to know that TechDocs is enabled for the
entity.
#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation?
If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation.
#### Is it possible for users to suggest changes or provide feedback on a TechDocs page?
This is supported for TechDocs sites whose source code is hosted in either
@@ -185,7 +185,7 @@ export const examplePlugin = createFrontendPlugin({
## Plugin specific extensions
There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](https://backstage.io/docs/features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page.
There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page.
```tsx title="in src/plugin.ts - An example entity content extension"
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
+1 -1
View File
@@ -184,7 +184,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
/* highlight-add-next-line */
backend.add(import('./extensions/catalogPermissionRules'));
backend.add(import('./modules/catalogPermissionRules'));
```
5. Now when you run you Backstage instance - `yarn start` - the rule will be added to the catalog plugin.
@@ -0,0 +1,19 @@
import React from 'react';
type Props = {
searchTerm: string;
onSearchTermChange: (newTerm: string) => void;
};
export const PluginsSearch = (props: Props) => {
const { searchTerm, onSearchTermChange } = props;
return (
<input
name="search"
onInput={e => onSearchTermChange((e.target as HTMLInputElement).value)}
value={searchTerm}
placeholder="Search plugins..."
className="DocSearch-Input search"
/>
);
};
+55 -29
View File
@@ -5,10 +5,11 @@ import { truncateDescription } from '@site/src/util/truncateDescription';
import { ChipCategory } from '@site/src/util/types';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { IPluginData, PluginCard } from './_pluginCard';
import pluginsStyles from './plugins.module.scss';
import { PluginsSearch } from '@site/src/components/pluginsSearch/pluginsSearch';
interface IPluginsList {
corePlugins: IPluginData[];
@@ -55,6 +56,7 @@ const Plugins = () => {
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [showCoreFeatures, setShowCoreFeatures] = useState(true);
const [showOtherPlugins, setShowOtherPlugins] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const handleChipClick = (categoryName: string) => {
const isSelected =
@@ -97,6 +99,34 @@ const Plugins = () => {
}
};
const matchesSearch = (pluginData: IPluginData, term: string) => {
if (!term) return true;
const lowerTerm = term.toLowerCase();
return (
pluginData.title.toLowerCase().includes(lowerTerm) ||
pluginData.description.toLowerCase().includes(lowerTerm) ||
pluginData.category.toLowerCase().includes(lowerTerm) ||
(pluginData.author && pluginData.author.toLowerCase().includes(lowerTerm))
);
};
const matchesCategory = (pluginData: IPluginData, categories: string[]) => {
if (categories.length === 0) return true;
return categories.includes(pluginData.category);
};
const corePlugins = useMemo(() => {
return plugins.corePlugins
.filter(pluginData => matchesCategory(pluginData, selectedCategories))
.filter(pluginData => matchesSearch(pluginData, searchTerm));
}, [selectedCategories, searchTerm]);
const otherPlugins = useMemo(() => {
return plugins.otherPlugins
.filter(pluginData => matchesCategory(pluginData, selectedCategories))
.filter(pluginData => matchesSearch(pluginData, searchTerm));
}, [selectedCategories, searchTerm]);
return (
<Layout>
<div
@@ -126,31 +156,36 @@ const Plugins = () => {
categories={categories}
handleChipClick={handleChipClick}
/>
<PluginsSearch
searchTerm={searchTerm}
onSearchTermChange={setSearchTerm}
/>
</div>
{showCoreFeatures && (
{corePlugins.length === 0 && otherPlugins.length === 0 && (
<div className="margin-vert--lg">
<h3>No plugins found</h3>
<p>
We couldn't find any plugins matching your criteria. Please try
adjusting your search or filter settings.
</p>
</div>
)}
{showCoreFeatures && corePlugins.length > 0 && (
<div>
<h2>Core Features</h2>
<h2>Core Features ({corePlugins.length})</h2>
<div className="pluginsContainer margin-bottom--lg">
{plugins.corePlugins
.filter(
pluginData =>
!selectedCategories.length ||
selectedCategories.includes(pluginData.category),
)
.map(pluginData => (
<PluginCard
key={pluginData.title}
{...pluginData}
></PluginCard>
))}
{corePlugins.map(pluginData => (
<PluginCard key={pluginData.title} {...pluginData}></PluginCard>
))}
</div>
</div>
)}
{showOtherPlugins && (
{showOtherPlugins && otherPlugins.length > 0 && (
<div>
<h2>All Plugins</h2>
<h2>All Plugins ({otherPlugins.length})</h2>
<p>
Friendly reminder: While we love the variety and contributions of
our open source plugins, they haven't been fully vetted by the
@@ -158,18 +193,9 @@ const Plugins = () => {
your due diligence before installing. Happy exploring!
</p>
<div className="pluginsContainer margin-bottom--lg">
{plugins.otherPlugins
.filter(
pluginData =>
!selectedCategories.length ||
selectedCategories.includes(pluginData.category),
)
.map(pluginData => (
<PluginCard
key={pluginData.title}
{...pluginData}
></PluginCard>
))}
{otherPlugins.map(pluginData => (
<PluginCard key={pluginData.title} {...pluginData}></PluginCard>
))}
</div>
</div>
)}
@@ -76,6 +76,20 @@
height: 1rem;
}
:global(.search) {
float: right;
margin-bottom: 1rem;
margin-right: 1rem;
font-size: calc(0.875rem * var(--ifm-button-size-multiplier));
font-weight: var(--ifm-button-font-weight);
background-color: transparent;
border: var(--ifm-button-border-width) solid var(--ifm-color-primary);
border-radius: var(--ifm-button-border-radius);
color: var(--ifm-color-primary);
width: 220px;
height: 2.2rem;
}
:global(.dropdown) {
float: right;
margin-bottom: 1rem;
+2 -2
View File
@@ -25,9 +25,9 @@
"scripts": {
"build-storybook": "storybook build --output-dir dist-storybook",
"build:all": "backstage-cli repo build --all",
"build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(adr|adr-backend|adr-common|airbrake|airbrake-backend|allure|analytics-module-ga|analytics-module-ga4|analytics-module-newrelic-browser|apache-airflow|api-docs|api-docs-module-protoc-gen-doc|apollo-explorer|app-visualizer|azure-devops|azure-devops-backend|azure-devops-common|azure-sites|azure-sites-backend|azure-sites-common|badges|badges-backend|bazaar|bazaar-backend|bitbucket-cloud-common|bitrise|catalog-graph|catalog-graphql|catalog-import|catalog-unprocessed-entities|cicd-statistics|cicd-statistics-module-gitlab|circleci|cloudbuild|code-climate|code-coverage|code-coverage-backend|codescene|config-schema|cost-insights|cost-insights-common|dynatrace|entity-feedback|entity-feedback-backend|entity-feedback-common|entity-validation|example-todo-list|example-todo-list-backend|example-todo-list-common|firehydrant|fossa|gcalendar|gcp-projects|git-release-manager|github-actions|github-deployments|github-issues|github-pull-requests-board|gitops-profiles|gocd|graphiql|graphql-backend|graphql-voyager|ilert|jenkins|jenkins-backend|jenkins-common|kafka|kafka-backend|lighthouse|lighthouse-backend|lighthouse-common|linguist|linguist-backend|linguist-common|microsoft-calendar|newrelic|newrelic-dashboard|nomad|nomad-backend|octopus-deploy|opencost|pagerduty|periskop|periskop-backend|playlist|playlist-backend|playlist-common|proxy-backend|puppetdb|rollbar|rollbar-backend|sentry|shortcuts|splunk-on-call|stack-overflow|stack-overflow-backend|stackstorm|tech-radar|tech-radar-2|todo|todo-backend|xcmetrics)'",
"build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags",
"build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags",
"build:backend": "yarn workspace example-backend build",
"build:knip-reports": "backstage-repo-tools knip-reports",
"build:plugins-report": "node ./scripts/build-plugins-report",
@@ -236,115 +236,14 @@ export class DefaultApiClient {
}
/**
* Get all entities matching a given filter.
* @param fields - By default the full entities are returned, but you can pass in a &#x60;fields&#x60; query
parameter which selects what parts of the entity data to retain. This makes the
response smaller and faster to transfer, and may allow the catalog to perform
more efficient queries.
The query parameter value is a comma separated list of simplified JSON paths
like above. Each path corresponds to the key of either a value, or of a subtree
root that you want to keep in the output. The rest is pruned away. For example,
specifying &#x60;?fields&#x3D;metadata.name,metadata.annotations,spec&#x60; retains only the
&#x60;name&#x60; and &#x60;annotations&#x60; fields of the &#x60;metadata&#x60; of each entity (it&#39;ll be an
object with at most two keys), keeps the entire &#x60;spec&#x60; unchanged, and cuts out
all other roots such as &#x60;relations&#x60;.
Some more real world usable examples:
- Return only enough data to form the full ref of each entity:
&#x60;/entities/by-query?fields&#x3D;kind,metadata.namespace,metadata.name&#x60;
* @param limit - Number of records to return in the response.
* @param filter - You can pass in one or more filter sets that get matched against each entity.
Each filter set is a number of conditions that all have to match for the
condition to be true (conditions effectively have an AND between them). At least
one filter set has to be true for the entity to be part of the result set
(filter sets effectively have an OR between them).
Example:
&#x60;&#x60;&#x60;text
/entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type
Return entities that match
Filter set 1:
Condition 1: kind &#x3D; user
AND
Condition 2: metadata.namespace &#x3D; default
OR
Filter set 2:
Condition 1: kind &#x3D; group
AND
Condition 2: spec.type exists
&#x60;&#x60;&#x60;
Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;.
The first form asserts on the existence of a certain key (with any value), and
the second asserts that the key exists and has a certain value. All checks are
always case _insensitive_.
In all cases, the key is a simplified JSON path in a given piece of entity data.
Each part of the path is a key of an object, and the traversal also descends
through arrays. There are two special forms:
- Array items that are simple value types (such as strings) match on a key-value
pair where the key is the item as a string, and the value is the string &#x60;true&#x60;
- Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form
Let&#39;s look at a simplified example to illustrate the concept:
&#x60;&#x60;&#x60;json
{
&quot;a&quot;: {
&quot;b&quot;: [&quot;c&quot;, { &quot;d&quot;: 1 }],
&quot;e&quot;: 7
}
}
&#x60;&#x60;&#x60;
This would match any one of the following conditions:
- &#x60;a&#x60;
- &#x60;a.b&#x60;
- &#x60;a.b.c&#x60;
- &#x60;a.b.c&#x3D;true&#x60;
- &#x60;a.b.d&#x60;
- &#x60;a.b.d&#x3D;1&#x60;
- &#x60;a.e&#x60;
- &#x60;a.e&#x3D;7&#x60;
Some more real world usable examples:
- Return all orphaned entities:
&#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60;
- Return all users and groups:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60;
- Return all service components:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60;
- Return all entities with the &#x60;java&#x60; tag:
&#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60;
- Return all users who are members of the &#x60;ops&#x60; group (note that the full
[reference](references.md) of the group is used):
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param offset - Number of records to skip in the query page.
* @param after - Pointer to the previous page of results.
* @param order -
*/
* Get all entities matching a given filter.
* @param fields - By default the full entities are returned, but you can pass in a &#x60;fields&#x60; query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying &#x60;?fields&#x3D;metadata.name,metadata.annotations,spec&#x60; retains only the &#x60;name&#x60; and &#x60;annotations&#x60; fields of the &#x60;metadata&#x60; of each entity (it&#39;ll be an object with at most two keys), keeps the entire &#x60;spec&#x60; unchanged, and cuts out all other roots such as &#x60;relations&#x60;. Some more real world usable examples: - Return only enough data to form the full ref of each entity: &#x60;/entities/by-query?fields&#x3D;kind,metadata.namespace,metadata.name&#x60;
* @param limit - Number of records to return in the response.
* @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: &#x60;&#x60;&#x60;text /entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type Return entities that match Filter set 1: Condition 1: kind &#x3D; user AND Condition 2: metadata.namespace &#x3D; default OR Filter set 2: Condition 1: kind &#x3D; group AND Condition 2: spec.type exists &#x60;&#x60;&#x60; Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string &#x60;true&#x60; - Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form Let&#39;s look at a simplified example to illustrate the concept: &#x60;&#x60;&#x60;json { \&quot;a\&quot;: { \&quot;b\&quot;: [\&quot;c\&quot;, { \&quot;d\&quot;: 1 }], \&quot;e\&quot;: 7 } } &#x60;&#x60;&#x60; This would match any one of the following conditions: - &#x60;a&#x60; - &#x60;a.b&#x60; - &#x60;a.b.c&#x60; - &#x60;a.b.c&#x3D;true&#x60; - &#x60;a.b.d&#x60; - &#x60;a.b.d&#x3D;1&#x60; - &#x60;a.e&#x60; - &#x60;a.e&#x3D;7&#x60; Some more real world usable examples: - Return all orphaned entities: &#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60; - Return all users and groups: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60; - Return all service components: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60; - Return all entities with the &#x60;java&#x60; tag: &#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60; - Return all users who are members of the &#x60;ops&#x60; group (note that the full [reference](references.md) of the group is used): &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param offset - Number of records to skip in the query page.
* @param after - Pointer to the previous page of results.
* @param order -
*/
public async getEntities(
// @ts-ignore
request: GetEntities,
@@ -368,148 +267,16 @@ Some more real world usable examples:
}
/**
* Search for entities by a given query.
* @param fields - By default the full entities are returned, but you can pass in a &#x60;fields&#x60; query
parameter which selects what parts of the entity data to retain. This makes the
response smaller and faster to transfer, and may allow the catalog to perform
more efficient queries.
The query parameter value is a comma separated list of simplified JSON paths
like above. Each path corresponds to the key of either a value, or of a subtree
root that you want to keep in the output. The rest is pruned away. For example,
specifying &#x60;?fields&#x3D;metadata.name,metadata.annotations,spec&#x60; retains only the
&#x60;name&#x60; and &#x60;annotations&#x60; fields of the &#x60;metadata&#x60; of each entity (it&#39;ll be an
object with at most two keys), keeps the entire &#x60;spec&#x60; unchanged, and cuts out
all other roots such as &#x60;relations&#x60;.
Some more real world usable examples:
- Return only enough data to form the full ref of each entity:
&#x60;/entities/by-query?fields&#x3D;kind,metadata.namespace,metadata.name&#x60;
* @param limit - Number of records to return in the response.
* @param offset - Number of records to skip in the query page.
* @param orderField - By default the entities are returned ordered by their internal uid. You can
customize the &#x60;orderField&#x60; query parameters to affect that ordering.
For example, to return entities by their name:
&#x60;/entities/by-query?orderField&#x3D;metadata.name,asc&#x60;
Each parameter can be followed by &#x60;asc&#x60; for ascending lexicographical order or
&#x60;desc&#x60; for descending (reverse) lexicographical order.
* @param cursor - You may pass the &#x60;cursor&#x60; query parameters to perform cursor based pagination
through the set of entities. The value of &#x60;cursor&#x60; will be returned in the response, under the &#x60;pageInfo&#x60; property:
&#x60;&#x60;&#x60;json
&quot;pageInfo&quot;: {
&quot;nextCursor&quot;: &quot;a-cursor&quot;,
&quot;prevCursor&quot;: &quot;another-cursor&quot;
}
&#x60;&#x60;&#x60;
If &#x60;nextCursor&#x60; exists, it can be used to retrieve the next batch of entities. Following the same approach,
if &#x60;prevCursor&#x60; exists, it can be used to retrieve the previous batch of entities.
- [&#x60;filter&#x60;](#filtering), for selecting only a subset of all entities
- [&#x60;fields&#x60;](#field-selection), for selecting only parts of the full data
structure of each entity
- &#x60;limit&#x60; for limiting the number of entities returned (20 is the default)
- [&#x60;orderField&#x60;](#ordering), for deciding the order of the entities
- &#x60;fullTextFilter&#x60;
**NOTE**: [&#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60;] and &#x60;cursor&#x60; are mutually exclusive. This means that,
it isn&#39;t possible to change any of [&#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60;] when passing &#x60;cursor&#x60; as query parameters,
as changing any of these properties will affect pagination. If any of &#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60; is specified together with &#x60;cursor&#x60;, only the latter is taken into consideration.
* @param filter - You can pass in one or more filter sets that get matched against each entity.
Each filter set is a number of conditions that all have to match for the
condition to be true (conditions effectively have an AND between them). At least
one filter set has to be true for the entity to be part of the result set
(filter sets effectively have an OR between them).
Example:
&#x60;&#x60;&#x60;text
/entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type
Return entities that match
Filter set 1:
Condition 1: kind &#x3D; user
AND
Condition 2: metadata.namespace &#x3D; default
OR
Filter set 2:
Condition 1: kind &#x3D; group
AND
Condition 2: spec.type exists
&#x60;&#x60;&#x60;
Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;.
The first form asserts on the existence of a certain key (with any value), and
the second asserts that the key exists and has a certain value. All checks are
always case _insensitive_.
In all cases, the key is a simplified JSON path in a given piece of entity data.
Each part of the path is a key of an object, and the traversal also descends
through arrays. There are two special forms:
- Array items that are simple value types (such as strings) match on a key-value
pair where the key is the item as a string, and the value is the string &#x60;true&#x60;
- Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form
Let&#39;s look at a simplified example to illustrate the concept:
&#x60;&#x60;&#x60;json
{
&quot;a&quot;: {
&quot;b&quot;: [&quot;c&quot;, { &quot;d&quot;: 1 }],
&quot;e&quot;: 7
}
}
&#x60;&#x60;&#x60;
This would match any one of the following conditions:
- &#x60;a&#x60;
- &#x60;a.b&#x60;
- &#x60;a.b.c&#x60;
- &#x60;a.b.c&#x3D;true&#x60;
- &#x60;a.b.d&#x60;
- &#x60;a.b.d&#x3D;1&#x60;
- &#x60;a.e&#x60;
- &#x60;a.e&#x3D;7&#x60;
Some more real world usable examples:
- Return all orphaned entities:
&#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60;
- Return all users and groups:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60;
- Return all service components:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60;
- Return all entities with the &#x60;java&#x60; tag:
&#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60;
- Return all users who are members of the &#x60;ops&#x60; group (note that the full
[reference](references.md) of the group is used):
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param fullTextFilterTerm - Text search term.
* @param fullTextFilterFields - A comma separated list of fields to sort returned results by.
*/
* Search for entities by a given query.
* @param fields - By default the full entities are returned, but you can pass in a &#x60;fields&#x60; query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying &#x60;?fields&#x3D;metadata.name,metadata.annotations,spec&#x60; retains only the &#x60;name&#x60; and &#x60;annotations&#x60; fields of the &#x60;metadata&#x60; of each entity (it&#39;ll be an object with at most two keys), keeps the entire &#x60;spec&#x60; unchanged, and cuts out all other roots such as &#x60;relations&#x60;. Some more real world usable examples: - Return only enough data to form the full ref of each entity: &#x60;/entities/by-query?fields&#x3D;kind,metadata.namespace,metadata.name&#x60;
* @param limit - Number of records to return in the response.
* @param offset - Number of records to skip in the query page.
* @param orderField - By default the entities are returned ordered by their internal uid. You can customize the &#x60;orderField&#x60; query parameters to affect that ordering. For example, to return entities by their name: &#x60;/entities/by-query?orderField&#x3D;metadata.name,asc&#x60; Each parameter can be followed by &#x60;asc&#x60; for ascending lexicographical order or &#x60;desc&#x60; for descending (reverse) lexicographical order.
* @param cursor - You may pass the &#x60;cursor&#x60; query parameters to perform cursor based pagination through the set of entities. The value of &#x60;cursor&#x60; will be returned in the response, under the &#x60;pageInfo&#x60; property: &#x60;&#x60;&#x60;json \&quot;pageInfo\&quot;: { \&quot;nextCursor\&quot;: \&quot;a-cursor\&quot;, \&quot;prevCursor\&quot;: \&quot;another-cursor\&quot; } &#x60;&#x60;&#x60; If &#x60;nextCursor&#x60; exists, it can be used to retrieve the next batch of entities. Following the same approach, if &#x60;prevCursor&#x60; exists, it can be used to retrieve the previous batch of entities. - [&#x60;filter&#x60;](#filtering), for selecting only a subset of all entities - [&#x60;fields&#x60;](#field-selection), for selecting only parts of the full data structure of each entity - &#x60;limit&#x60; for limiting the number of entities returned (20 is the default) - [&#x60;orderField&#x60;](#ordering), for deciding the order of the entities - &#x60;fullTextFilter&#x60; **NOTE**: [&#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60;] and &#x60;cursor&#x60; are mutually exclusive. This means that, it isn&#39;t possible to change any of [&#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60;] when passing &#x60;cursor&#x60; as query parameters, as changing any of these properties will affect pagination. If any of &#x60;filter&#x60;, &#x60;orderField&#x60;, &#x60;fullTextFilter&#x60; is specified together with &#x60;cursor&#x60;, only the latter is taken into consideration.
* @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: &#x60;&#x60;&#x60;text /entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type Return entities that match Filter set 1: Condition 1: kind &#x3D; user AND Condition 2: metadata.namespace &#x3D; default OR Filter set 2: Condition 1: kind &#x3D; group AND Condition 2: spec.type exists &#x60;&#x60;&#x60; Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string &#x60;true&#x60; - Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form Let&#39;s look at a simplified example to illustrate the concept: &#x60;&#x60;&#x60;json { \&quot;a\&quot;: { \&quot;b\&quot;: [\&quot;c\&quot;, { \&quot;d\&quot;: 1 }], \&quot;e\&quot;: 7 } } &#x60;&#x60;&#x60; This would match any one of the following conditions: - &#x60;a&#x60; - &#x60;a.b&#x60; - &#x60;a.b.c&#x60; - &#x60;a.b.c&#x3D;true&#x60; - &#x60;a.b.d&#x60; - &#x60;a.b.d&#x3D;1&#x60; - &#x60;a.e&#x60; - &#x60;a.e&#x3D;7&#x60; Some more real world usable examples: - Return all orphaned entities: &#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60; - Return all users and groups: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60; - Return all service components: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60; - Return all entities with the &#x60;java&#x60; tag: &#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60; - Return all users who are members of the &#x60;ops&#x60; group (note that the full [reference](references.md) of the group is used): &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param fullTextFilterTerm - Text search term.
* @param fullTextFilterFields - A comma separated list of fields to sort returned results by.
*/
public async getEntitiesByQuery(
// @ts-ignore
request: GetEntitiesByQuery,
@@ -533,93 +300,10 @@ Some more real world usable examples:
}
/**
* Get a batch set of entities given an array of entityRefs.
* @param filter - You can pass in one or more filter sets that get matched against each entity.
Each filter set is a number of conditions that all have to match for the
condition to be true (conditions effectively have an AND between them). At least
one filter set has to be true for the entity to be part of the result set
(filter sets effectively have an OR between them).
Example:
&#x60;&#x60;&#x60;text
/entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type
Return entities that match
Filter set 1:
Condition 1: kind &#x3D; user
AND
Condition 2: metadata.namespace &#x3D; default
OR
Filter set 2:
Condition 1: kind &#x3D; group
AND
Condition 2: spec.type exists
&#x60;&#x60;&#x60;
Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;.
The first form asserts on the existence of a certain key (with any value), and
the second asserts that the key exists and has a certain value. All checks are
always case _insensitive_.
In all cases, the key is a simplified JSON path in a given piece of entity data.
Each part of the path is a key of an object, and the traversal also descends
through arrays. There are two special forms:
- Array items that are simple value types (such as strings) match on a key-value
pair where the key is the item as a string, and the value is the string &#x60;true&#x60;
- Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form
Let&#39;s look at a simplified example to illustrate the concept:
&#x60;&#x60;&#x60;json
{
&quot;a&quot;: {
&quot;b&quot;: [&quot;c&quot;, { &quot;d&quot;: 1 }],
&quot;e&quot;: 7
}
}
&#x60;&#x60;&#x60;
This would match any one of the following conditions:
- &#x60;a&#x60;
- &#x60;a.b&#x60;
- &#x60;a.b.c&#x60;
- &#x60;a.b.c&#x3D;true&#x60;
- &#x60;a.b.d&#x60;
- &#x60;a.b.d&#x3D;1&#x60;
- &#x60;a.e&#x60;
- &#x60;a.e&#x3D;7&#x60;
Some more real world usable examples:
- Return all orphaned entities:
&#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60;
- Return all users and groups:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60;
- Return all service components:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60;
- Return all entities with the &#x60;java&#x60; tag:
&#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60;
- Return all users who are members of the &#x60;ops&#x60; group (note that the full
[reference](references.md) of the group is used):
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param getEntitiesByRefsRequest -
*/
* Get a batch set of entities given an array of entityRefs.
* @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: &#x60;&#x60;&#x60;text /entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type Return entities that match Filter set 1: Condition 1: kind &#x3D; user AND Condition 2: metadata.namespace &#x3D; default OR Filter set 2: Condition 1: kind &#x3D; group AND Condition 2: spec.type exists &#x60;&#x60;&#x60; Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string &#x60;true&#x60; - Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form Let&#39;s look at a simplified example to illustrate the concept: &#x60;&#x60;&#x60;json { \&quot;a\&quot;: { \&quot;b\&quot;: [\&quot;c\&quot;, { \&quot;d\&quot;: 1 }], \&quot;e\&quot;: 7 } } &#x60;&#x60;&#x60; This would match any one of the following conditions: - &#x60;a&#x60; - &#x60;a.b&#x60; - &#x60;a.b.c&#x60; - &#x60;a.b.c&#x3D;true&#x60; - &#x60;a.b.d&#x60; - &#x60;a.b.d&#x3D;1&#x60; - &#x60;a.e&#x60; - &#x60;a.e&#x3D;7&#x60; Some more real world usable examples: - Return all orphaned entities: &#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60; - Return all users and groups: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60; - Return all service components: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60; - Return all entities with the &#x60;java&#x60; tag: &#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60; - Return all users who are members of the &#x60;ops&#x60; group (note that the full [reference](references.md) of the group is used): &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
* @param getEntitiesByRefsRequest -
*/
public async getEntitiesByRefs(
// @ts-ignore
request: GetEntitiesByRefs,
@@ -730,93 +414,10 @@ Some more real world usable examples:
}
/**
* Get all entity facets that match the given filters.
* @param facet -
* @param filter - You can pass in one or more filter sets that get matched against each entity.
Each filter set is a number of conditions that all have to match for the
condition to be true (conditions effectively have an AND between them). At least
one filter set has to be true for the entity to be part of the result set
(filter sets effectively have an OR between them).
Example:
&#x60;&#x60;&#x60;text
/entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type
Return entities that match
Filter set 1:
Condition 1: kind &#x3D; user
AND
Condition 2: metadata.namespace &#x3D; default
OR
Filter set 2:
Condition 1: kind &#x3D; group
AND
Condition 2: spec.type exists
&#x60;&#x60;&#x60;
Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;.
The first form asserts on the existence of a certain key (with any value), and
the second asserts that the key exists and has a certain value. All checks are
always case _insensitive_.
In all cases, the key is a simplified JSON path in a given piece of entity data.
Each part of the path is a key of an object, and the traversal also descends
through arrays. There are two special forms:
- Array items that are simple value types (such as strings) match on a key-value
pair where the key is the item as a string, and the value is the string &#x60;true&#x60;
- Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form
Let&#39;s look at a simplified example to illustrate the concept:
&#x60;&#x60;&#x60;json
{
&quot;a&quot;: {
&quot;b&quot;: [&quot;c&quot;, { &quot;d&quot;: 1 }],
&quot;e&quot;: 7
}
}
&#x60;&#x60;&#x60;
This would match any one of the following conditions:
- &#x60;a&#x60;
- &#x60;a.b&#x60;
- &#x60;a.b.c&#x60;
- &#x60;a.b.c&#x3D;true&#x60;
- &#x60;a.b.d&#x60;
- &#x60;a.b.d&#x3D;1&#x60;
- &#x60;a.e&#x60;
- &#x60;a.e&#x3D;7&#x60;
Some more real world usable examples:
- Return all orphaned entities:
&#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60;
- Return all users and groups:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60;
- Return all service components:
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60;
- Return all entities with the &#x60;java&#x60; tag:
&#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60;
- Return all users who are members of the &#x60;ops&#x60; group (note that the full
[reference](references.md) of the group is used):
&#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
*/
* Get all entity facets that match the given filters.
* @param facet -
* @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: &#x60;&#x60;&#x60;text /entities/by-query?filter&#x3D;kind&#x3D;user,metadata.namespace&#x3D;default&amp;filter&#x3D;kind&#x3D;group,spec.type Return entities that match Filter set 1: Condition 1: kind &#x3D; user AND Condition 2: metadata.namespace &#x3D; default OR Filter set 2: Condition 1: kind &#x3D; group AND Condition 2: spec.type exists &#x60;&#x60;&#x60; Each condition is either on the form &#x60;&lt;key&gt;&#x60;, or on the form &#x60;&lt;key&gt;&#x3D;&lt;value&gt;&#x60;. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string &#x60;true&#x60; - Relations can be matched on a &#x60;relations.&lt;type&gt;&#x3D;&lt;targetRef&gt;&#x60; form Let&#39;s look at a simplified example to illustrate the concept: &#x60;&#x60;&#x60;json { \&quot;a\&quot;: { \&quot;b\&quot;: [\&quot;c\&quot;, { \&quot;d\&quot;: 1 }], \&quot;e\&quot;: 7 } } &#x60;&#x60;&#x60; This would match any one of the following conditions: - &#x60;a&#x60; - &#x60;a.b&#x60; - &#x60;a.b.c&#x60; - &#x60;a.b.c&#x3D;true&#x60; - &#x60;a.b.d&#x60; - &#x60;a.b.d&#x3D;1&#x60; - &#x60;a.e&#x60; - &#x60;a.e&#x3D;7&#x60; Some more real world usable examples: - Return all orphaned entities: &#x60;/entities/by-query?filter&#x3D;metadata.annotations.backstage.io/orphan&#x3D;true&#x60; - Return all users and groups: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user&amp;filter&#x3D;kind&#x3D;group&#x60; - Return all service components: &#x60;/entities/by-query?filter&#x3D;kind&#x3D;component,spec.type&#x3D;service&#x60; - Return all entities with the &#x60;java&#x60; tag: &#x60;/entities/by-query?filter&#x3D;metadata.tags.java&#x60; - Return all users who are members of the &#x60;ops&#x60; group (note that the full [reference](references.md) of the group is used): &#x60;/entities/by-query?filter&#x3D;kind&#x3D;user,relations.memberof&#x3D;group:default/ops&#x60;
*/
public async getEntityFacets(
// @ts-ignore
request: GetEntityFacets,
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2025 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.
@@ -430,10 +430,10 @@ export async function createConfig(
path: paths.targetDist,
publicPath:
options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`,
filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js',
filename: isDev ? '[name].js' : 'static/[name].[contenthash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
: 'static/[name].[chunkhash:8].chunk.js',
: 'static/[name].[contenthash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: (info: any) =>
@@ -72,7 +72,7 @@ export const optimization = (
},
filename: isDev
? 'module-[name].js'
: 'static/module-[name].[chunkhash:8].js',
: 'static/module-[name].[contenthash:8].js',
priority: 10,
minSize: 100000,
minChunks: 1,
@@ -772,6 +772,7 @@ describe('bump', () => {
res(
ctx.status(200),
ctx.json({
releaseVersion: '1.0.0',
packages: [],
}),
),
@@ -797,7 +798,7 @@ describe('bump', () => {
'bumping @backstage-extra/custom in b to ^1.1.0',
'bumping @backstage-extra/custom-two in b to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'Skipping backstage.json update as custom pattern is used',
'Your project is now at version 1.0.0, which has been written to backstage.json',
'Running yarn install to install new versions',
'Checking for moved packages to the @backstage-community namespace...',
'⚠️ The following packages may have breaking changes:',
@@ -18,6 +18,7 @@ maybeBootstrapProxy();
import fs from 'fs-extra';
import chalk from 'chalk';
import { minimatch } from 'minimatch';
import semver from 'semver';
import { OptionValues } from 'commander';
import yaml from 'yaml';
@@ -78,6 +79,14 @@ type PkgVersionInfo = {
location: string;
};
function extendsDefaultPattern(pattern: string): boolean {
if (!pattern.endsWith('/*')) {
return false;
}
return minimatch('@backstage/', pattern.slice(0, -1));
}
export default async (opts: OptionValues) => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
@@ -245,8 +254,8 @@ export default async (opts: OptionValues) => {
console.log();
// Do not update backstage.json when upgrade patterns are used.
if (pattern === DEFAULT_PATTERN_GLOB) {
// Do not update backstage.json when default pattern is not covered
if (extendsDefaultPattern(pattern)) {
await bumpBackstageJsonVersion(
releaseManifest.releaseVersion,
hasYarnPlugin,
@@ -144,11 +144,26 @@ describe('AppLanguageSelector', () => {
AppLanguageSelector.createWithStorage({
availableLanguages: ['de'],
}),
).toThrow("Supported languages must include 'en'");
).toThrow(
"Initial language must be one of the supported languages, got 'en'",
);
const selector = AppLanguageSelector.createWithStorage(baseOptions);
expect(() => selector.setLanguage('sv')).toThrow(
"Failed to change language to 'sv', available languages are 'en', 'de'",
);
});
it('should support no en languages', () => {
const selector = AppLanguageSelector.createWithStorage({
availableLanguages: ['de'],
defaultLanguage: 'de',
});
expect(selector.getLanguage()).toEqual({ language: 'de' });
expect(() => selector.setLanguage('en')).toThrow(
"Failed to change language to 'en', available languages are 'de'",
);
});
});
@@ -44,11 +44,9 @@ export class AppLanguageSelector implements AppLanguageApi {
)}'`,
);
}
if (!languages.includes(DEFAULT_LANGUAGE)) {
throw new Error(`Supported languages must include '${DEFAULT_LANGUAGE}'`);
}
const initialLanguage = options?.defaultLanguage ?? DEFAULT_LANGUAGE;
if (!languages.includes(initialLanguage)) {
throw new Error(
`Initial language must be one of the supported languages, got '${initialLanguage}'`,
-25
View File
@@ -16,28 +16,3 @@
// package: the name of the package, e.g. @testing-library/react
// query: the version query to pin the version for, e.g. ^14.0.0
// version: the version to pin to, must be in range of the query, e.g. 14.11.0
"@google-cloud/storage@^7.0.0":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.14.0.tgz#eda9715f68507949214af804c906eba6d168a214"
integrity sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==
"@octokit/types@npm:^13.0.0":
version "13.6.2"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd"
integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==
"@octokit/types@npm:^13.1.0":
version "13.6.2"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd"
integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==
"@octokit/types@npm:^13.5.0":
version "13.6.2"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd"
integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==
"@octokit/openapi-types@^22.2.0":
version "22.2.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e"
integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==
@@ -12,7 +12,7 @@
# Alternatively, there is also a multi-stage Dockerfile documented here:
# https://backstage.io/docs/deployment/docker#multi-stage-build
FROM node:20-bookworm-slim
FROM node:22-bookworm-slim
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
@@ -12,7 +12,7 @@
# Alternatively, there is also a multi-stage Dockerfile documented here:
# https://backstage.io/docs/deployment/docker#multi-stage-build
FROM node:20-bookworm-slim
FROM node:22-bookworm-slim
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
@@ -231,6 +231,42 @@ describe('SingleInstanceGithubCredentialsProvider tests', () => {
expect(token).toEqual(undefined);
});
it('should not fail to issue tokens for an organization when there is a case mismatch in the organization name', async () => {
octokit.apps.listInstallations.mockResolvedValue({
headers: {
etag: '123',
},
data: [
{
id: 1,
repository_selection: 'selected',
account: {
login: 'backstage',
},
},
],
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({
data: {
expires_at: DateTime.local().plus({ hours: 1 }).toString(),
token: 'secret_token',
repository_selection: 'selected',
},
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
data: [{ name: 'some-repo' }],
} as unknown as RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']);
const { token, headers } = await github.getCredentials({
url: 'https://github.com/Backstage',
});
const expectedToken = 'secret_token';
expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` });
expect(token).toEqual('secret_token');
});
it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => {
octokit.apps.listInstallations.mockResolvedValue({
headers: {
@@ -102,7 +102,9 @@ class GithubAppManager {
private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations
constructor(config: GithubAppConfig, baseUrl?: string) {
this.allowedInstallationOwners = config.allowedInstallationOwners;
this.allowedInstallationOwners = config.allowedInstallationOwners?.map(
owner => owner.toLocaleLowerCase('en-US'),
);
this.baseUrl = baseUrl;
this.baseAuthConfig = {
appId: config.appId,
@@ -121,7 +123,11 @@ class GithubAppManager {
repo?: string,
): Promise<{ accessToken: string | undefined }> {
if (this.allowedInstallationOwners) {
if (!this.allowedInstallationOwners?.includes(owner)) {
if (
!this.allowedInstallationOwners?.includes(
owner.toLocaleLowerCase('en-US'),
)
) {
return { accessToken: undefined }; // An empty token allows anonymous access to public repos
}
}
@@ -113,6 +113,7 @@ export const DisabledWithIcon: Story = {
...WithIcon.args,
isDisabled: true,
},
render: WithIcon.render,
};
export const ShowError: Story = {
+5 -1
View File
@@ -22,7 +22,8 @@ type UseMediaQueryOptions = {
initializeWithValue?: boolean;
};
const IS_SERVER = typeof window === 'undefined';
const IS_SERVER =
typeof window === 'undefined' || typeof window.matchMedia === 'undefined';
export function useMediaQuery(
query: string,
@@ -51,6 +52,9 @@ export function useMediaQuery(
}
useIsomorphicLayoutEffect(() => {
if (IS_SERVER) {
return;
}
const matchMedia = window.matchMedia(query);
// Triggered at the first client-side load and if query changes
@@ -59,6 +59,7 @@ function setupFakeServer(
page: number;
include_subgroups: boolean;
archived: boolean;
simple?: boolean;
}) => {
data: GitLabProject[];
nextPage?: number;
@@ -78,10 +79,12 @@ function setupFakeServer(
const page = req.url.searchParams.get('page');
const include_subgroups = req.url.searchParams.get('include_subgroups');
const archived = req.url.searchParams.get('archived');
const simple = req.url.searchParams.get('simple');
const response = listProjectsCallback({
page: parseInt(page!, 10),
include_subgroups: include_subgroups === 'true',
archived: archived === 'true',
simple: simple === 'true',
});
// Filter the fake results based on the `last_activity_after` parameter
@@ -471,6 +474,110 @@ describe('GitlabDiscoveryProcessor', () => {
});
expect(result2).toHaveLength(1);
});
it('sets simple=true when skipForkedRepos is false', async () => {
const processor = getProcessor({
options: { skipForkedRepos: false },
});
setupFakeServer(
PROJECTS_URL,
_ => {
return {
data: [
{
id: 1,
archived: false,
default_branch: 'main',
last_activity_at: '2021-08-05T11:03:05.774Z',
web_url: 'https://gitlab.fake/1',
path_with_namespace: '1',
},
],
};
},
request => {
// Verify that simple=true is set in the request
expect(request.url.searchParams.get('simple')).toBe('true');
},
);
const result: any[] = [];
await processor.readLocation(PROJECT_LOCATION, false, e => {
result.push(e);
});
expect(result).toHaveLength(1);
});
it('does not set simple when skipForkedRepos is true', async () => {
const processor = getProcessor({
options: { skipForkedRepos: true },
});
setupFakeServer(
PROJECTS_URL,
_ => {
return {
data: [
{
id: 1,
archived: false,
default_branch: 'main',
last_activity_at: '2021-08-05T11:03:05.774Z',
web_url: 'https://gitlab.fake/1',
path_with_namespace: '1',
// Include forked_from_project to test fork filtering
forked_from_project: {
id: 100,
name: 'original-project',
},
},
],
};
},
request => {
// Verify that simple parameter is not set
expect(request.url.searchParams.get('simple')).toBeNull();
},
);
const result: any[] = [];
await processor.readLocation(PROJECT_LOCATION, false, e => {
result.push(e);
});
// Should be empty because forked repo is skipped
expect(result).toHaveLength(0);
});
it('sets default parameters correctly (archived=false, simple=true)', async () => {
const processor = getProcessor(); // Uses defaults: skipForkedRepos=false, includeArchivedRepos=false
setupFakeServer(
PROJECTS_URL,
_ => {
return {
data: [
{
id: 1,
archived: false,
default_branch: 'main',
last_activity_at: '2021-08-05T11:03:05.774Z',
web_url: 'https://gitlab.fake/1',
path_with_namespace: '1',
},
],
};
},
request => {
// Verify default parameters: archived=false and simple=true
expect(request.url.searchParams.get('archived')).toBe('false');
expect(request.url.searchParams.get('simple')).toBe('true');
},
);
const result: any[] = [];
await processor.readLocation(PROJECT_LOCATION, false, e => {
result.push(e);
});
expect(result).toHaveLength(1);
});
});
describe('handles failure', () => {
@@ -115,6 +115,11 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor {
// that the options doesn't include the key so that the API doesn't receive an empty query parameter.
...(lastActivity && { last_activity_after: lastActivity }),
...(!this.includeArchivedRepos && { archived: false }),
// Only use simple=true when we don't need to skip forked repos.
// The simple=true parameter reduces response size by returning fewer fields,
// but it excludes the 'forked_from_project' field which is required for fork detection.
// Therefore, we can only optimize with simple=true when skipForkedRepos is false.
...(!this.skipForkedRepos && { simple: true }),
};
const projects = paginated(options => client.listProjects(options), opts);
@@ -187,6 +187,55 @@ describe('GitLabClient', () => {
expect(allProjects).toHaveLength(mock.all_projects_response.length);
});
it('should pass simple parameter to API when provided', async () => {
const client = new GitLabClient({
config: readGitLabIntegrationConfig(
new ConfigReader(mock.config_self_managed),
),
logger: mockServices.logger.mock(),
});
// Mock the pagedRequest method to verify parameters
const mockPagedRequest = jest.fn().mockResolvedValue({
items: [],
nextPage: undefined,
});
(client as any).pagedRequest = mockPagedRequest;
await client.listProjects({ simple: true });
expect(mockPagedRequest).toHaveBeenCalledWith('/projects', {
simple: true,
});
});
it('should pass simple parameter to group projects API when provided', async () => {
const client = new GitLabClient({
config: readGitLabIntegrationConfig(
new ConfigReader(mock.config_self_managed),
),
logger: mockServices.logger.mock(),
});
// Mock the pagedRequest method to verify parameters
const mockPagedRequest = jest.fn().mockResolvedValue({
items: [],
nextPage: undefined,
});
(client as any).pagedRequest = mockPagedRequest;
await client.listProjects({ group: 'test-group', simple: true });
expect(mockPagedRequest).toHaveBeenCalledWith(
'/groups/test-group/projects',
{
group: 'test-group',
simple: true,
include_subgroups: true,
},
);
});
});
describe('listUsers', () => {
@@ -43,6 +43,7 @@ interface ListProjectOptions extends CommonListOptions {
group?: string;
membership?: boolean;
topics?: string;
simple?: boolean;
}
interface UserListOptions extends CommonListOptions {
@@ -147,6 +147,21 @@ describe('GitlabDiscoveryEntityProvider - refresh', () => {
'GitlabDiscoveryEntityProvider:test-id',
);
// Mock the GitLabClient listProjects method to verify default parameters
const originalListProjects = (provider as any).gitLabClient.listProjects;
const mockListProjects = jest.fn().mockImplementation(async options => {
// Verify default parameters: archived=false and simple=true (since skipForkedRepos=false by default)
expect(options).toMatchObject({
group: 'group1',
per_page: 50,
archived: false,
simple: true, // Should be set since skipForkedRepos defaults to false
});
// Call the original method to maintain test behavior
return originalListProjects.call((provider as any).gitLabClient, options);
});
(provider as any).gitLabClient.listProjects = mockListProjects;
await provider.connect(entityProviderConnection);
const taskDef = schedule.getTasks()[0];
@@ -675,3 +690,115 @@ describe('GitlabDiscoveryEntityProvider - events', () => {
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
});
});
describe('GitlabDiscoveryEntityProvider - simple parameter', () => {
it('should pass simple=true when skipForkedRepos is false', async () => {
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'example.com',
apiBaseUrl: 'https://example.com/api/v4',
token: 'test-token',
},
],
},
catalog: {
providers: {
gitlab: {
'test-id': {
host: 'example.com',
group: 'test-group',
skipForkedRepos: false,
},
},
},
},
});
const schedule = new PersistingTaskRunner();
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
const provider = GitlabDiscoveryEntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
// Mock the GitLabClient listProjects method to verify parameters
const mockListProjects = jest.fn().mockResolvedValue({
items: [],
nextPage: undefined,
});
(provider as any).gitLabClient.listProjects = mockListProjects;
await provider.connect(entityProviderConnection);
await provider.refresh(logger);
expect(mockListProjects).toHaveBeenCalledWith({
group: 'test-group',
page: undefined,
per_page: 50,
archived: false,
simple: true, // Should be set when skipForkedRepos is false
});
});
it('should not pass simple when skipForkedRepos is true', async () => {
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'example.com',
apiBaseUrl: 'https://example.com/api/v4',
token: 'test-token',
},
],
},
catalog: {
providers: {
gitlab: {
'test-id': {
host: 'example.com',
group: 'test-group',
skipForkedRepos: true,
},
},
},
},
});
const schedule = new PersistingTaskRunner();
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
const provider = GitlabDiscoveryEntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
// Mock the GitLabClient listProjects method to verify parameters
const mockListProjects = jest.fn().mockResolvedValue({
items: [],
nextPage: undefined,
});
(provider as any).gitLabClient.listProjects = mockListProjects;
await provider.connect(entityProviderConnection);
await provider.refresh(logger);
expect(mockListProjects).toHaveBeenCalledWith({
group: 'test-group',
page: undefined,
per_page: 50,
archived: false,
// simple should not be present when skipForkedRepos is true
});
});
});
@@ -326,6 +326,11 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider {
...(!this.config.includeArchivedRepos && { archived: false }),
...(this.config.membership && { membership: true }),
...(this.config.topics && { topics: this.config.topics }),
// Only use simple=true when we don't need to skip forked repos.
// The simple=true parameter reduces response size by returning fewer fields,
// but it excludes the 'forked_from_project' field which is required for fork detection.
// Therefore, we can only optimize with simple=true when skipForkedRepos is false.
...(!this.config.skipForkedRepos && { simple: true }),
},
);
@@ -1102,6 +1102,14 @@ describe('read microsoft graph', () => {
name: 'c',
},
});
const groupD = group({
metadata: {
annotations: {
'graph.microsoft.com/group-id': 'group-id-d',
},
name: 'd',
},
});
const user1 = user({
metadata: {
annotations: {
@@ -1118,16 +1126,17 @@ describe('read microsoft graph', () => {
name: 'user2',
},
});
const groups = [rootGroup, groupA, groupB, groupC];
const groups = [rootGroup, groupA, groupB, groupC, groupD];
const users = [user1, user2];
const groupMember = new Map<string, Set<string>>();
groupMember.set('group-id-b', new Set(['group-id-c']));
groupMember.set('group-id-d', new Set(['group-id-c']));
const groupMemberOf = new Map<string, Set<string>>();
groupMemberOf.set('user-id-1', new Set(['group-id-a']));
groupMemberOf.set('user-id-2', new Set(['group-id-c']));
// We have a root groups
// We have three groups: a, b, c. c is child of b
// We have three groups: a, b, c. c is child of b and d, b should be picked as it's first
// we have two users: u1, u2. u1 is member of a, u2 is member of c
resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf);
@@ -1147,6 +1156,11 @@ describe('read microsoft graph', () => {
expect(groupC.spec.parent).toEqual('group:default/b');
expect(groupC.spec.children).toEqual(expect.arrayContaining([]));
expect(groupD.spec.parent).toEqual('group:default/root');
expect(groupD.spec.children).toEqual(
expect.arrayContaining(['group:default/c']),
);
expect(user1.spec.memberOf).toEqual(
expect.arrayContaining(['group:default/a']),
);
@@ -359,7 +359,14 @@ export function resolveRelations(
}
});
// TODO: Until we have better support for multiple parents in the model,
// the order of the parents is important as changing it causes
// unnecessary entity stitching randomly.
retrieveItems(parentGroups, id).forEach(p => {
// Only set the parent if it doesn't exist yet
if (group.spec.parent) {
return;
}
const parentGroup = groupMap.get(p);
if (parentGroup) {
// TODO: Only having a single parent group might not match every companies model, but fine for now.
@@ -546,5 +553,5 @@ function retrieveItems(
target: Map<string, Set<string>>,
key: string,
): Set<string> {
return target.get(key) ?? new Set();
return new Set([...(target.get(key) ?? [])].sort());
}
+4 -1
View File
@@ -38,8 +38,11 @@ export interface Config {
* Allow entities of these particular kinds.
*
* E.g. ["Component", "API", "Template", "Location"]
*
* You can also specify the type of the entity by using an object with `kind` and optional `spec.type` properties.
* E.g. [{ kind: "Component", 'spec.type': "service" }]
*/
allow: Array<string>;
allow: Array<string | { kind: string; 'spec.type'?: string }>;
/**
* Limit this rule to a specific location
*
@@ -28,6 +28,11 @@ export const createGetCatalogEntityAction = ({
actionsRegistry.register({
name: 'get-catalog-entity',
title: 'Get Catalog Entity',
attributes: {
destructive: false,
readOnly: true,
idempotent: true,
},
description: `
This allows you to get a single entity from the software catalog.
Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default".

Some files were not shown because too many files have changed in this diff Show More