Merge branch 'master' of https://github.com/backstage/backstage into marley/7641-consume-exported-ado-types
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
## API Report File for "@backstage/plugin-azure-devops-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// Warning: (ae-missing-release-tag) "BuildResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export enum BuildResult {
|
||||
Canceled = 32,
|
||||
Failed = 8,
|
||||
None = 0,
|
||||
PartiallySucceeded = 4,
|
||||
Succeeded = 2,
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "BuildStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export enum BuildStatus {
|
||||
All = 47,
|
||||
Cancelling = 4,
|
||||
Completed = 2,
|
||||
InProgress = 1,
|
||||
None = 0,
|
||||
NotStarted = 32,
|
||||
Postponed = 8,
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type RepoBuild = {
|
||||
id?: number;
|
||||
title: string;
|
||||
link?: string;
|
||||
status?: BuildStatus;
|
||||
result?: BuildResult;
|
||||
queueTime?: Date;
|
||||
startTime?: Date;
|
||||
finishTime?: Date;
|
||||
source: string;
|
||||
uniqueName?: string;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@backstage/plugin-azure-devops-common",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/azure-devops-common"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test --passWithNoTests",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.8.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum BuildResult {
|
||||
/**
|
||||
* No result
|
||||
*/
|
||||
None = 0,
|
||||
/**
|
||||
* The build completed successfully.
|
||||
*/
|
||||
Succeeded = 2,
|
||||
/**
|
||||
* The build completed compilation successfully but had other errors.
|
||||
*/
|
||||
PartiallySucceeded = 4,
|
||||
/**
|
||||
* The build completed unsuccessfully.
|
||||
*/
|
||||
Failed = 8,
|
||||
/**
|
||||
* The build was canceled before starting.
|
||||
*/
|
||||
Canceled = 32,
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
/**
|
||||
* No status.
|
||||
*/
|
||||
None = 0,
|
||||
/**
|
||||
* The build is currently in progress.
|
||||
*/
|
||||
InProgress = 1,
|
||||
/**
|
||||
* The build has completed.
|
||||
*/
|
||||
Completed = 2,
|
||||
/**
|
||||
* The build is cancelling
|
||||
*/
|
||||
Cancelling = 4,
|
||||
/**
|
||||
* The build is inactive in the queue.
|
||||
*/
|
||||
Postponed = 8,
|
||||
/**
|
||||
* The build has not yet started.
|
||||
*/
|
||||
NotStarted = 32,
|
||||
/**
|
||||
* All status.
|
||||
*/
|
||||
All = 47,
|
||||
}
|
||||
|
||||
export type RepoBuild = {
|
||||
id?: number;
|
||||
title: string;
|
||||
link?: string;
|
||||
status?: BuildStatus;
|
||||
result?: BuildResult;
|
||||
queueTime?: Date;
|
||||
startTime?: Date;
|
||||
finishTime?: Date;
|
||||
source: string;
|
||||
uniqueName?: string;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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,
|
||||
RepoBuild,
|
||||
} from '@backstage/plugin-azure-devops-common';
|
||||
|
||||
import { BuildTable } from './BuildTable';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Azure Devops/Build Table',
|
||||
component: BuildTable,
|
||||
};
|
||||
|
||||
const buildStatuses: Array<[BuildStatus, BuildResult]> = [
|
||||
[BuildStatus.InProgress, BuildResult.None], // In Progress
|
||||
[BuildStatus.Completed, BuildResult.Succeeded], // Succeeded
|
||||
[BuildStatus.Completed, BuildResult.Failed], // Failed
|
||||
[BuildStatus.Completed, BuildResult.PartiallySucceeded], // Partially Succeeded
|
||||
[BuildStatus.Completed, BuildResult.Canceled], // Cancelled
|
||||
[BuildStatus.Completed, BuildResult.None], // Unknown
|
||||
[BuildStatus.Cancelling, BuildResult.None], // Cancelling
|
||||
[BuildStatus.Postponed, BuildResult.None], // Postponed
|
||||
[BuildStatus.NotStarted, BuildResult.None], // Not Started
|
||||
[BuildStatus.None, BuildResult.None], // Unknown
|
||||
];
|
||||
|
||||
const generateTestData = (rows = 10): RepoBuild[] => {
|
||||
const repoBuilds: RepoBuild[] = [];
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const [status, result] = buildStatuses[i] ?? [
|
||||
BuildStatus.Completed,
|
||||
BuildResult.Succeeded,
|
||||
];
|
||||
|
||||
repoBuilds.push({
|
||||
id: rows - i + 12534,
|
||||
title: `backstage ci - 1.0.0-preview-${rows - i}`,
|
||||
status,
|
||||
result,
|
||||
queueTime: new Date(Date.now() - i * 60000),
|
||||
source: 'refs/heads/main',
|
||||
link: '',
|
||||
});
|
||||
}
|
||||
|
||||
return repoBuilds;
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<MemoryRouter>
|
||||
<BuildTable items={generateTestData()} loading={false} error={undefined} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const Empty = () => (
|
||||
<MemoryRouter>
|
||||
<BuildTable items={[]} loading={false} error={undefined} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const Loading = () => (
|
||||
<MemoryRouter>
|
||||
<BuildTable items={[]} loading error={undefined} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const ErrorMessage = () => (
|
||||
<MemoryRouter>
|
||||
<BuildTable
|
||||
items={[]}
|
||||
loading={false}
|
||||
error={new Error('Failed to load builds!')}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -149,9 +149,7 @@ const columns: TableColumn[] = [
|
||||
field: 'queueTime',
|
||||
width: 'auto',
|
||||
render: (row: Partial<RepoBuild>) =>
|
||||
DateTime.fromISO(
|
||||
row.queueTime ? row.queueTime.toString() : new Date().toString(),
|
||||
).toRelative(),
|
||||
DateTime.fromJSDate(row.queueTime ?? new Date()).toRelative(),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -870,11 +870,14 @@ export type EntityAncestryResponse = {
|
||||
// Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type EntityFilter = {
|
||||
anyOf: {
|
||||
allOf: EntitiesSearchFilter[];
|
||||
}[];
|
||||
};
|
||||
export type EntityFilter =
|
||||
| {
|
||||
allOf: EntityFilter[];
|
||||
}
|
||||
| {
|
||||
anyOf: EntityFilter[];
|
||||
}
|
||||
| EntitiesSearchFilter;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityPagination" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -22,9 +22,10 @@ import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
|
||||
* Any (at least one) of the outer sets must match, within which all of the
|
||||
* individual filters must match.
|
||||
*/
|
||||
export type EntityFilter = {
|
||||
anyOf: { allOf: EntitiesSearchFilter[] }[];
|
||||
};
|
||||
export type EntityFilter =
|
||||
| { allOf: EntityFilter[] }
|
||||
| { anyOf: EntityFilter[] }
|
||||
| EntitiesSearchFilter;
|
||||
|
||||
/**
|
||||
* A pagination rule for entities.
|
||||
|
||||
@@ -46,7 +46,11 @@ import {
|
||||
DbPageInfo,
|
||||
Transaction,
|
||||
} from './types';
|
||||
import { EntityPagination } from '../../catalog/types';
|
||||
import { EntityPagination, EntitiesSearchFilter } from '../../catalog/types';
|
||||
|
||||
type LegacyEntityFilter = {
|
||||
anyOf: { allOf: EntitiesSearchFilter[] }[];
|
||||
};
|
||||
|
||||
// The number of items that are sent per batch to the database layer, when
|
||||
// doing .batchInsert calls to knex. This needs to be low enough to not cause
|
||||
@@ -217,13 +221,28 @@ export class CommonDatabase implements Database {
|
||||
|
||||
let entitiesQuery = tx<DbEntitiesRow>('entities');
|
||||
|
||||
for (const singleFilter of request?.filter?.anyOf ?? []) {
|
||||
if (
|
||||
request?.filter &&
|
||||
(request.filter.hasOwnProperty('key') ||
|
||||
request.filter.hasOwnProperty('allOf'))
|
||||
) {
|
||||
throw new Error(
|
||||
'Filters for the legacy CommonDatabase must obey the { anyOf: [{ allOf: [] }] } format.',
|
||||
);
|
||||
}
|
||||
for (const singleFilter of (request?.filter as LegacyEntityFilter)?.anyOf ??
|
||||
[]) {
|
||||
entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
|
||||
for (const {
|
||||
key,
|
||||
matchValueIn,
|
||||
matchValueExists,
|
||||
} of singleFilter.allOf) {
|
||||
for (const filter of singleFilter.allOf) {
|
||||
if (
|
||||
filter.hasOwnProperty('anyOf') ||
|
||||
filter.hasOwnProperty('allOf')
|
||||
) {
|
||||
throw new Error(
|
||||
'Nested filters are not supported in the legacy CommonDatabase',
|
||||
);
|
||||
}
|
||||
const { key, matchValueIn, matchValueExists } = filter;
|
||||
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
|
||||
// make a lot of sense. However, it had abysmal performance on sqlite
|
||||
// when datasets grew large, so we're using IN instead.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DbFinalEntitiesRow,
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
DbSearchRow,
|
||||
} from '../database/tables';
|
||||
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
|
||||
|
||||
@@ -73,6 +74,52 @@ describe('NextEntitiesCatalog', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function addEntityToSearch(knex: Knex, entity: Entity) {
|
||||
const id = uuid();
|
||||
const entityRef = stringifyEntityRef(entity);
|
||||
const entityJson = JSON.stringify(entity);
|
||||
|
||||
await knex<DbRefreshStateRow>('refresh_state').insert({
|
||||
entity_id: id,
|
||||
entity_ref: entityRef,
|
||||
unprocessed_entity: entityJson,
|
||||
errors: '[]',
|
||||
next_update_at: '2031-01-01 23:00:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
await knex<DbFinalEntitiesRow>('final_entities').insert({
|
||||
entity_id: id,
|
||||
final_entity: entityJson,
|
||||
hash: 'h',
|
||||
stitch_ticket: '',
|
||||
});
|
||||
|
||||
await insertSearchRow(knex, id, null, entity);
|
||||
}
|
||||
|
||||
async function insertSearchRow(
|
||||
knex: Knex,
|
||||
id: string,
|
||||
previousKey: string | null,
|
||||
previousValue: Object,
|
||||
) {
|
||||
return Promise.all(
|
||||
Object.entries(previousValue).map(async ([key, value]) => {
|
||||
const currentKey = `${previousKey ? `${previousKey}.` : ``}${key}`;
|
||||
if (typeof value === 'object') {
|
||||
await insertSearchRow(knex, id, currentKey, value);
|
||||
} else {
|
||||
await knex<DbSearchRow>('search').insert({
|
||||
entity_id: id,
|
||||
key: currentKey,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe('entityAncestry', () => {
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should return the ancestry with one parent, %p',
|
||||
@@ -209,4 +256,106 @@ describe('NextEntitiesCatalog', () => {
|
||||
60_000,
|
||||
);
|
||||
});
|
||||
|
||||
describe('entities', () => {
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should return correct entity for simple filter',
|
||||
async databaseId => {
|
||||
const { knex } = await createDatabase(databaseId);
|
||||
const entity1: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'one' },
|
||||
spec: {},
|
||||
};
|
||||
const entity2: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'two' },
|
||||
spec: {
|
||||
test: 'test value',
|
||||
},
|
||||
};
|
||||
await addEntityToSearch(knex, entity1);
|
||||
await addEntityToSearch(knex, entity2);
|
||||
const catalog = new NextEntitiesCatalog(knex);
|
||||
|
||||
const testFilter = {
|
||||
key: 'spec.test',
|
||||
matchValueExists: true,
|
||||
};
|
||||
const request = { filter: testFilter };
|
||||
const { entities } = await catalog.entities(request);
|
||||
|
||||
expect(entities.length).toBe(1);
|
||||
expect(entities[0]).toEqual(entity2);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should return correct entity for nested filter',
|
||||
async databaseId => {
|
||||
const { knex } = await createDatabase(databaseId);
|
||||
const entity1: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'one', org: 'a', desc: 'description' },
|
||||
spec: {},
|
||||
};
|
||||
const entity2: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'two', org: 'b', desc: 'description' },
|
||||
spec: {},
|
||||
};
|
||||
const entity3: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'three', org: 'b', color: 'red' },
|
||||
spec: {},
|
||||
};
|
||||
const entity4: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'four', org: 'b', color: 'blue' },
|
||||
spec: {},
|
||||
};
|
||||
await addEntityToSearch(knex, entity1);
|
||||
await addEntityToSearch(knex, entity2);
|
||||
await addEntityToSearch(knex, entity3);
|
||||
await addEntityToSearch(knex, entity4);
|
||||
const catalog = new NextEntitiesCatalog(knex);
|
||||
|
||||
const testFilter1 = {
|
||||
key: 'metadata.org',
|
||||
matchValueExists: true,
|
||||
matchValueIn: ['b'],
|
||||
};
|
||||
const testFilter2 = {
|
||||
key: 'metadata.desc',
|
||||
matchValueExists: true,
|
||||
};
|
||||
const testFilter3 = {
|
||||
key: 'metadata.color',
|
||||
matchValueExists: true,
|
||||
matchValueIn: ['blue'],
|
||||
};
|
||||
const request = {
|
||||
filter: {
|
||||
allOf: [
|
||||
testFilter1,
|
||||
{
|
||||
anyOf: [testFilter2, testFilter3],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const { entities } = await catalog.entities(request);
|
||||
|
||||
expect(entities.length).toBe(2);
|
||||
expect(entities).toContainEqual(entity2);
|
||||
expect(entities).toContainEqual(entity4);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
EntitiesResponse,
|
||||
EntityAncestryResponse,
|
||||
EntityPagination,
|
||||
EntityFilter,
|
||||
EntitiesSearchFilter,
|
||||
} from '../catalog/types';
|
||||
import {
|
||||
DbFinalEntitiesRow,
|
||||
@@ -73,6 +75,90 @@ function stringifyPagination(input: { limit: number; offset: number }) {
|
||||
return base64;
|
||||
}
|
||||
|
||||
function addCondition(
|
||||
queryBuilder: Knex.QueryBuilder,
|
||||
db: Knex,
|
||||
{ key, matchValueIn, matchValueExists }: EntitiesSearchFilter,
|
||||
) {
|
||||
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
|
||||
// make a lot of sense. However, it had abysmal performance on sqlite
|
||||
// when datasets grew large, so we're using IN instead.
|
||||
const matchQuery = db<DbSearchRow>('search')
|
||||
.select('entity_id')
|
||||
.where(function keyFilter() {
|
||||
this.andWhere({ key: key.toLowerCase() });
|
||||
if (matchValueExists !== false && matchValueIn) {
|
||||
if (matchValueIn.length === 1) {
|
||||
this.andWhere({ value: matchValueIn[0].toLowerCase() });
|
||||
} else if (matchValueIn.length > 1) {
|
||||
this.andWhere(
|
||||
'value',
|
||||
'in',
|
||||
matchValueIn.map(v => v.toLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Explicitly evaluate matchValueExists as a boolean since it may be undefined
|
||||
queryBuilder.andWhere(
|
||||
'entity_id',
|
||||
matchValueExists === false ? 'not in' : 'in',
|
||||
matchQuery,
|
||||
);
|
||||
}
|
||||
|
||||
function isEntitiesSearchFilter(
|
||||
filter: EntitiesSearchFilter | EntityFilter,
|
||||
): filter is EntitiesSearchFilter {
|
||||
return filter.hasOwnProperty('key');
|
||||
}
|
||||
|
||||
function isAndEntityFilter(
|
||||
filter: { allOf: EntityFilter[] } | EntityFilter,
|
||||
): filter is { allOf: EntityFilter[] } {
|
||||
return filter.hasOwnProperty('allOf');
|
||||
}
|
||||
|
||||
function isOrEntityFilter(
|
||||
filter: { anyOf: EntityFilter[] } | EntityFilter,
|
||||
): filter is { anyOf: EntityFilter[] } {
|
||||
return filter.hasOwnProperty('anyOf');
|
||||
}
|
||||
|
||||
function parseFilter(
|
||||
filter: EntityFilter,
|
||||
query: Knex.QueryBuilder,
|
||||
db: Knex,
|
||||
): Knex.QueryBuilder {
|
||||
if (isEntitiesSearchFilter(filter)) {
|
||||
return query.where(function filterFunction() {
|
||||
addCondition(this, db, filter);
|
||||
});
|
||||
}
|
||||
|
||||
if (isOrEntityFilter(filter)) {
|
||||
let cumulativeQuery = query;
|
||||
for (const subFilter of filter.anyOf ?? []) {
|
||||
cumulativeQuery = cumulativeQuery.orWhere(subQuery =>
|
||||
parseFilter(subFilter, subQuery, db),
|
||||
);
|
||||
}
|
||||
return cumulativeQuery;
|
||||
}
|
||||
|
||||
if (isAndEntityFilter(filter)) {
|
||||
let cumulativeQuery = query;
|
||||
for (const subFilter of filter.allOf ?? []) {
|
||||
cumulativeQuery = cumulativeQuery.andWhere(subQuery =>
|
||||
parseFilter(subFilter, subQuery, db),
|
||||
);
|
||||
}
|
||||
return cumulativeQuery;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export class NextEntitiesCatalog implements EntitiesCatalog {
|
||||
constructor(private readonly database: Knex) {}
|
||||
|
||||
@@ -80,41 +166,8 @@ export class NextEntitiesCatalog implements EntitiesCatalog {
|
||||
const db = this.database;
|
||||
|
||||
let entitiesQuery = db<DbFinalEntitiesRow>('final_entities');
|
||||
|
||||
for (const singleFilter of request?.filter?.anyOf ?? []) {
|
||||
entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
|
||||
for (const {
|
||||
key,
|
||||
matchValueIn,
|
||||
matchValueExists,
|
||||
} of singleFilter.allOf) {
|
||||
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
|
||||
// make a lot of sense. However, it had abysmal performance on sqlite
|
||||
// when datasets grew large, so we're using IN instead.
|
||||
const matchQuery = db<DbSearchRow>('search')
|
||||
.select('entity_id')
|
||||
.where(function keyFilter() {
|
||||
this.andWhere({ key: key.toLowerCase() });
|
||||
if (matchValueExists !== false && matchValueIn) {
|
||||
if (matchValueIn.length === 1) {
|
||||
this.andWhere({ value: matchValueIn[0].toLowerCase() });
|
||||
} else if (matchValueIn.length > 1) {
|
||||
this.andWhere(
|
||||
'value',
|
||||
'in',
|
||||
matchValueIn.map(v => v.toLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Explicitly evaluate matchValueExists as a boolean since it may be undefined
|
||||
this.andWhere(
|
||||
'entity_id',
|
||||
matchValueExists === false ? 'not in' : 'in',
|
||||
matchQuery,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (request?.filter) {
|
||||
entitiesQuery = parseFilter(request.filter, entitiesQuery, db);
|
||||
}
|
||||
|
||||
// TODO: move final_entities to use entity_ref
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Context } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { LinkProps } from '@backstage/core-components';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PropsWithChildren } from 'react';
|
||||
@@ -736,6 +737,17 @@ export function getEntitySourceLocation(
|
||||
// @public
|
||||
export function isOwnerOf(owner: Entity, owned: Entity): boolean;
|
||||
|
||||
// @public
|
||||
export function loadCatalogOwnerRefs(
|
||||
catalogApi: CatalogApi,
|
||||
identityOwnerRefs: string[],
|
||||
): Promise<string[]>;
|
||||
|
||||
// @public
|
||||
export function loadIdentityOwnerRefs(
|
||||
identityApi: IdentityApi,
|
||||
): Promise<string[]>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MockEntityListContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -823,8 +835,6 @@ export function useEntityListProvider<
|
||||
EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
>(): EntityListContextProps<EntityFilters>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "useEntityOwnership" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function useEntityOwnership(): {
|
||||
loading: boolean;
|
||||
|
||||
@@ -37,4 +37,8 @@ export { useOwnUser } from './useOwnUser';
|
||||
export { useRelatedEntities } from './useRelatedEntities';
|
||||
export { useStarredEntities } from './useStarredEntities';
|
||||
export { useStarredEntity } from './useStarredEntity';
|
||||
export { useEntityOwnership } from './useEntityOwnership';
|
||||
export {
|
||||
loadCatalogOwnerRefs,
|
||||
useEntityOwnership,
|
||||
loadIdentityOwnerRefs,
|
||||
} from './useEntityOwnership';
|
||||
|
||||
@@ -49,9 +49,16 @@ function extendUserId(id: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Takes the relevant parts of the Backstage identity, and translates them into
|
||||
// a list of entity refs on string form that represent the user's ownership
|
||||
// connections.
|
||||
/**
|
||||
* Takes the relevant parts of the Backstage identity, and translates them into
|
||||
* a list of entity refs on string form that represent the user's ownership
|
||||
* connections.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @param identityApi - The IdentityApi implementation
|
||||
* @returns IdentityOwner refs as a string array
|
||||
*/
|
||||
export async function loadIdentityOwnerRefs(
|
||||
identityApi: IdentityApi,
|
||||
): Promise<string[]> {
|
||||
@@ -81,9 +88,17 @@ export async function loadIdentityOwnerRefs(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Takes the relevant parts of the User entity corresponding to the Backstage
|
||||
// identity, and translates them into a list of entity refs on string form that
|
||||
// represent the user's ownership connections.
|
||||
/**
|
||||
* Takes the relevant parts of the User entity corresponding to the Backstage
|
||||
* identity, and translates them into a list of entity refs on string form that
|
||||
* represent the user's ownership connections.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @param catalogApi - The Catalog API implementation
|
||||
* @param identityOwnerRefs - List of identity owner refs as strings
|
||||
* @returns OwnerRefs as a string array
|
||||
*/
|
||||
export async function loadCatalogOwnerRefs(
|
||||
catalogApi: CatalogApi,
|
||||
identityOwnerRefs: string[],
|
||||
@@ -113,6 +128,10 @@ export async function loadCatalogOwnerRefs(
|
||||
* owner of a given entity. When the hook is initially mounted, the loading
|
||||
* flag will be true and the results returned from the function will always be
|
||||
* false.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @returns a function that checks if the signed in user owns an entity
|
||||
*/
|
||||
export function useEntityOwnership(): {
|
||||
loading: boolean;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@material-ui/styles": "^4.11.0",
|
||||
"highlight.js": "^10.6.0",
|
||||
"luxon": "^2.0.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
|
||||
@@ -46,6 +46,8 @@ import { codeCoverageApiRef } from '../../api';
|
||||
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type Coverage = 'line' | 'branch';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
@@ -69,6 +71,13 @@ const getTrendIcon = (trend: number, classes: ClassNameMap) => {
|
||||
}
|
||||
};
|
||||
|
||||
// convert timestamp to human friendly form
|
||||
function formatDateToHuman(timeStamp: string | number) {
|
||||
return DateTime.fromMillis(Number(timeStamp)).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
);
|
||||
}
|
||||
|
||||
export const CoverageHistoryChart = () => {
|
||||
const { entity } = useEntity();
|
||||
const codeCoverageApi = useApi(codeCoverageApiRef);
|
||||
@@ -149,10 +158,10 @@ export const CoverageHistoryChart = () => {
|
||||
margin={{ right: 48, top: 32 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="timestamp" />
|
||||
<XAxis dataKey="timestamp" tickFormatter={formatDateToHuman} />
|
||||
<YAxis dataKey="line.percentage" />
|
||||
<YAxis dataKey="branch.percentage" />
|
||||
<Tooltip />
|
||||
<Tooltip labelFormatter={formatDateToHuman} />
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
|
||||
@@ -232,7 +232,7 @@ export const FileExplorer = () => {
|
||||
title: 'Coverage',
|
||||
type: 'numeric',
|
||||
field: 'coverage',
|
||||
render: (row: CoverageTableRow) => `${row.coverage}%`,
|
||||
render: (row: CoverageTableRow) => `${row.coverage.toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
title: 'Missing lines',
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.8",
|
||||
"@backstage/search-common": "^0.2.0",
|
||||
"@elastic/elasticsearch": "^7.13.0",
|
||||
"@elastic/elasticsearch": "7.13.0",
|
||||
"@acuris/aws-es-connection": "^2.2.0",
|
||||
"aws-sdk": "^2.948.0",
|
||||
"elastic-builder": "^2.16.0",
|
||||
|
||||
Reference in New Issue
Block a user