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:
Andre Wanlin
2022-11-19 16:43:29 -06:00
parent e2489e8f86
commit 75cfee5688
51 changed files with 2005 additions and 11 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+88
View File
@@ -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:
![Example of empty Linguist card](./docs/linguist-no-data.png)
Clicking on the refresh icon will generate the language breakdown:
![Example of Linguist card with data](./docs/linguist-with-data.png)
Here's an example where the permissions are hiding the refresh button:
![Example of Linguist card without the refresh button](./docs/linguist-no-refresh.png)
## 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
+21
View File
@@ -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)
```
+27
View File
@@ -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

+63
View File
@@ -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"
]
}
+26
View File
@@ -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>;
}
}
+18
View File
@@ -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';
+17
View File
@@ -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,
};
}
+20
View File
@@ -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';
+22
View File
@@ -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();
});
});
+53
View File
@@ -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),
},
}),
);
+18
View File
@@ -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';