Merge branch 'master' into scaffolder-improvement

This commit is contained in:
Brian Fletcher
2021-10-26 08:55:33 +01:00
committed by GitHub
201 changed files with 1740 additions and 749 deletions
+2
View File
@@ -33,6 +33,7 @@
"@backstage/backend-common": "^0.9.7",
"@backstage/config-loader": "^0.7.0",
"@backstage/config": "^0.1.8",
"@backstage/types": "^0.1.1",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
@@ -42,6 +43,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.8.0",
"@backstage/types": "^0.1.1",
"@types/supertest": "^2.0.8",
"msw": "^0.29.0",
"supertest": "^6.1.3"
+2 -1
View File
@@ -17,7 +17,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { Logger } from 'winston';
import { AppConfig, Config, JsonObject } from '@backstage/config';
import { AppConfig, Config } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { loadConfigSchema, readEnvConfig } from '@backstage/config-loader';
type InjectOptions = {
@@ -81,7 +81,10 @@ export type RepoBuild = {
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)
@@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi';
import { PullRequest, RepoBuild } from './types';
import {
Build,
BuildResult,
BuildStatus,
DefinitionReference,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
GitPullRequest,
GitRepository,
PullRequest,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { GitRepository } from 'azure-devops-node-api/interfaces/TfvcInterfaces';
RepoBuild,
} from './types';
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', () => {
@@ -41,16 +42,24 @@ describe('AzureDevOpsApi', () => {
},
};
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
uniqueName: 'DOMAINjdoe',
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: inputBuildDefinition,
_links: inputLinks,
requestedFor: inputIdentityRef,
};
const outputRepoBuild: RepoBuild = {
@@ -60,7 +69,10 @@ describe('AzureDevOpsApi', () => {
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
uniqueName: 'DOMAINjdoe',
};
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
@@ -75,16 +87,24 @@ describe('AzureDevOpsApi', () => {
},
};
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
uniqueName: 'DOMAINjdoe',
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
requestedFor: inputIdentityRef,
};
const outputRepoBuild: RepoBuild = {
@@ -94,7 +114,10 @@ describe('AzureDevOpsApi', () => {
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
uniqueName: 'DOMAINjdoe',
};
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
@@ -109,16 +132,24 @@ describe('AzureDevOpsApi', () => {
},
};
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
uniqueName: 'DOMAINjdoe',
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: undefined,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
requestedFor: inputIdentityRef,
};
const outputRepoBuild: RepoBuild = {
@@ -128,7 +159,10 @@ describe('AzureDevOpsApi', () => {
status: BuildStatus.None,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
uniqueName: 'DOMAINjdoe',
};
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
@@ -143,16 +177,24 @@ describe('AzureDevOpsApi', () => {
},
};
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
uniqueName: 'DOMAINjdoe',
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.InProgress,
result: undefined,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
requestedFor: inputIdentityRef,
};
const outputRepoBuild: RepoBuild = {
@@ -162,7 +204,10 @@ describe('AzureDevOpsApi', () => {
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
uniqueName: 'DOMAINjdoe',
};
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
@@ -171,16 +216,24 @@ describe('AzureDevOpsApi', () => {
describe('mappedRepoBuild with undefined link', () => {
it('should return empty string for link', () => {
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
uniqueName: 'DOMAINjdoe',
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.InProgress,
result: undefined,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: undefined,
requestedFor: inputIdentityRef,
};
const outputRepoBuild: RepoBuild = {
@@ -190,7 +243,10 @@ describe('AzureDevOpsApi', () => {
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
startTime: new Date('2020-09-12T06:15:23.9325232Z'),
finishTime: new Date('2020-09-12T06:20:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
uniqueName: 'DOMAINjdoe',
};
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
@@ -111,7 +111,7 @@ export class AzureDevOpsApi {
options: PullRequestOptions,
): Promise<PullRequest[]> {
this.logger?.debug(
`Calling Azure DevOps REST API, getting up to ${top} Pull Requests for Repository ${repoName} for Project ${projectName}`,
`Calling Azure DevOps REST API, getting up to ${options.top} Pull Requests for Repository ${repoName} for Project ${projectName}`,
);
const gitRepository = await this.getGitRepository(projectName, repoName);
@@ -144,11 +144,14 @@ export function mappedRepoBuild(build: Build): RepoBuild {
title: [build.definition?.name, build.buildNumber]
.filter(Boolean)
.join(' - '),
link: build._links?.web.href ? build._links?.web.href : '',
status: build.status ? build.status : BuildStatus.None,
result: build.result ? build.result : BuildResult.None,
link: build._links?.web.href ?? '',
status: build.status ?? BuildStatus.None,
result: build.result ?? BuildResult.None,
queueTime: build.queueTime,
startTime: build.startTime,
finishTime: build.finishTime,
source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`,
uniqueName: build.requestedFor?.uniqueName ?? 'N/A',
};
}
@@ -160,8 +163,8 @@ export function mappedPullRequest(
pullRequestId: pullRequest.pullRequestId,
repoName: pullRequest.repository?.name,
title: pullRequest.title,
uniqueName: pullRequest.createdBy?.uniqueName,
createdBy: pullRequest.createdBy?.displayName,
uniqueName: pullRequest.createdBy?.uniqueName ?? 'N/A',
createdBy: pullRequest.createdBy?.displayName ?? 'N/A',
creationDate: pullRequest.creationDate,
sourceRefName: pullRequest.sourceRefName,
targetRefName: pullRequest.targetRefName,
@@ -41,7 +41,10 @@ export type RepoBuild = {
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
startTime?: Date;
finishTime?: Date;
source: string;
uniqueName?: string;
};
export type PullRequest = {
+32 -84
View File
@@ -2,74 +2,50 @@
Website: [https://dev.azure.com/](https://dev.azure.com/)
![Azure DevOps Builds Example](./docs/azure-devops-builds.png)
## Features
### Azure Pipelines
Lists the top _n_ builds for a given repository where _n_ is a configurable value
![Azure Pipelines Builds Example](./docs/azure-devops-builds.png)
## Setup
The following sections will help you get the Azure DevOps plugin setup and running
### Configuration
### Azure DevOps Backend
The Azure DevOps plugin requires the following YAML to be added to your app-config.yaml:
You need to setup the [Azure DevOps backend plugin](https://github.com/backstage/backstage/tree/master/plugins/azure-devops-backend) before you move forward with any of these steps if you haven't already
### Entity Annotation
To be able to use the Azure DevOps plugin you need to add the following annotation to any entities you want to use it with:
```yaml
azureDevOps:
host: dev.azure.com
token: ${AZURE_TOKEN}
organization: my-company
dev.azure.com/project-repo: <project-name>/<repo-name>
```
Configuration Details:
Let's break this down a little: `<project-name>` will be the name of your Team Project and `<repo-name>` will be the name of your repository which needs to be part of the Team Project you entered for `<project-name>`.
- `host` and `token` can be the same as the ones used for the `integration` section
- `AZURE_TOKEN` environment variable must be set to a [Personal Access Token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) with read access to both Code and Build
- `organization` is your Azure DevOps Organization name or for Azure DevOps Server (on-premise) this will be your Collection name
Here's what that will look like in action:
### Backend
```yaml
# Example catalog-info.yaml entity definition file
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
dev.azure.com/project-repo: my-project/my-repo
spec:
type: service
# ...
```
Here's how to get the backend up and running:
### Azure Pipelines Component
1. First we need to add the `@backstage/plugin-azure-devops-backend` package to your backend:
```sh
# From the Backstage root directory
cd packages/backend
yarn add @backstage/plugin-azure-devops-backend
```
2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the
following to it:
```ts
import { createRouter } from '@backstage/plugin-azure-devops-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return createRouter({ logger, config });
}
```
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
```ts
import azureDevOps from './plugins/azuredevops';
// ...
async function main() {
// ...
const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv));
```
4. Now run `yarn start-backend` from the repo root
5. Finally open `http://localhost:7000/api/azure-devops/health` in a browser and it should return `{"status":"ok"}`
### Frontend
To get the frontend working you'll need to do the following two steps:
To get the Azure Pipelines component working you'll need to do the following two steps:
1. First we need to add the @backstage/plugin-azure-devops package to your frontend app:
@@ -101,35 +77,7 @@ To get the frontend working you'll need to do the following two steps:
</EntitySwitch>
```
### Entity Annotation
You need to add the following annotation to any entities you want to be able to use the Azure Devops plugin with:
```yaml
dev.azure.com/project-repo: <project-name>/<repo-name>
```
Let's break this down a little: `<project-name>` will be the name of your Team Project and `<repo-name>` will be the name of your repository which needs to be part of the Team Project you entered for `<project-name>`.
Here's what that will look like in action:
```yaml
# Example catalog-info.yaml entity definition file
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
dev.azure.com/project-repo: my-project/my-repo
spec:
type: service
# ...
```
## Features
- Lists the top _n_ builds for a given repository where _n_ is the value configured for `top`
## Limitations
- Currently multiple organizations is not supported
- Currently multiple organizations are not supported
- Mixing Azure DevOps Services (cloud) and Azure DevOps Server (on-premise) is not supported
@@ -0,0 +1,241 @@
/*
* 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';
import { getBuildResultComponent, getBuildStateComponent } from './BuildTable';
import { renderInTestApp } from '@backstage/test-utils';
describe('getBuildResultComponent', () => {
describe('getBuildResultComponent with Succeeded result', () => {
it('should return Status ok Succeeded', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(BuildResult.Succeeded),
);
expect(getByLabelText('Status ok')).toBeInTheDocument();
expect(getByText('Succeeded')).toBeInTheDocument();
});
});
describe('getBuildResultComponent with Partially Succeeded result', () => {
it('should return Status warning Partially Succeeded', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(BuildResult.PartiallySucceeded),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Partially Succeeded')).toBeInTheDocument();
});
});
describe('getBuildResultComponent with Failed result', () => {
it('should return Status error Failed', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(BuildResult.Failed),
);
expect(getByLabelText('Status error')).toBeInTheDocument();
expect(getByText('Failed')).toBeInTheDocument();
});
});
describe('getBuildResultComponent with Canceled result', () => {
it('should return Status aborted Canceled', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(BuildResult.Canceled),
);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
expect(getByText('Canceled')).toBeInTheDocument();
});
});
describe('getBuildResultComponent with None result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(BuildResult.None),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
describe('getBuildResultComponent with undefined result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildResultComponent(undefined),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
});
describe('getBuildStateComponent', () => {
describe('getBuildStateComponent with In Progress status and undefined result', () => {
it('should return Status running In Progress', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.InProgress, undefined),
);
expect(getByLabelText('Status running')).toBeInTheDocument();
expect(getByText('In Progress')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and Succeeded result', () => {
it('should return Status ok Succeeded', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Completed, BuildResult.Succeeded),
);
expect(getByLabelText('Status ok')).toBeInTheDocument();
expect(getByText('Succeeded')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and Partially Succeeded result', () => {
it('should return Status warning Partially Succeeded', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(
BuildStatus.Completed,
BuildResult.PartiallySucceeded,
),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Partially Succeeded')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and Failed result', () => {
it('should return Status error Failed', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Completed, BuildResult.Failed),
);
expect(getByLabelText('Status error')).toBeInTheDocument();
expect(getByText('Failed')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and Canceled result', () => {
it('should return Status aborted Canceled', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Completed, BuildResult.Canceled),
);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
expect(getByText('Canceled')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and None result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Completed, BuildResult.None),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Completed status and undefined result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Completed, undefined),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
// TODO: Add remaining Completed iterations
describe('getBuildStateComponent with Cancelling status and undefined result', () => {
it('should return Status aborted Cancelling', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Cancelling, undefined),
);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
expect(getByText('Cancelling')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Postponed status and undefined result', () => {
it('should return Status pending Postponed', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.Postponed, undefined),
);
expect(getByLabelText('Status pending')).toBeInTheDocument();
expect(getByText('Postponed')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with Not Started status and undefined result', () => {
it('should return Status aborted Not Started', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.NotStarted, undefined),
);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
expect(getByText('Not Started')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with None status and undefined result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(BuildStatus.None, undefined),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with undefined and undefined result', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(undefined, undefined),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
describe('getBuildStateComponent with undefined and any valid BuildResult', () => {
it('should return Status warning Unknown', async () => {
const { getByLabelText, getByText } = await renderInTestApp(
getBuildStateComponent(undefined, BuildResult.Succeeded),
);
expect(getByLabelText('Status warning')).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
});
});
});
@@ -36,7 +36,7 @@ import { DateTime } from 'luxon';
import React from 'react';
import { RepoBuild } from '../../api/types';
const getBuildResultComponent = (result: number | undefined) => {
export const getBuildResultComponent = (result: number | undefined) => {
switch (result) {
case BuildResult.Succeeded:
return (
@@ -72,7 +72,7 @@ const getBuildResultComponent = (result: number | undefined) => {
}
};
const getBuildStateComponent = (
export const getBuildStateComponent = (
status: number | undefined,
result: number | undefined,
) => {
@@ -1,43 +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 React from 'react';
import { Routes, Route } from 'react-router';
import { azureDevOpsRouteRef } from '../routes';
import { EntityPageAzurePipelines } from './EntityPageAzurePipelines';
import { AZURE_DEVOPS_ANNOTATION } from '../constants';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { MissingAnnotationEmptyState } from '@backstage/core-components';
export const isAzureDevOpsAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
export const Router = ({ defaultLimit }: { defaultLimit?: number }) => {
const { entity } = useEntity();
if (!isAzureDevOpsAvailable(entity)) {
return <MissingAnnotationEmptyState annotation={AZURE_DEVOPS_ANNOTATION} />;
}
return (
<Routes>
<Route
path={`/${azureDevOpsRouteRef.path}`}
element={<EntityPageAzurePipelines defaultLimit={defaultLimit} />}
/>
</Routes>
);
};
+5 -2
View File
@@ -13,5 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { azureDevOpsPlugin, EntityAzurePipelinesContent } from './plugin';
export { isAzureDevOpsAvailable } from './components/Router';
export {
azureDevOpsPlugin,
EntityAzurePipelinesContent,
isAzureDevOpsAvailable,
} from './plugin';
+12 -3
View File
@@ -14,8 +14,6 @@
* limitations under the License.
*/
import { azureDevOpsApiRef } from './api/AzureDevOpsApi';
import { AzureDevOpsClient } from './api/AzureDevOpsClient';
import {
createApiFactory,
createPlugin,
@@ -23,8 +21,16 @@ import {
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { AZURE_DEVOPS_ANNOTATION } from './constants';
import { AzureDevOpsClient } from './api/AzureDevOpsClient';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from './api/AzureDevOpsApi';
import { azureDevOpsRouteRef } from './routes';
export const isAzureDevOpsAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
export const azureDevOpsPlugin = createPlugin({
id: 'azureDevOps',
apis: [
@@ -43,7 +49,10 @@ export const azureDevOpsPlugin = createPlugin({
export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'EntityAzurePipelinesContent',
component: () => import('./components/Router').then(m => m.Router),
component: () =>
import('./components/EntityPageAzurePipelines').then(
m => m.EntityPageAzurePipelines,
),
mountPoint: azureDevOpsRouteRef,
}),
);
@@ -10,7 +10,7 @@ import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { GroupEntity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { LocationSpec } from '@backstage/catalog-model';
import { Logger as Logger_2 } from 'winston';
import { SearchEntry } from 'ldapjs';
@@ -33,6 +33,7 @@
"@backstage/config": "^0.1.10",
"@backstage/errors": "^0.1.3",
"@backstage/plugin-catalog-backend": "^0.17.1",
"@backstage/types": "^0.1.1",
"@types/ldapjs": "^2.2.0",
"ldapjs": "^2.2.0",
"lodash": "^4.17.21",
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Config, JsonValue } from '@backstage/config';
import { Config } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { SearchOptions } from 'ldapjs';
import mergeWith from 'lodash/mergeWith';
import { RecursivePartial } from '@backstage/plugin-catalog-backend';
+2 -2
View File
@@ -18,8 +18,8 @@ import { EntityRelationSpec } from '@backstage/catalog-model';
import express from 'express';
import { GitHubIntegrationConfig } from '@backstage/integration';
import { IndexableDocument } from '@backstage/search-common';
import { JsonObject } from '@backstage/config';
import { JsonValue } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { Location as Location_2 } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/catalog-model';
+1
View File
@@ -37,6 +37,7 @@
"@backstage/errors": "^0.1.3",
"@backstage/integration": "^0.6.8",
"@backstage/search-common": "^0.2.0",
"@backstage/types": "^0.1.1",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
"aws-sdk": "^2.840.0",
@@ -15,7 +15,7 @@
*/
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { DateTime } from 'luxon';
import { DeferredEntity } from '../processing/types';
@@ -16,7 +16,7 @@
import { UrlReader } from '@backstage/backend-common';
import { Entity, LocationSpec } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
import yaml from 'yaml';
import { CatalogProcessor } from './types';
@@ -19,7 +19,7 @@ import {
EntityRelationSpec,
LocationSpec,
} from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
export type CatalogProcessor = {
/**
@@ -29,7 +29,7 @@ import {
InputError,
NotAllowedError,
} from '@backstage/errors';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
import path from 'path';
import { Logger } from 'winston';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { CatalogProcessor } from '../ingestion/processors';
import { CatalogProcessorCache } from '../ingestion/processors/types';
import { isObject } from './util';
@@ -15,7 +15,7 @@
*/
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
export type EntityProcessingRequest = {
entity: Entity;
@@ -24,7 +24,7 @@ import {
ORIGIN_LOCATION_ANNOTATION,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import path from 'path';
+1
View File
@@ -33,6 +33,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.9.5",
"@backstage/config": "^0.1.8",
"@backstage/types": "^0.1.1",
"@graphql-modules/core": "^0.7.17",
"apollo-server": "^2.16.1",
"cross-fetch": "^3.0.6",
@@ -16,7 +16,7 @@
import { Entity, EntityMeta } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
export interface ReaderEntityMeta extends EntityMeta {
uid: string;
+1 -1
View File
@@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { IconButton } from '@material-ui/core';
import { LinkProps } from '@backstage/core-components';
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
+1
View File
@@ -36,6 +36,7 @@
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/errors": "^0.1.3",
"@backstage/integration": "^0.6.8",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Observable, StorageApi } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
import { performMigrationToTheNewBucket } from './migration';
import { StarredEntitiesApi } from './StarredEntitiesApi';
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { ApiRef, createApiRef, Observable } from '@backstage/core-plugin-api';
import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
/**
* An API to store starred entities
+1 -1
View File
@@ -7,7 +7,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { RouteRef } from '@backstage/core-plugin-api';
import { Schema } from 'jsonschema';
+1
View File
@@ -26,6 +26,7 @@
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/errors": "^0.1.3",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -18,7 +18,7 @@ import ObservableImpl from 'zen-observable';
import { ResponseError } from '@backstage/errors';
import { Schema } from 'jsonschema';
import { ConfigSchemaApi, ConfigSchemaResult } from './types';
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
const DEFAULT_URL = 'config-schema.json';
+2 -1
View File
@@ -15,7 +15,8 @@
*/
import { Schema } from 'jsonschema';
import { createApiRef, Observable } from '@backstage/core-plugin-api';
import { createApiRef } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
export interface ConfigSchemaResult {
schema?: Schema;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { Box, Chip, Divider, makeStyles, Typography } from '@material-ui/core';
import { Schema } from 'jsonschema';
import React, { useEffect, useRef } from 'react';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import {
Paper,
Table,
@@ -0,0 +1,60 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Cluster } from './Cluster';
jest.mock('../../hooks');
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
describe('Cluster', () => {
it('render 1 cluster', async () => {
const { getByText } = render(
wrapInTestApp(
<Cluster
{...({
clusterObjects: {
cluster: {
name: 'cluster-1',
},
resources: [
{
type: 'deployments',
resources: oneDeployment.deployments,
},
{
type: 'replicasets',
resources: oneDeployment.replicaSets,
},
{
type: 'pods',
resources: oneDeployment.pods,
},
],
errors: [],
},
podsWithErrors: new Set<string>(),
} as any)}
/>,
),
);
expect(getByText('cluster-1')).toBeInTheDocument();
expect(getByText('10 pods')).toBeInTheDocument();
});
});
@@ -23,32 +23,20 @@ import {
Grid,
Typography,
} from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { ClusterObjects } from '@backstage/plugin-kubernetes-common';
import { ErrorPanel } from './ErrorPanel';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { DeploymentsAccordions } from '../DeploymentsAccordions';
import { ErrorReporting } from '../ErrorReporting';
import { groupResponses } from '../../utils/response';
import { DetectedError, detectErrors } from '../../error-detection';
import { IngressesAccordions } from '../IngressesAccordions';
import { ServicesAccordions } from '../ServicesAccordions';
import { CustomResources } from '../CustomResources';
import EmptyStateImage from '../../assets/emptystate.svg';
import {
ClusterContext,
GroupedResponsesContext,
PodNamesWithErrorsContext,
useKubernetesObjects,
} from '../../hooks';
import {
Content,
Page,
Progress,
StatusError,
StatusOK,
} from '@backstage/core-components';
import { StatusError, StatusOK } from '@backstage/core-components';
type ClusterSummaryProps = {
clusterName: string;
@@ -117,7 +105,7 @@ type ClusterProps = {
children?: React.ReactNode;
};
const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
const groupedResponses = groupResponses(clusterObjects.resources);
return (
<ClusterContext.Provider value={clusterObjects.cluster}>
@@ -153,107 +141,3 @@ const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
</ClusterContext.Provider>
);
};
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
const detectedErrors =
kubernetesObjects !== undefined
? detectErrors(kubernetesObjects)
: new Map<string, DetectedError[]>();
return (
<Page themeId="tool">
<Content>
{kubernetesObjects === undefined && error === undefined && <Progress />}
{/* errors retrieved from the kubernetes clusters */}
{clustersWithErrors.length > 0 && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
clustersWithErrors={clustersWithErrors}
/>
</Grid>
</Grid>
)}
{/* other errors */}
{error !== undefined && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
errorMessage={error}
/>
</Grid>
</Grid>
)}
{kubernetesObjects && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorReporting detectedErrors={detectedErrors} />
</Grid>
<Grid item>
<Divider />
</Grid>
<Grid item>
<Typography variant="h3">Your Clusters</Typography>
</Grid>
<Grid item container>
{kubernetesObjects?.items.length <= 0 && (
<Grid
container
justifyContent="space-around"
direction="row"
alignItems="center"
spacing={2}
>
<Grid item xs={4}>
<Typography variant="h5">
No resources on any known clusters for{' '}
{entity.metadata.name}
</Typography>
</Grid>
<Grid item xs={4}>
<img
src={EmptyStateImage}
alt="EmptyState"
data-testid="emptyStateImg"
/>
</Grid>
</Grid>
)}
{kubernetesObjects?.items.length > 0 &&
kubernetesObjects?.items.map((item, i) => {
const podsWithErrors = new Set<string>(
detectedErrors
.get(item.cluster.name)
?.filter(de => de.kind === 'Pod')
.map(de => de.names)
.flat() ?? [],
);
return (
<Grid item key={i} xs={12}>
<Cluster
clusterObjects={item}
podsWithErrors={podsWithErrors}
/>
</Grid>
);
})}
</Grid>
</Grid>
)}
</Content>
</Page>
);
};
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { KubernetesContent } from './KubernetesContent';
export { Cluster } from './Cluster';
@@ -0,0 +1,16 @@
/*
* Copyright 2020 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.
*/
export { ErrorPanel } from './ErrorPanel';
@@ -18,11 +18,11 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { KubernetesContent } from './KubernetesContent';
import { useKubernetesObjects } from '../../hooks';
import { useKubernetesObjects } from '../hooks';
jest.mock('../../hooks');
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
import * as twoDeployments from '../../__fixtures__/2-deployments.json';
jest.mock('../hooks');
import * as oneDeployment from '../__fixtures__/1-deployments.json';
import * as twoDeployments from '../__fixtures__/2-deployments.json';
describe('KubernetesContent', () => {
it('render empty response', async () => {
@@ -0,0 +1,130 @@
/*
* 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 React from 'react';
import { Divider, Grid, Typography } from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { ErrorPanel } from './ErrorPanel';
import { ErrorReporting } from './ErrorReporting';
import { DetectedError, detectErrors } from '../error-detection';
import { Cluster } from './Cluster';
import EmptyStateImage from '../assets/emptystate.svg';
import { useKubernetesObjects } from '../hooks';
import { Content, Page, Progress } from '@backstage/core-components';
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
const detectedErrors =
kubernetesObjects !== undefined
? detectErrors(kubernetesObjects)
: new Map<string, DetectedError[]>();
return (
<Page themeId="tool">
<Content>
{kubernetesObjects === undefined && error === undefined && <Progress />}
{/* errors retrieved from the kubernetes clusters */}
{clustersWithErrors.length > 0 && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
clustersWithErrors={clustersWithErrors}
/>
</Grid>
</Grid>
)}
{/* other errors */}
{error !== undefined && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
errorMessage={error}
/>
</Grid>
</Grid>
)}
{kubernetesObjects && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorReporting detectedErrors={detectedErrors} />
</Grid>
<Grid item>
<Divider />
</Grid>
<Grid item>
<Typography variant="h3">Your Clusters</Typography>
</Grid>
<Grid item container>
{kubernetesObjects?.items.length <= 0 && (
<Grid
container
justifyContent="space-around"
direction="row"
alignItems="center"
spacing={2}
>
<Grid item xs={4}>
<Typography variant="h5">
No resources on any known clusters for{' '}
{entity.metadata.name}
</Typography>
</Grid>
<Grid item xs={4}>
<img
src={EmptyStateImage}
alt="EmptyState"
data-testid="emptyStateImg"
/>
</Grid>
</Grid>
)}
{kubernetesObjects?.items.length > 0 &&
kubernetesObjects?.items.map((item, i) => {
const podsWithErrors = new Set<string>(
detectedErrors
.get(item.cluster.name)
?.filter(de => de.kind === 'Pod')
.map(de => de.names)
.flat() ?? [],
);
return (
<Grid item key={i} xs={12}>
<Cluster
clusterObjects={item}
podsWithErrors={podsWithErrors}
/>
</Grid>
);
})}
</Grid>
</Grid>
)}
</Content>
</Page>
);
};
@@ -71,10 +71,10 @@ describe('useKubernetesObjects', () => {
const mockGetObjectsByEntity = jest.fn();
const mockDecorateRequestBodyForAuth = jest.fn();
const expectMocksCalledCorrectly = () => {
expect(mockGetClusters).toBeCalledTimes(1);
const expectMocksCalledCorrectly = (numOfCalls: number = 1) => {
expect(mockGetClusters).toBeCalledTimes(numOfCalls);
expect(mockGetClusters).toHaveBeenLastCalledWith();
expect(mockDecorateRequestBodyForAuth).toBeCalledTimes(2);
expect(mockDecorateRequestBodyForAuth).toBeCalledTimes(numOfCalls * 2);
expect(mockDecorateRequestBodyForAuth).toHaveBeenCalledWith('google', {
entity,
});
@@ -82,7 +82,7 @@ describe('useKubernetesObjects', () => {
'authprovider2',
entityWithAuthToken,
);
expect(mockGetObjectsByEntity).toBeCalledTimes(1);
expect(mockGetObjectsByEntity).toBeCalledTimes(numOfCalls);
expect(mockGetObjectsByEntity).toHaveBeenLastCalledWith(
entityWithAuthToken,
);
@@ -110,6 +110,26 @@ describe('useKubernetesObjects', () => {
expectMocksCalledCorrectly();
});
it('should update on an interval', async () => {
(useApi as any).mockReturnValue({
getClusters: mockGetClusters.mockResolvedValue(getClustersResponse),
getObjectsByEntity:
mockGetObjectsByEntity.mockResolvedValue(mockResponse),
decorateRequestBodyForAuth:
mockDecorateRequestBodyForAuth.mockResolvedValue(entityWithAuthToken),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current.error).toBeUndefined();
expect(result.current.kubernetesObjects).toStrictEqual(mockResponse);
expectMocksCalledCorrectly(2);
});
it('should return error when getObjectsByEntity throws', async () => {
(useApi as any).mockReturnValue({
getClusters: mockGetClusters.mockResolvedValue(getClustersResponse),
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../api/types';
import { kubernetesAuthProvidersApiRef } from '../kubernetes-auth-provider/types';
import { useEffect, useState } from 'react';
import { useInterval } from 'react-use';
import {
KubernetesRequestBody,
ObjectsByEntityResponse,
@@ -29,7 +30,10 @@ export interface KubernetesObjects {
error: string | undefined;
}
export const useKubernetesObjects = (entity: Entity): KubernetesObjects => {
export const useKubernetesObjects = (
entity: Entity,
intervalMs: number = 10000,
): KubernetesObjects => {
const kubernetesApi = useApi(kubernetesApiRef);
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
const [kubernetesObjects, setKubernetesObjects] = useState<
@@ -38,51 +42,55 @@ export const useKubernetesObjects = (entity: Entity): KubernetesObjects => {
const [error, setError] = useState<string | undefined>(undefined);
const getObjects = async () => {
let clusters = [];
try {
clusters = await kubernetesApi.getClusters();
} catch (e) {
setError(e.message);
return;
}
const authProviders: string[] = [
...new Set(clusters.map(c => c.authProvider)),
];
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: KubernetesRequestBody = {
entity,
};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
try {
requestBody =
await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
authProviderStr,
requestBody,
);
} catch (e) {
setError(e.message);
return;
}
}
try {
setKubernetesObjects(await kubernetesApi.getObjectsByEntity(requestBody));
} catch (e) {
setError(e.message);
return;
}
};
useEffect(() => {
(async () => {
let clusters = [];
try {
clusters = await kubernetesApi.getClusters();
} catch (e) {
setError(e.message);
return;
}
const authProviders: string[] = [
...new Set(clusters.map(c => c.authProvider)),
];
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: KubernetesRequestBody = {
entity,
};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
try {
requestBody =
await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
authProviderStr,
requestBody,
);
} catch (e) {
setError(e.message);
return;
}
}
try {
setKubernetesObjects(
await kubernetesApi.getObjectsByEntity(requestBody),
);
} catch (e) {
setError(e.message);
return;
}
})();
getObjects();
/* eslint-disable react-hooks/exhaustive-deps */
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
/* eslint-enable react-hooks/exhaustive-deps */
useInterval(() => {
getObjects();
}, intervalMs);
return {
kubernetesObjects,
error,
@@ -25,6 +25,7 @@
"@backstage/integration": "^0.6.2",
"@backstage/plugin-scaffolder-backend": "^0.15.10",
"@backstage/config": "^0.1.8",
"@backstage/types": "^0.1.1",
"command-exists": "^1.2.9",
"fs-extra": "10.0.0",
"winston": "^3.2.1",
@@ -29,7 +29,8 @@ import {
UrlReader,
ContainerRunner,
} from '@backstage/backend-common';
import { ConfigReader, JsonObject } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { ScmIntegrations } from '@backstage/integration';
import mockFs from 'mock-fs';
import os from 'os';
@@ -19,7 +19,7 @@ import {
UrlReader,
resolveSafeChildPath,
} from '@backstage/backend-common';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import commandExists from 'command-exists';
@@ -26,6 +26,7 @@
"@backstage/config": "^0.1.8",
"@backstage/errors": "^0.1.3",
"@backstage/integration": "^0.6.2",
"@backstage/types": "^0.1.1",
"command-exists": "^1.2.9",
"fs-extra": "^9.0.0"
},
@@ -15,7 +15,7 @@
*/
import { ContainerRunner, UrlReader } from '@backstage/backend-common';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import fs from 'fs-extra';
@@ -23,7 +23,7 @@ import {
railsArgumentResolver,
RailsRunOptions,
} from './railsArgumentResolver';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { Writable } from 'stream';
export class RailsNewRunner {
@@ -22,6 +22,7 @@
"dependencies": {
"@backstage/config": "^0.1.8",
"@backstage/plugin-scaffolder-backend": "^0.15.2",
"@backstage/types": "^0.1.1",
"winston": "^3.2.1",
"yeoman-environment": "^3.6.0"
},
@@ -23,7 +23,7 @@ import os from 'os';
import { PassThrough } from 'stream';
import { createRunYeomanAction } from './yeoman';
import type { ActionContext } from '@backstage/plugin-scaffolder-backend';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
describe('run:yeoman', () => {
const mockTmpDir = os.tmpdir();
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { createTemplateAction } from '@backstage/plugin-scaffolder-backend';
import { yeomanRun } from './yeomanRun';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
/*
* This module should use '@types/yeoman-environment' eventually as soon as '@types/yeoman-environment' supports
+1 -1
View File
@@ -189,7 +189,7 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export const createTemplateAction: <
Input extends Partial<{
[name: string]: JsonValue_2 | Partial<JsonObject_2> | undefined;
[name: string]: JsonValue | Partial<JsonObject> | undefined;
}>,
>(
templateAction: TemplateAction<Input>,
+1
View File
@@ -39,6 +39,7 @@
"@backstage/plugin-catalog-backend": "^0.17.1",
"@backstage/plugin-scaffolder-common": "^0.1.0",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2",
"@backstage/types": "^0.1.1",
"@gitbeaker/core": "^30.2.0",
"@gitbeaker/node": "^30.2.0",
"@octokit/rest": "^18.5.3",
@@ -15,7 +15,7 @@
*/
import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import fs from 'fs-extra';
@@ -17,7 +17,7 @@ import { createTemplateAction } from '../../createTemplateAction';
import { resolveSafeChildPath } from '@backstage/backend-common';
import { InputError } from '@backstage/errors';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import fs from 'fs-extra';
interface FilesToRename extends JsonObject {
@@ -16,7 +16,7 @@
import { Logger } from 'winston';
import { Writable } from 'stream';
import { JsonValue, JsonObject } from '@backstage/config';
import { JsonValue, JsonObject } from '@backstage/types';
import { Schema } from 'jsonschema';
type PartialJsonObject = Partial<JsonObject>;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { resolvePackagePath } from '@backstage/backend-common';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { Knex } from 'knex';
@@ -26,7 +26,7 @@ import * as winston from 'winston';
import nunjucks from 'nunjucks';
import fs from 'fs-extra';
import path from 'path';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { PassThrough } from 'stream';
import { isTruthy } from './helper';
@@ -31,7 +31,7 @@ import { Logger } from 'winston';
import path from 'path';
import fs from 'fs-extra';
import { validate as validateJsonSchema } from 'jsonschema';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
type Options = {
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { assertError } from '@backstage/errors';
import { Logger } from 'winston';
import {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue, JsonObject } from '@backstage/config';
import { JsonValue, JsonObject } from '@backstage/types';
/**
* Status
+1 -1
View File
@@ -4,7 +4,7 @@
```ts
import { Entity } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { JSONSchema } from '@backstage/catalog-model';
// @public (undocumented)
+1 -1
View File
@@ -37,7 +37,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.9.5",
"@backstage/config": "^0.1.10"
"@backstage/types": "^0.1.1"
},
"devDependencies": {
"@backstage/cli": "^0.8.0"
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { Entity } from '@backstage/catalog-model';
/** @public */
+26 -3
View File
@@ -19,10 +19,10 @@ import { FieldProps } from '@rjsf/core';
import { FieldValidation } from '@rjsf/core';
import { IconButton } from '@material-ui/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { Observable } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { ScmIntegrationRegistry } from '@backstage/integration';
@@ -240,6 +240,29 @@ const scaffolderPlugin: BackstagePlugin<
export { scaffolderPlugin as plugin };
export { scaffolderPlugin };
// Warning: (ae-missing-release-tag) "TemplateList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TemplateList: ({
TemplateCardComponent,
}: TemplateListProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type TemplateListProps = {
TemplateCardComponent?:
| ComponentType<{
template: TemplateEntityV1beta2;
}>
| undefined;
};
// Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TemplateTypePicker: () => JSX.Element | null;
// Warning: (ae-missing-release-tag) "TextValuePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1
View File
@@ -41,6 +41,7 @@
"@backstage/integration-react": "^0.1.12",
"@backstage/plugin-catalog-react": "^0.6.1",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
+1 -2
View File
@@ -15,7 +15,7 @@
*/
import { EntityName } from '@backstage/catalog-model';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue, Observable } from '@backstage/types';
import { ResponseError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { Field, FieldValidation } from '@rjsf/core';
@@ -25,7 +25,6 @@ import {
createApiRef,
DiscoveryApi,
IdentityApi,
Observable,
} from '@backstage/core-plugin-api';
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
Box,
Button,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { FormProps } from '@rjsf/core';
function isObject(value: unknown): value is JsonObject {
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { TemplateList } from './TemplateList';
export type { TemplateListProps } from './TemplateList';
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { LinearProgress } from '@material-ui/core';
import { FormValidation, IChangeEvent } from '@rjsf/core';
import React, { useCallback, useState } from 'react';
@@ -17,7 +17,8 @@ import { useImmerReducer } from 'use-immer';
import { useEffect } from 'react';
import { scaffolderApiRef, LogEvent } from '../../api';
import { ScaffolderTask, Status, TaskOutput } from '../../types';
import { Subscription, useApi } from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
import { Subscription } from '@backstage/types';
type Step = {
id: string;
+3
View File
@@ -44,3 +44,6 @@ export {
TextValuePicker,
} from './components/fields';
export { FavouriteTemplate } from './components/FavouriteTemplate';
export { TemplateList } from './components/TemplateList';
export type { TemplateListProps } from './components/TemplateList';
export { TemplateTypePicker } from './components/TemplateTypePicker';
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped';
export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
+1 -1
View File
@@ -9,7 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncState } from 'react-use/lib/useAsync';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { IndexableDocument } from '@backstage/search-common';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { default as React_2 } from 'react';
import { ReactElement } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
+1
View File
@@ -38,6 +38,7 @@
"@backstage/plugin-catalog-react": "^0.6.1",
"@backstage/search-common": "^0.2.0",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { useApi } from '@backstage/core-plugin-api';
import { SearchResultSet } from '@backstage/search-common';
import React, {
@@ -19,7 +19,7 @@ import { usePrevious } from 'react-use';
import qs from 'qs';
import { useLocation, useOutlet } from 'react-router';
import { SearchContextProvider, useSearch } from '../SearchContext';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { LegacySearchPage } from '../LegacySearchPage';
export const UrlUpdater = () => {
+14 -4
View File
@@ -36,7 +36,11 @@ export class MockSentryApi implements SentryApi {
export class ProductionSentryApi implements SentryApi {
constructor(discoveryApi: DiscoveryApi, organization: string);
// (undocumented)
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
fetchIssues(
project: string,
statsFor: string,
query?: string,
): Promise<SentryIssue[]>;
}
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -49,7 +53,11 @@ export const Router: ({ entity }: { entity: Entity }) => JSX.Element;
// @public (undocumented)
export interface SentryApi {
// (undocumented)
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
fetchIssues(
project: string,
statsFor: string,
query?: string,
): Promise<SentryIssue[]>;
}
// Warning: (ae-missing-release-tag) "sentryApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -67,7 +75,7 @@ export type SentryIssue = {
userCount: number;
stats: {
'24h'?: EventPoint[];
'12h'?: EventPoint[];
'14d'?: EventPoint[];
};
culprit: string;
title: string;
@@ -100,10 +108,12 @@ export const SentryIssuesWidget: ({
entity,
statsFor,
variant,
query,
}: {
entity: Entity;
statsFor?: '12h' | '24h' | undefined;
statsFor?: '' | '14d' | '24h' | undefined;
variant?: InfoCardVariants | undefined;
query?: string | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "sentryPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+1 -1
View File
@@ -20,7 +20,7 @@ import mockData from './sentry-issue-mock.json';
function getMockIssue(): SentryIssue {
const randomizedStats = {
'12h': new Array(12)
'24h': new Array(24)
.fill(0)
.map(() => [0, Math.floor(Math.random() * 100)]),
};
@@ -4,7 +4,7 @@
"numComments": 0,
"userCount": 0,
"stats": {
"12h": [
"24h": [
[1589450400, 7],
[1589454000, 2],
[1589457600, 6],
+8 -2
View File
@@ -24,15 +24,21 @@ export class ProductionSentryApi implements SentryApi {
private readonly organization: string,
) {}
async fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]> {
async fetchIssues(
project: string,
statsFor: string,
query?: string,
): Promise<SentryIssue[]> {
if (!project) {
return [];
}
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`;
const queryPart = query ? `&query=${query}` : '';
const response = await fetch(
`${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}`,
`${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`,
);
if (response.status >= 400 && response.status < 600) {
+5 -1
View File
@@ -23,5 +23,9 @@ export const sentryApiRef = createApiRef<SentryApi>({
});
export interface SentryApi {
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
fetchIssues(
project: string,
statsFor: string,
query?: string,
): Promise<SentryIssue[]>;
}
+1 -1
View File
@@ -39,7 +39,7 @@ export type SentryIssue = {
userCount: number;
stats: {
'24h'?: EventPoint[];
'12h'?: EventPoint[];
'14d'?: EventPoint[];
};
culprit: string;
title: string;
@@ -20,8 +20,8 @@ import { Sparklines, SparklinesBars } from 'react-sparklines';
export const ErrorGraph = ({ sentryIssue }: { sentryIssue: SentryIssue }) => {
const data =
'12h' in sentryIssue.stats
? sentryIssue.stats['12h']
'14d' in sentryIssue.stats
? sentryIssue.stats['14d']
: sentryIssue.stats['24h'];
return (
@@ -59,7 +59,7 @@ const columns: TableColumn[] = [
type SentryIssuesTableProps = {
sentryIssues: SentryIssue[];
statsFor?: '24h' | '12h';
statsFor?: '24h' | '14d' | '';
};
const SentryIssuesTable = ({
@@ -38,10 +38,12 @@ export const SentryIssuesWidget = ({
entity,
statsFor = '24h',
variant = 'gridItem',
query = '',
}: {
entity: Entity;
statsFor?: '24h' | '12h';
statsFor?: '24h' | '14d' | '';
variant?: InfoCardVariants;
query?: string;
}) => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const sentryApi = useApi(sentryApiRef);
@@ -49,8 +51,8 @@ export const SentryIssuesWidget = ({
const projectId = useProjectSlug(entity);
const { loading, value, error } = useAsync(
() => sentryApi.fetchIssues(projectId, statsFor),
[sentryApi, statsFor, projectId],
() => sentryApi.fetchIssues(projectId, statsFor, query),
[sentryApi, statsFor, projectId, query],
);
useEffect(() => {
+1 -1
View File
@@ -7,7 +7,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
import { StorageApi } from '@backstage/core-plugin-api';
+1
View File
@@ -24,6 +24,7 @@
"@backstage/core-components": "^0.7.1",
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
+2 -1
View File
@@ -15,7 +15,8 @@
*/
import { Shortcut } from '../types';
import { createApiRef, Observable } from '@backstage/core-plugin-api';
import { createApiRef } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
export const shortcutsApiRef = createApiRef<ShortcutApi>({
id: 'plugin.shortcuts.api',
@@ -321,6 +321,7 @@ export const useTechDocsReaderDom = (): Element | null => {
baseUrl: window.location.origin,
onClick: (_: MouseEvent, url: string) => {
const parsedUrl = new URL(url);
// hash exists when anchor is clicked on secondary sidebar
if (parsedUrl.hash) {
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
// Scroll to hash if it's on the current page
@@ -329,6 +330,10 @@ export const useTechDocsReaderDom = (): Element | null => {
?.scrollIntoView();
} else {
navigate(parsedUrl.pathname);
// Scroll to top of reader if primary sidebar link is clicked
transformedElement
?.querySelector('.md-content__inner')
?.scrollIntoView();
}
},
}),