Merge remote-tracking branch 'upstream/master' into mcalus3/enable-pending-locations-for-repo-importing
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
EntityPolicy,
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
LocationSpec,
|
||||
} from '@backstage/catalog-model';
|
||||
@@ -44,6 +45,7 @@ type Options = {
|
||||
config: Config;
|
||||
processors: CatalogProcessor[];
|
||||
rulesEnforcer: CatalogRulesEnforcer;
|
||||
policy: EntityPolicy;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,10 +76,12 @@ export class LocationReaders implements LocationReader {
|
||||
} else if (item.type === 'entity') {
|
||||
if (rulesEnforcer.isAllowed(item.entity, item.location)) {
|
||||
const entity = await this.handleEntity(item, emit);
|
||||
output.entities.push({
|
||||
entity,
|
||||
location: item.location,
|
||||
});
|
||||
if (entity) {
|
||||
output.entities.push({
|
||||
entity,
|
||||
location: item.location,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
output.errors.push({
|
||||
location: item.location,
|
||||
@@ -162,25 +166,65 @@ export class LocationReaders implements LocationReader {
|
||||
private async handleEntity(
|
||||
item: CatalogProcessorEntityResult,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<Entity> {
|
||||
): Promise<Entity | undefined> {
|
||||
const { processors, logger } = this.options;
|
||||
|
||||
let current = item.entity;
|
||||
|
||||
// Construct the name carefully, this happens before validation below
|
||||
// so we do not want to crash here due to missing metadata or so
|
||||
const kind = current.kind || '';
|
||||
const namespace = !current.metadata
|
||||
? ''
|
||||
: current.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE;
|
||||
const name = !current.metadata ? '' : current.metadata.name;
|
||||
|
||||
for (const processor of processors) {
|
||||
if (processor.processEntity) {
|
||||
if (processor.preProcessEntity) {
|
||||
try {
|
||||
current = await processor.processEntity(current, item.location, emit);
|
||||
current = await processor.preProcessEntity(
|
||||
current,
|
||||
item.location,
|
||||
emit,
|
||||
);
|
||||
} catch (e) {
|
||||
// Construct the name carefully, if we got validation errors we do
|
||||
// not want to crash here due to missing metadata or so
|
||||
const namespace = !current.metadata
|
||||
? ''
|
||||
: current.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE;
|
||||
const name = !current.metadata ? '' : current.metadata.name;
|
||||
const message = `Processor ${processor.constructor.name} threw an error while processing entity ${current.kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${processor.constructor.name} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const next = await this.options.policy.enforce(current);
|
||||
if (!next) {
|
||||
const message = `Policy unexpectedly returned no data while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
}
|
||||
current = next;
|
||||
} catch (e) {
|
||||
const message = `Policy check failed while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
emit(result.inputError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const processor of processors) {
|
||||
if (processor.postProcessEntity) {
|
||||
try {
|
||||
current = await processor.postProcessEntity(
|
||||
current,
|
||||
item.location,
|
||||
emit,
|
||||
);
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while postprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import lodash from 'lodash';
|
||||
import { CatalogProcessor } from './types';
|
||||
|
||||
export class AnnotateLocationEntityProcessor implements CatalogProcessor {
|
||||
async processEntity(entity: Entity, location: LocationSpec): Promise<Entity> {
|
||||
async preProcessEntity(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
): Promise<Entity> {
|
||||
return lodash.merge(
|
||||
{
|
||||
metadata: {
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
spec: { owner: '@acme/foo-team' },
|
||||
});
|
||||
|
||||
const result = await processor.processEntity(
|
||||
const result = await processor.preProcessEntity(
|
||||
entity as any,
|
||||
mockLocation(),
|
||||
);
|
||||
@@ -235,7 +235,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
it('should ignore url locations', async () => {
|
||||
const { entity, processor } = setupTest();
|
||||
|
||||
const result = await processor.processEntity(
|
||||
const result = await processor.preProcessEntity(
|
||||
entity as any,
|
||||
mockLocation({ type: 'url' }),
|
||||
);
|
||||
@@ -246,7 +246,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
it('should ignore invalid kinds', async () => {
|
||||
const { entity, processor } = setupTest({ kind: 'Group' });
|
||||
|
||||
const result = await processor.processEntity(
|
||||
const result = await processor.preProcessEntity(
|
||||
entity as any,
|
||||
mockLocation(),
|
||||
);
|
||||
@@ -257,7 +257,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
it('should set owner from codeowner', async () => {
|
||||
const { entity, processor } = setupTest();
|
||||
|
||||
const result = await processor.processEntity(
|
||||
const result = await processor.preProcessEntity(
|
||||
entity as any,
|
||||
mockLocation(),
|
||||
);
|
||||
|
||||
@@ -40,7 +40,10 @@ type Options = {
|
||||
export class CodeOwnersProcessor implements CatalogProcessor {
|
||||
constructor(private readonly options: Options) {}
|
||||
|
||||
async processEntity(entity: Entity, location: LocationSpec): Promise<Entity> {
|
||||
async preProcessEntity(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
): Promise<Entity> {
|
||||
// Only continue if the owner is not set
|
||||
if (
|
||||
!entity ||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Entity, EntityPolicy } from '@backstage/catalog-model';
|
||||
import { CatalogProcessor } from './types';
|
||||
|
||||
export class EntityPolicyProcessor implements CatalogProcessor {
|
||||
private readonly policy: EntityPolicy;
|
||||
|
||||
constructor(policy: EntityPolicy) {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
async processEntity(entity: Entity): Promise<Entity> {
|
||||
const output = await this.policy.enforce(entity);
|
||||
if (!output) {
|
||||
throw new Error(`Entity did not match any known schema`);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import * as result from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
|
||||
export class LocationRefProcessor implements CatalogProcessor {
|
||||
async processEntity(
|
||||
async postProcessEntity(
|
||||
entity: Entity,
|
||||
_location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('PlaceholderProcessor', () => {
|
||||
reader,
|
||||
});
|
||||
await expect(
|
||||
processor.processEntity(input, { type: 't', target: 'l' }),
|
||||
processor.preProcessEntity(input, { type: 't', target: 'l' }),
|
||||
).resolves.toBe(input);
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -98,7 +98,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -122,7 +122,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -143,7 +143,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -177,7 +177,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -209,7 +209,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -241,7 +241,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -277,7 +277,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
@@ -316,7 +316,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
processor.processEntity(
|
||||
processor.preProcessEntity(
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
|
||||
@@ -45,7 +45,10 @@ type Options = {
|
||||
export class PlaceholderProcessor implements CatalogProcessor {
|
||||
constructor(private readonly options: Options) {}
|
||||
|
||||
async processEntity(entity: Entity, location: LocationSpec): Promise<Entity> {
|
||||
async preProcessEntity(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
): Promise<Entity> {
|
||||
const process = async (data: any): Promise<[any, boolean]> => {
|
||||
if (!data || !(data instanceof Object)) {
|
||||
// Scalars can't have placeholders
|
||||
|
||||
@@ -23,7 +23,6 @@ export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcess
|
||||
export { AzureApiReaderProcessor } from './AzureApiReaderProcessor';
|
||||
export { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor';
|
||||
export { CodeOwnersProcessor } from './CodeOwnersProcessor';
|
||||
export { EntityPolicyProcessor } from './EntityPolicyProcessor';
|
||||
export { FileReaderProcessor } from './FileReaderProcessor';
|
||||
export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';
|
||||
export { GithubReaderProcessor } from './GithubReaderProcessor';
|
||||
|
||||
@@ -46,15 +46,33 @@ export type CatalogProcessor = {
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Processes an emitted entity, e.g. by validating or modifying it.
|
||||
* Pre-processes an emitted entity, after it has been emitted but before it
|
||||
* has been validated.
|
||||
*
|
||||
* @param entity The entity to process
|
||||
* This type of processing usually involves enriching the entity with
|
||||
* additional data, and the input entity may actually still be incomplete
|
||||
* when the processor is invoked.
|
||||
*
|
||||
* @param entity The (possibly partial) entity to process
|
||||
* @param location The location that the entity came from
|
||||
* @param read Reads the contents of a location
|
||||
* @param emit A sink for auxiliary items resulting from the processing
|
||||
* @returns The same entity or a modified version of it
|
||||
*/
|
||||
processEntity?(
|
||||
preProcessEntity?(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<Entity>;
|
||||
|
||||
/**
|
||||
* Post-processes an emitted entity, after it has been validated.
|
||||
*
|
||||
* @param entity The entity to process
|
||||
* @param location The location that the entity came from
|
||||
* @param emit A sink for auxiliary items resulting from the processing
|
||||
* @returns The same entity or a modified version of it
|
||||
*/
|
||||
postProcessEntity?(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
|
||||
@@ -49,49 +49,6 @@ describe('CatalogBuilder', () => {
|
||||
reader.read.mockResolvedValue(Buffer.from('junk'));
|
||||
|
||||
const builder = new CatalogBuilder(env)
|
||||
.replaceReaderProcessors([
|
||||
{
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
_optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
) {
|
||||
expect(location.type).toBe('test');
|
||||
emit(result.data(location, await reader.read('ignored')));
|
||||
return true;
|
||||
},
|
||||
},
|
||||
])
|
||||
.replaceParserProcessors([
|
||||
{
|
||||
async parseData(
|
||||
data: Buffer,
|
||||
location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
) {
|
||||
expect(data.toString()).toEqual('junk');
|
||||
emit(
|
||||
result.entity(location, {
|
||||
apiVersion: 'av',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'n' },
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
])
|
||||
.replacePreProcessors([
|
||||
{
|
||||
async processEntity(entity: Entity) {
|
||||
expect(entity.apiVersion).toBe('av');
|
||||
return {
|
||||
...entity,
|
||||
metadata: { ...entity.metadata, namespace: 'ns' },
|
||||
};
|
||||
},
|
||||
},
|
||||
])
|
||||
.replaceEntityPolicies([
|
||||
{
|
||||
async enforce(entity: Entity) {
|
||||
@@ -108,9 +65,51 @@ describe('CatalogBuilder', () => {
|
||||
},
|
||||
},
|
||||
])
|
||||
.replacePostProcessors([
|
||||
.setPlaceholderResolver('t', async ({ value }) => {
|
||||
expect(value).toBe('tt');
|
||||
return 'tt2';
|
||||
})
|
||||
.setFieldFormatValidators({
|
||||
isValidEntityName: n => {
|
||||
expect(n).toBe('n');
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.replaceProcessors([
|
||||
{
|
||||
async processEntity(entity: Entity) {
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
_optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
) {
|
||||
expect(location.type).toBe('test');
|
||||
emit(result.data(location, await reader.read('ignored')));
|
||||
return true;
|
||||
},
|
||||
async parseData(
|
||||
data: Buffer,
|
||||
location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
) {
|
||||
expect(data.toString()).toEqual('junk');
|
||||
emit(
|
||||
result.entity(location, {
|
||||
apiVersion: 'av',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'n', replaced: { $t: 'tt' } },
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
},
|
||||
async preProcessEntity(entity: Entity) {
|
||||
expect(entity.apiVersion).toBe('av');
|
||||
return {
|
||||
...entity,
|
||||
metadata: { ...entity.metadata, namespace: 'ns' },
|
||||
};
|
||||
},
|
||||
async postProcessEntity(entity: Entity) {
|
||||
expect(entity.metadata.namespace).toBe('ns');
|
||||
return {
|
||||
...entity,
|
||||
metadata: { ...entity.metadata, post: 'p' },
|
||||
@@ -124,6 +123,7 @@ describe('CatalogBuilder', () => {
|
||||
type: 'test',
|
||||
target: '',
|
||||
});
|
||||
expect.assertions(8);
|
||||
expect(added.entities).toEqual([
|
||||
{
|
||||
apiVersion: 'av',
|
||||
@@ -132,6 +132,7 @@ describe('CatalogBuilder', () => {
|
||||
name: 'n',
|
||||
namespace: 'ns',
|
||||
post: 'p',
|
||||
replaced: 'tt2',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
locationEntityV1alpha1Policy,
|
||||
makeValidator,
|
||||
NoForeignRootFieldsEntityPolicy,
|
||||
ReservedFieldsEntityPolicy,
|
||||
SchemaValidEntityPolicy,
|
||||
templateEntityV1alpha1Policy,
|
||||
userEntityV1alpha1Policy,
|
||||
@@ -48,7 +47,6 @@ import {
|
||||
BitbucketApiReaderProcessor,
|
||||
CatalogProcessor,
|
||||
CodeOwnersProcessor,
|
||||
EntityPolicyProcessor,
|
||||
FileReaderProcessor,
|
||||
GithubOrgReaderProcessor,
|
||||
GithubReaderProcessor,
|
||||
@@ -84,36 +82,24 @@ export type CatalogEnvironment = {
|
||||
*
|
||||
* The touch points where you can replace or extend behavior are as follows:
|
||||
*
|
||||
* - Reader processors can be added or replaced. These implement the
|
||||
* functionality of reading raw data from a location, in the form of an
|
||||
* entity definition file.
|
||||
* - Parser processors can be added or replaced. These accept the raw data as
|
||||
* read by the previous processors and parse it into raw structured data
|
||||
* (for example, from a binary buffer containing yaml text, to a JS object
|
||||
* structure).
|
||||
* - Entity policies can be added or replaced. These are automatically run
|
||||
* after the processors' pre-processing steps. All policies are given the
|
||||
* chance to inspect the entity, and all of them have to pass in order for
|
||||
* the entity to be considered valid from an overall point of view.
|
||||
* - Entity kinds can be added or replaced. These are the second line of
|
||||
* validation that is applied after the entity policies, which adds
|
||||
* additional kind-specific validation (usually based on a schema). Only one
|
||||
* of the entity kinds has to accept the entity, but if none of them do, the
|
||||
* entity is rejected as a whole.
|
||||
* - Placeholder resolvers can be replaced or added. These run on the raw
|
||||
* structured data between the parsing and pre-processing steps, to replace
|
||||
* dollar-prefixed entries with their actual values (like $file).
|
||||
* - Pre-processors can be added or replaced. These take the raw unvalidated
|
||||
* data from the parser processors and can enrich or extend it before
|
||||
* validation. This is the place where for example codeowners data can be
|
||||
* injected into partial entity definitions.
|
||||
* - Entity policies can be added or replaced. These are the first line of
|
||||
* validation from the output of the pre-processing step. All policies are
|
||||
* given the chance to inspect the entity, and all of them have to pass in
|
||||
* order for the entity to be considered valid from an overall point of
|
||||
* view.
|
||||
* - Field format validators can be replaced. These check the format of
|
||||
* individual core fields such as metadata.name, such that they adhere to
|
||||
* certain rules.
|
||||
* - Entity kinds can be added or replaced. These are the second line of
|
||||
* validation that is applied after the entity policies, which add additional
|
||||
* kind-specific validation (usually based on a schema). Only one of the
|
||||
* entity kinds has to accept the entity, but if none of them do, the
|
||||
* entity is rejected as a whole.
|
||||
* - Post-processors can be added or replaced. These take the validated
|
||||
* entities out of the validation step and can perform additional actions on
|
||||
* them.
|
||||
* individual core fields such as metadata.name, to ensure that they adhere
|
||||
* to certain rules.
|
||||
* - Processors can be added or replaced. These implement the functionality of
|
||||
* reading, parsing and processing the entity data before it is persisted in
|
||||
* the catalog.
|
||||
*/
|
||||
export class CatalogBuilder {
|
||||
private readonly env: CatalogEnvironment;
|
||||
@@ -121,16 +107,10 @@ export class CatalogBuilder {
|
||||
private entityPoliciesReplace: boolean;
|
||||
private entityKinds: EntityPolicy[];
|
||||
private entityKindsReplace: boolean;
|
||||
private readerProcessors: CatalogProcessor[];
|
||||
private readerProcessorsReplace: boolean;
|
||||
private parserProcessors: CatalogProcessor[];
|
||||
private parserProcessorsReplace: boolean;
|
||||
private preProcessors: CatalogProcessor[];
|
||||
private preProcessorsReplace: boolean;
|
||||
private postProcessors: CatalogProcessor[];
|
||||
private postProcessorsReplace: boolean;
|
||||
private placeholderResolvers: Record<string, PlaceholderResolver>;
|
||||
private fieldFormatValidators: Partial<Validators>;
|
||||
private processors: CatalogProcessor[];
|
||||
private processorsReplace: boolean;
|
||||
|
||||
constructor(env: CatalogEnvironment) {
|
||||
this.env = env;
|
||||
@@ -138,16 +118,10 @@ export class CatalogBuilder {
|
||||
this.entityPoliciesReplace = false;
|
||||
this.entityKinds = [];
|
||||
this.entityKindsReplace = false;
|
||||
this.readerProcessors = [];
|
||||
this.readerProcessorsReplace = false;
|
||||
this.parserProcessors = [];
|
||||
this.parserProcessorsReplace = false;
|
||||
this.preProcessors = [];
|
||||
this.preProcessorsReplace = false;
|
||||
this.postProcessors = [];
|
||||
this.postProcessorsReplace = false;
|
||||
this.placeholderResolvers = {};
|
||||
this.fieldFormatValidators = {};
|
||||
this.processors = [];
|
||||
this.processorsReplace = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,114 +186,6 @@ export class CatalogBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds processors that support reading of definition files. These are run
|
||||
* before the entities are parsed, pre-processed, validated and post-
|
||||
* processed.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
addReaderProcessor(...processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.readerProcessors.push(...processors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what processors to use for the reading of definition files. These are
|
||||
* run before the entities are parsed, pre-processed, validated and post-
|
||||
* processed.
|
||||
*
|
||||
* This function replaces the default set of processors in this stage; use
|
||||
* with care.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
replaceReaderProcessors(processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.readerProcessors = [...processors];
|
||||
this.readerProcessorsReplace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds processors that run after each definition file has been read, in
|
||||
* order to parse the raw data. These are run before the entities are
|
||||
* pre-processed, validated and post-processed.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
addParserProcessor(...processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.parserProcessors.push(...processors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what processors to run after each definition file has been read, in
|
||||
* order to parse the raw data. These are run before the entities are
|
||||
* pre-processed, validated and post-processed.
|
||||
*
|
||||
* This function replaces the default set of processors in this stage; use
|
||||
* with care.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
replaceParserProcessors(processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.parserProcessors = [...processors];
|
||||
this.parserProcessorsReplace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds processors that run after each entity has been read and parsed,
|
||||
* but before being validated and post-processed.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
addPreProcessor(...processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.preProcessors.push(...processors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what processors to run after each entity has been read and parsed,
|
||||
* but before being validated and post-processed.
|
||||
*
|
||||
* This function replaces the default set of processors in this stage; use
|
||||
* with care.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
replacePreProcessors(processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.preProcessors = [...processors];
|
||||
this.preProcessorsReplace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds processors that run after each entity has been read, parsed,
|
||||
* run through the pre-processors, and validated.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
addPostProcessor(...processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.postProcessors.push(...processors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what processors to run after each entity has been read, parsed,
|
||||
* run through the pre-processors, and validated.
|
||||
*
|
||||
* This function replaces the default set of processors in this stage; use
|
||||
* with care.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
replacePostProcessors(processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.postProcessors = [...processors];
|
||||
this.postProcessorsReplace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds, or overwrites, a handler for placeholders (e.g. $file) in entity
|
||||
* definition files.
|
||||
@@ -327,8 +193,12 @@ export class CatalogBuilder {
|
||||
* @param key The key that identifies the placeholder, e.g. "file"
|
||||
* @param resolver The resolver that gets values for this placeholder
|
||||
*/
|
||||
setPlaceholderResolver(key: string, resolver: PlaceholderResolver) {
|
||||
setPlaceholderResolver(
|
||||
key: string,
|
||||
resolver: PlaceholderResolver,
|
||||
): CatalogBuilder {
|
||||
this.placeholderResolvers[key] = resolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,8 +211,34 @@ export class CatalogBuilder {
|
||||
*
|
||||
* @param validators The (subset of) validators to set
|
||||
*/
|
||||
setFieldFormatValidators(validators: Partial<Validators>) {
|
||||
setFieldFormatValidators(validators: Partial<Validators>): CatalogBuilder {
|
||||
lodash.merge(this.fieldFormatValidators, validators);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entity processors. These are responsible for reading, parsing, and
|
||||
* processing entities before they are persisted in the catalog.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
addProcessor(...processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.processors.push(...processors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what entity processors to use. These are responsible for reading,
|
||||
* parsing, and processing entities before they are persisted in the catalog.
|
||||
*
|
||||
* This function replaces the default set of processors; use with care.
|
||||
*
|
||||
* @param processors One or more processors
|
||||
*/
|
||||
replaceProcessors(processors: CatalogProcessor[]): CatalogBuilder {
|
||||
this.processors = [...processors];
|
||||
this.processorsReplace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,14 +251,15 @@ export class CatalogBuilder {
|
||||
}> {
|
||||
const { config, database, logger } = this.env;
|
||||
|
||||
const entityPolicy = this.buildEntityPolicy();
|
||||
const processors = this.buildProcessors(entityPolicy);
|
||||
const policy = this.buildEntityPolicy();
|
||||
const processors = this.buildProcessors();
|
||||
const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config);
|
||||
|
||||
const locationReader = new LocationReaders({
|
||||
...this.env,
|
||||
processors,
|
||||
rulesEnforcer,
|
||||
policy,
|
||||
});
|
||||
|
||||
const db = await DatabaseManager.createDatabase(
|
||||
@@ -396,7 +293,6 @@ export class CatalogBuilder {
|
||||
new FieldFormatEntityPolicy(
|
||||
makeValidator(this.fieldFormatValidators),
|
||||
),
|
||||
new ReservedFieldsEntityPolicy(),
|
||||
...this.entityPolicies,
|
||||
];
|
||||
|
||||
@@ -418,106 +314,74 @@ export class CatalogBuilder {
|
||||
]);
|
||||
}
|
||||
|
||||
private buildProcessors(entityPolicy: EntityPolicy): CatalogProcessor[] {
|
||||
const { config, reader } = this.env;
|
||||
private buildProcessors(): CatalogProcessor[] {
|
||||
const { config, logger, reader } = this.env;
|
||||
|
||||
const placeholderResolvers = lodash.merge(
|
||||
{
|
||||
json: jsonPlaceholderResolver,
|
||||
yaml: yamlPlaceholderResolver,
|
||||
text: textPlaceholderResolver,
|
||||
},
|
||||
this.placeholderResolvers,
|
||||
);
|
||||
const placeholderResolvers: Record<string, PlaceholderResolver> = {
|
||||
json: jsonPlaceholderResolver,
|
||||
yaml: yamlPlaceholderResolver,
|
||||
text: textPlaceholderResolver,
|
||||
...this.placeholderResolvers,
|
||||
};
|
||||
|
||||
const processors = this.processorsReplace
|
||||
? this.processors
|
||||
: [
|
||||
new FileReaderProcessor(),
|
||||
GithubOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
LdapOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
new UrlReaderProcessor({ reader, logger }),
|
||||
new YamlProcessor(),
|
||||
new CodeOwnersProcessor({ reader }),
|
||||
new LocationRefProcessor(),
|
||||
new AnnotateLocationEntityProcessor(),
|
||||
];
|
||||
|
||||
return [
|
||||
StaticLocationProcessor.fromConfig(config),
|
||||
...this.buildReaderProcessors(),
|
||||
...this.buildParserProcessors(),
|
||||
new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }),
|
||||
...this.buildPreProcessors(),
|
||||
new EntityPolicyProcessor(entityPolicy),
|
||||
...this.buildPostProcessors(),
|
||||
...this.buildDeprecatedReaderProcessors(),
|
||||
...processors,
|
||||
];
|
||||
}
|
||||
|
||||
private buildReaderProcessors(): CatalogProcessor[] {
|
||||
const { config, logger, reader } = this.env;
|
||||
// TODO(Rugvip): These are added for backwards compatibility if config exists
|
||||
// The idea is to have everyone migrate from using the old processors to
|
||||
// the new integration config driven UrlReaders. In an upcoming release we
|
||||
// can then completely remove support for the old processors, but still
|
||||
// keep handling the deprecated location types for a while, but with a
|
||||
// warning.
|
||||
private buildDeprecatedReaderProcessors(): CatalogProcessor[] {
|
||||
const { config, logger } = this.env;
|
||||
|
||||
if (this.readerProcessorsReplace) {
|
||||
return this.readerProcessors;
|
||||
}
|
||||
|
||||
// TODO(Rugvip): These are added for backwards compatibility if config exists
|
||||
// The idea is to have everyone migrate from using the old processors to the new
|
||||
// integration config driven UrlReaders. In an upcoming release we can then completely
|
||||
// remove support for the old processors, but still keep handling the deprecated location
|
||||
// types for a while, but with a warning.
|
||||
const oldProcessors = [];
|
||||
const result = [];
|
||||
const pc = config.getOptionalConfig('catalog.processors');
|
||||
if (pc?.has('github')) {
|
||||
logger.warn(
|
||||
`Using deprecated configuration for catalog.processors.github, move to using integrations.github instead`,
|
||||
);
|
||||
oldProcessors.push(GithubReaderProcessor.fromConfig(config, logger));
|
||||
result.push(GithubReaderProcessor.fromConfig(config, logger));
|
||||
}
|
||||
if (pc?.has('gitlabApi')) {
|
||||
logger.warn(
|
||||
`Using deprecated configuration for catalog.processors.gitlabApi, move to using integrations.gitlab instead`,
|
||||
);
|
||||
oldProcessors.push(new GitlabApiReaderProcessor(config));
|
||||
oldProcessors.push(new GitlabReaderProcessor());
|
||||
result.push(new GitlabApiReaderProcessor(config));
|
||||
result.push(new GitlabReaderProcessor());
|
||||
}
|
||||
if (pc?.has('bitbucketApi')) {
|
||||
logger.warn(
|
||||
`Using deprecated configuration for catalog.processors.bitbucketApi, move to using integrations.bitbucket instead`,
|
||||
);
|
||||
oldProcessors.push(new BitbucketApiReaderProcessor(config));
|
||||
result.push(new BitbucketApiReaderProcessor(config));
|
||||
}
|
||||
if (pc?.has('azureApi')) {
|
||||
logger.warn(
|
||||
`Using deprecated configuration for catalog.processors.azureApi, move to using integrations.azure instead`,
|
||||
);
|
||||
oldProcessors.push(new AzureApiReaderProcessor(config));
|
||||
result.push(new AzureApiReaderProcessor(config));
|
||||
}
|
||||
|
||||
return [
|
||||
new FileReaderProcessor(),
|
||||
...oldProcessors,
|
||||
GithubOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
LdapOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
new UrlReaderProcessor({ reader, logger }),
|
||||
...this.readerProcessors,
|
||||
];
|
||||
}
|
||||
|
||||
private buildParserProcessors(): CatalogProcessor[] {
|
||||
if (this.parserProcessorsReplace) {
|
||||
return this.parserProcessors;
|
||||
}
|
||||
|
||||
return [new YamlProcessor(), ...this.parserProcessors];
|
||||
}
|
||||
|
||||
private buildPreProcessors(): CatalogProcessor[] {
|
||||
const { reader } = this.env;
|
||||
|
||||
if (this.preProcessorsReplace) {
|
||||
return this.preProcessors;
|
||||
}
|
||||
|
||||
return [new CodeOwnersProcessor({ reader }), ...this.preProcessors];
|
||||
}
|
||||
|
||||
private buildPostProcessors(): CatalogProcessor[] {
|
||||
if (this.postProcessorsReplace) {
|
||||
return this.postProcessors;
|
||||
}
|
||||
|
||||
return [
|
||||
new LocationRefProcessor(),
|
||||
new AnnotateLocationEntityProcessor(),
|
||||
...this.postProcessors,
|
||||
];
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,24 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Chip,
|
||||
IconButton,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Chip,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { IconLinkVertical } from './IconLinkVertical';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import React from 'react';
|
||||
import { IconLinkVertical } from './IconLinkVertical';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
links: {
|
||||
@@ -59,6 +58,15 @@ const useStyles = makeStyles(theme => ({
|
||||
description: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
gridItemCard: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100% - 10px)', // for pages without content header
|
||||
marginBottom: '10px',
|
||||
},
|
||||
gridItemCardContent: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
@@ -83,14 +91,15 @@ function getCodeLinkInfo(entity: Entity): CodeLinkInfo {
|
||||
|
||||
type AboutCardProps = {
|
||||
entity: Entity;
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export function AboutCard({ entity }: AboutCardProps) {
|
||||
export function AboutCard({ entity, variant }: AboutCardProps) {
|
||||
const classes = useStyles();
|
||||
const codeLink = getCodeLinkInfo(entity);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className={variant === 'gridItem' ? classes.gridItemCard : ''}>
|
||||
<CardHeader
|
||||
title="About"
|
||||
action={
|
||||
@@ -107,15 +116,17 @@ export function AboutCard({ entity }: AboutCardProps) {
|
||||
}
|
||||
label="View Techdocs"
|
||||
icon={<DocsIcon />}
|
||||
href={`/docs/${entity.kind}:${entity.metadata.namespace ?? ''}:${
|
||||
entity.metadata.name
|
||||
}`}
|
||||
href={`/docs/${
|
||||
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
|
||||
}/${entity.kind}/${entity.metadata.name}`}
|
||||
/>
|
||||
</nav>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<CardContent
|
||||
className={variant === 'gridItem' ? classes.gridItemCardContent : ''}
|
||||
>
|
||||
<Grid container>
|
||||
<AboutField label="Description" gridSizes={{ xs: 12 }}>
|
||||
<Typography
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { Box, useTheme } from '@material-ui/core';
|
||||
import BarChartTick from './BarChartTick';
|
||||
import BarChartStepper from './BarChartStepper';
|
||||
import Tooltip, { TooltipItemProps } from '../Tooltip';
|
||||
import { Tooltip, TooltipItemProps } from '../Tooltip';
|
||||
|
||||
import { currencyFormatter } from '../../utils/formatters';
|
||||
import {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { TooltipPayload, TooltipProps } from 'recharts';
|
||||
import Tooltip, { TooltipItemProps } from '../../components/Tooltip';
|
||||
import { Tooltip, TooltipItemProps } from '../../components/Tooltip';
|
||||
import { DEFAULT_DATE_FORMAT } from '../../types';
|
||||
|
||||
export type CostOverviewTooltipProps = TooltipProps & {
|
||||
|
||||
@@ -14,5 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default } from './Tooltip';
|
||||
export * from './TooltipItem';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export type { TooltipProps } from './Tooltip';
|
||||
export { default as TooltipItem } from './TooltipItem';
|
||||
export type { TooltipItemProps } from './TooltipItem';
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { default as BarChart } from './BarChart';
|
||||
export { default as LegendItem } from './LegendItem';
|
||||
export * from './Tooltip';
|
||||
|
||||
@@ -17,4 +17,5 @@
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export { useCurrency } from './hooks';
|
||||
export * from './types';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
export type Step = {
|
||||
name: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
conclusion?: string;
|
||||
number: number; // starts from 1
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
|
||||
@@ -81,10 +81,9 @@ const WidgetContent = ({
|
||||
export const LatestWorkflowRunCard = ({
|
||||
entity,
|
||||
branch = 'master',
|
||||
}: {
|
||||
entity: Entity;
|
||||
branch: string;
|
||||
}) => {
|
||||
// Display the card full height suitable for
|
||||
variant,
|
||||
}: Props) => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const [owner, repo] = (
|
||||
entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'
|
||||
@@ -102,7 +101,7 @@ export const LatestWorkflowRunCard = ({
|
||||
}, [error, errorApi]);
|
||||
|
||||
return (
|
||||
<InfoCard title={`Last ${branch} build`}>
|
||||
<InfoCard title={`Last ${branch} build`} variant={variant}>
|
||||
<WidgetContent
|
||||
error={error}
|
||||
loading={loading}
|
||||
@@ -113,14 +112,18 @@ export const LatestWorkflowRunCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
branch: string;
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export const LatestWorkflowsForBranchCard = ({
|
||||
entity,
|
||||
branch = 'master',
|
||||
}: {
|
||||
entity: Entity;
|
||||
branch: string;
|
||||
}) => (
|
||||
<InfoCard title={`Last ${branch} build`}>
|
||||
variant,
|
||||
}: Props) => (
|
||||
<InfoCard title={`Last ${branch} build`} variant={variant}>
|
||||
<WorkflowRunsTable branch={branch} entity={entity} />
|
||||
</InfoCard>
|
||||
);
|
||||
|
||||
@@ -18,9 +18,9 @@ import { errorApiRef, useApi } from '@backstage/core-api';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName';
|
||||
import { useWorkflowRuns } from '../useWorkflowRuns';
|
||||
import React, { useEffect } from 'react';
|
||||
import { EmptyState, Table } from '@backstage/core';
|
||||
import { EmptyState, InfoCard, Table } from '@backstage/core';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import { Button, Card, Link, TableContainer } from '@material-ui/core';
|
||||
import { Button, Link } from '@material-ui/core';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
const firstLine = (message: string): string => message.split('\n')[0];
|
||||
@@ -30,6 +30,7 @@ export type Props = {
|
||||
branch?: string;
|
||||
dense?: boolean;
|
||||
limit?: number;
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export const RecentWorkflowRunsCard = ({
|
||||
@@ -37,6 +38,7 @@ export const RecentWorkflowRunsCard = ({
|
||||
branch,
|
||||
dense = false,
|
||||
limit = 5,
|
||||
variant,
|
||||
}: Props) => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const [owner, repo] = (
|
||||
@@ -70,15 +72,19 @@ export const RecentWorkflowRunsCard = ({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TableContainer component={Card}>
|
||||
<InfoCard
|
||||
title="Recent Workflow Runs"
|
||||
subheader={branch ? `Branch: ${branch}` : 'All Branches'}
|
||||
noPadding
|
||||
variant={variant}
|
||||
>
|
||||
<Table
|
||||
title="Recent Workflow Runs"
|
||||
subtitle={branch ? `Branch: ${branch}` : 'All Branches'}
|
||||
isLoading={loading}
|
||||
options={{
|
||||
search: false,
|
||||
paging: false,
|
||||
padding: dense ? 'dense' : 'default',
|
||||
toolbar: false,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
@@ -98,6 +104,6 @@ export const RecentWorkflowRunsCard = ({
|
||||
]}
|
||||
data={runs}
|
||||
/>
|
||||
</TableContainer>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,8 +77,9 @@ const JobsList = ({ jobs, entity }: { jobs?: Jobs; entity: Entity }) => {
|
||||
<Box>
|
||||
{jobs &&
|
||||
jobs.total_count > 0 &&
|
||||
jobs.jobs.map((job: Job) => (
|
||||
jobs.jobs.map(job => (
|
||||
<JobListItem
|
||||
key={job.id}
|
||||
job={job}
|
||||
className={
|
||||
job.status !== 'success' ? classes.failed : classes.success
|
||||
@@ -108,7 +109,7 @@ const StepView = ({ step }: { step: Step }) => {
|
||||
<TableCell>
|
||||
<WorkflowRunStatus
|
||||
status={step.status.toUpperCase()}
|
||||
conclusion={step.conclusion.toUpperCase()}
|
||||
conclusion={step.conclusion?.toUpperCase()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -142,8 +143,8 @@ const JobListItem = ({
|
||||
<AccordionDetails className={classes.accordionDetails}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
{job.steps.map((step: Step) => (
|
||||
<StepView step={step} />
|
||||
{job.steps.map(step => (
|
||||
<StepView key={step.number} step={step} />
|
||||
))}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -61,12 +61,18 @@ const WidgetContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const LatestRunCard = ({ branch = 'master' }: { branch: string }) => {
|
||||
export const LatestRunCard = ({
|
||||
branch = 'master',
|
||||
variant,
|
||||
}: {
|
||||
branch: string;
|
||||
variant?: string;
|
||||
}) => {
|
||||
const { owner, repo } = useProjectSlugFromEntity();
|
||||
const [{ builds, loading }] = useBuilds(owner, repo, branch);
|
||||
const lastRun = builds ?? {};
|
||||
return (
|
||||
<InfoCard title={`Last ${branch} build`}>
|
||||
<InfoCard title={`Last ${branch} build`} variant={variant}>
|
||||
<WidgetContent loading={loading} branch={branch} lastRun={lastRun} />
|
||||
</InfoCard>
|
||||
);
|
||||
|
||||
@@ -88,9 +88,10 @@ const LighthouseAuditSummary: FC<{ audit: Audit; dense?: boolean }> = ({
|
||||
return <StructuredMetadataTable metadata={tableData} dense={dense} />;
|
||||
};
|
||||
|
||||
export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({
|
||||
dense = false,
|
||||
}) => {
|
||||
export const LastLighthouseAuditCard: FC<{
|
||||
dense?: boolean;
|
||||
variant?: string;
|
||||
}> = ({ dense = false, variant }) => {
|
||||
const { value: website, loading, error } = useWebsiteForEntity();
|
||||
|
||||
let content;
|
||||
@@ -105,5 +106,9 @@ export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({
|
||||
<LighthouseAuditSummary audit={website.lastAudit} dense={dense} />
|
||||
);
|
||||
}
|
||||
return <InfoCard title="Lighthouse Audit">{content}</InfoCard>;
|
||||
return (
|
||||
<InfoCard title="Lighthouse Audit" variant={variant}>
|
||||
{content}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@rjsf/core": "^2.1.0",
|
||||
"@rjsf/material-ui": "^2.1.0",
|
||||
"@rjsf/core": "^2.4.0",
|
||||
"@rjsf/material-ui": "^2.4.0",
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
|
||||
Reference in New Issue
Block a user