Added linguist frontend, backend, and common plugins
Signed-off-by: Andre Wanlin <andrewanlin@gmail.com> Removed refresh and permissions Signed-off-by: Andre Wanlin <andrewanlin@gmail.com> Small database refactor and improved naming Signed-off-by: Andre Wanlin <andrewanlin@gmail.com> Refactor stringifyEntityRef usage Signed-off-by: Andre Wanlin <andrewanlin@gmail.com> Fixed up yarn.lock Signed-off-by: Andre Wanlin <andrewanlin@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-linguist': minor
|
||||
'@backstage/plugin-linguist-backend': minor
|
||||
'@backstage/plugin-linguist-common': minor
|
||||
---
|
||||
|
||||
Introduced the Linguist plugin, checkout the plugin's `README.md` for more details!
|
||||
@@ -42,6 +42,8 @@
|
||||
"@backstage/plugin-kubernetes": "workspace:^",
|
||||
"@backstage/plugin-lighthouse": "workspace:^",
|
||||
"@backstage/plugin-microsoft-calendar": "workspace:^",
|
||||
"@backstage/plugin-linguist": "workspace:^",
|
||||
"@backstage/plugin-linguist-common": "workspace:^",
|
||||
"@backstage/plugin-newrelic": "workspace:^",
|
||||
"@backstage/plugin-newrelic-dashboard": "workspace:^",
|
||||
"@backstage/plugin-org": "workspace:^",
|
||||
|
||||
@@ -157,6 +157,10 @@ import {
|
||||
LightBox,
|
||||
} from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
import { EntityCostInsightsContent } from '@backstage/plugin-cost-insights';
|
||||
import {
|
||||
isLinguistAvailable,
|
||||
EntityLinguistCard,
|
||||
} from '@backstage/plugin-linguist';
|
||||
|
||||
const customEntityFilterKind = ['Component', 'API', 'System'];
|
||||
|
||||
@@ -406,6 +410,14 @@ const overviewContent = (
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isLinguistAvailable}>
|
||||
<Grid item md={6}>
|
||||
<EntityLinguistCard />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
|
||||
<Grid item md={8} xs={12}>
|
||||
<EntityHasSubcomponentsCard variant="gridItem" />
|
||||
</Grid>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"@backstage/plugin-kafka-backend": "workspace:^",
|
||||
"@backstage/plugin-kubernetes-backend": "workspace:^",
|
||||
"@backstage/plugin-lighthouse-backend": "workspace:^",
|
||||
"@backstage/plugin-linguist-backend": "workspace:^",
|
||||
"@backstage/plugin-permission-backend": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/plugin-permission-node": "workspace:^",
|
||||
|
||||
@@ -63,6 +63,7 @@ import permission from './plugins/permission';
|
||||
import playlist from './plugins/playlist';
|
||||
import adr from './plugins/adr';
|
||||
import lighthouse from './plugins/lighthouse';
|
||||
import linguist from './plugins/linguist';
|
||||
import { PluginEnvironment } from './types';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
@@ -154,6 +155,7 @@ async function main() {
|
||||
const eventBasedEntityProviders = await catalogEventBasedProviders(
|
||||
catalogEnv,
|
||||
);
|
||||
const linguistEnv = useHotMemoize(module, () => createEnv('linguist'));
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(
|
||||
@@ -180,6 +182,7 @@ async function main() {
|
||||
apiRouter.use('/playlist', await playlist(playlistEnv));
|
||||
apiRouter.use('/explore', await explore(exploreEnv));
|
||||
apiRouter.use('/adr', await adr(adrEnv));
|
||||
apiRouter.use('/linguist', await linguist(linguistEnv));
|
||||
apiRouter.use(notFoundHandler());
|
||||
|
||||
await lighthouse(lighthouseEnv);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2022 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 { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { createRouter } from '@backstage/plugin-linguist-backend';
|
||||
import { Router } from 'express';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const schedule: TaskScheduleDefinition = {
|
||||
frequency: { minutes: 2 },
|
||||
timeout: { minutes: 15 },
|
||||
initialDelay: { seconds: 15 },
|
||||
};
|
||||
|
||||
return createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
reader: env.reader,
|
||||
discovery: env.discovery,
|
||||
database: env.database,
|
||||
scheduler: env.scheduler,
|
||||
schedule: schedule,
|
||||
});
|
||||
}
|
||||
@@ -28,6 +28,8 @@ metadata:
|
||||
- url: https://example.com/alert
|
||||
title: Alerts
|
||||
icon: alert
|
||||
annotations:
|
||||
backstage.io/linguist: 'https://github.com/backstage/backstage/tree/master/plugins/playlist'
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,94 @@
|
||||
# Linguist Backend
|
||||
|
||||
Welcome to the Linguist backend plugin! This plugin provides data for the Linguist frontend features.
|
||||
|
||||
## Setup
|
||||
|
||||
The following sections will help you get the Linguist Backend plugin setup and running.
|
||||
|
||||
### Up and Running
|
||||
|
||||
Here's how to get the backend up and running:
|
||||
|
||||
1. First we need to add the `@backstage/plugin-linguist-backend` package to your backend:
|
||||
|
||||
```sh
|
||||
# From the Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-linguist-backend
|
||||
```
|
||||
|
||||
2. Then we will create a new file named `packages/backend/src/plugins/linguist.ts`, and add the
|
||||
following to it:
|
||||
|
||||
```ts
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { createRouter } from '@backstage/plugin-linguist-backend';
|
||||
import { Router } from 'express';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const schedule: TaskScheduleDefinition = {
|
||||
frequency: { hours: 24 },
|
||||
timeout: { minutes: 30 },
|
||||
initialDelay: { seconds: 90 },
|
||||
};
|
||||
|
||||
return createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
reader: env.reader,
|
||||
discovery: env.discovery,
|
||||
database: env.database,
|
||||
scheduler: env.scheduler,
|
||||
schedule: schedule,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
import linguist from './plugins/linguist';
|
||||
// ...
|
||||
async function main() {
|
||||
// ...
|
||||
// Add this line under the other lines that follow the useHotMemoize pattern
|
||||
const linguistEnv = useHotMemoize(module, () => createEnv('linguist'));
|
||||
// ...
|
||||
// Insert this line under the other lines that add their routers to apiRouter in the same way
|
||||
apiRouter.use('/linguist', await linguist(linguistEnv));
|
||||
```
|
||||
|
||||
4. Now run `yarn start-backend` from the repo root
|
||||
5. Finally open `http://localhost:7007/api/linguist/health` in a browser and it should return `{"status":"ok"}`
|
||||
|
||||
## Scheduling
|
||||
|
||||
The Linguist backend can be configured to generate the language breakdown for all the entities with the linguist annotation. This is done by passing a `TaskScheduleDefinition` to the `createRouter` function like in Step 2 of the [Up and Running](#up-and-running) documentation.
|
||||
|
||||
Here's another example of the `TaskScheduleDefinition` that will run the language breakdown processing every 12 hours:
|
||||
|
||||
```ts
|
||||
const schedule: TaskScheduleDefinition = {
|
||||
frequency: { hours: 12 },
|
||||
timeout: { minutes: 15 },
|
||||
initialDelay: { seconds: 10 },
|
||||
};
|
||||
```
|
||||
|
||||
The default setup will only generate the language breakdown for entities with the linguist annotation that have not been generated yet. If you want this process to also refresh the data you can do so by adding the following configuration to your `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
linguist:
|
||||
age: 15 # Days
|
||||
```
|
||||
|
||||
With the configuration setup like this if the language breakdown is older than 15 days it will get regenerated. It's recommended that if you choose to use this configuration to set it to a large value - 30, 90, or 180 - as this data generally does not change drastically.
|
||||
|
||||
## Links
|
||||
|
||||
- [Frontend part of the plugin](../linguist/README.md)
|
||||
- [The Backstage homepage](https://backstage.io)
|
||||
@@ -0,0 +1,86 @@
|
||||
## API Report File for "@backstage/plugin-linguist-backend"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Config } from '@backstage/config';
|
||||
import { EntitiesOverview } from '@backstage/plugin-linguist-common';
|
||||
import { EntityResults } from '@backstage/plugin-linguist-common';
|
||||
import express from 'express';
|
||||
import { Knex } from 'knex';
|
||||
import { Languages } from '@backstage/plugin-linguist-common';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { ProcessedEntity } from '@backstage/plugin-linguist-common';
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class LinguistBackendApi {
|
||||
constructor(
|
||||
config: Config,
|
||||
logger: Logger,
|
||||
store: LinguistBackendStore,
|
||||
urlReader: UrlReader,
|
||||
discovery: PluginEndpointDiscovery,
|
||||
);
|
||||
// (undocumented)
|
||||
getEntitiesOverview(): Promise<EntitiesOverview>;
|
||||
// (undocumented)
|
||||
getEntityLanguages(entityRef: string): Promise<Languages>;
|
||||
// (undocumented)
|
||||
processEntities(): Promise<void>;
|
||||
// (undocumented)
|
||||
processEntity(entityRef: string, url: string): Promise<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class LinguistBackendDatabase implements LinguistBackendStore {
|
||||
constructor(db: Knex);
|
||||
// (undocumented)
|
||||
static create(knex: Knex): Promise<LinguistBackendStore>;
|
||||
// (undocumented)
|
||||
getEntityResults(entityRef: string): Promise<Languages>;
|
||||
// (undocumented)
|
||||
getProcessedEntities(): Promise<ProcessedEntity[]>;
|
||||
// (undocumented)
|
||||
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface LinguistBackendStore {
|
||||
// (undocumented)
|
||||
getEntityResults(entityRef: string): Promise<Languages>;
|
||||
// (undocumented)
|
||||
getProcessedEntities(): Promise<ProcessedEntity[]>;
|
||||
// (undocumented)
|
||||
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
database: PluginDatabaseManager;
|
||||
// (undocumented)
|
||||
discovery: PluginEndpointDiscovery;
|
||||
// (undocumented)
|
||||
linguistBackendApi?: LinguistBackendApi;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
// (undocumented)
|
||||
reader: UrlReader;
|
||||
// (undocumented)
|
||||
schedule?: TaskScheduleDefinition;
|
||||
// (undocumented)
|
||||
scheduler?: PluginTaskScheduler;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 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 interface Config {
|
||||
/** Configuration options for the linguist-backend plugin */
|
||||
linguist?: {
|
||||
/**
|
||||
* The age of an entity's languages before a refresh occurs,
|
||||
* an age of 30 would mean the languages would be regenerated after 30 days.
|
||||
* The default is 0 meaning languages won't be refreshed once generated.
|
||||
*/
|
||||
age: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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('entity_result', table => {
|
||||
table.comment('Table containing results from running Linguist');
|
||||
table
|
||||
.bigIncrements('index')
|
||||
.notNullable()
|
||||
.comment('An insert counter to ensure ordering');
|
||||
table.uuid('id').notNullable().comment('The ID of the Linguist result');
|
||||
table
|
||||
.text('entity_ref')
|
||||
.unique()
|
||||
.notNullable()
|
||||
.comment('The entity ref that this Linguist result applies to');
|
||||
table
|
||||
.text('languages')
|
||||
.notNullable()
|
||||
.comment('The results json as a string');
|
||||
table
|
||||
.dateTime('created_at')
|
||||
.defaultTo(knex.fn.now())
|
||||
.notNullable()
|
||||
.comment('The timestamp when this entry was created');
|
||||
table
|
||||
.dateTime('processed_date')
|
||||
.defaultTo(knex.fn.now())
|
||||
.notNullable()
|
||||
.comment('The timestamp when this entity was processed');
|
||||
table.index('index', 'entity_result_index_idx');
|
||||
table.index('entity_ref', 'entity_result_entity_ref_idx');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
await knex.schema.alterTable('entity_result', table => {
|
||||
table.dropIndex([], 'entity_result_index_idx');
|
||||
table.dropIndex([], 'entity_result_entity_ref_idx');
|
||||
});
|
||||
await knex.schema.dropTable('entity_result');
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@backstage/plugin-linguist-backend",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-tasks": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-linguist-common": "workspace:^",
|
||||
"@types/express": "*",
|
||||
"express": "^4.18.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"knex": "^2.0.0",
|
||||
"linguist-js": "^2.5.3",
|
||||
"luxon": "^2.0.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.49.0",
|
||||
"supertest": "^6.2.4"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts",
|
||||
"migrations/**/*.{js,d.ts}"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
EntitiesOverview,
|
||||
EntityResults,
|
||||
Language,
|
||||
Languages,
|
||||
} from '@backstage/plugin-linguist-common';
|
||||
import {
|
||||
CATALOG_FILTER_EXISTS,
|
||||
CatalogClient,
|
||||
GetEntitiesRequest,
|
||||
} from '@backstage/catalog-client';
|
||||
import { PluginEndpointDiscovery, UrlReader } from '@backstage/backend-common';
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { DateTime } from 'luxon';
|
||||
import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common';
|
||||
import { LinguistBackendStore } from '../db';
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs-extra';
|
||||
import linguist from 'linguist-js';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { assertError } from '@backstage/errors';
|
||||
|
||||
/** @public */
|
||||
export class LinguistBackendApi {
|
||||
public constructor(
|
||||
private readonly config: Config,
|
||||
private readonly logger: Logger,
|
||||
private readonly store: LinguistBackendStore,
|
||||
private readonly urlReader: UrlReader,
|
||||
private readonly discovery: PluginEndpointDiscovery,
|
||||
) {}
|
||||
|
||||
public async getEntityLanguages(entityRef: string): Promise<Languages> {
|
||||
this.logger?.debug(`Getting languages for entity "${entityRef}"`);
|
||||
|
||||
return this.store.getEntityResults(entityRef);
|
||||
}
|
||||
|
||||
public async processEntity(entityRef: string, url: string): Promise<string> {
|
||||
this.logger?.info(
|
||||
`Processing languages for entity ${entityRef} from ${url}`,
|
||||
);
|
||||
|
||||
const readTreeResponse = await this.urlReader.readTree(url);
|
||||
const dir = await readTreeResponse.dir();
|
||||
|
||||
const results = await linguist(dir);
|
||||
|
||||
try {
|
||||
const totalBytes = results.languages.bytes;
|
||||
const langResults = results.languages.results;
|
||||
|
||||
const breakdown: Language[] = [];
|
||||
for (const key in langResults) {
|
||||
if (Object.prototype.hasOwnProperty.call(langResults, key)) {
|
||||
const lang: Language = {
|
||||
name: key,
|
||||
percentage: +((langResults[key].bytes / totalBytes) * 100).toFixed(
|
||||
2,
|
||||
),
|
||||
bytes: langResults[key].bytes,
|
||||
type: langResults[key].type,
|
||||
color: langResults[key].color,
|
||||
};
|
||||
breakdown.push(lang);
|
||||
}
|
||||
}
|
||||
|
||||
const languages: Languages = {
|
||||
languageCount: results.languages.count,
|
||||
totalBytes: totalBytes,
|
||||
processedDate: new Date().toISOString(),
|
||||
breakdown: breakdown,
|
||||
};
|
||||
|
||||
const entityResults: EntityResults = {
|
||||
entityRef: entityRef,
|
||||
results: languages,
|
||||
};
|
||||
|
||||
return await this.store.insertEntityResults(entityResults);
|
||||
} finally {
|
||||
this.logger?.info(`Cleaning up files from ${dir}`);
|
||||
await fs.remove(dir);
|
||||
}
|
||||
}
|
||||
|
||||
public async processEntities() {
|
||||
this.logger?.info('Processing applicable entities through Linguist');
|
||||
|
||||
const eo = await this.getEntitiesOverview();
|
||||
this.logger?.info(
|
||||
`Entities overview: Entity: ${eo.entityCount}, Processed: ${eo.processedCount}, Pending: ${eo.pendingCount}, Stale ${eo.staleCount}`,
|
||||
);
|
||||
|
||||
const entities = eo.filteredEntities;
|
||||
for (const key in entities) {
|
||||
if (Object.prototype.hasOwnProperty.call(entities, key)) {
|
||||
const entityRef = stringifyEntityRef(entities[key]);
|
||||
|
||||
const url =
|
||||
entities[key].metadata.annotations?.[LINGUIST_ANNOTATION] ?? '';
|
||||
|
||||
try {
|
||||
await this.processEntity(entityRef, url);
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
this.logger.error(
|
||||
`Unable to process "${entityRef}" using "${url}", ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getEntitiesOverview(): Promise<EntitiesOverview> {
|
||||
this.logger?.debug('Getting pending entities');
|
||||
const age = this.config.getOptionalNumber('linguist.age') ?? 0;
|
||||
const catalogApi = new CatalogClient({ discoveryApi: this.discovery });
|
||||
const request: GetEntitiesRequest = {
|
||||
filter: {
|
||||
kind: ['API', 'Component', 'Template'],
|
||||
[`metadata.annotations.${LINGUIST_ANNOTATION}`]: CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
fields: ['kind', 'metadata'],
|
||||
};
|
||||
|
||||
const response = await catalogApi.getEntities(request);
|
||||
const entities = response.items;
|
||||
|
||||
const processedEntities = await this.store.getProcessedEntities();
|
||||
|
||||
const staleEntities = processedEntities.filter(pe => {
|
||||
if (age === 0) return false;
|
||||
const staleDate = DateTime.now().minus({ days: age });
|
||||
return DateTime.fromJSDate(pe.processed_date) >= staleDate;
|
||||
});
|
||||
|
||||
const filteredEntities = entities.filter(en => {
|
||||
return processedEntities.every(pe => {
|
||||
const entityRef = stringifyEntityRef(en);
|
||||
return pe.entityRef !== entityRef;
|
||||
});
|
||||
});
|
||||
|
||||
const entitiesOverview: EntitiesOverview = {
|
||||
entityCount: entities.length,
|
||||
processedCount: processedEntities.length,
|
||||
staleCount: staleEntities.length,
|
||||
pendingCount: filteredEntities.length,
|
||||
filteredEntities: filteredEntities,
|
||||
};
|
||||
|
||||
return entitiesOverview;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 { LinguistBackendApi } from './LinguistBackendApi';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2022 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 { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
EntityResults,
|
||||
Languages,
|
||||
ProcessedEntity,
|
||||
} from '@backstage/plugin-linguist-common';
|
||||
|
||||
export type RawDbEntityResultRow = {
|
||||
id: string;
|
||||
entity_ref: string;
|
||||
languages: string;
|
||||
processed_date: Date;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface LinguistBackendStore {
|
||||
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
|
||||
getEntityResults(entityRef: string): Promise<Languages>;
|
||||
getProcessedEntities(): Promise<ProcessedEntity[]>;
|
||||
}
|
||||
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-linguist-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export class LinguistBackendDatabase implements LinguistBackendStore {
|
||||
static async create(knex: Knex): Promise<LinguistBackendStore> {
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
});
|
||||
return new LinguistBackendDatabase(knex);
|
||||
}
|
||||
|
||||
constructor(private readonly db: Knex) {}
|
||||
|
||||
async insertEntityResults(entityLanguages: EntityResults): Promise<string> {
|
||||
const entityLanguageId = uuid();
|
||||
const entityRef = entityLanguages.entityRef;
|
||||
|
||||
const [result] = await this.db<RawDbEntityResultRow>('entity_result')
|
||||
.insert({
|
||||
id: entityLanguageId,
|
||||
entity_ref: entityRef,
|
||||
languages: JSON.stringify(entityLanguages.results),
|
||||
processed_date: new Date(entityLanguages.results.processedDate),
|
||||
})
|
||||
.onConflict('entity_ref')
|
||||
.merge(['languages', 'processed_date'])
|
||||
.returning('id');
|
||||
|
||||
return result.id;
|
||||
}
|
||||
|
||||
async getEntityResults(entityRef: string): Promise<Languages> {
|
||||
const entityResults = await this.db<RawDbEntityResultRow>('entity_result')
|
||||
.where({ entity_ref: entityRef })
|
||||
.first();
|
||||
|
||||
if (!entityResults) {
|
||||
const emptyResults: Languages = {
|
||||
languageCount: 0,
|
||||
totalBytes: 0,
|
||||
processedDate: 'undefined',
|
||||
breakdown: [],
|
||||
};
|
||||
return emptyResults;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(entityResults.languages);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse languages for '${entityRef}', ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getProcessedEntities(): Promise<ProcessedEntity[]> {
|
||||
const entityResults = await this.db<ProcessedEntity>(
|
||||
'entity_result',
|
||||
).select('entity_ref', 'processed_date');
|
||||
|
||||
return entityResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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 { LinguistBackendDatabase } from './LinguistBackendDatabase';
|
||||
export type { LinguistBackendStore } from './LinguistBackendDatabase';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2022 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 './service/router';
|
||||
export { LinguistBackendApi } from './api';
|
||||
export { LinguistBackendDatabase } from './db';
|
||||
export type { LinguistBackendStore } from './db';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2022 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 { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
DatabaseManager,
|
||||
getVoidLogger,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
UrlReaders,
|
||||
} from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createRouter } from './router';
|
||||
import { LinguistBackendApi } from '../api';
|
||||
|
||||
function createDatabase(): PluginDatabaseManager {
|
||||
return DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'better-sqlite3',
|
||||
connection: ':memory:',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).forPlugin('code-coverage');
|
||||
}
|
||||
|
||||
const testDiscovery: jest.Mocked<PluginEndpointDiscovery> = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('http://localhost:7007/api/code-coverage'),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUrlReader = UrlReaders.default({
|
||||
logger: getVoidLogger(),
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
describe('createRouter', () => {
|
||||
let linguistBackendApi: jest.Mocked<LinguistBackendApi>;
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
linguistBackendApi,
|
||||
config: new ConfigReader({}),
|
||||
discovery: testDiscovery,
|
||||
database: createDatabase(),
|
||||
reader: mockUrlReader,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
errorHandler,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { LinguistBackendApi } from '../api';
|
||||
import { LinguistBackendDatabase } from '../db';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
|
||||
/** @public */
|
||||
export interface RouterOptions {
|
||||
linguistBackendApi?: LinguistBackendApi;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
reader: UrlReader;
|
||||
database: PluginDatabaseManager;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
schedule?: TaskScheduleDefinition;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { config, logger, reader, database, discovery, scheduler, schedule } =
|
||||
options;
|
||||
|
||||
const linguistBackendStore = await LinguistBackendDatabase.create(
|
||||
await database.getClient(),
|
||||
);
|
||||
|
||||
const linguistBackendApi =
|
||||
options.linguistBackendApi ||
|
||||
new LinguistBackendApi(
|
||||
config,
|
||||
logger,
|
||||
linguistBackendStore,
|
||||
reader,
|
||||
discovery,
|
||||
);
|
||||
|
||||
if (scheduler && schedule) {
|
||||
logger.info(
|
||||
`Scheduling processing of entities with: ${JSON.stringify(schedule)}`,
|
||||
);
|
||||
await scheduler.scheduleTask({
|
||||
id: 'linguist_process_entities',
|
||||
frequency: schedule.frequency,
|
||||
timeout: schedule.timeout,
|
||||
initialDelay: schedule.initialDelay,
|
||||
scope: schedule.scope,
|
||||
fn: async () => {
|
||||
await linguistBackendApi.processEntities();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
response.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
/**
|
||||
* /entity-languages?entity=component:default/my-component
|
||||
*/
|
||||
router.get('/entity-languages', async (req, res) => {
|
||||
const { entityRef: entityRef } = req.query;
|
||||
|
||||
if (!entityRef) {
|
||||
throw new Error('No entityRef was provided');
|
||||
}
|
||||
|
||||
const entityLanguages = await linguistBackendApi.getEntityLanguages(
|
||||
entityRef as string,
|
||||
);
|
||||
res.status(200).json(entityLanguages);
|
||||
});
|
||||
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import knexFactory from 'knex';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'linguist-backend' });
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
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: async () => db },
|
||||
config,
|
||||
discovery: SingleHostDiscovery.fromConfig(config),
|
||||
reader: UrlReaders.default({ logger, config }),
|
||||
logger,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/linguist', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,3 @@
|
||||
# Linguist Common
|
||||
|
||||
Common types, permissions, and constants for the Linguist plugin.
|
||||
@@ -0,0 +1,50 @@
|
||||
## API Report File for "@backstage/plugin-linguist-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
// @public (undocumented)
|
||||
export type EntitiesOverview = {
|
||||
entityCount: number;
|
||||
processedCount: number;
|
||||
pendingCount: number;
|
||||
staleCount: number;
|
||||
filteredEntities: Entity[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type EntityResults = {
|
||||
entityRef: string;
|
||||
results: Languages;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type Language = {
|
||||
name: string;
|
||||
percentage: number;
|
||||
bytes: number;
|
||||
type: string;
|
||||
color?: `#${string}`;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type Languages = {
|
||||
languageCount: number;
|
||||
totalBytes: number;
|
||||
processedDate: string;
|
||||
breakdown: Language[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const LINGUIST_ANNOTATION = 'backstage.io/linguist';
|
||||
|
||||
// @public (undocumented)
|
||||
export type ProcessedEntity = {
|
||||
entityRef: string;
|
||||
processed_date: Date;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@backstage/plugin-linguist-common",
|
||||
"description": "Common functionalities for the linguist plugin",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "common-library"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export const LINGUIST_ANNOTATION = 'backstage.io/linguist';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 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 {};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
export type EntityResults = {
|
||||
entityRef: string;
|
||||
results: Languages;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type Languages = {
|
||||
languageCount: number;
|
||||
totalBytes: number;
|
||||
processedDate: string;
|
||||
breakdown: Language[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type Language = {
|
||||
name: string;
|
||||
percentage: number;
|
||||
bytes: number;
|
||||
type: string;
|
||||
color?: `#${string}`;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ProcessedEntity = {
|
||||
entityRef: string;
|
||||
processed_date: Date;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type EntitiesOverview = {
|
||||
entityCount: number;
|
||||
processedCount: number;
|
||||
pendingCount: number;
|
||||
staleCount: number;
|
||||
filteredEntities: Entity[];
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,88 @@
|
||||
# Linguist
|
||||
|
||||
Welcome to the Linguist plugin!
|
||||
|
||||
## Features
|
||||
|
||||
The Linguist plugin consists of a card that will give you the breakdown of the languages used by the configured Entity in the Catalog.
|
||||
|
||||
When there is no Linguist data yet the card will be empty:
|
||||
|
||||

|
||||
|
||||
Clicking on the refresh icon will generate the language breakdown:
|
||||
|
||||

|
||||
|
||||
Here's an example where the permissions are hiding the refresh button:
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
The following sections will help you get the Linguist plugin setup and running.
|
||||
|
||||
### Backend
|
||||
|
||||
You need to setup the [Linguist backend plugin](../linguist-backend/README.md) before you move forward with any of the following steps if you haven't already.
|
||||
|
||||
### Entity Annotation
|
||||
|
||||
To be able to use the Linguist plugin you need to add the following annotation to any entities you want to use it with:
|
||||
|
||||
```yaml
|
||||
backstage.io/linguist: https://url.to/your/srouce/code
|
||||
```
|
||||
|
||||
Linguist uses the `UrlReader` to pull down the files before it scans them to determine the languages, this means you can pull files from all the supported providers - GitHub, GitLab, Bitbucket, Azure, Google GCS, AWS S3, etc.
|
||||
|
||||
Here's what that will look like in action using the Backstage repo as an example:
|
||||
|
||||
```yaml
|
||||
# Example catalog-info.yaml entity definition file
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
# ...
|
||||
annotations:
|
||||
backstage.io/linguist: https://github.com/backstage/backstage
|
||||
spec:
|
||||
type: service
|
||||
# ...
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
To setup the Linguist Card frontend you'll need to do the following steps:
|
||||
|
||||
1. First we need to add the `@backstage/plugin-linguist` package to your frontend app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/app @backstage/plugin-linguist
|
||||
```
|
||||
|
||||
2. Second we need to add the `EntityLinguistCard` extension to the entity page in your app:
|
||||
|
||||
```tsx
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { isLinguistAvailable, EntityLinguistCard } from '@backstage/plugin-linguist';
|
||||
|
||||
// For example in the Overview section
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
// ...
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isLinguistAvailable}>
|
||||
<Grid item md={6}>
|
||||
<EntityLinguistCard />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
// ...
|
||||
</Grid>
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation
|
||||
@@ -0,0 +1,21 @@
|
||||
## API Report File for "@backstage/plugin-linguist"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityLinguistCard: () => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const isLinguistAvailable: (entity: Entity) => boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export const linguistPlugin: BackstagePlugin<{}, {}, {}>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { linguistPlugin, EntityLinguistCard } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(linguistPlugin)
|
||||
.addPage({
|
||||
element: <EntityLinguistCard />,
|
||||
title: 'Root Page',
|
||||
path: '/linguist',
|
||||
})
|
||||
.render();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@backstage/plugin-linguist",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/linguist"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-linguist-common": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"luxon": "^2.0.2",
|
||||
"react-use": "^17.2.4",
|
||||
"slugify": "^1.6.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/node": "*",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^0.49.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2022 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { Languages } from '@backstage/plugin-linguist-common';
|
||||
|
||||
export const linguistApiRef = createApiRef<LinguistApi>({
|
||||
id: 'plugin.linguist.service',
|
||||
});
|
||||
|
||||
export interface LinguistApi {
|
||||
getLanguages(entityRef: string): Promise<Languages>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 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 { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { Languages } from '@backstage/plugin-linguist-common';
|
||||
import { LinguistApi } from './LinguistApi';
|
||||
|
||||
export class LinguistClient implements LinguistApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
public constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
public async getLanguages(entityRef: string): Promise<Languages> {
|
||||
const queryString = new URLSearchParams();
|
||||
queryString.append('entityRef', entityRef);
|
||||
|
||||
const urlSegment = `entity-languages?${queryString}`;
|
||||
|
||||
const items = await this.get<Languages>(urlSegment);
|
||||
return items;
|
||||
}
|
||||
|
||||
private async get<T>(path: string): Promise<T> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl('linguist')}/`;
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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 './LinguistApi';
|
||||
export * from './LinguistClient';
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
Box,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Grid,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { InfoCard, Progress } from '@backstage/core-components';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { DateTime } from 'luxon';
|
||||
import React from 'react';
|
||||
import slugify from 'slugify';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useLanguages } from '../../hooks';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
infoCard: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
barContainer: {
|
||||
height: theme.spacing(2),
|
||||
marginBottom: theme.spacing(3),
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
bar: {
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
languageDot: {
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
marginRight: theme.spacing(1),
|
||||
display: 'inline-block',
|
||||
},
|
||||
label: {
|
||||
color: 'inherit',
|
||||
},
|
||||
}));
|
||||
|
||||
export const LinguistCard = () => {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const { entity } = useEntity();
|
||||
const { items, loading, error } = useLanguages(entity);
|
||||
let barWidth = 0;
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
if (items && items.languageCount === 0 && items.totalBytes === 0) {
|
||||
return (
|
||||
<InfoCard title="Languages" className={classes.infoCard}>
|
||||
<Grid container spacing={3}>
|
||||
<Box p={2}>
|
||||
<Typography>
|
||||
There is currently no language data for this entity.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
const breakdown = items?.breakdown.sort((a, b) =>
|
||||
a.percentage < b.percentage ? 1 : -1,
|
||||
);
|
||||
const processedDate = items?.processedDate;
|
||||
|
||||
return breakdown && processedDate ? (
|
||||
<InfoCard title="Languages" className={classes.infoCard}>
|
||||
<Box className={classes.barContainer}>
|
||||
{breakdown.map((language, index: number) => {
|
||||
barWidth = barWidth + language.percentage;
|
||||
return (
|
||||
<Tooltip
|
||||
title={language.name}
|
||||
placement="bottom-end"
|
||||
key={slugify(language.name, { lower: true })}
|
||||
>
|
||||
<Box
|
||||
className={classes.bar}
|
||||
key={slugify(language.name, { lower: true })}
|
||||
style={{
|
||||
marginTop: index === 0 ? '0' : `-16px`,
|
||||
zIndex: Object.keys(breakdown).length - index,
|
||||
backgroundColor:
|
||||
language.color?.toString() ||
|
||||
theme.palette.background.default,
|
||||
width: `${barWidth}%`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Tooltip
|
||||
title={`Generated ${DateTime.fromISO(processedDate).toRelative()}`}
|
||||
>
|
||||
<Box>
|
||||
{breakdown.map(languages => (
|
||||
<Chip
|
||||
classes={{
|
||||
label: classes.label,
|
||||
}}
|
||||
label={
|
||||
<Box>
|
||||
<Box
|
||||
component="span"
|
||||
className={classes.languageDot}
|
||||
style={{
|
||||
backgroundColor:
|
||||
languages?.color?.toString() ||
|
||||
theme.palette.background.default,
|
||||
}}
|
||||
/>
|
||||
{languages.name} - {languages.percentage}%
|
||||
</Box>
|
||||
}
|
||||
variant="outlined"
|
||||
key={slugify(languages.name, { lower: true })}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</InfoCard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 { LinguistCard } from './LinguistCard';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 './useLanguages';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { linguistApiRef } from '../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { Languages } from '@backstage/plugin-linguist-common';
|
||||
|
||||
export function useLanguages(entity: Entity): {
|
||||
items?: Languages;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
} {
|
||||
const entityRef = stringifyEntityRef(entity);
|
||||
|
||||
const api = useApi(linguistApiRef);
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
return api.getLanguages(entityRef);
|
||||
}, [api, entityRef]);
|
||||
|
||||
return {
|
||||
items: value,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
linguistPlugin,
|
||||
isLinguistAvailable,
|
||||
EntityLinguistCard,
|
||||
} from './plugin';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2022 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 { linguistPlugin } from './plugin';
|
||||
|
||||
describe('linguist', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(linguistPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
createApiFactory,
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { linguistApiRef, LinguistClient } from './api';
|
||||
import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
export const isLinguistAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[LINGUIST_ANNOTATION]);
|
||||
|
||||
/** @public */
|
||||
export const linguistPlugin = createPlugin({
|
||||
id: 'linguist',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: linguistApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new LinguistClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const EntityLinguistCard = linguistPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityLinguistCard',
|
||||
component: {
|
||||
lazy: () => import('./components/LinguistCard').then(m => m.LinguistCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
@@ -6895,6 +6895,78 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-linguist-backend@workspace:^, @backstage/plugin-linguist-backend@workspace:plugins/linguist-backend":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-linguist-backend@workspace:plugins/linguist-backend"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-tasks": "workspace:^"
|
||||
"@backstage/catalog-client": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-linguist-common": "workspace:^"
|
||||
"@types/express": "*"
|
||||
"@types/supertest": ^2.0.8
|
||||
express: ^4.18.1
|
||||
express-promise-router: ^4.1.0
|
||||
fs-extra: ^10.0.0
|
||||
knex: ^2.0.0
|
||||
linguist-js: ^2.5.3
|
||||
luxon: ^2.0.2
|
||||
msw: ^0.49.0
|
||||
node-fetch: ^2.6.7
|
||||
supertest: ^6.2.4
|
||||
uuid: ^8.3.2
|
||||
winston: ^3.2.1
|
||||
yn: ^4.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-linguist-common@workspace:^, @backstage/plugin-linguist-common@workspace:plugins/linguist-common":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-linguist-common@workspace:plugins/linguist-common"
|
||||
dependencies:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-linguist@workspace:^, @backstage/plugin-linguist@workspace:plugins/linguist":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-linguist@workspace:plugins/linguist"
|
||||
dependencies:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
"@backstage/plugin-linguist-common": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@material-ui/icons": ^4.9.1
|
||||
"@material-ui/lab": ^4.0.0-alpha.57
|
||||
"@testing-library/jest-dom": ^5.10.1
|
||||
"@testing-library/react": ^12.1.3
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/node": "*"
|
||||
cross-fetch: ^3.1.5
|
||||
luxon: ^2.0.2
|
||||
msw: ^0.49.0
|
||||
react-use: ^17.2.4
|
||||
slugify: ^1.6.4
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-microsoft-calendar@workspace:^, @backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar"
|
||||
@@ -11244,7 +11316,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4":
|
||||
"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13":
|
||||
version: 4.12.4
|
||||
resolution: "@material-ui/core@npm:4.12.4"
|
||||
dependencies:
|
||||
@@ -17580,10 +17652,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"binary-extensions@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "binary-extensions@npm:2.0.0"
|
||||
checksum: 554f65d3378cf71c3185c17dec3ca58334b8ff6ae242db3107284765ce33b2af19efd20c11faec41907a40534929e34b3a98e7d391c61e4211b45732dccb1115
|
||||
"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "binary-extensions@npm:2.2.0"
|
||||
checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18943,7 +19015,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1":
|
||||
"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1, commander@npm:^9.5.0":
|
||||
version: 9.5.0
|
||||
resolution: "commander@npm:9.5.0"
|
||||
checksum: c7a3e27aa59e913b54a1bafd366b88650bc41d6651f0cbe258d4ff09d43d6a7394232a4dadd0bf518b3e696fdf595db1028a0d82c785b88bd61f8a440cecfade
|
||||
@@ -22227,6 +22299,8 @@ __metadata:
|
||||
"@backstage/plugin-kafka": "workspace:^"
|
||||
"@backstage/plugin-kubernetes": "workspace:^"
|
||||
"@backstage/plugin-lighthouse": "workspace:^"
|
||||
"@backstage/plugin-linguist": "workspace:^"
|
||||
"@backstage/plugin-linguist-common": "workspace:^"
|
||||
"@backstage/plugin-microsoft-calendar": "workspace:^"
|
||||
"@backstage/plugin-newrelic": "workspace:^"
|
||||
"@backstage/plugin-newrelic-dashboard": "workspace:^"
|
||||
@@ -22326,6 +22400,7 @@ __metadata:
|
||||
"@backstage/plugin-kafka-backend": "workspace:^"
|
||||
"@backstage/plugin-kubernetes-backend": "workspace:^"
|
||||
"@backstage/plugin-lighthouse-backend": "workspace:^"
|
||||
"@backstage/plugin-linguist-backend": "workspace:^"
|
||||
"@backstage/plugin-permission-backend": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/plugin-permission-node": "workspace:^"
|
||||
@@ -24806,10 +24881,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore@npm:^5.1.4, ignore@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "ignore@npm:5.2.0"
|
||||
checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77
|
||||
"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4":
|
||||
version: 5.2.4
|
||||
resolution: "ignore@npm:5.2.4"
|
||||
checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -25826,7 +25901,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8":
|
||||
"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8, isbinaryfile@npm:^4.0.8 <5":
|
||||
version: 4.0.10
|
||||
resolution: "isbinaryfile@npm:4.0.10"
|
||||
checksum: a6b28db7e23ac7a77d3707567cac81356ea18bd602a4f21f424f862a31d0e7ab4f250759c98a559ece35ffe4d99f0d339f1ab884ffa9795172f632ab8f88e686
|
||||
@@ -27699,6 +27774,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"linguist-js@npm:^2.5.3":
|
||||
version: 2.5.4
|
||||
resolution: "linguist-js@npm:2.5.4"
|
||||
dependencies:
|
||||
binary-extensions: ^2.2.0
|
||||
commander: ^9.5.0
|
||||
common-path-prefix: ^3.0.0
|
||||
cross-fetch: ^3.1.5
|
||||
ignore: ^5.2.4
|
||||
isbinaryfile: ^4.0.8 <5
|
||||
js-yaml: ^4.1.0
|
||||
node-cache: ^5.1.2
|
||||
bin:
|
||||
linguist: bin/index.js
|
||||
linguist-js: bin/index.js
|
||||
checksum: 92ebfc45f0f17a056153068f7e939a3e6755307d3696673597e824011d83b698063241aa7d082a7f9059416efce85a49056f0a31b3158c25f79e273e2f51e309
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"linkify-it@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "linkify-it@npm:3.0.2"
|
||||
@@ -28244,6 +28338,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"luxon@npm:^2.0.2":
|
||||
version: 2.5.2
|
||||
resolution: "luxon@npm:2.5.2"
|
||||
checksum: d8b671ffd2ff0b438af862ac11082a81b3aedd9f8b6a4ca636f944224f8dd381125cd4d274ca0a8aedcaf2cfca0fc2ca96fe4cc1b16a28b7af701afb95c0bff8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"luxon@npm:^3.0.0, luxon@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "luxon@npm:3.2.1"
|
||||
@@ -35075,6 +35176,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slugify@npm:^1.6.4":
|
||||
version: 1.6.5
|
||||
resolution: "slugify@npm:1.6.5"
|
||||
checksum: a955a1b600201030f4c1daa9bb74a17d4402a0693fc40978bbd17e44e64fd72dad3bac4037422aa8aed55b5170edd57f3f4cd8f59ba331f5cf0f10f1a7795609
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"smart-buffer@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "smart-buffer@npm:4.2.0"
|
||||
|
||||
Reference in New Issue
Block a user