Merge branch 'master' of https://github.com/backstage/backstage into bazaar-columns

This commit is contained in:
Lykke Axlin
2021-11-10 08:40:32 +01:00
50 changed files with 677 additions and 315 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
fix: kubernetes plugin shall pass id token on get clusters request if possible
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Tweak logic for msgraph catalog ingesting for display names with security groups
Previously security groups that weren't mail enabled were imported with UUIDs, now they use the display name.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Allow for `cellStyle` property on `TableColumn` to be a function as well as `React.CSSProperties` as per the Material UI Table component
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-backend': minor
'@backstage/plugin-azure-devops-common': patch
---
refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`.
Stop re-exporting types from `@backstage/plugin-azure-devops-backend`.
Added new types to `@backstage/plugin-azure-devops-common`.
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/techdocs-common': patch
---
1. Techdocs publisher constructors now use parameter objects when being
instantiated
2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for
creation to be aligned with other publishers; this does not impact
`LocalPublish` usage.
```diff
- const publisher = new LocalPublish(config, logger, discovery);
+ const publisher = LocalPublish.fromConfig(config, logger, discovery);
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Only use settings that have a value when creating a new FirestoreKeyStore instance
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/create-app': patch
---
Removed the version pinning of the packages `graphql-language-service-interface` and `graphql-language-service-parser`. This should no longer be necessary.
You can apply the same change in your repository by ensuring that the following does _NOT_ appear in your root `package.json`.
```json
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bumped `@spotify/eslint-config-typescript` from `v10` to `v12`, dropping support for Node.js v12.
+1
View File
@@ -64,3 +64,4 @@
| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. |
| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. |
| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling |
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem
+1 -3
View File
@@ -46,9 +46,7 @@
"resolutions": {
"**/@graphql-codegen/cli/**/ws": "^7.4.6",
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*",
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
"**/@roadiehq/**/@backstage/catalog-model": "*"
},
"version": "1.0.0",
"dependencies": {
+1 -1
View File
@@ -43,7 +43,7 @@
"@rollup/plugin-yaml": "^3.0.0",
"@spotify/eslint-config-base": "^12.0.0",
"@spotify/eslint-config-react": "^10.0.0",
"@spotify/eslint-config-typescript": "^10.0.0",
"@spotify/eslint-config-typescript": "^12.0.0",
"@sucrase/jest-plugin": "^2.1.1",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "5.5.x",
@@ -326,3 +326,49 @@ export const FilterTable = () => {
</div>
);
};
export const StyledTable = () => {
const classes = useStyles();
const columns: TableColumn[] = [
{
title: 'Column 1',
field: 'col1',
highlight: true,
cellStyle: (_, rowData: any & { tableData: { id: number } }) => {
return rowData.tableData.id % 2 === 0
? {
color: '#6CD75F',
}
: {
color: '#DC3D5A',
};
},
},
{
title: 'Column 2',
field: 'col2',
cellStyle: { color: '#2FA5DC' },
},
{
title: 'Numeric value',
field: 'number',
type: 'numeric',
},
{
title: 'A Date',
field: 'date',
type: 'date',
},
];
return (
<div className={classes.container}>
<Table
options={{ paging: false }}
data={testData10}
columns={columns}
title="Backstage Table"
/>
</div>
);
};
@@ -18,17 +18,18 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { Table } from './Table';
const column1 = {
title: 'Column 1',
field: 'col1',
};
const column2 = {
title: 'Column 2',
field: 'col2',
};
const minProps = {
columns: [
{
title: 'Column 1',
field: 'col1',
},
{
title: 'Column 2',
field: 'col2',
},
],
columns: [column1, column2],
data: [
{
col1: 'first value, first row',
@@ -47,6 +48,100 @@ describe('<Table />', () => {
expect(rendered.getByText('second value, second row')).toBeInTheDocument();
});
describe('with style rows', () => {
describe('with CSS Properties object', () => {
const styledColumn2 = {
...column2,
cellStyle: {
color: 'blue',
},
};
it('renders non-highlighted correctly', async () => {
const columns = [column1, styledColumn2];
const rendered = await renderInTestApp(
<Table data={minProps.data} columns={columns} />,
);
expect(rendered.getByText('second value, first row')).toHaveStyle({
color: 'blue',
});
});
it('renders highlighted column correctly', async () => {
const columns = [
column1,
{
...styledColumn2,
highlight: true,
},
];
const rendered = await renderInTestApp(
<Table data={minProps.data} columns={columns} />,
);
expect(rendered.getByText('second value, first row')).toHaveStyle({
color: 'blue',
'font-weight': 700,
});
});
});
describe('with CSS Properties function', () => {
const styledColumn2 = {
...column2,
cellStyle: (
_data: any,
rowData: any & { tableData: { id: number } },
) => {
return rowData.tableData.id % 2 === 0
? {
color: 'green',
}
: {
color: 'red',
};
},
};
it('renders non-highlighted columns correctly', async () => {
const columns = [column1, styledColumn2];
const rendered = await renderInTestApp(
<Table data={minProps.data} columns={columns} />,
);
expect(rendered.getByText('second value, first row')).toHaveStyle({
color: 'green',
});
expect(rendered.getByText('second value, second row')).toHaveStyle({
color: 'red',
});
});
it('renders highlighted columns correctly', async () => {
const columns = [
column1,
{
...styledColumn2,
highlight: true,
},
];
const rendered = await renderInTestApp(
<Table data={minProps.data} columns={columns} />,
);
expect(rendered.getByText('second value, first row')).toHaveStyle({
color: 'green',
'font-weight': 700,
});
expect(rendered.getByText('second value, second row')).toHaveStyle({
color: 'red',
'font-weight': 700,
});
});
});
});
it('renders with subtitle', async () => {
const rendered = await renderInTestApp(
<Table subtitle="subtitle" {...minProps} />,
@@ -169,12 +169,26 @@ function convertColumns<T extends object>(
): TableColumn<T>[] {
return columns.map(column => {
const headerStyle: React.CSSProperties = {};
const cellStyle: React.CSSProperties =
typeof column.cellStyle === 'object' ? column.cellStyle : {};
let cellStyle = column.cellStyle || {};
if (column.highlight) {
headerStyle.color = theme.palette.textContrast;
cellStyle.fontWeight = theme.typography.fontWeightBold;
if (typeof cellStyle === 'object') {
(cellStyle as React.CSSProperties).fontWeight =
theme.typography.fontWeightBold;
} else {
const cellStyleFn = cellStyle as (
data: any,
rowData: T,
column?: Column<T>,
) => React.CSSProperties;
cellStyle = (data, rowData, rowColumn) => {
const style = cellStyleFn(data, rowData, rowColumn);
return { ...style, fontWeight: theme.typography.fontWeightBold };
};
}
}
return {
@@ -23,10 +23,6 @@
"create-plugin": "backstage-cli create-plugin --scope internal",
"remove-plugin": "backstage-cli remove-plugin"
},
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
"workspaces": {
"packages": [
"packages/*",
@@ -57,6 +57,26 @@ const streamToBuffer = (stream: Readable): Promise<Buffer> => {
};
export class AwsS3Publish implements PublisherBase {
private readonly storageClient: aws.S3;
private readonly bucketName: string;
private readonly legacyPathCasing: boolean;
private readonly logger: Logger;
private readonly bucketRootPath: string;
constructor(options: {
storageClient: aws.S3;
bucketName: string;
legacyPathCasing: boolean;
logger: Logger;
bucketRootPath: string;
}) {
this.storageClient = options.storageClient;
this.bucketName = options.bucketName;
this.legacyPathCasing = options.legacyPathCasing;
this.logger = options.logger;
this.bucketRootPath = options.bucketRootPath;
}
static fromConfig(config: Config, logger: Logger): PublisherBase {
let bucketName = '';
try {
@@ -112,13 +132,13 @@ export class AwsS3Publish implements PublisherBase {
'techdocs.legacyUseCaseSensitiveTripletPaths',
) || false;
return new AwsS3Publish(
return new AwsS3Publish({
storageClient,
bucketName,
bucketRootPath,
legacyPathCasing,
logger,
bucketRootPath,
);
});
}
private static buildCredentials(
@@ -152,20 +172,6 @@ export class AwsS3Publish implements PublisherBase {
return explicitCredentials;
}
constructor(
private readonly storageClient: aws.S3,
private readonly bucketName: string,
private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
private readonly bucketRootPath: string,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
this.bucketRootPath = bucketRootPath;
}
/**
* Check if the defined bucket exists. Being able to connect means the configuration is good
* and the storage client will work.
@@ -47,6 +47,23 @@ import {
const BATCH_CONCURRENCY = 3;
export class AzureBlobStoragePublish implements PublisherBase {
private readonly storageClient: BlobServiceClient;
private readonly containerName: string;
private readonly legacyPathCasing: boolean;
private readonly logger: Logger;
constructor(options: {
storageClient: BlobServiceClient;
containerName: string;
legacyPathCasing: boolean;
logger: Logger;
}) {
this.storageClient = options.storageClient;
this.containerName = options.containerName;
this.legacyPathCasing = options.legacyPathCasing;
this.logger = options.logger;
}
static fromConfig(config: Config, logger: Logger): PublisherBase {
let containerName = '';
try {
@@ -95,24 +112,12 @@ export class AzureBlobStoragePublish implements PublisherBase {
'techdocs.legacyUseCaseSensitiveTripletPaths',
) || false;
return new AzureBlobStoragePublish(
storageClient,
containerName,
legacyPathCasing,
logger,
);
}
constructor(
private readonly storageClient: BlobServiceClient,
private readonly containerName: string,
private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
) {
this.storageClient = storageClient;
this.containerName = containerName;
this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
return new AzureBlobStoragePublish({
storageClient: storageClient,
containerName: containerName,
legacyPathCasing: legacyPathCasing,
logger: logger,
});
}
async getReadiness(): Promise<ReadinessResponse> {
@@ -41,6 +41,26 @@ import {
} from './types';
export class GoogleGCSPublish implements PublisherBase {
private readonly storageClient: Storage;
private readonly bucketName: string;
private readonly legacyPathCasing: boolean;
private readonly logger: Logger;
private readonly bucketRootPath: string;
constructor(options: {
storageClient: Storage;
bucketName: string;
legacyPathCasing: boolean;
logger: Logger;
bucketRootPath: string;
}) {
this.storageClient = options.storageClient;
this.bucketName = options.bucketName;
this.legacyPathCasing = options.legacyPathCasing;
this.logger = options.logger;
this.bucketRootPath = options.bucketRootPath;
}
static fromConfig(config: Config, logger: Logger): PublisherBase {
let bucketName = '';
try {
@@ -84,27 +104,13 @@ export class GoogleGCSPublish implements PublisherBase {
'techdocs.legacyUseCaseSensitiveTripletPaths',
) || false;
return new GoogleGCSPublish(
return new GoogleGCSPublish({
storageClient,
bucketName,
legacyPathCasing,
logger,
bucketRootPath,
);
}
constructor(
private readonly storageClient: Storage,
private readonly bucketName: string,
private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
private readonly bucketRootPath: string,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
this.bucketRootPath = bucketRootPath;
});
}
/**
@@ -63,7 +63,11 @@ describe('local publisher', () => {
const mockConfig = new ConfigReader({});
const publisher = new LocalPublish(mockConfig, logger, testDiscovery);
const publisher = LocalPublish.fromConfig(
mockConfig,
logger,
testDiscovery,
);
const mockEntity = createMockEntity();
const lowerMockEntity = createMockEntity(undefined, true);
@@ -90,7 +94,11 @@ describe('local publisher', () => {
},
});
const publisher = new LocalPublish(mockConfig, logger, testDiscovery);
const publisher = LocalPublish.fromConfig(
mockConfig,
logger,
testDiscovery,
);
const mockEntity = createMockEntity();
const lowerMockEntity = createMockEntity(undefined, true);
@@ -106,7 +114,11 @@ describe('local publisher', () => {
describe('docsRouter', () => {
const mockConfig = new ConfigReader({});
const publisher = new LocalPublish(mockConfig, logger, testDiscovery);
const publisher = LocalPublish.fromConfig(
mockConfig,
logger,
testDiscovery,
);
let app: express.Express;
beforeEach(() => {
@@ -166,7 +178,7 @@ describe('local publisher', () => {
legacyUseCaseSensitiveTripletPaths: true,
},
});
const legacyPublisher = new LocalPublish(
const legacyPublisher = LocalPublish.fromConfig(
legacyConfig,
logger,
testDiscovery,
@@ -59,24 +59,37 @@ try {
* called "static" at the root of techdocs-backend plugin.
*/
export class LocalPublish implements PublisherBase {
private legacyPathCasing: boolean;
private readonly legacyPathCasing: boolean;
private readonly logger: Logger;
private readonly discovery: PluginEndpointDiscovery;
// TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers.
// Move the logic of setting staticDocsDir based on config over to fromConfig,
// and set the value as a class parameter.
constructor(
// @ts-ignore
private readonly config: Config,
private readonly logger: Logger,
private readonly discovery: PluginEndpointDiscovery,
) {
this.config = config;
this.logger = logger;
this.discovery = discovery;
this.legacyPathCasing =
// TODO: Move the logic of setting staticDocsDir based on config over to
// fromConfig, and set the value as a class parameter.
constructor(options: {
logger: Logger;
discovery: PluginEndpointDiscovery;
legacyPathCasing: boolean;
}) {
this.logger = options.logger;
this.discovery = options.discovery;
this.legacyPathCasing = options.legacyPathCasing;
}
static fromConfig(
config: Config,
logger: Logger,
discovery: PluginEndpointDiscovery,
): PublisherBase {
const legacyPathCasing =
config.getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
) || false;
return new LocalPublish({
logger,
discovery,
legacyPathCasing,
});
}
async getReadiness(): Promise<ReadinessResponse> {
@@ -54,6 +54,20 @@ const bufferToStream = (buffer: Buffer): Readable => {
};
export class OpenStackSwiftPublish implements PublisherBase {
private readonly storageClient: SwiftClient;
private readonly containerName: string;
private readonly logger: Logger;
constructor(options: {
storageClient: SwiftClient;
containerName: string;
logger: Logger;
}) {
this.storageClient = options.storageClient;
this.containerName = options.containerName;
this.logger = options.logger;
}
static fromConfig(config: Config, logger: Logger): PublisherBase {
let containerName = '';
try {
@@ -78,17 +92,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
secret: openStackSwiftConfig.getString('credentials.secret'),
});
return new OpenStackSwiftPublish(storageClient, containerName, logger);
}
constructor(
private readonly storageClient: SwiftClient,
private readonly containerName: string,
private readonly logger: Logger,
) {
this.storageClient = storageClient;
this.containerName = containerName;
this.logger = logger;
return new OpenStackSwiftPublish({ storageClient, containerName, logger });
}
/*
@@ -61,10 +61,10 @@ export class Publisher {
return OpenStackSwiftPublish.fromConfig(config, logger);
case 'local':
logger.info('Creating Local publisher for TechDocs');
return new LocalPublish(config, logger, discovery);
return LocalPublish.fromConfig(config, logger, discovery);
default:
logger.info('Creating Local publisher for TechDocs');
return new LocalPublish(config, logger, discovery);
return LocalPublish.fromConfig(config, logger, discovery);
}
}
}
+1
View File
@@ -75,6 +75,7 @@ export interface Config {
logoutUrl?: string;
issuer: string;
cert: string;
audience?: string;
privateKey?: string;
authnContext?: string[];
identifierFormat?: string;
+15 -10
View File
@@ -15,6 +15,7 @@
*/
import { Logger } from 'winston';
import { pickBy } from 'lodash';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { Config } from '@backstage/config';
@@ -64,16 +65,20 @@ export class KeyStores {
if (provider === 'firestore') {
const settings = ks?.getConfig(provider);
const keyStore = await FirestoreKeyStore.create({
projectId: settings?.getOptionalString('projectId'),
keyFilename: settings?.getOptionalString('keyFilename'),
host: settings?.getOptionalString('host'),
port: settings?.getOptionalNumber('port'),
ssl: settings?.getOptionalBoolean('ssl'),
path: settings?.getOptionalString('path'),
timeout: settings?.getOptionalNumber('timeout'),
});
const keyStore = await FirestoreKeyStore.create(
pickBy(
{
projectId: settings?.getOptionalString('projectId'),
keyFilename: settings?.getOptionalString('keyFilename'),
host: settings?.getOptionalString('host'),
port: settings?.getOptionalNumber('port'),
ssl: settings?.getOptionalBoolean('ssl'),
path: settings?.getOptionalString('path'),
timeout: settings?.getOptionalNumber('timeout'),
},
value => value !== undefined,
),
);
await FirestoreKeyStore.verifyConnection(keyStore, logger);
return keyStore;
@@ -125,6 +125,7 @@ export const createSamlProvider = (
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
entryPoint: config.getString('entryPoint'),
logoutUrl: config.getOptionalString('logoutUrl'),
audience: config.getOptionalString('audience'),
issuer: config.getString('issuer'),
cert: config.getString('cert'),
privateCert: config.getOptionalString('privateKey'),
+3 -42
View File
@@ -4,13 +4,13 @@
```ts
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { BuildResult } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Config } from '@backstage/config';
import express from 'express';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Logger as Logger_2 } from 'winston';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PullRequest } from '@backstage/plugin-azure-devops-common';
import { PullRequestOptions } from '@backstage/plugin-azure-devops-common';
import { RepoBuild } from '@backstage/plugin-azure-devops-common';
import { WebApi } from 'azure-devops-node-api';
// Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -29,8 +29,6 @@ export class AzureDevOpsApi {
projectName: string,
repoName: string,
): Promise<GitRepository>;
// Warning: (ae-forgotten-export) The symbol "PullRequestOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getPullRequests(
projectName: string,
@@ -45,48 +43,11 @@ export class AzureDevOpsApi {
): Promise<RepoBuild[]>;
}
export { BuildResult };
export { BuildStatus };
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
uniqueName?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
startTime?: Date;
finishTime?: Date;
source: string;
uniqueName?: string;
};
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -22,6 +22,7 @@
"dependencies": {
"@backstage/backend-common": "^0.9.8",
"@backstage/config": "^0.1.11",
"@backstage/plugin-azure-devops-common": "^0.0.1",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^11.0.1",
"express": "^4.17.1",
@@ -16,17 +16,21 @@
import {
Build,
DefinitionReference,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
BuildResult,
BuildStatus,
GitPullRequest,
GitRepository,
PullRequest,
PullRequestStatus,
RepoBuild,
} from './types';
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitRepository,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi';
import { DefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
describe('AzureDevOpsApi', () => {
@@ -15,17 +15,19 @@
*/
import {
Build,
BuildResult,
BuildStatus,
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
PullRequest,
PullRequestOptions,
RepoBuild,
} from './types';
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Logger } from 'winston';
import { WebApi } from 'azure-devops-node-api';
@@ -15,5 +15,3 @@
*/
export { AzureDevOpsApi } from './AzureDevOpsApi';
export { BuildResult, BuildStatus } from './types';
export type { RepoBuild, PullRequest } from './types';
@@ -1,67 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Build,
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
export { BuildResult, BuildStatus, PullRequestStatus };
export type {
Build,
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
};
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
startTime?: Date;
finishTime?: Date;
source: string;
uniqueName?: string;
};
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
uniqueName?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
export type PullRequestOptions = {
top: number;
status: PullRequestStatus;
};
+1 -2
View File
@@ -13,6 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AzureDevOpsApi, BuildResult, BuildStatus } from './api';
export type { RepoBuild, PullRequest } from './api';
export { AzureDevOpsApi } from './api';
export * from './service/router';
@@ -14,22 +14,22 @@
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import { AzureDevOpsApi } from '../api';
import { createRouter } from './router';
import { PullRequest, RepoBuild } from '../api/types';
import {
GitRepository,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
Build,
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
PullRequest,
PullRequestStatus,
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import { AzureDevOpsApi } from '../api';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { ConfigReader } from '@backstage/config';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { createRouter } from './router';
import express from 'express';
import { getVoidLogger } from '@backstage/backend-common';
import request from 'supertest';
describe('createRouter', () => {
let azureDevOpsApi: jest.Mocked<AzureDevOpsApi>;
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { PullRequestOptions, PullRequestStatus } from '../api/types';
import {
PullRequestOptions,
PullRequestStatus,
} from '@backstage/plugin-azure-devops-common';
import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api';
import { AzureDevOpsApi } from '../api';
+43
View File
@@ -27,6 +27,42 @@ export enum BuildStatus {
Postponed = 8,
}
// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
uniqueName?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
// Warning: (ae-missing-release-tag) "PullRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequestOptions = {
top: number;
status: PullRequestStatus;
};
// Warning: (ae-missing-release-tag) "PullRequestStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum PullRequestStatus {
Abandoned = 2,
Active = 1,
All = 4,
Completed = 3,
NotSet = 0,
}
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -43,5 +79,12 @@ export type RepoBuild = {
uniqueName?: string;
};
// Warning: (ae-missing-release-tag) "RepoBuildOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RepoBuildOptions = {
top?: number;
};
// (No @packageDocumentation comment for this package)
```
+46
View File
@@ -80,3 +80,49 @@ export type RepoBuild = {
source: string;
uniqueName?: string;
};
export type RepoBuildOptions = {
top?: number;
};
export enum PullRequestStatus {
/**
* Status not set. Default state.
*/
NotSet = 0,
/**
* Pull request is active.
*/
Active = 1,
/**
* Pull request is abandoned.
*/
Abandoned = 2,
/**
* Pull request is completed.
*/
Completed = 3,
/**
* Used in pull request search criteria to include all statuses.
*/
All = 4,
}
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
uniqueName?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
export type PullRequestOptions = {
top: number;
status: PullRequestStatus;
};
-1
View File
@@ -37,7 +37,6 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"azure-devops-node-api": "^11.0.1",
"luxon": "^2.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { RepoBuild, RepoBuildOptions } from './types';
import {
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
import { createApiRef } from '@backstage/core-plugin-api';
export const azureDevOpsApiRef = createApiRef<AzureDevOpsApi>({
@@ -14,9 +14,13 @@
* limitations under the License.
*/
import { AzureDevOpsApi } from './AzureDevOpsApi';
import { RepoBuild, RepoBuildOptions } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
import { AzureDevOpsApi } from './AzureDevOpsApi';
import { ResponseError } from '@backstage/errors';
export class AzureDevOpsClient implements AzureDevOpsApi {
-34
View File
@@ -1,34 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
source: string;
};
export type RepoBuildOptions = {
top?: number;
};
@@ -17,7 +17,7 @@
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
} from '@backstage/plugin-azure-devops-common';
import { getBuildResultComponent, getBuildStateComponent } from './BuildTable';
import { renderInTestApp } from '@backstage/test-utils';
@@ -18,7 +18,8 @@ import { Box, Typography } from '@material-ui/core';
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import {
Link,
ResponseErrorPanel,
@@ -34,7 +35,6 @@ import {
import { DateTime } from 'luxon';
import React from 'react';
import { RepoBuild } from '../../api/types';
export const getBuildResultComponent = (result: number | undefined) => {
switch (result) {
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { RepoBuild, RepoBuildOptions } from '../api/types';
import {
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
import { Entity } from '@backstage/catalog-model';
@@ -21,6 +21,14 @@ describe('normalizeEntityName', () => {
expect(normalizeEntityName('User Name')).toBe('user_name');
});
it('should normalize complex name to valid entity name', () => {
expect(normalizeEntityName('User (Name)')).toBe('user_name');
});
it('should normalize complex name to valid entity name without extra underscore', () => {
expect(normalizeEntityName('User :(Name:)')).toBe('user_name');
});
it('should normalize e-mail to valid entity name', () => {
expect(normalizeEntityName('user.name@example.com')).toBe(
'user.name_example.com',
@@ -15,8 +15,21 @@
*/
export function normalizeEntityName(name: string): string {
return name
let cleaned = name
.trim()
.toLocaleLowerCase()
.replace(/[^a-zA-Z0-9_\-\.]/g, '_');
// invalid to end with _
while (cleaned.endsWith('_')) {
cleaned = cleaned.substring(0, cleaned.length - 1);
}
// cleans up format for groups like 'my group (Reader)'
while (cleaned.includes('__')) {
// replaceAll from node.js >= 15
cleaned = cleaned.replace('__', '_');
}
return cleaned;
}
@@ -445,6 +445,92 @@ describe('read microsoft graph', () => {
// expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1);
// expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120);
});
it('should read security groups', async () => {
async function* getExampleGroups() {
yield {
id: 'groupid',
displayName: 'Group Name',
description: 'Group Description',
mail: 'group@example.com',
mailNickname: 'df546d53-4f5f-4462-b371-d4a855787047',
mailEnabled: false,
securityEnabled: true,
};
}
async function* getExampleGroupMembers(): AsyncIterable<GroupMember> {
yield {
'@odata.type': '#microsoft.graph.group',
id: 'childgroupid',
};
yield {
'@odata.type': '#microsoft.graph.user',
id: 'userid',
};
}
client.getGroups.mockImplementation(getExampleGroups);
client.getGroupMembers.mockImplementation(getExampleGroupMembers);
client.getOrganization.mockResolvedValue({
id: 'tenantid',
displayName: 'Organization Name',
});
client.getGroupPhotoWithSizeLimit.mockResolvedValue(
'data:image/jpeg;base64,...',
);
const { groups, rootGroup } = await readMicrosoftGraphGroups(
client,
'tenantid',
{
groupFilter: 'securityEnabled eq true',
},
);
const expectedRootGroup = group({
metadata: {
annotations: {
'graph.microsoft.com/tenant-id': 'tenantid',
},
name: 'organization_name',
description: 'Organization Name',
},
spec: {
type: 'root',
profile: {
displayName: 'Organization Name',
},
children: [],
},
});
expect(groups).toEqual([
expectedRootGroup,
group({
metadata: {
annotations: {
'graph.microsoft.com/group-id': 'groupid',
},
name: 'group_name',
description: 'Group Description',
},
spec: {
type: 'team',
profile: {
displayName: 'Group Name',
email: 'group@example.com',
},
children: [],
},
}),
]);
expect(rootGroup).toEqual(expectedRootGroup);
expect(client.getGroups).toBeCalledWith({
filter: 'securityEnabled eq true',
});
expect(client.getGroupMembers).toBeCalledTimes(1);
expect(client.getGroupMembers).toBeCalledWith('groupid');
});
});
describe('resolveRelations', () => {
@@ -251,6 +251,13 @@ export async function readMicrosoftGraphOrganization(
return { rootGroup };
}
function extractGroupName(group: MicrosoftGraph.Group): string {
if (group.securityEnabled) {
return group.displayName as string;
}
return (group.mailNickname || group.displayName) as string;
}
export async function defaultGroupTransformer(
group: MicrosoftGraph.Group,
groupPhoto?: string,
@@ -259,7 +266,7 @@ export async function defaultGroupTransformer(
return undefined;
}
const name = normalizeEntityName(group.mailNickname || group.displayName);
const name = normalizeEntityName(extractGroupName(group));
const entity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
@@ -79,10 +79,14 @@ export class KubernetesBackendClient implements KubernetesApi {
}
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
const idToken = await this.identityApi.getIdToken();
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`;
const response = await fetch(url, {
method: 'GET',
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
return (await this.handleResponse(response)).items;
+27 -22
View File
@@ -5486,10 +5486,10 @@
resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-10.0.0.tgz#6f83ada05f79b49c1f9def5b8815e3231ed24969"
integrity sha512-MozX6W3aMp7EQPliuUQYI58Ni5vh65mItXMG0CgZBj0v1ZEeZVM5XS/nqhsCaIHYXskmZM2O1qqLFaEg5PqGdg==
"@spotify/eslint-config-typescript@^10.0.0":
version "10.0.0"
resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-10.0.0.tgz#4df7074f3f4ef31d76c617e55d335f9a36cfed5b"
integrity sha512-qR4WOU3gJrpz26O8BlNbXas4Yj93NeVH7yvULVYO2j9bCAEZJu2sfl1BGfOy4qAsYGutZhJtNwMqK0Rl4DcFHQ==
"@spotify/eslint-config-typescript@^12.0.0":
version "12.0.0"
resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz#4c7af3f74a47668bec0c860b72e2a0103e78a138"
integrity sha512-nMVll8ZkN/W8+IHn6Iz3YzCKW0qhrn3TVfyxkAr3qmXm5cex+GzyUdZEuxb8rdN2inZL6A1Il2NFfO5p/UKxog==
"@spotify/prettier-config@^11.0.0":
version "11.0.0"
@@ -15591,20 +15591,20 @@ graphql-extensions@^0.15.0:
apollo-server-env "^3.1.0"
apollo-server-types "^0.9.0"
graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.9.0:
version "2.8.2"
resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b"
integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ==
graphql-language-service-interface@^2.9.0:
version "2.9.1"
resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.9.1.tgz#be0b11b06b78730ea9d250e0e2290e7ed9c8d283"
integrity sha512-yGsE67fxJBXxY82+rLDMvUpmzpOUM8XFB+k+xOTUyABWs27osKaoGiuDDXAVGg1adhm+cpunWbipe763ZJkAVA==
dependencies:
graphql-language-service-parser "^1.9.0"
graphql-language-service-types "^1.8.0"
graphql-language-service-utils "^2.5.1"
graphql-language-service-parser "^1.10.0"
graphql-language-service-types "^1.8.3"
graphql-language-service-utils "^2.6.0"
vscode-languageserver-types "^3.15.1"
graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.10.0, graphql-language-service-parser@^1.9.0:
version "1.9.0"
resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724"
integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q==
graphql-language-service-parser@^1.10.0:
version "1.10.0"
resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.0.tgz#116f4be849754f6afb4c196421a43fe96d87b278"
integrity sha512-cLExv0EjqT2hsKdwVTPmKU6eMfjZAjxqywgCPnWD48eJn6tyuePMyG7ye+jpX1PRPPx/cDHfFJGf8sUclchvng==
dependencies:
graphql-language-service-types "^1.8.0"
@@ -15618,13 +15618,10 @@ graphql-language-service-types@^1.8.2:
resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.2.tgz#50ae56f69cc24fcfc3daa129b68b0eb9421e8578"
integrity sha512-Sj07RHnMwAhEvAt7Jdt1l/x56ZpoNh+V6g+T58CF6GiYqI5l4vXqqRB4d4xHDcNQX98GpJfnf3o8BqPgP3C5Sw==
graphql-language-service-utils@^2.5.1:
version "2.5.1"
resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.1.tgz#832ad4b0a9da03fdded756932c27e057ccf71302"
integrity sha512-Lzz723cYrYlVN4WVzIyFGg3ogoe+QYAIBfdtDboiIILoy0FTmqbyC2TOErqbmWKqO4NK9xDA95cSRFbWiHYj0g==
dependencies:
graphql-language-service-types "^1.8.0"
nullthrows "^1.0.0"
graphql-language-service-types@^1.8.3:
version "1.8.3"
resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.3.tgz#d7d688d74c122c4d9cc4cceae761a1f2a3c396a1"
integrity sha512-m+RHnlGkKDcesW/gC4M7I2pSmWJB84uWS6LtnjplO/07JN312nJCJYCwV/DBny2m1fmSOxN7H/o+JW0l56KwBA==
graphql-language-service-utils@^2.5.3:
version "2.5.3"
@@ -15634,6 +15631,14 @@ graphql-language-service-utils@^2.5.3:
graphql-language-service-types "^1.8.0"
nullthrows "^1.0.0"
graphql-language-service-utils@^2.6.0:
version "2.6.0"
resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.6.0.tgz#d04904641248167ccbb381d8705ba97daa784954"
integrity sha512-idqwmbREixhDuQMcYp8WH0btQT02xZny8MO/HduNTVjnPrmTYnZUbpZ9AejdflmaKoS0o8nNvgXQ0GpIOzbG5g==
dependencies:
graphql-language-service-types "^1.8.3"
nullthrows "^1.0.0"
graphql-language-service@^3.1.6:
version "3.2.0"
resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.2.0.tgz#e0eb6d5dea2cab92549a253d7a6b4fa0cce178b7"