Merge pull request #7865 from SDA-SE/feat/api-docs-customization

Lazy load API definition widgets
This commit is contained in:
Oliver Sand
2021-11-11 12:03:00 +01:00
committed by GitHub
23 changed files with 533 additions and 317 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-circleci': patch
'@backstage/plugin-github-actions': patch
---
Use `<Progress>` instead of `<LinearProgress />` in combination with `Suspense`.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-techdocs': patch
---
Lazy load `LazyLog` as it is rarely used.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-api-docs': patch
---
Lazy load all API definition widgets. The widgets use libraries like
`swagger-ui`, `graphiql`, and `@asyncapi/react-component` which are quite heavy
weight. To improve initial load times, the widgets are only loaded once used.
+33 -11
View File
@@ -84,11 +84,19 @@ export const ApiTypeTitle: ({
apiEntity: ApiEntity;
}) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const AsyncApiDefinitionWidget: ({ definition }: Props_5) => JSX.Element;
export const AsyncApiDefinitionWidget: (
props: AsyncApiDefinitionWidgetProps,
) => JSX.Element;
// Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AsyncApiDefinitionWidgetProps = {
definition: string;
};
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -100,7 +108,7 @@ export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element;
// Warning: (ae-missing-release-tag) "ConsumingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ConsumingComponentsCard: ({ variant }: Props_6) => JSX.Element;
export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element;
// Warning: (ae-missing-release-tag) "defaultDefinitionWidgets" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -169,20 +177,34 @@ export const EntityProvidingComponentsCard: ({
// @public (undocumented)
export const HasApisCard: ({ variant }: Props_3) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "OpenApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element;
export const OpenApiDefinitionWidget: (
props: OpenApiDefinitionWidgetProps,
) => JSX.Element;
// Warning: (ae-missing-release-tag) "OpenApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type OpenApiDefinitionWidgetProps = {
definition: string;
};
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "PlainApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const PlainApiDefinitionWidget: ({
definition,
language,
}: Props_9) => JSX.Element;
export const PlainApiDefinitionWidget: (
props: PlainApiDefinitionWidgetProps,
) => JSX.Element;
// Warning: (ae-missing-release-tag) "PlainApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PlainApiDefinitionWidgetProps = {
definition: any;
language: string;
};
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -194,5 +216,5 @@ export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element;
// Warning: (ae-missing-release-tag) "ProvidingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ProvidingComponentsCard: ({ variant }: Props_7) => JSX.Element;
export const ProvidingComponentsCard: ({ variant }: Props_6) => JSX.Element;
```
@@ -16,9 +16,9 @@
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
import { AsyncApiDefinition } from './AsyncApiDefinition';
describe('<AsyncApiDefinitionWidget />', () => {
describe('<AsyncApiDefinition />', () => {
it('renders asyncapi spec', async () => {
const definition = `
asyncapi: 2.0.0
@@ -40,7 +40,7 @@ components:
type: string
`;
const { getByText, getAllByText } = await renderInTestApp(
<AsyncApiDefinitionWidget definition={definition} />,
<AsyncApiDefinition definition={definition} />,
);
expect(getByText(/Account Service/i)).toBeInTheDocument();
@@ -51,7 +51,7 @@ components:
it('renders error if definition is missing', async () => {
const { getByText } = await renderInTestApp(
<AsyncApiDefinitionWidget definition="" />,
<AsyncApiDefinition definition="" />,
);
expect(getByText(/Error/i)).toBeInTheDocument();
expect(getByText(/Document can't be null or falsey/i)).toBeInTheDocument();
@@ -0,0 +1,145 @@
/*
* Copyright 2020 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 AsyncApi from '@asyncapi/react-component';
import '@asyncapi/react-component/lib/styles/fiori.css';
import { alpha, makeStyles } from '@material-ui/core/styles';
import React from 'react';
const useStyles = makeStyles(theme => ({
root: {
'& .asyncapi': {
'font-family': 'inherit',
background: 'none',
},
'& h2': {
...theme.typography.h6,
},
'& .text-teal': {
color: theme.palette.primary.main,
},
'& button': {
...theme.typography.button,
background: 'none',
boxSizing: 'border-box',
minWidth: 64,
borderRadius: theme.shape.borderRadius,
transition: theme.transitions.create(
['background-color', 'box-shadow', 'border'],
{
duration: theme.transitions.duration.short,
},
),
padding: '5px 15px',
color: theme.palette.primary.main,
border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`,
'&:hover': {
textDecoration: 'none',
'&.Mui-disabled': {
backgroundColor: 'transparent',
},
border: `1px solid ${theme.palette.primary.main}`,
backgroundColor: alpha(
theme.palette.primary.main,
theme.palette.action.hoverOpacity,
),
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent',
},
},
'&.Mui-disabled': {
color: theme.palette.action.disabled,
},
},
'& .asyncapi__collapse-button:hover': {
color: theme.palette.primary.main,
},
'& button.asyncapi__toggle-button': {
'min-width': 'inherit',
},
'& .asyncapi__info-list li': {
'border-color': theme.palette.primary.main,
'&:hover': {
color: theme.palette.text.primary,
'border-color': theme.palette.primary.main,
'background-color': theme.palette.primary.main,
},
},
'& .asyncapi__info-list li a': {
color: theme.palette.primary.main,
'&:hover': {
color: theme.palette.getContrastText(theme.palette.primary.main),
},
},
'& .asyncapi__enum': {
color: theme.palette.secondary.main,
},
'& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div':
{
'background-color': 'inherit',
},
'& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4':
{
'background-color': 'inherit',
color: theme.palette.text.primary,
},
'& .asyncapi__additional-properties-notice': {
color: theme.palette.text.hint,
},
'& .asyncapi__code, .asyncapi__code-pre': {
background: theme.palette.background.default,
},
'& .asyncapi__schema-example-header-title': {
color: theme.palette.text.secondary,
},
'& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header':
{
'background-color': 'inherit',
color: theme.palette.text.secondary,
},
'& .asyncapi__table-header': {
background: theme.palette.background.default,
},
'& .asyncapi__table-body': {
color: theme.palette.text.primary,
},
'& .asyncapi__server-security-flow': {
background: theme.palette.background.default,
border: 'none',
},
'& .asyncapi__server-security-flows-list a': {
color: theme.palette.primary.main,
},
'& .asyncapi__table-row--nested': {
color: theme.palette.text.secondary,
},
},
}));
type Props = {
definition: string;
};
export const AsyncApiDefinition = ({ definition }: Props) => {
const classes = useStyles();
return (
<div className={classes.root}>
<AsyncApi schema={definition} />
</div>
);
};
@@ -14,132 +14,27 @@
* limitations under the License.
*/
import AsyncApi from '@asyncapi/react-component';
import '@asyncapi/react-component/lib/styles/fiori.css';
import { fade, makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { Progress } from '@backstage/core-components';
import React, { Suspense } from 'react';
const useStyles = makeStyles(theme => ({
root: {
'& .asyncapi': {
'font-family': 'inherit',
background: 'none',
},
'& h2': {
...theme.typography.h6,
},
'& .text-teal': {
color: theme.palette.primary.main,
},
'& button': {
...theme.typography.button,
background: 'none',
boxSizing: 'border-box',
minWidth: 64,
borderRadius: theme.shape.borderRadius,
transition: theme.transitions.create(
['background-color', 'box-shadow', 'border'],
{
duration: theme.transitions.duration.short,
},
),
padding: '5px 15px',
color: theme.palette.primary.main,
border: `1px solid ${fade(theme.palette.primary.main, 0.5)}`,
'&:hover': {
textDecoration: 'none',
'&.Mui-disabled': {
backgroundColor: 'transparent',
},
border: `1px solid ${theme.palette.primary.main}`,
backgroundColor: fade(
theme.palette.primary.main,
theme.palette.action.hoverOpacity,
),
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent',
},
},
'&.Mui-disabled': {
color: theme.palette.action.disabled,
},
},
'& .asyncapi__collapse-button:hover': {
color: theme.palette.primary.main,
},
'& button.asyncapi__toggle-button': {
'min-width': 'inherit',
},
'& .asyncapi__info-list li': {
'border-color': theme.palette.primary.main,
'&:hover': {
color: theme.palette.text.primary,
'border-color': theme.palette.primary.main,
'background-color': theme.palette.primary.main,
},
},
'& .asyncapi__info-list li a': {
color: theme.palette.primary.main,
'&:hover': {
color: theme.palette.getContrastText(theme.palette.primary.main),
},
},
'& .asyncapi__enum': {
color: theme.palette.secondary.main,
},
'& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div':
{
'background-color': 'inherit',
},
'& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4':
{
'background-color': 'inherit',
color: theme.palette.text.primary,
},
'& .asyncapi__additional-properties-notice': {
color: theme.palette.text.hint,
},
'& .asyncapi__code, .asyncapi__code-pre': {
background: theme.palette.background.default,
},
'& .asyncapi__schema-example-header-title': {
color: theme.palette.text.secondary,
},
'& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header':
{
'background-color': 'inherit',
color: theme.palette.text.secondary,
},
'& .asyncapi__table-header': {
background: theme.palette.background.default,
},
'& .asyncapi__table-body': {
color: theme.palette.text.primary,
},
'& .asyncapi__server-security-flow': {
background: theme.palette.background.default,
border: 'none',
},
'& .asyncapi__server-security-flows-list a': {
color: theme.palette.primary.main,
},
'& .asyncapi__table-row--nested': {
color: theme.palette.text.secondary,
},
},
}));
// The asyncapi component and related CSS has a significant size, only load it
// if the element is actually used.
const LazyAsyncApiDefinition = React.lazy(() =>
import('./AsyncApiDefinition').then(m => ({
default: m.AsyncApiDefinition,
})),
);
type Props = {
export type AsyncApiDefinitionWidgetProps = {
definition: string;
};
export const AsyncApiDefinitionWidget = ({ definition }: Props) => {
const classes = useStyles();
export const AsyncApiDefinitionWidget = (
props: AsyncApiDefinitionWidgetProps,
) => {
return (
<div className={classes.root}>
<AsyncApi schema={definition} />
</div>
<Suspense fallback={<Progress />}>
<LazyAsyncApiDefinition {...props} />
</Suspense>
);
};
@@ -15,3 +15,4 @@
*/
export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
export type { AsyncApiDefinitionWidgetProps } from './AsyncApiDefinitionWidget';
@@ -16,9 +16,9 @@
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget';
import { GraphQlDefinition } from './GraphQlDefinition';
describe('<GraphQlDefinitionWidget />', () => {
describe('<GraphQlDefinition />', () => {
it('renders graphql schema', async () => {
const definition = `
"""Hello World!"""
@@ -53,7 +53,7 @@ type Film {
};
const { getByText } = await renderInTestApp(
<GraphQlDefinitionWidget definition={definition} />,
<GraphQlDefinition definition={definition} />,
);
expect(getByText(/Film/i)).toBeInTheDocument();
@@ -0,0 +1,63 @@
/*
* Copyright 2020 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 { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core/styles';
import GraphiQL from 'graphiql';
import 'graphiql/graphiql.css';
import { buildSchema } from 'graphql';
import React from 'react';
const useStyles = makeStyles<BackstageTheme>(() => ({
root: {
height: '100%',
display: 'flex',
flexFlow: 'column nowrap',
},
graphiQlWrapper: {
flex: 1,
'@global': {
'.graphiql-container': {
boxSizing: 'initial',
height: '100%',
minHeight: '600px',
flex: '1 1 auto',
},
},
},
}));
type Props = {
definition: string;
};
export const GraphQlDefinition = ({ definition }: Props) => {
const classes = useStyles();
const schema = buildSchema(definition);
return (
<div className={classes.root}>
<div className={classes.graphiQlWrapper}>
<GraphiQL
fetcher={() => Promise.resolve(null) as any}
schema={schema}
docExplorerOpen
defaultSecondaryEditorOpen={false}
/>
</div>
</div>
);
};
@@ -14,54 +14,27 @@
* limitations under the License.
*/
import { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core/styles';
import 'graphiql/graphiql.css';
import { buildSchema } from 'graphql';
import React, { Suspense } from 'react';
import { Progress } from '@backstage/core-components';
import React, { Suspense } from 'react';
const GraphiQL = React.lazy(() => import('graphiql'));
// The graphql component, graphql and related CSS has a significant size, only
// load it if the element is actually used.
const LazyGraphQlDefinition = React.lazy(() =>
import('./GraphQlDefinition').then(m => ({
default: m.GraphQlDefinition,
})),
);
const useStyles = makeStyles<BackstageTheme>(() => ({
root: {
height: '100%',
display: 'flex',
flexFlow: 'column nowrap',
},
graphiQlWrapper: {
flex: 1,
'@global': {
'.graphiql-container': {
boxSizing: 'initial',
height: '100%',
minHeight: '600px',
flex: '1 1 auto',
},
},
},
}));
type Props = {
definition: any;
export type GraphQlDefinitionWidgetProps = {
definition: string;
};
export const GraphQlDefinitionWidget = ({ definition }: Props) => {
const classes = useStyles();
const schema = buildSchema(definition);
export const GraphQlDefinitionWidget = (
props: GraphQlDefinitionWidgetProps,
) => {
return (
<Suspense fallback={<Progress />}>
<div className={classes.root}>
<div className={classes.graphiQlWrapper}>
<GraphiQL
fetcher={() => Promise.resolve(null) as any}
schema={schema}
docExplorerOpen
defaultSecondaryEditorOpen={false}
/>
</div>
</div>
<LazyGraphQlDefinition {...props} />
</Suspense>
);
};
@@ -15,3 +15,4 @@
*/
export { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget';
export type { GraphQlDefinitionWidgetProps } from './GraphQlDefinitionWidget';
@@ -17,9 +17,9 @@
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
import { OpenApiDefinition } from './OpenApiDefinition';
describe('<OpenApiDefinitionWidget />', () => {
describe('<OpenApiDefinition />', () => {
it('renders openapi spec', async () => {
const definition = `
openapi: "3.0.0"
@@ -39,7 +39,7 @@ paths:
description: Success
`;
const { getByText } = await renderInTestApp(
<OpenApiDefinitionWidget definition={definition} />,
<OpenApiDefinition definition={definition} />,
);
// swagger-ui loads the documentation asynchronously
@@ -51,7 +51,7 @@ paths:
it('renders error if definition is missing', async () => {
const { getByText } = await renderInTestApp(
<OpenApiDefinitionWidget definition="" />,
<OpenApiDefinition definition="" />,
);
expect(getByText(/No API definition provided/i)).toBeInTheDocument();
});
@@ -0,0 +1,132 @@
/*
* Copyright 2020 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 { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useState } from 'react';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
const useStyles = makeStyles(theme => ({
root: {
'& .swagger-ui': {
fontFamily: 'inherit',
color: theme.palette.text.primary,
[`& .info h1,
.info h2,
.info h3,
.info h4,
.info h5,
.info h6`]: {
fontFamily: 'inherit',
color: theme.palette.text.primary,
},
[`& .scheme-container`]: {
backgroundColor: theme.palette.background.default,
},
[`& .opblock-tag,
.opblock-tag small,
table thead tr td,
table thead tr th`]: {
fontFamily: 'inherit',
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
},
[`& section.models,
section.models.is-open h4`]: {
borderColor: theme.palette.divider,
},
[`& .opblock .opblock-summary-description,
.parameter__type,
table.headers td,
.model-title,
.model .property.primitive,
section h3`]: {
fontFamily: 'inherit',
color: theme.palette.text.secondary,
},
[`& .opblock .opblock-summary-operation-id,
.opblock .opblock-summary-path,
.opblock .opblock-summary-path__deprecated,
.opblock .opblock-section-header h4,
.parameter__name,
.response-col_status,
.response-col_links,
.responses-inner h4,
.responses-inner h5,
.opblock-section-header .btn,
.tab li,
.info li,
.info p,
.info table,
section.models h4,
.info .title,
table.model tr.description,
.property-row`]: {
fontFamily: 'inherit',
color: theme.palette.text.primary,
},
[`& .opblock .opblock-section-header,
.model-box,
section.models .model-container`]: {
background: theme.palette.background.default,
},
[`& .prop-format,
.parameter__in`]: {
color: theme.palette.text.disabled,
},
[`& .opblock-description-wrapper p,
.opblock-external-docs-wrapper p,
.opblock-title_normal p,
.response-control-media-type__accept-message,
.opblock .opblock-section-header>label,
.scheme-container .schemes>label,
.info .base-url,
.model`]: {
color: theme.palette.text.hint,
},
[`& .parameter__name.required:after`]: {
color: theme.palette.warning.dark,
},
[`& .prop-type`]: {
color: theme.palette.primary.main,
},
},
},
}));
export type OpenApiDefinitionProps = {
definition: string;
};
export const OpenApiDefinition = ({ definition }: OpenApiDefinitionProps) => {
const classes = useStyles();
// Due to a bug in the swagger-ui-react component, the component needs
// to be created without content first.
const [def, setDef] = useState('');
useEffect(() => {
const timer = setTimeout(() => setDef(definition), 0);
return () => clearTimeout(timer);
}, [definition, setDef]);
return (
<div className={classes.root}>
<SwaggerUI spec={def} deepLinking />
</div>
);
};
@@ -14,81 +14,27 @@
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useState } from 'react';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
import { Progress } from '@backstage/core-components';
import React, { Suspense } from 'react';
// TODO: Schemas
// The swagger-ui component and related CSS has a significant size, only load it
// if the element is actually used.
const LazyOpenApiDefinition = React.lazy(() =>
import('./OpenApiDefinition').then(m => ({
default: m.OpenApiDefinition,
})),
);
const useStyles = makeStyles(theme => ({
root: {
'& .swagger-ui, .info h1, .info h2, .info h3, .info h4, .info h': {
'font-family': 'inherit',
color: theme.palette.text.primary,
},
'& .scheme-container': {
'background-color': theme.palette.background.default,
},
'& .opblock-tag, .opblock-tag small, table thead tr td, table thead tr th':
{
color: theme.palette.text.primary,
'border-color': theme.palette.divider,
},
'& section.models, section.models.is-open h4': {
'border-color': theme.palette.divider,
},
'& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3':
{
color: theme.palette.text.secondary,
},
'& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row':
{
color: theme.palette.text.primary,
},
'& .opblock .opblock-section-header, .model-box, section.models .model-container':
{
background: theme.palette.background.default,
},
'& .prop-format, .parameter__in': {
color: theme.palette.text.disabled,
},
'& ': {
color: theme.palette.text.primary,
'border-color': theme.palette.divider,
},
'& .opblock-description-wrapper p, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, .opblock .opblock-section-header>label, .scheme-container .schemes>label, .info .base-url, .model':
{
color: theme.palette.text.hint,
},
'& .parameter__name.required:after': {
color: theme.palette.warning.dark,
},
'& .prop-type': {
color: theme.palette.primary.main,
},
},
}));
type Props = {
export type OpenApiDefinitionWidgetProps = {
definition: string;
};
export const OpenApiDefinitionWidget = ({ definition }: Props) => {
const classes = useStyles();
// Due to a bug in the swagger-ui-react component, the component needs
// to be created without content first.
const [def, setDef] = useState('');
useEffect(() => {
const timer = setTimeout(() => setDef(definition), 0);
return () => clearTimeout(timer);
}, [definition, setDef]);
export const OpenApiDefinitionWidget = (
props: OpenApiDefinitionWidgetProps,
) => {
return (
<div className={classes.root}>
<SwaggerUI spec={def} deepLinking />
</div>
<Suspense fallback={<Progress />}>
<LazyOpenApiDefinition {...props} />
</Suspense>
);
};
@@ -15,3 +15,4 @@
*/
export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
export type { OpenApiDefinitionWidgetProps } from './OpenApiDefinitionWidget';
@@ -17,13 +17,19 @@
import React from 'react';
import { CodeSnippet } from '@backstage/core-components';
type Props = {
export type PlainApiDefinitionWidgetProps = {
definition: any;
language: string;
};
export const PlainApiDefinitionWidget = ({ definition, language }: Props) => {
export const PlainApiDefinitionWidget = (
props: PlainApiDefinitionWidgetProps,
) => {
return (
<CodeSnippet text={definition} language={language} showCopyCodeButton />
<CodeSnippet
text={props.definition}
language={props.language}
showCopyCodeButton
/>
);
};
@@ -15,3 +15,4 @@
*/
export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
export type { PlainApiDefinitionWidgetProps } from './PlainApiDefinitionWidget';
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { Progress } from '@backstage/core-components';
import {
Accordion,
AccordionDetails,
AccordionSummary,
LinearProgress,
Typography,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
@@ -85,7 +85,7 @@ export const ActionOutput = ({
{messages.length === 0 ? (
'Nothing here...'
) : (
<Suspense fallback={<LinearProgress />}>
<Suspense fallback={<Progress />}>
<div style={{ height: '20vh', width: '100%' }}>
<LazyLog text={messages.join('\n')} extraLines={1} enableSearch />
</div>
@@ -14,12 +14,15 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Progress } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import {
Accordion,
AccordionSummary,
CircularProgress,
Fade,
LinearProgress,
makeStyles,
Modal,
Theme,
@@ -27,15 +30,11 @@ import {
Typography,
Zoom,
} from '@material-ui/core';
import React, { Suspense } from 'react';
import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs';
import { useProjectName } from '../useProjectName';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import DescriptionIcon from '@material-ui/icons/Description';
import { Entity } from '@backstage/catalog-model';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import React, { Suspense } from 'react';
import { useProjectName } from '../useProjectName';
import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
const LinePart = React.lazy(() => import('react-lazylog/build/LinePart'));
@@ -72,7 +71,7 @@ const DisplayLog = ({
className: string;
}) => {
return (
<Suspense fallback={<LinearProgress />}>
<Suspense fallback={<Progress />}>
<div className={className}>
<LazyLog
text={jobLogs ?? 'No Values Found'}
@@ -14,38 +14,40 @@
* limitations under the License.
*/
import React, { useState, useEffect, memo, useMemo } from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import { useParams } from 'react-router';
import { useTaskEventStream } from '../hooks/useEventStream';
import LazyLog from 'react-lazylog/build/LazyLog';
import {
Content,
ErrorPage,
Header,
Lifecycle,
Page,
Progress,
} from '@backstage/core-components';
import { BackstageTheme } from '@backstage/theme';
import {
CircularProgress,
Paper,
StepButton,
StepIconProps,
} from '@material-ui/core';
import { Status, TaskOutput } from '../../types';
import { DateTime, Interval } from 'luxon';
import { useInterval } from 'react-use';
import Check from '@material-ui/icons/Check';
import Grid from '@material-ui/core/Grid';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Stepper from '@material-ui/core/Stepper';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Cancel from '@material-ui/icons/Cancel';
import Check from '@material-ui/icons/Check';
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import classNames from 'classnames';
import { BackstageTheme } from '@backstage/theme';
import { DateTime, Interval } from 'luxon';
import React, { memo, Suspense, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { useInterval } from 'react-use';
import { Status, TaskOutput } from '../../types';
import { useTaskEventStream } from '../hooks/useEventStream';
import { TaskPageLinks } from './TaskPageLinks';
import {
Page,
Header,
Lifecycle,
Content,
ErrorPage,
} from '@backstage/core-components';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
// typings are wrong for this library, so fallback to not parsing types.
const humanizeDuration = require('humanize-duration');
@@ -213,9 +215,17 @@ export const TaskStatusStepper = memo(
const TaskLogger = memo(({ log }: { log: string }) => {
return (
<div style={{ height: '80vh' }}>
<LazyLog text={log} extraLines={1} follow selectableLines enableSearch />
</div>
<Suspense fallback={<Progress />}>
<div style={{ height: '80vh' }}>
<LazyLog
text={log}
extraLines={1}
follow
selectableLines
enableSearch
/>
</div>
</Suspense>
);
});
@@ -23,9 +23,9 @@ import {
// react-lazylog is based on a react-virtualized component which doesn't
// write the content to the dom, so we mock it.
jest.mock('react-lazylog', () => {
jest.mock('react-lazylog/build/LazyLog', () => {
return {
LazyLog: ({ text }: { text: string }) => {
default: ({ text }: { text: string }) => {
return <p>{text}</p>;
},
};
@@ -46,18 +46,20 @@ describe('<TechDocsBuildLogs />', () => {
});
describe('<TechDocsBuildLogsDrawerContent />', () => {
it('should render with empty log', () => {
it('should render with empty log', async () => {
const onClose = jest.fn();
const rendered = render(
<TechDocsBuildLogsDrawerContent buildLog={[]} onClose={onClose} />,
);
expect(rendered.getByText(/Build Details/i)).toBeInTheDocument();
expect(rendered.getByText(/Waiting for logs.../i)).toBeInTheDocument();
expect(
await rendered.findByText(/Waiting for logs.../i),
).toBeInTheDocument();
expect(onClose).toBeCalledTimes(0);
});
it('should render with empty logs', () => {
it('should render logs', async () => {
const onClose = jest.fn();
const rendered = render(
<TechDocsBuildLogsDrawerContent
@@ -66,11 +68,8 @@ describe('<TechDocsBuildLogsDrawerContent />', () => {
/>,
);
expect(rendered.getByText(/Build Details/i)).toBeInTheDocument();
expect(
rendered.queryByText(/Waiting for logs.../i),
).not.toBeInTheDocument();
expect(rendered.getByText(/Line 1/i)).toBeInTheDocument();
expect(rendered.getByText(/Line 2/i)).toBeInTheDocument();
expect(await rendered.findByText(/Line 1/i)).toBeInTheDocument();
expect(await rendered.findByText(/Line 2/i)).toBeInTheDocument();
expect(onClose).toBeCalledTimes(0);
});
@@ -80,7 +79,7 @@ describe('<TechDocsBuildLogsDrawerContent />', () => {
const rendered = render(
<TechDocsBuildLogsDrawerContent buildLog={[]} onClose={onClose} />,
);
rendered.getByRole('button').click();
rendered.getByTitle('Close the drawer').click();
expect(onClose).toBeCalledTimes(1);
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { Progress } from '@backstage/core-components';
import {
Button,
createStyles,
@@ -25,9 +26,9 @@ import {
Typography,
} from '@material-ui/core';
import Close from '@material-ui/icons/Close';
import * as React from 'react';
import { useState } from 'react';
import { LazyLog } from 'react-lazylog';
import React, { Suspense, useState } from 'react';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
const useDrawerStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -83,15 +84,17 @@ export const TechDocsBuildLogsDrawerContent = ({
</IconButton>
</Grid>
<LazyLog
text={
buildLog.length === 0 ? 'Waiting for logs...' : buildLog.join('\n')
}
extraLines={1}
follow
selectableLines
enableSearch
/>
<Suspense fallback={<Progress />}>
<LazyLog
text={
buildLog.length === 0 ? 'Waiting for logs...' : buildLog.join('\n')
}
extraLines={1}
follow
selectableLines
enableSearch
/>
</Suspense>
</Grid>
);
};