Merge branch 'backstage:master' into plugin-azure-functions
This commit is contained in:
@@ -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',
|
||||
}),
|
||||
];
|
||||
```
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user