From d34b3267f9628b1ec7678083e40b0e885c714d4b Mon Sep 17 00:00:00 2001 From: ivgo Date: Mon, 30 Jan 2023 16:13:35 +0100 Subject: [PATCH] Entity Validation UI Signed-off-by: ivgo --- .changeset/silly-socks-lie.md | 5 + plugins/entity-validation/.eslintrc.js | 1 + plugins/entity-validation/README.md | 51 ++++++ plugins/entity-validation/api-report.md | 30 ++++ plugins/entity-validation/dev/index.tsx | 27 +++ plugins/entity-validation/package.json | 63 +++++++ .../EntityTextArea/EntityTextArea.tsx | 97 +++++++++++ .../src/components/EntityTextArea/index.ts | 16 ++ .../EntityValidationOutput/EntityResult.tsx | 107 ++++++++++++ .../EntityValidationOutput.tsx | 162 ++++++++++++++++++ .../EntityValidationOutput/index.ts | 17 ++ .../EntityValidationPage.tsx | 117 +++++++++++++ .../components/EntityValidationPage/index.ts | 16 ++ plugins/entity-validation/src/index.ts | 16 ++ plugins/entity-validation/src/plugin.test.ts | 22 +++ plugins/entity-validation/src/plugin.ts | 42 +++++ plugins/entity-validation/src/routes.ts | 20 +++ plugins/entity-validation/src/setupTests.ts | 17 ++ plugins/entity-validation/src/types.ts | 30 ++++ plugins/entity-validation/src/utils/index.ts | 16 ++ plugins/entity-validation/src/utils/utils.ts | 58 +++++++ yarn.lock | 38 ++++ 22 files changed, 968 insertions(+) create mode 100644 .changeset/silly-socks-lie.md create mode 100644 plugins/entity-validation/.eslintrc.js create mode 100644 plugins/entity-validation/README.md create mode 100644 plugins/entity-validation/api-report.md create mode 100644 plugins/entity-validation/dev/index.tsx create mode 100644 plugins/entity-validation/package.json create mode 100644 plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx create mode 100644 plugins/entity-validation/src/components/EntityTextArea/index.ts create mode 100644 plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx create mode 100644 plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx create mode 100644 plugins/entity-validation/src/components/EntityValidationOutput/index.ts create mode 100644 plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx create mode 100644 plugins/entity-validation/src/components/EntityValidationPage/index.ts create mode 100644 plugins/entity-validation/src/index.ts create mode 100644 plugins/entity-validation/src/plugin.test.ts create mode 100644 plugins/entity-validation/src/plugin.ts create mode 100644 plugins/entity-validation/src/routes.ts create mode 100644 plugins/entity-validation/src/setupTests.ts create mode 100644 plugins/entity-validation/src/types.ts create mode 100644 plugins/entity-validation/src/utils/index.ts create mode 100644 plugins/entity-validation/src/utils/utils.ts diff --git a/.changeset/silly-socks-lie.md b/.changeset/silly-socks-lie.md new file mode 100644 index 0000000000..ae7e1c0b2a --- /dev/null +++ b/.changeset/silly-socks-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-validation': minor +--- + +First implementation for the entity validation UI. For more information refer to its `README.md`. diff --git a/plugins/entity-validation/.eslintrc.js b/plugins/entity-validation/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-validation/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-validation/README.md b/plugins/entity-validation/README.md new file mode 100644 index 0000000000..0e80ebd29b --- /dev/null +++ b/plugins/entity-validation/README.md @@ -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 = ( + + {/* ...other routes */} + } /> + +); +``` + +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<{}>) => ( + + + {/* ...other elements */} + }> + {/* Global nav, not org-specific */} + {/* ...other sidebars */} + + {/* End global nav */} + + + {/* ...other elements */} + + {children} + + ); +``` diff --git a/plugins/entity-validation/api-report.md b/plugins/entity-validation/api-report.md new file mode 100644 index 0000000000..1a3b92a7c1 --- /dev/null +++ b/plugins/entity-validation/api-report.md @@ -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 +/// + +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; + }, + {}, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/entity-validation/dev/index.tsx b/plugins/entity-validation/dev/index.tsx new file mode 100644 index 0000000000..1e1dfa760c --- /dev/null +++ b/plugins/entity-validation/dev/index.tsx @@ -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: , + title: 'Root Page', + path: '/entity-validation', + }) + .render(); diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json new file mode 100644 index 0000000000..fa0c132e67 --- /dev/null +++ b/plugins/entity-validation/package.json @@ -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" + ] +} diff --git a/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx new file mode 100644 index 0000000000..c39f8d5ed7 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx @@ -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 ( + + { + // Prevent new line if Ctrl + Enter was clicked + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) e.preventDefault(); + }} + /> + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityTextArea/index.ts b/plugins/entity-validation/src/components/EntityTextArea/index.ts new file mode 100644 index 0000000000..d2a5e41d0f --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/index.ts @@ -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'; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx new file mode 100644 index 0000000000..85d47fa2c2 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx @@ -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 ( + <> + + + + + setExpanded(!expanded)} + /> + {!item.response.valid && ( + + setExpanded(!expanded)}> + {expanded ? : } + + + )} + + {!item.response.valid && ( + + + + + + )} + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx new file mode 100644 index 0000000000..f58fc90559 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx @@ -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) { + 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 ; + } + + 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 => ( + {err.processingError} + ))} + + ); + } + + return ( + + {results.length === 0 ? ( +
+ + No entity definitions found or validated yet + +
+ ) : ( + <> + + {results.map((item, key) => ( + + ))} + +
+ {results.every(r => r.response.valid) ? ( + + All the entities are valid! + + ) : ( + + One or more entities have validation errors + + )} +
+ + )} +
+ ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/index.ts b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts new file mode 100644 index 0000000000..8d327581c6 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts @@ -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'; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx new file mode 100644 index 0000000000..d39b234342 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx @@ -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(); + const [locationUrl, setLocationUrl] = useState(''); + + const parseYaml = () => { + const parsedFiles = [ + ...parseEntityYaml(Buffer.from(catalogYaml), { + type: 'url', + target: locationUrl ? locationUrl : 'http://localhost', + }), + ]; + setYamlFiles(parsedFiles); + }; + + return ( + +
+ + + + setLocationUrl(e.target.value)} + /> + + + + + + + + + + + + setCatalogYaml(value)} + catalogYaml={catalogYaml} + /> + + + + + + + + + + + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/index.ts b/plugins/entity-validation/src/components/EntityValidationPage/index.ts new file mode 100644 index 0000000000..24149a9387 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/index.ts @@ -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'; diff --git a/plugins/entity-validation/src/index.ts b/plugins/entity-validation/src/index.ts new file mode 100644 index 0000000000..33ae575dfe --- /dev/null +++ b/plugins/entity-validation/src/index.ts @@ -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'; diff --git a/plugins/entity-validation/src/plugin.test.ts b/plugins/entity-validation/src/plugin.test.ts new file mode 100644 index 0000000000..eae69e8d3a --- /dev/null +++ b/plugins/entity-validation/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/entity-validation/src/plugin.ts b/plugins/entity-validation/src/plugin.ts new file mode 100644 index 0000000000..9a3014da28 --- /dev/null +++ b/plugins/entity-validation/src/plugin.ts @@ -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, + ), + }, + }), +); diff --git a/plugins/entity-validation/src/routes.ts b/plugins/entity-validation/src/routes.ts new file mode 100644 index 0000000000..8cdc138782 --- /dev/null +++ b/plugins/entity-validation/src/routes.ts @@ -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', +}); diff --git a/plugins/entity-validation/src/setupTests.ts b/plugins/entity-validation/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/entity-validation/src/setupTests.ts @@ -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'; diff --git a/plugins/entity-validation/src/types.ts b/plugins/entity-validation/src/types.ts new file mode 100644 index 0000000000..69caf79e33 --- /dev/null +++ b/plugins/entity-validation/src/types.ts @@ -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; diff --git a/plugins/entity-validation/src/utils/index.ts b/plugins/entity-validation/src/utils/index.ts new file mode 100644 index 0000000000..8d45a67a36 --- /dev/null +++ b/plugins/entity-validation/src/utils/index.ts @@ -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'; diff --git a/plugins/entity-validation/src/utils/utils.ts b/plugins/entity-validation/src/utils/utils.ts new file mode 100644 index 0000000000..dde50d2f9c --- /dev/null +++ b/plugins/entity-validation/src/utils/utils.ts @@ -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 { + 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); + } + } + } +} diff --git a/yarn.lock b/yarn.lock index f68a2d20d0..a3e9612ca6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"