Merge pull request #2394 from SDA-SE/feat/raw-api-type
Add option to display raw API definitions
This commit is contained in:
@@ -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';
|
||||
+1
-1
@@ -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) => {
|
||||
|
||||
+3
-1
@@ -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 />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user