Entity Validation UI

Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
ivgo
2023-01-30 16:13:35 +01:00
parent 7f11883e24
commit d34b3267f9
22 changed files with 968 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-entity-validation': minor
---
First implementation for the entity validation UI. For more information refer to its `README.md`.
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+51
View File
@@ -0,0 +1,51 @@
# entity-validation
This plugin creates a new page in Backstage where the user can validate the entities.
## Getting started
First of all, install the package in the `app` package by running the following command:
```bash
yarn add --cwd packages/app @backstage/plugin-entity-validation
```
Add the new route to the app by adding the following line:
```typescript
// In packages/app/src/App.tsx
import { EntityValidationPage } from '@backstage/plugin-entity-validation';
const routes = (
<FlatRoutes>
{/* ...other routes */}
<Route path="/entity-validation" element={<EntityValidationPage />} />
</FlatRoutes>
);
```
To add the new page to your sidebar, you must include these lines in your `Root.tsx` file:
```typescript
// In packages/app/src/components/Root/Root.tsx
import BuildIcon from '@material-ui/icons/Build';
...
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
{/* ...other elements */}
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
{/* ...other sidebars */}
<SidebarItem icon={BuildIcon} to="entity-validation" text="Validator" />
{/* End global nav */}
<SidebarDivider />
</SidebarGroup>
{/* ...other elements */}
</Sidebar>
{children}
</SidebarPage>
);
```
+30
View File
@@ -0,0 +1,30 @@
## API Report File for "@backstage/plugin-entity-validation"
> 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 { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export const EntityValidationPage: ({
defaultPreviewCatalog,
locationPlaceholder,
}: {
defaultPreviewCatalog?: string | undefined;
locationPlaceholder?: string | undefined;
}) => JSX.Element;
// @public (undocumented)
export const entityValidationPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{},
{}
>;
// (No @packageDocumentation comment for this package)
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2023 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 { entityValidationPlugin, EntityValidationPage } from '../src/plugin';
createDevApp()
.registerPlugin(entityValidationPlugin)
.addPage({
element: <EntityValidationPage />,
title: 'Root Page',
path: '/entity-validation',
})
.render();
+63
View File
@@ -0,0 +1,63 @@
{
"name": "@backstage/plugin-entity-validation",
"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"
},
"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-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/view": "^6.0.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"@react-hookz/web": "^20.0.0",
"@uiw/react-codemirror": "^4.9.3",
"lodash": "^4.17.21",
"react-use": "^17.2.4",
"yaml": "^2.0.0"
},
"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,97 @@
/*
* Copyright 2023 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, { useMemo, useState } from 'react';
import { Box, makeStyles } from '@material-ui/core';
import CodeMirror from '@uiw/react-codemirror';
import { showPanel } from '@codemirror/view';
import { StreamLanguage } from '@codemirror/language';
import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml';
import { useKeyboardEvent } from '@react-hookz/web';
const useStyles = makeStyles(theme => ({
container: {
position: 'relative',
width: '100%',
height: '100%',
minHeight: '400px',
},
codeMirror: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
infoPanel: {
color: theme.palette.info.main,
lineHeight: 2,
margin: theme.spacing(0, 1),
},
}));
type TemplateTextAreaProps = {
catalogYaml: string;
onChange: (value: string) => void;
onValidate: () => void;
};
export const EntityTextArea = ({
catalogYaml,
onChange,
onValidate,
}: TemplateTextAreaProps) => {
const classes = useStyles();
const [close, setClose] = useState(false);
const panelExtension = useMemo(() => {
if (close) {
return showPanel.of(null);
}
const dom = document.createElement('div');
dom.classList.add(classes.infoPanel);
dom.textContent =
'To validate your catalog-info.yaml click on the "Validate" button or use "Ctrl + S" or "Ctrl + Enter"';
dom.onclick = () => setClose(true);
return showPanel.of(() => ({ dom, top: true }));
}, [classes, close]);
// Triggers a validation when Ctrl+S or Ctrl+Enter instead of default behavior
useKeyboardEvent(
e => (e.key === 's' || e.key === 'Enter') && (e.ctrlKey || e.metaKey),
e => {
e.preventDefault();
onValidate();
},
);
return (
<Box className={classes.container}>
<CodeMirror
className={classes.codeMirror}
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport), panelExtension]}
value={catalogYaml}
onChange={onChange}
onKeyDownCapture={e => {
// Prevent new line if Ctrl + Enter was clicked
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) e.preventDefault();
}}
/>
</Box>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 './EntityTextArea';
@@ -0,0 +1,107 @@
/*
* Copyright 2023 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, { useState } from 'react';
import { ValidateEntityResponse } from '@backstage/catalog-client';
import { useApp } from '@backstage/core-plugin-api';
import {
Collapse,
IconButton,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
makeStyles,
Paper,
} from '@material-ui/core';
import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
import WorkIcon from '@material-ui/icons/Work';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { MarkdownContent } from '@backstage/core-components';
import { ValidationOutputOk } from '../../types';
const useStyles = makeStyles(theme => ({
validationOk: {
color: theme.palette.success.main,
},
validationNotOk: {
color: theme.palette.error.main,
},
errorContainer: {
color: theme.palette.error.main,
background: theme.palette.background.paper,
padding: theme.spacing(2),
},
}));
type EntityResultProps = {
isFirstError?: boolean;
item: ValidationOutputOk;
};
export const EntityResult = ({
isFirstError = false,
item,
}: EntityResultProps) => {
const classes = useStyles();
const app = useApp();
const [expanded, setExpanded] = useState(isFirstError);
const Icon =
app.getSystemIcon(`kind:${item.entity.kind.toLocaleLowerCase('en-US')}`) ??
WorkIcon;
const fetchErrorMessages = (response: ValidateEntityResponse) => {
if (!response.valid) {
return response.errors.map(err => err.message).join('\n\n');
}
return '';
};
return (
<>
<ListItem key={humanizeEntityRef(item.entity)}>
<ListItemIcon>
<Icon
className={
item.response.valid
? classes.validationOk
: classes.validationNotOk
}
/>
</ListItemIcon>
<ListItemText
primary={humanizeEntityRef(item.entity)}
onClick={() => setExpanded(!expanded)}
/>
{!item.response.valid && (
<ListItemSecondaryAction>
<IconButton edge="end" onClick={() => setExpanded(!expanded)}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
{!item.response.valid && (
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Paper className={classes.errorContainer}>
<MarkdownContent content={fetchErrorMessages(item.response)} />
</Paper>
</Collapse>
)}
</>
);
};
@@ -0,0 +1,162 @@
/*
* Copyright 2023 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 { identityApiRef, useApi } from '@backstage/core-plugin-api';
import { CatalogProcessorResult } from '@backstage/plugin-catalog-node';
import {
catalogApiRef,
humanizeEntityRef,
} from '@backstage/plugin-catalog-react';
import { List, makeStyles, Typography } from '@material-ui/core';
import useAsync from 'react-use/lib/useAsync';
import { InfoCard, Progress } from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import {
ValidationOutput,
ValidationOutputError,
ValidationOutputOk,
} from '../../types';
import { EntityResult } from './EntityResult';
const useStyles = makeStyles(theme => ({
validationOk: {
color: theme.palette.success.main,
},
validationNotOk: {
color: theme.palette.error.main,
},
}));
function sortResults(items: Array<ValidationOutputOk>) {
return items.sort((a, b) =>
humanizeEntityRef(a.entity).localeCompare(humanizeEntityRef(b.entity)),
);
}
type EntityValidationOutputProps = {
processorResults?: CatalogProcessorResult[];
locationUrl: string;
};
export const EntityValidationOutput = ({
processorResults,
locationUrl,
}: EntityValidationOutputProps) => {
const classes = useStyles();
const catalogApi = useApi(catalogApiRef);
const identityApi = useApi(identityApiRef);
const { value = [], loading } = useAsync(async (): Promise<
ValidationOutput[] | undefined
> => {
if (!processorResults) {
return undefined;
}
const location = locationUrl
? `url:${locationUrl}`
: 'url:http://localhost';
return await Promise.all(
processorResults.map(async item => {
if (item.type === 'entity') {
const { token } = await identityApi.getCredentials();
const validateResult = await catalogApi.validateEntity(
item.entity,
location,
{
token,
},
);
return {
type: 'valid',
entity: item.entity,
response: validateResult,
};
} else if (item.type === 'error') {
return {
type: 'error',
processingError: `Malformed entity YAML, ${item.error}`,
};
}
return {
type: 'error',
processingError: `Internal error, failed to parse entity`,
};
}),
);
}, [processorResults]);
if (loading) {
return <Progress />;
}
const errors = value.filter(
v => v.type === 'error',
) as ValidationOutputError[];
const results = sortResults(
value.filter(v => v.type === 'valid') as ValidationOutputOk[],
);
// If there are any errors, only the first one will be expanded
const firstErrorIndex = results.findIndex(r => !r.response.valid);
if (errors.length !== 0) {
return (
<>
{errors.map(err => (
<Alert severity="error">{err.processingError}</Alert>
))}
</>
);
}
return (
<InfoCard>
{results.length === 0 ? (
<div style={{ padding: '15px' }}>
<Typography variant="body1">
No entity definitions found or validated yet
</Typography>
</div>
) : (
<>
<List disablePadding dense>
{results.map((item, key) => (
<EntityResult
key={key}
item={item}
isFirstError={key === firstErrorIndex}
/>
))}
</List>
<div style={{ marginTop: '25px', textAlign: 'center' }}>
{results.every(r => r.response.valid) ? (
<Typography variant="body1" className={classes.validationOk}>
All the entities are valid!
</Typography>
) : (
<Typography variant="body1" className={classes.validationNotOk}>
One or more entities have validation errors
</Typography>
)}
</div>
</>
)}
</InfoCard>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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 './EntityValidationOutput';
export * from './EntityResult';
@@ -0,0 +1,117 @@
/*
* Copyright 2023 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, { useState } from 'react';
import { Button, Content, Header, Page } from '@backstage/core-components';
import { EntityTextArea } from '../EntityTextArea';
import { Grid, TextField } from '@material-ui/core';
import { CatalogProcessorResult } from '@backstage/plugin-catalog-node';
import { parseEntityYaml } from '../../utils';
import { EntityValidationOutput } from '../EntityValidationOutput';
const EXAMPLE_CATALOG_INFO_YAML = `# Put your catalog-info.yaml below and validate it
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: test
description: Component description
links: []
tags: []
annotations: {}
spec:
type: service
lifecycle: experimental
owner: owner
`;
export const EntityValidationPage = ({
defaultPreviewCatalog = EXAMPLE_CATALOG_INFO_YAML,
locationPlaceholder = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
}: {
defaultPreviewCatalog?: string;
locationPlaceholder?: string;
}) => {
const [catalogYaml, setCatalogYaml] = useState(defaultPreviewCatalog);
const [yamlFiles, setYamlFiles] = useState<CatalogProcessorResult[]>();
const [locationUrl, setLocationUrl] = useState('');
const parseYaml = () => {
const parsedFiles = [
...parseEntityYaml(Buffer.from(catalogYaml), {
type: 'url',
target: locationUrl ? locationUrl : 'http://localhost',
}),
];
setYamlFiles(parsedFiles);
};
return (
<Page themeId="tool">
<Header
title="Entity Validator"
subtitle="Tool to validate catalog-info.yaml files"
/>
<Content>
<Grid container direction="row">
<Grid item md={9} xs={12}>
<TextField
style={{ width: '100%' }}
label="File Location"
margin="normal"
variant="outlined"
required
placeholder={locationPlaceholder}
helperText="Location where you catalog-info.yaml file is, or will be, located"
onChange={e => setLocationUrl(e.target.value)}
/>
</Grid>
<Grid item md={3} xs={12}>
<Grid container alignItems="center" style={{ height: '100%' }}>
<Grid item>
<Button
style={{ textDecoration: 'none' }}
variant="outlined"
to="#"
onClick={parseYaml}
>
Validate
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid container direction="row" style={{ height: '90%' }}>
<Grid item md={6} xs={12}>
<EntityTextArea
onValidate={parseYaml}
onChange={(value: string) => setCatalogYaml(value)}
catalogYaml={catalogYaml}
/>
</Grid>
<Grid item md={6} xs={12}>
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityValidationOutput
processorResults={yamlFiles}
locationUrl={locationUrl}
/>
</Grid>
</Grid>
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 './EntityValidationPage';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { entityValidationPlugin, EntityValidationPage } from './plugin';
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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 { entityValidationPlugin } from './plugin';
describe('entity-validation', () => {
it('should export plugin', () => {
expect(entityValidationPlugin).toBeDefined();
});
});
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2023 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 {
createComponentExtension,
createPlugin,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
/** @public */
export const entityValidationPlugin = createPlugin({
id: 'entity-validation',
routes: {
root: rootRouteRef,
},
});
/** @public */
export const EntityValidationPage = entityValidationPlugin.provide(
createComponentExtension({
name: 'EntityValidationPage',
component: {
lazy: () =>
import('./components/EntityValidationPage').then(
m => m.EntityValidationPage,
),
},
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2023 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'entity-validation',
});
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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';
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2023 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';
import { ValidateEntityResponse } from '@backstage/catalog-client';
export type ValidationOutputError = {
type: 'error';
processingError: string;
};
export type ValidationOutputOk = {
type: 'valid';
entity: Entity;
response: ValidateEntityResponse;
};
export type ValidationOutput = ValidationOutputOk | ValidationOutputError;
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 './utils';
@@ -0,0 +1,58 @@
/*
* Copyright 2023 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, stringifyLocationRef } from '@backstage/catalog-model';
import lodash from 'lodash';
import yaml from 'yaml';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessorResult,
processingResult,
} from '@backstage/plugin-catalog-node';
// Copy from: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/modules/util/parse.ts
export function* parseEntityYaml(
data: Buffer,
location: LocationSpec,
): Iterable<CatalogProcessorResult> {
let documents: yaml.Document.Parsed[];
try {
documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d);
} catch (e) {
const loc = stringifyLocationRef(location);
const message = `Failed to parse YAML at ${loc}, ${e}`;
yield processingResult.generalError(location, message);
return;
}
for (const document of documents) {
if (document.errors?.length) {
const loc = stringifyLocationRef(location);
const message = `YAML error at ${loc}, ${document.errors[0]}`;
yield processingResult.generalError(location, message);
} else {
const json = document.toJSON();
if (lodash.isPlainObject(json)) {
yield processingResult.entity(location, json as Entity);
} else if (json === null) {
// Ignore null values, these happen if there is an empty document in the
// YAML file, for example if --- is added to the end of the file.
} else {
const message = `Expected object at root, got ${typeof json}`;
yield processingResult.generalError(location, message);
}
}
}
}
+38
View File
@@ -5759,6 +5759,44 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-entity-validation@workspace:plugins/entity-validation":
version: 0.0.0-use.local
resolution: "@backstage/plugin-entity-validation@workspace:plugins/entity-validation"
dependencies:
"@backstage/catalog-client": "workspace:^"
"@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/plugin-catalog-common": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@codemirror/language": ^6.0.0
"@codemirror/legacy-modes": ^6.1.0
"@codemirror/view": ^6.0.0
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@material-ui/lab": ^4.0.0-alpha.57
"@react-hookz/web": ^20.0.0
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
"@types/node": "*"
"@uiw/react-codemirror": ^4.9.3
cross-fetch: ^3.1.5
lodash: ^4.17.21
msw: ^0.49.0
react-use: ^17.2.4
yaml: ^2.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs":
version: 0.0.0-use.local
resolution: "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs"