Merge branch 'backstage:master' into plugin-azure-functions

This commit is contained in:
Wesley
2022-10-03 14:19:27 +02:00
committed by GitHub
9 changed files with 269 additions and 7 deletions
+37
View File
@@ -0,0 +1,37 @@
---
'@backstage/plugin-catalog': minor
---
Added new column `Label` to `CatalogTable.columns`, this new column allows you make use of labels from metadata.
For example: category and visibility are type of labels associated with API entity illustrated below.
YAML code snippet for API entity
```yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: sample-api
description: API for sample
links:
- url: http://localhost:8080/swagger-ui.html
title: Swagger UI
tags:
- http
labels:
category: legacy
visibility: protected
```
Consumers can customise columns to include label column and show in api-docs list
```typescript
const columns = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
CatalogTable.columns.createLabelColumn('visibility', {
title: 'Visibility',
defaultValue: 'public',
}),
];
```
+5 -1
View File
@@ -53,6 +53,10 @@ jobs:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
script: |
const releaseVersion = require('./backstage/package.json').version;
if(releaseVersion.includes('next')) {
return;
}
console.log('Dispatching upgrade helper sync');
await github.rest.actions.createWorkflowDispatch({
owner: 'backstage',
@@ -61,6 +65,6 @@ jobs:
ref: 'master',
inputs: {
version: require('./backstage/packages/create-app/package.json').version,
releaseVersion: require('./backstage/package.json').version
releaseVersion
},
});
+6
View File
@@ -96,6 +96,10 @@ catalog:
topic:
include: ['backstage-include'] # optional array of strings
exclude: ['experiments'] # optional array of strings
enterpriseProviderId:
host: ghe.example.net
organization: 'backstage' # string
catalogPath: '/catalog-info.yaml' # string
```
This provider supports multiple organizations via unique provider IDs.
@@ -125,6 +129,8 @@ This provider supports multiple organizations via unique provider IDs.
- **organization**:
Name of your organization account/workspace.
If you want to add multiple organizations, you need to add one provider config each.
- **host** _(optional)_:
The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md).
## GitHub API Rate Limits
+1
View File
@@ -6,6 +6,7 @@ description: Support and Community Details and Links
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the
project.
- [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow.
- [Good First Issues](https://github.com/backstage/backstage/contribute) - Start
here if you want to contribute.
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the
+139 -5
View File
@@ -34,7 +34,10 @@ import { ScmIntegrations } from '@backstage/integration';
import { DefaultCatalogRulesEnforcer } from './ingestion/CatalogRules';
import { Stitcher } from './stitching/Stitcher';
import { DefaultEntitiesCatalog } from './service/DefaultEntitiesCatalog';
import { DefaultCatalogProcessingEngine } from './processing/DefaultCatalogProcessingEngine';
import {
DefaultCatalogProcessingEngine,
ProgressTracker,
} from './processing/DefaultCatalogProcessingEngine';
import { createHash } from 'crypto';
import { DefaultRefreshService } from './service/DefaultRefreshService';
import { connectEntityProviders } from './processing/connectEntityProviders';
@@ -69,10 +72,6 @@ class TestProvider implements EntityProvider {
}
}
type ProgressTracker = NonNullable<
ConstructorParameters<typeof DefaultCatalogProcessingEngine>[7]
>;
class ProxyProgressTracker implements ProgressTracker {
#inner: ProgressTracker;
@@ -540,4 +539,139 @@ describe('Catalog Backend Integration', () => {
await expect(harness.getOutputEntities()).resolves.toEqual(outputEntities);
});
// NOTE(freben): This test documents existing behavior, but it would be more correct to mark the cycle as orphans
it('leaves behind orphaned cycles without orphan markers', async () => {
function mkEntity(name: string) {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name,
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
};
}
const harness = await TestHarness.create({
async processEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
) {
if (entity.spec?.noEmit) {
return entity;
}
switch (entity.metadata.name) {
case 'a':
emit(processingResult.entity(location, mkEntity('b')));
break;
case 'b':
emit(processingResult.entity(location, mkEntity('c')));
break;
case 'c':
emit(processingResult.entity(location, mkEntity('d')));
break;
case 'd':
emit(processingResult.entity(location, mkEntity('b')));
break;
default:
}
return entity;
},
});
await harness.setInputEntities([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'a',
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
},
]);
await expect(harness.getOutputEntities()).resolves.toEqual({});
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': expect.objectContaining({
metadata: expect.objectContaining({ name: 'a' }),
}),
'component:default/b': expect.objectContaining({
metadata: expect.objectContaining({ name: 'b' }),
}),
'component:default/c': expect.objectContaining({
metadata: expect.objectContaining({ name: 'c' }),
}),
'component:default/d': expect.objectContaining({
metadata: expect.objectContaining({ name: 'd' }),
}),
});
// NOTE(freben): Avoid .toHaveProperty here, since it treats dots as path separators
expect(
(await harness.getOutputEntities())['component:default/b'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/c'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/d'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
await harness.setInputEntities([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'a',
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
spec: { noEmit: true },
},
]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': expect.objectContaining({
metadata: expect.objectContaining({ name: 'a' }),
}),
'component:default/b': expect.objectContaining({
metadata: expect.objectContaining({ name: 'b' }),
}),
'component:default/c': expect.objectContaining({
metadata: expect.objectContaining({ name: 'c' }),
}),
'component:default/d': expect.objectContaining({
metadata: expect.objectContaining({ name: 'd' }),
}),
});
// TODO(freben): Ideally these should be orphaned now
expect(
(await harness.getOutputEntities())['component:default/b'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/c'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/d'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
});
});
@@ -31,6 +31,8 @@ import { startTaskPipeline } from './TaskPipeline';
const CACHE_TTL = 5;
export type ProgressTracker = ReturnType<typeof progressTracker>;
export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
private stopFunc?: () => void;
@@ -45,7 +47,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
unprocessedEntity: Entity;
errors: Error[];
}) => Promise<void> | void,
private readonly tracker = progressTracker(),
private readonly tracker: ProgressTracker = progressTracker(),
) {}
async start() {
+9
View File
@@ -148,6 +148,15 @@ export const CatalogTable: {
}
| undefined,
): TableColumn<CatalogTableRow>;
createLabelColumn(
key: string,
options?:
| {
title?: string | undefined;
defaultValue?: string | undefined;
}
| undefined,
): TableColumn<CatalogTableRow>;
}>;
};
@@ -331,4 +331,42 @@ describe('CatalogTable component', () => {
expect(getByText('Should be rendered')).toBeInTheDocument();
});
it('should render the label column with customised title and value as specified', async () => {
const columns = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
];
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'APIWithLabel',
labels: { category: 'generic' },
},
};
const expectedColumns = ['Name', 'Category', 'Actions'];
const { getAllByRole, getByText } = await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable columns={columns} />
</MockEntityListContextProvider>
</ApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(expectedColumns);
const labelCellValue = getByText('generic');
expect(labelCellValue).toBeInTheDocument();
});
});
@@ -162,4 +162,35 @@ export const columnFactories = Object.freeze({
searchable: true,
};
},
createLabelColumn(
key: string,
options?: { title?: string; defaultValue?: string },
): TableColumn<CatalogTableRow> {
return {
title: options?.title || 'Label',
field: 'entity.metadata.labels',
cellStyle: {
padding: '0px 16px 0px 20px',
},
render: ({ entity }: { entity: Entity }) => {
const labels: Record<string, string> | undefined =
entity.metadata?.labels;
const specifiedLabelValue =
(labels && labels[key]) || options?.defaultValue;
return (
<>
{specifiedLabelValue && (
<Chip
key={specifiedLabelValue}
label={specifiedLabelValue}
size="small"
variant="outlined"
/>
)}
</>
);
},
width: 'auto',
};
},
});