Merge pull request #2394 from SDA-SE/feat/raw-api-type

Add option to display raw API definitions
This commit is contained in:
Oliver Sand
2020-09-10 18:38:21 +02:00
committed by GitHub
13 changed files with 125 additions and 90 deletions
@@ -86,3 +86,9 @@ export const Languages = () => (
<CodeSnippet text={PYTHON} language="python" showLineNumbers />
</InfoCard>
);
export const CopyCode = () => (
<InfoCard title="Copy Code">
<CodeSnippet text={JAVASCRIPT} language="javascript" showCopyCodeButton />
</InfoCard>
);
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CodeSnippet } from './CodeSnippet';
@@ -55,4 +55,14 @@ describe('<CodeSnippet />', () => {
expect(queryByText(/2/)).toBeInTheDocument();
expect(queryByText(/3/)).toBeInTheDocument();
});
it('copy code using button', async () => {
document.execCommand = jest.fn();
const rendered = render(
wrapInTestApp(<CodeSnippet {...minProps} showCopyCodeButton />),
);
const button = rendered.getByTitle('Text copied to clipboard');
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
});
});
@@ -20,19 +20,22 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { useTheme } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { CopyTextButton } from '../CopyTextButton';
type Props = {
text: string;
language: string;
showLineNumbers?: boolean;
showCopyCodeButton?: boolean;
};
const defaultProps = {
showLineNumbers: false,
showCopyCodeButton: false,
};
export const CodeSnippet: FC<Props> = props => {
const { text, language, showLineNumbers } = {
const { text, language, showLineNumbers, showCopyCodeButton } = {
...defaultProps,
...props,
};
@@ -41,13 +44,20 @@ export const CodeSnippet: FC<Props> = props => {
const mode = theme.palette.type === 'dark' ? dark : docco;
return (
<SyntaxHighlighter
language={language}
style={mode}
showLineNumbers={showLineNumbers}
>
{text}
</SyntaxHighlighter>
<div style={{ position: 'relative' }}>
<SyntaxHighlighter
language={language}
style={mode}
showLineNumbers={showLineNumbers}
>
{text}
</SyntaxHighlighter>
{showCopyCodeButton && (
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<CopyTextButton text={text} />
</div>
)}
</div>
);
};
@@ -56,4 +66,5 @@ CodeSnippet.propTypes = {
text: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
showLineNumbers: PropTypes.bool,
showCopyCodeButton: PropTypes.bool,
};
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CopyTextButton } from './CopyTextButton';
import {
@@ -76,7 +76,7 @@ describe('<CopyTextButton />', () => {
),
);
const button = rendered.getByTitle('mockTooltip');
button.click();
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
rendered.getByText('mockTooltip');
});
@@ -63,7 +63,7 @@ export const CopyTextButton: FC<Props> = props => {
};
const classes = useStyles(props);
const errorApi = useApi(errorApiRef);
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [open, setOpen] = useState(false);
const handleCopyClick: MouseEventHandler = e => {
@@ -82,9 +82,8 @@ export const CopyTextButton: FC<Props> = props => {
return (
<>
<input
<textarea
ref={inputRef}
type="text"
style={{ position: 'absolute', top: -9999, left: 9999 }}
defaultValue={text}
/>
@@ -39,7 +39,7 @@ export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => {
<Grid container spacing={3}>
{apiNames.map(api => (
<Grid item xs={12} key={api}>
<ApiDefinitionCard title={api} apiEntity={apiEntities!.get(api)} />
<ApiDefinitionCard apiEntity={apiEntities!.get(api)} />
</Grid>
))}
</Grid>
@@ -15,31 +15,92 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
import { InfoCard } from '@backstage/core';
import { TabbedCard, CardTab } from '@backstage/core';
import React from 'react';
import { ApiDefinitionWidget } from '../ApiDefinitionWidget';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
import { Alert } from '@material-ui/lab';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
type Props = {
title?: string;
apiEntity?: ApiEntity;
type ApiDefinitionWidget = {
type: string;
title: string;
component: (definition: string) => React.ReactElement;
rawLanguage?: string;
};
export const ApiDefinitionCard = ({ title, apiEntity }: Props) => {
export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
return [
{
type: 'openapi',
title: 'OpenAPI',
rawLanguage: 'yaml',
component: definition => (
<OpenApiDefinitionWidget definition={definition} />
),
},
{
type: 'asyncapi',
title: 'AsyncAPI',
rawLanguage: 'yaml',
component: definition => (
<AsyncApiDefinitionWidget definition={definition} />
),
},
];
}
type Props = {
apiEntity?: ApiEntity;
definitionWidgets?: ApiDefinitionWidget[];
};
const defaultProps = {
definitionWidgets: defaultDefinitionWidgets(),
};
export const ApiDefinitionCard = (props: Props) => {
const { apiEntity, definitionWidgets } = {
...defaultProps,
...props,
};
if (!apiEntity) {
return <Alert severity="error">Could not fetch the API</Alert>;
}
const definitionWidget = definitionWidgets.find(
d => d.type === apiEntity.spec.type,
);
if (definitionWidget) {
return (
<InfoCard title={title}>
<Alert severity="error">Could not fetch the API</Alert>
</InfoCard>
<TabbedCard title={apiEntity.metadata.name}>
<CardTab label={definitionWidget.title}>
{definitionWidget.component(apiEntity.spec.definition)}
</CardTab>
<CardTab label="Raw">
<PlainApiDefinitionWidget
definition={apiEntity.spec.definition}
language={definitionWidget.rawLanguage || apiEntity.spec.type}
/>
</CardTab>
</TabbedCard>
);
}
return (
<InfoCard title={title} subheader={apiEntity.spec.type}>
<ApiDefinitionWidget
type={apiEntity.spec.type}
definition={apiEntity.spec.definition}
/>
</InfoCard>
<TabbedCard
title={apiEntity.metadata.name}
children={[
// Has to be an array, otherwise typescript doesn't like that this has only a single child
<CardTab label={apiEntity.spec.type}>
<PlainApiDefinitionWidget
definition={apiEntity.spec.definition}
language={apiEntity.spec.type}
/>
</CardTab>,
]}
/>
);
};
@@ -1,40 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
type Props = {
type: string;
definition: string;
};
export const ApiDefinitionWidget = ({ type, definition }: Props) => {
switch (type) {
case 'openapi':
return <OpenApiDefinitionWidget definition={definition} />;
case 'asyncapi':
return <AsyncApiDefinitionWidget definition={definition} />;
default:
return (
<PlainApiDefinitionWidget definition={definition} language={type} />
);
}
};
@@ -1,17 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { ApiDefinitionWidget } from './ApiDefinitionWidget';
@@ -136,7 +136,7 @@ const useStyles = makeStyles(theme => ({
}));
type Props = {
definition: any;
definition: string;
};
export const AsyncApiDefinitionWidget = ({ definition }: Props) => {
@@ -66,7 +66,7 @@ const useStyles = makeStyles(theme => ({
}));
type Props = {
definition: any;
definition: string;
};
export const OpenApiDefinitionWidget = ({ definition }: Props) => {
@@ -23,5 +23,7 @@ type Props = {
};
export const PlainApiDefinitionWidget = ({ definition, language }: Props) => {
return <CodeSnippet text={definition} language={language} />;
return (
<CodeSnippet text={definition} language={language} showCopyCodeButton />
);
};
+3
View File
@@ -15,5 +15,8 @@
*/
export { ApiDefinitionCard } from './ApiDefinitionCard';
export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
export { useComponentApiNames } from './useComponentApiNames';
export { useComponentApiEntities } from './useComponentApiEntities';