@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user