@@ -1,19 +1,23 @@
|
||||
# code-coverage
|
||||
# code-coverage-backend
|
||||
|
||||
This is the backend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend.
|
||||
This is the backend part of the `code-coverage` plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend.
|
||||
|
||||
## Configuring your entity
|
||||
|
||||
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: enabled
|
||||
metadata:
|
||||
annotations:
|
||||
backstage.io/code-coverage: enabled
|
||||
```
|
||||
|
||||
There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: scm-only
|
||||
metadata:
|
||||
annotations:
|
||||
backstage.io/code-coverage: scm-only
|
||||
```
|
||||
|
||||
Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed.
|
||||
@@ -31,7 +35,7 @@ Example:
|
||||
{
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:7000/api/code-coverage/Component/default/entity-name",
|
||||
"href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name",
|
||||
"rel": "coverage"
|
||||
}
|
||||
]
|
||||
@@ -49,7 +53,7 @@ Example:
|
||||
{
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:7000/api/code-coverage/Component/default/entity-name",
|
||||
"href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name",
|
||||
"rel": "coverage"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,14 +20,23 @@
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
// Note for the reader: the knex increments types automatically make it a
|
||||
// primary column, whether you like it or not. That's why the id column is
|
||||
// not marked as primary as one might have expected; it's only used for
|
||||
// lookups by ID. Because, SQLite and MySQL don't return RETURNING on
|
||||
// inserts ... so we want a manually generated key for lookups (an uuid),
|
||||
// and also an index for ordering guarantees :)
|
||||
await knex.schema.createTable('code_coverage', table => {
|
||||
table.comment('The table of code coverage');
|
||||
table
|
||||
.uuid('id')
|
||||
.primary()
|
||||
.bigIncrements('index')
|
||||
.notNullable()
|
||||
.comment('The ID of the code coverage');
|
||||
table.text('entity').notNullable().comment('entity string reference');
|
||||
.comment('An insert counter to ensure ordering');
|
||||
table.uuid('id').notNullable().comment('The ID of the code coverage');
|
||||
table
|
||||
.text('entity')
|
||||
.notNullable()
|
||||
.comment('The entity ref that this code coverage applies to');
|
||||
table
|
||||
.text('coverage')
|
||||
.notNullable()
|
||||
@@ -37,6 +46,9 @@ exports.up = async function up(knex) {
|
||||
.defaultTo(knex.fn.now())
|
||||
.notNullable()
|
||||
.comment('The timestamp when this entry was created');
|
||||
table.index('index', 'code_coverage_index_idx');
|
||||
table.index('id', 'code_coverage_id_idx');
|
||||
table.index('entity', 'code_coverage_entity_idx');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,5 +56,10 @@ exports.up = async function up(knex) {
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
await knex.schema.alterTable('code_coverage', table => {
|
||||
table.dropIndex([], 'code_coverage_index_idx');
|
||||
table.dropIndex([], 'code_coverage_id_idx');
|
||||
table.dropIndex([], 'code_coverage_entity_idx');
|
||||
});
|
||||
await knex.schema.dropTable('code_coverage');
|
||||
};
|
||||
|
||||
@@ -19,13 +19,12 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.5.4",
|
||||
"@backstage/catalog-client": "^0.3.6",
|
||||
"@backstage/catalog-model": "^0.7.2",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/backend-common": "^0.6.2",
|
||||
"@backstage/catalog-client": "^0.3.10",
|
||||
"@backstage/catalog-model": "^0.7.7",
|
||||
"@backstage/config": "^0.1.4",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.0",
|
||||
"@backstage/plugin-catalog-backend": "^0.6.5",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"express": "^4.17.1",
|
||||
@@ -37,7 +36,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.2",
|
||||
"@backstage/cli": "^0.6.8",
|
||||
"@types/express-xml-bodyparser": "^0.3.2",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.21.2",
|
||||
|
||||
@@ -15,5 +15,3 @@
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
|
||||
export * from './service/jsoncoverage-types';
|
||||
|
||||
@@ -13,17 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
getVoidLogger,
|
||||
SingleConnectionDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
CodeCoverageDatabase,
|
||||
CodeCoverageStore,
|
||||
} from './CodeCoverageDatabase';
|
||||
import { JsonCodeCoverage } from './jsoncoverage-types';
|
||||
import { JsonCodeCoverage } from './types';
|
||||
|
||||
const db = SingleConnectionDatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
@@ -103,10 +100,9 @@ let database: CodeCoverageStore;
|
||||
describe('CodeCoverageDatabase', () => {
|
||||
beforeAll(async () => {
|
||||
const client = await db.getClient();
|
||||
database = await CodeCoverageDatabase.create(client, getVoidLogger());
|
||||
database.insertCodeCoverage(coverage[0]);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
database.insertCodeCoverage(coverage[1]);
|
||||
database = await CodeCoverageDatabase.create(client);
|
||||
await database.insertCodeCoverage(coverage[0]);
|
||||
await database.insertCodeCoverage(coverage[1]);
|
||||
});
|
||||
|
||||
describe('insertCodeCoverage', () => {
|
||||
|
||||
@@ -18,15 +18,15 @@ import { NotFoundError } from '@backstage/errors';
|
||||
import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { aggregateCoverage } from './CoverageUtils';
|
||||
import { JsonCodeCoverage, JsonCoverageHistory } from './jsoncoverage-types';
|
||||
import { JsonCodeCoverage, JsonCoverageHistory } from './types';
|
||||
|
||||
export type RawDbCoverageRow = {
|
||||
id: string;
|
||||
entity: string;
|
||||
coverage: string;
|
||||
};
|
||||
|
||||
export interface CodeCoverageStore {
|
||||
insertCodeCoverage(
|
||||
coverage: JsonCodeCoverage,
|
||||
@@ -39,44 +39,49 @@ const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-code-coverage-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
export class CodeCoverageDatabase implements CodeCoverageStore {
|
||||
static async create(knex: Knex, logger: Logger): Promise<CodeCoverageStore> {
|
||||
static async create(knex: Knex): Promise<CodeCoverageStore> {
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
});
|
||||
return new CodeCoverageDatabase(knex, logger);
|
||||
return new CodeCoverageDatabase(knex);
|
||||
}
|
||||
|
||||
constructor(private readonly db: Knex, private readonly logger: Logger) {}
|
||||
constructor(private readonly db: Knex) {}
|
||||
|
||||
async insertCodeCoverage(
|
||||
coverage: JsonCodeCoverage,
|
||||
): Promise<{ codeCoverageId: string }> {
|
||||
const codeCoverageId = uuid();
|
||||
this.logger.error(JSON.stringify(coverage.entity));
|
||||
const entity = stringifyEntityRef({
|
||||
kind: coverage.entity.kind,
|
||||
namespace: coverage.entity.namespace,
|
||||
name: coverage.entity.name,
|
||||
});
|
||||
|
||||
await this.db<RawDbCoverageRow>('code_coverage').insert({
|
||||
id: codeCoverageId,
|
||||
entity: entity,
|
||||
coverage: JSON.stringify(coverage),
|
||||
});
|
||||
|
||||
return { codeCoverageId };
|
||||
}
|
||||
|
||||
async getCodeCoverage(entity: string): Promise<JsonCodeCoverage> {
|
||||
const [result] = await this.db<RawDbCoverageRow>('code_coverage')
|
||||
.where({ entity: entity })
|
||||
.orderBy('created_at', 'desc')
|
||||
.orderBy('index', 'desc')
|
||||
.limit(1)
|
||||
.select();
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundError(
|
||||
`No coverage for entity '${JSON.stringify(entity)}' found`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(result.coverage);
|
||||
} catch (error) {
|
||||
@@ -90,7 +95,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
|
||||
): Promise<JsonCoverageHistory> {
|
||||
const res = await this.db<RawDbCoverageRow>('code_coverage')
|
||||
.where({ entity: entity })
|
||||
.orderBy('created_at', 'desc')
|
||||
.orderBy('index', 'desc')
|
||||
.limit(limit)
|
||||
.select();
|
||||
|
||||
@@ -99,6 +104,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
|
||||
.map(c => aggregateCoverage(c));
|
||||
|
||||
const entityName = parseEntityName(entity);
|
||||
|
||||
return {
|
||||
entity: {
|
||||
name: entityName.name,
|
||||
|
||||
@@ -22,11 +22,7 @@ import {
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ScmIntegration, ScmIntegrations } from '@backstage/integration';
|
||||
import {
|
||||
AggregateCoverage,
|
||||
FileEntry,
|
||||
JsonCodeCoverage,
|
||||
} from './jsoncoverage-types';
|
||||
import { AggregateCoverage, FileEntry, JsonCodeCoverage } from './types';
|
||||
|
||||
export const calculatePercentage = (
|
||||
available: number,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { FileEntry } from '../jsoncoverage-types';
|
||||
import { FileEntry } from '../types';
|
||||
|
||||
export interface Converter {
|
||||
convert(xml: unknown, scmFiles: Array<string>): Array<FileEntry>;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BranchHit, FileEntry } from '../jsoncoverage-types';
|
||||
import { BranchHit, FileEntry } from '../types';
|
||||
import { CoberturaXML, InnerClass, LineHit } from './types';
|
||||
import { Logger } from 'winston';
|
||||
import { Converter } from './Converter';
|
||||
@@ -63,7 +63,7 @@ export class Cobertura implements Converter {
|
||||
});
|
||||
|
||||
const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename));
|
||||
this.logger.info(`matched ${packageAndFilename} to ${currentFile}`);
|
||||
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
|
||||
if (
|
||||
scmFiles.length === 0 ||
|
||||
(Object.keys(lineHits).length > 0 && currentFile)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BranchHit, FileEntry } from '../jsoncoverage-types';
|
||||
import { BranchHit, FileEntry } from '../types';
|
||||
import { JacocoSourceFile, JacocoXML } from './types';
|
||||
import { Logger } from 'winston';
|
||||
import { Converter } from './Converter';
|
||||
@@ -35,7 +35,7 @@ export class Jacoco implements Converter {
|
||||
* Converts jacoco into shared json coverage format
|
||||
*
|
||||
* @param xml jacoco xml object
|
||||
* @param scmFiles list of files that are commited to SCM
|
||||
* @param scmFiles list of files that are committed to SCM
|
||||
*/
|
||||
convert(xml: JacocoXML, scmFiles: Array<string>): Array<FileEntry> {
|
||||
const jscov: Array<FileEntry> = [];
|
||||
@@ -64,7 +64,7 @@ export class Jacoco implements Converter {
|
||||
|
||||
const packageAndFilename = `${packageName}/${fileName}`;
|
||||
const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename));
|
||||
this.logger.info(`matched ${packageAndFilename} to ${currentFile}`);
|
||||
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
|
||||
if (Object.keys(lineHits).length > 0 && currentFile) {
|
||||
jscov.push({
|
||||
filename: currentFile,
|
||||
|
||||
@@ -58,7 +58,6 @@ export const makeRouter = async (
|
||||
|
||||
const codeCoverageDatabase = await CodeCoverageDatabase.create(
|
||||
await database.getClient(),
|
||||
logger,
|
||||
);
|
||||
const codecovUrl = await discovery.getExternalBaseUrl('code-coverage');
|
||||
const catalogApi = new CatalogClient({ discoveryApi: discovery });
|
||||
@@ -147,8 +146,8 @@ export const makeRouter = async (
|
||||
const scmTree = await urlReader.readTree(sourceLocation.target);
|
||||
const scmFile = (await scmTree.files()).find(f => f.path === path);
|
||||
if (!scmFile) {
|
||||
res.status(400).json({
|
||||
message: "couldn't find file in SCM",
|
||||
res.status(404).json({
|
||||
message: "Couldn't find file in SCM",
|
||||
file: path,
|
||||
scm: vcs.title,
|
||||
});
|
||||
@@ -157,7 +156,7 @@ export const makeRouter = async (
|
||||
const content = await scmFile?.content();
|
||||
if (!content) {
|
||||
res.status(400).json({
|
||||
message: "couldn't process content of file in SCM",
|
||||
message: "Couldn't process content of file in SCM",
|
||||
file: path,
|
||||
scm: vcs.title,
|
||||
});
|
||||
@@ -187,7 +186,7 @@ export const makeRouter = async (
|
||||
} else if (coverageType === 'cobertura') {
|
||||
converter = new Cobertura(logger);
|
||||
} else {
|
||||
throw new NotFoundError(`unsupported coverage type '${coverageType}`);
|
||||
throw new InputError(`Unsupported coverage type '${coverageType}`);
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import knexFactory from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import { DatabaseManager } from '@backstage/plugin-catalog-backend';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
@@ -38,13 +38,23 @@ export async function startStandaloneServer(
|
||||
const logger = options.logger.child({ service: 'code-coverage-backend' });
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
|
||||
const db = useHotMemoize(module, () =>
|
||||
DatabaseManager.createInMemoryDatabaseConnection(),
|
||||
);
|
||||
const db = useHotMemoize(module, () => {
|
||||
const knex = knexFactory({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
return knex;
|
||||
});
|
||||
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
database: { getClient: () => db },
|
||||
database: { getClient: async () => db },
|
||||
config,
|
||||
discovery: SingleHostDiscovery.fromConfig(config),
|
||||
urlReader: UrlReaders.default({ logger, config }),
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
# code-coverage
|
||||
|
||||
This is the frontend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend.
|
||||
This is the frontend part of the code-coverage plugin. It displays code coverage summaries for your entities.
|
||||
|
||||
## Configuring your entity
|
||||
|
||||
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation.
|
||||
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation on entities for which coverage ingestion has been enabled.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: enabled
|
||||
metadata:
|
||||
annotations:
|
||||
backstage.io/code-coverage: enabled
|
||||
```
|
||||
|
||||
There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: scm-only
|
||||
metadata:
|
||||
annotations:
|
||||
backstage.io/code-coverage: scm-only
|
||||
```
|
||||
|
||||
Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed.
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.2",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/catalog-model": "^0.7.7",
|
||||
"@backstage/config": "^0.1.4",
|
||||
"@backstage/core": "^0.7.5",
|
||||
"@backstage/core-api": "^0.2.12",
|
||||
"@backstage/errors": "^0.1.0",
|
||||
"@backstage/plugin-catalog-react": "^0.1.0",
|
||||
"@backstage/plugin-code-coverage-backend": "^0.1.0",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/plugin-catalog-react": "^0.1.4",
|
||||
"@backstage/theme": "^0.2.5",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -14,27 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef, DiscoveryApi } from '@backstage/core';
|
||||
import { EntityName, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
JsonCodeCoverage,
|
||||
JsonCoverageHistory,
|
||||
} from '@backstage/plugin-code-coverage-backend';
|
||||
import { createApiRef, DiscoveryApi } from '@backstage/core';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
export class FetchError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
static async forResponse(resp: Response): Promise<FetchError> {
|
||||
return new FetchError(
|
||||
`Request failed with status code ${
|
||||
resp.status
|
||||
}.\nReason: ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
import { JsonCodeCoverage, JsonCoverageHistory } from './types';
|
||||
|
||||
export type CodeCoverageApi = {
|
||||
discovery: DiscoveryApi;
|
||||
@@ -79,7 +62,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi {
|
||||
async getCoverageForEntity(
|
||||
entityName: EntityName,
|
||||
): Promise<JsonCodeCoverage> {
|
||||
const entity = encodeURI(stringifyEntityRef(entityName));
|
||||
const entity = encodeURIComponent(stringifyEntityRef(entityName));
|
||||
return (await this.fetch<JsonCodeCoverage>(
|
||||
`/report?entity=${entity}`,
|
||||
)) as JsonCodeCoverage;
|
||||
@@ -89,7 +72,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi {
|
||||
entityName: EntityName,
|
||||
filePath: string,
|
||||
): Promise<string> {
|
||||
const entity = encodeURI(stringifyEntityRef(entityName));
|
||||
const entity = encodeURIComponent(stringifyEntityRef(entityName));
|
||||
return await this.fetch<string>(
|
||||
`/file-content?entity=${entity}&path=${encodeURI(filePath)}`,
|
||||
);
|
||||
@@ -99,11 +82,11 @@ export class CodeCoverageRestApi implements CodeCoverageApi {
|
||||
entityName: EntityName,
|
||||
limit?: number,
|
||||
): Promise<JsonCoverageHistory> {
|
||||
const entity = encodeURI(stringifyEntityRef(entityName));
|
||||
const entity = encodeURIComponent(stringifyEntityRef(entityName));
|
||||
const hasValidLimit = limit && limit > 0;
|
||||
return (await this.fetch<JsonCoverageHistory>(
|
||||
`/history?entity=${entity}${
|
||||
hasValidLimit ? `&limit=${encodeURI(`${limit}`)}` : ''
|
||||
hasValidLimit ? `&limit=${encodeURIComponent(String(limit))}` : ''
|
||||
}`,
|
||||
)) as JsonCoverageHistory;
|
||||
}
|
||||
|
||||
+15
-16
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Progress, ResponseErrorPanel, useApi } from '@backstage/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -23,27 +25,24 @@ import {
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import TrendingDownIcon from '@material-ui/icons/TrendingDown';
|
||||
import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
|
||||
import TrendingUpIcon from '@material-ui/icons/TrendingUp';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { ClassNameMap } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
LineChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
CartesianGrid,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useApi } from '@backstage/core-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import { Progress, ResponseErrorPanel } from '@backstage/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import TrendingDownIcon from '@material-ui/icons/TrendingDown';
|
||||
import TrendingUpIcon from '@material-ui/icons/TrendingUp';
|
||||
import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { ClassNameMap } from '@material-ui/styles';
|
||||
|
||||
type Coverage = 'line' | 'branch';
|
||||
|
||||
|
||||
+3
-5
@@ -13,9 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
// styles
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -64,7 +63,8 @@ type CodeRowProps = {
|
||||
lineContent: string;
|
||||
lineHits?: number | null;
|
||||
};
|
||||
const CodeRow = ({
|
||||
|
||||
export const CodeRow = ({
|
||||
lineNumber,
|
||||
lineContent,
|
||||
lineHits = null,
|
||||
@@ -98,5 +98,3 @@ const CodeRow = ({
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRow;
|
||||
@@ -14,17 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useApi } from '@backstage/core-api';
|
||||
import { Progress, ResponseErrorPanel, useApi } from '@backstage/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { makeStyles, Paper } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import { Progress, ResponseErrorPanel } from '@backstage/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { makeStyles, Paper } from '@material-ui/core';
|
||||
import { FileEntry } from '../../types';
|
||||
import { CodeRow } from './CodeRow';
|
||||
import { highlightLines } from './Highlighter';
|
||||
import CoverageRow from './CoverageRow';
|
||||
import { FileEntry } from '@backstage/plugin-code-coverage-backend';
|
||||
|
||||
type Props = {
|
||||
filename: string;
|
||||
@@ -61,7 +60,7 @@ const FormattedLines = ({
|
||||
{highlightedLines.map((lineContent, idx) => {
|
||||
const line = idx + 1;
|
||||
return (
|
||||
<CoverageRow
|
||||
<CodeRow
|
||||
key={line}
|
||||
lineNumber={line}
|
||||
lineContent={lineContent}
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useApi } from '@backstage/core-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
Table,
|
||||
TableColumn,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -30,13 +30,13 @@ import {
|
||||
Modal,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import DescriptionIcon from '@material-ui/icons/Description';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import DescriptionIcon from '@material-ui/icons/Description';
|
||||
import { FileEntry } from '../../types';
|
||||
import { FileContent } from './FileContent';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { FileEntry } from '@backstage/plugin-code-coverage-backend';
|
||||
|
||||
type FileStructureObject = Record<string, any>;
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2021 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 { EntityName } from '@backstage/catalog-model';
|
||||
|
||||
export type JsonCodeCoverage = {
|
||||
metadata: CoverageMetadata;
|
||||
entity: EntityName;
|
||||
files: Array<FileEntry>;
|
||||
};
|
||||
|
||||
export type JsonCoverageHistory = {
|
||||
entity: EntityName;
|
||||
history: Array<AggregateCoverage>;
|
||||
};
|
||||
|
||||
export type CoverageHistory = {
|
||||
line: {
|
||||
available: number;
|
||||
covered: number;
|
||||
};
|
||||
branch: BranchHit;
|
||||
};
|
||||
|
||||
export type CoverageMetadata = {
|
||||
vcs: {
|
||||
type: string;
|
||||
location: string;
|
||||
};
|
||||
generationTime: number;
|
||||
};
|
||||
|
||||
export type BranchHit = {
|
||||
covered: number;
|
||||
missed: number;
|
||||
available: number;
|
||||
};
|
||||
|
||||
export type FileEntry = {
|
||||
filename: string;
|
||||
lineHits: Record<number, number>;
|
||||
branchHits: Record<number, BranchHit>;
|
||||
};
|
||||
|
||||
export type AggregateCoverage = {
|
||||
timestamp: number;
|
||||
line: {
|
||||
available: number;
|
||||
covered: number;
|
||||
missed: number;
|
||||
percentage: number;
|
||||
};
|
||||
branch: {
|
||||
available: number;
|
||||
covered: number;
|
||||
missed: number;
|
||||
percentage: number;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user