@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-entity-validation': minor
|
||||
---
|
||||
|
||||
First implementation for the entity validation UI. For more information refer to its `README.md`.
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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>
|
||||
);
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+162
@@ -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';
|
||||
+117
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user