Provide a Drawer component to follow a running build
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Provide a Drawer component to follow a running build.
|
||||
This can be used to debug the rendering and get build logs in case an error occurs.
|
||||
@@ -47,6 +47,7 @@
|
||||
"eventsource": "^1.1.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4",
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
*/
|
||||
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { useTheme } from '@material-ui/core';
|
||||
import { CircularProgress, useTheme } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -35,8 +36,8 @@ import {
|
||||
simplifyMkdocsFooter,
|
||||
transform as transformer,
|
||||
} from '../transformers';
|
||||
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
|
||||
import { TechDocsNotFound } from './TechDocsNotFound';
|
||||
import TechDocsProgressBar from './TechDocsProgressBar';
|
||||
import { useReaderState } from './useReaderState';
|
||||
|
||||
type Props = {
|
||||
@@ -49,7 +50,7 @@ export const Reader = ({ entityId, onReady }: Props) => {
|
||||
const { '*': path } = useParams();
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
|
||||
const { state, content: rawPage, errorMessage } = useReaderState(
|
||||
const { state, content: rawPage, errorMessage, buildLog } = useReaderState(
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
@@ -313,11 +314,25 @@ export const Reader = ({ entityId, onReady }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(state === 'CHECKING' || state === 'INITIAL_BUILD') && (
|
||||
<TechDocsProgressBar />
|
||||
{state === 'CHECKING' && <Progress />}
|
||||
{state === 'INITIAL_BUILD' && (
|
||||
<Alert
|
||||
variant="outlined"
|
||||
severity="info"
|
||||
icon={<CircularProgress size="24px" />}
|
||||
action={<TechDocsBuildLogs buildLog={buildLog} />}
|
||||
>
|
||||
Documentation is accessed for the first time and is being prepared.
|
||||
The subsequent loads are much faster.
|
||||
</Alert>
|
||||
)}
|
||||
{state === 'CONTENT_STALE_REFRESHING' && (
|
||||
<Alert variant="outlined" severity="info">
|
||||
<Alert
|
||||
variant="outlined"
|
||||
severity="info"
|
||||
icon={<CircularProgress size="24px" />}
|
||||
action={<TechDocsBuildLogs buildLog={buildLog} />}
|
||||
>
|
||||
A newer version of this documentation is being prepared and will be
|
||||
available shortly.
|
||||
</Alert>
|
||||
@@ -329,7 +344,11 @@ export const Reader = ({ entityId, onReady }: Props) => {
|
||||
</Alert>
|
||||
)}
|
||||
{state === 'CONTENT_STALE_ERROR' && (
|
||||
<Alert variant="outlined" severity="error">
|
||||
<Alert
|
||||
variant="outlined"
|
||||
severity="error"
|
||||
action={<TechDocsBuildLogs buildLog={buildLog} />}
|
||||
>
|
||||
Building a newer version of this documentation failed. {errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2021 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
TechDocsBuildLogs,
|
||||
TechDocsBuildLogsDrawerContent,
|
||||
} from './TechDocsBuildLogs';
|
||||
|
||||
// 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', () => {
|
||||
return {
|
||||
LazyLog: ({ text }: { text: string }) => {
|
||||
return <p>{text}</p>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('<TechDocsBuildLogs />', () => {
|
||||
it('should render with button', () => {
|
||||
const rendered = render(<TechDocsBuildLogs buildLog={[]} />);
|
||||
expect(rendered.getByText(/Show Build Logs/i)).toBeInTheDocument();
|
||||
expect(rendered.queryByText(/Build Details/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open drawer', () => {
|
||||
const rendered = render(<TechDocsBuildLogs buildLog={[]} />);
|
||||
rendered.getByText(/Show Build Logs/i).click();
|
||||
expect(rendered.getByText(/Build Details/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<TechDocsBuildLogsDrawerContent />', () => {
|
||||
it('should render with empty log', () => {
|
||||
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(onClose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should render with empty logs', () => {
|
||||
const onClose = jest.fn();
|
||||
const rendered = render(
|
||||
<TechDocsBuildLogsDrawerContent
|
||||
buildLog={['Line 1', 'Line 2']}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
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(onClose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should call onClose', () => {
|
||||
const onClose = jest.fn();
|
||||
const rendered = render(
|
||||
<TechDocsBuildLogsDrawerContent buildLog={[]} onClose={onClose} />,
|
||||
);
|
||||
rendered.getByRole('button').click();
|
||||
|
||||
expect(onClose).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
Button,
|
||||
createStyles,
|
||||
Drawer,
|
||||
Grid,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
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';
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: '75%',
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '50%',
|
||||
},
|
||||
padding: theme.spacing(2.5),
|
||||
},
|
||||
root: {
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const TechDocsBuildLogsDrawerContent = ({
|
||||
buildLog,
|
||||
onClose,
|
||||
}: {
|
||||
buildLog: string[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const classes = useDrawerStyles();
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
className={classes.root}
|
||||
spacing={0}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
justify="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Typography variant="h5">Build Details</Typography>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={onClose}
|
||||
color="inherit"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<LazyLog
|
||||
text={
|
||||
buildLog.length === 0 ? 'Waiting for logs...' : buildLog.join('\n')
|
||||
}
|
||||
extraLines={1}
|
||||
follow
|
||||
selectableLines
|
||||
enableSearch
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const TechDocsBuildLogs = ({ buildLog }: { buildLog: string[] }) => {
|
||||
const classes = useDrawerStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button color="inherit" onClick={() => setOpen(true)}>
|
||||
Show Build Logs
|
||||
</Button>
|
||||
<Drawer
|
||||
classes={{ paper: classes.paper }}
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<TechDocsBuildLogsDrawerContent
|
||||
buildLog={buildLog}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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 TechDocsProgressBar from './TechDocsProgressBar';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('<TechDocsProgressBar />', () => {
|
||||
it('should render a message if techdocs page takes more time to load', () => {
|
||||
const rendered = render(wrapInTestApp(<TechDocsProgressBar />));
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250);
|
||||
});
|
||||
expect(rendered.getByTestId('progress')).toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('delay-reason')).toBeNull();
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(rendered.getByTestId('delay-reason')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
import { useMountedState } from 'react-use';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
|
||||
const TechDocsProgressBar = () => {
|
||||
const isMounted = useMountedState();
|
||||
const [hasBeenDelayed, setHasBeenDelayed] = useState(false);
|
||||
|
||||
const delayReason = `Docs are still loading...Backstage takes some extra time to load docs
|
||||
for the first time. The subsequent loads are much faster.`;
|
||||
|
||||
// Allowed time that docs can take to load (in milliseconds)
|
||||
const allowedDelayTime = 5000;
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (isMounted()) {
|
||||
setHasBeenDelayed(true);
|
||||
}
|
||||
}, allowedDelayTime);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasBeenDelayed ? (
|
||||
<Typography data-testid="delay-reason">{delayReason}</Typography>
|
||||
) : null}
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TechDocsProgressBar;
|
||||
@@ -82,6 +82,7 @@ describe('useReaderState', () => {
|
||||
activeSyncState: 'CHECKING',
|
||||
contentLoading: false,
|
||||
path: '',
|
||||
buildLog: ['1', '2'],
|
||||
};
|
||||
|
||||
it('should return a copy of the state', () => {
|
||||
@@ -89,12 +90,14 @@ describe('useReaderState', () => {
|
||||
activeSyncState: 'CHECKING',
|
||||
contentLoading: false,
|
||||
path: '/',
|
||||
buildLog: ['1', '2'],
|
||||
});
|
||||
|
||||
expect(oldState).toEqual({
|
||||
activeSyncState: 'CHECKING',
|
||||
contentLoading: false,
|
||||
path: '',
|
||||
buildLog: ['1', '2'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,6 +211,33 @@ describe('useReaderState', () => {
|
||||
activeSyncState: 'BUILDING',
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear buildLog on "CHECKING"', () => {
|
||||
expect(
|
||||
reducer(oldState, {
|
||||
type: 'sync',
|
||||
state: 'CHECKING',
|
||||
}),
|
||||
).toEqual({
|
||||
...oldState,
|
||||
activeSyncState: 'CHECKING',
|
||||
buildLog: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"buildLog" action', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
reducer(oldState, {
|
||||
type: 'buildLog',
|
||||
log: 'Another Line',
|
||||
}),
|
||||
).toEqual({
|
||||
...oldState,
|
||||
buildLog: ['1', '2', 'Another Line'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,6 +258,7 @@ describe('useReaderState', () => {
|
||||
state: 'CHECKING',
|
||||
content: undefined,
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
@@ -236,17 +267,21 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_FRESH',
|
||||
content: 'my content',
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toBeCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
});
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,10 +292,14 @@ describe('useReaderState', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return 'my content';
|
||||
});
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
return 'updated';
|
||||
});
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(
|
||||
async (_, logHandler) => {
|
||||
logHandler?.call(this, 'Line 1');
|
||||
logHandler?.call(this, 'Line 2');
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
return 'updated';
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
@@ -272,6 +311,7 @@ describe('useReaderState', () => {
|
||||
state: 'CHECKING',
|
||||
content: undefined,
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
@@ -280,6 +320,7 @@ describe('useReaderState', () => {
|
||||
state: 'INITIAL_BUILD',
|
||||
content: undefined,
|
||||
errorMessage: ' Load error: NotFoundError: Page Not Found',
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
@@ -288,6 +329,7 @@ describe('useReaderState', () => {
|
||||
state: 'CHECKING',
|
||||
content: undefined,
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
@@ -296,6 +338,7 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_FRESH',
|
||||
content: 'my content',
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toBeCalledTimes(2);
|
||||
@@ -304,20 +347,27 @@ describe('useReaderState', () => {
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledTimes(1);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
});
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle stale content', async () => {
|
||||
techdocsStorageApi.getEntityDocs.mockResolvedValue('my content');
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
return 'updated';
|
||||
});
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(
|
||||
async (_, logHandler) => {
|
||||
logHandler?.call(this, 'Line 1');
|
||||
logHandler?.call(this, 'Line 2');
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
return 'updated';
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
@@ -329,6 +379,7 @@ describe('useReaderState', () => {
|
||||
state: 'CHECKING',
|
||||
content: undefined,
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
// the content is returned but the sync is in progress
|
||||
@@ -337,6 +388,7 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_FRESH',
|
||||
content: 'my content',
|
||||
errorMessage: '',
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
});
|
||||
|
||||
// the sync takes longer than 1 seconds so the refreshing state starts
|
||||
@@ -345,6 +397,7 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_STALE_REFRESHING',
|
||||
content: 'my content',
|
||||
errorMessage: '',
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
});
|
||||
|
||||
// the content is up-to-date
|
||||
@@ -353,17 +406,21 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_STALE_READY',
|
||||
content: 'my content',
|
||||
errorMessage: '',
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toBeCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
});
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -383,6 +440,7 @@ describe('useReaderState', () => {
|
||||
state: 'CHECKING',
|
||||
content: undefined,
|
||||
errorMessage: '',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
// the content loading threw an error
|
||||
@@ -391,17 +449,21 @@ describe('useReaderState', () => {
|
||||
state: 'CONTENT_NOT_FOUND',
|
||||
content: undefined,
|
||||
errorMessage: ' Load error: NotFoundError: Some error description',
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toBeCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
});
|
||||
expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,7 +137,8 @@ type ReducerActions =
|
||||
contentLoading?: true;
|
||||
contentError?: Error;
|
||||
}
|
||||
| { type: 'navigate'; path: string };
|
||||
| { type: 'navigate'; path: string }
|
||||
| { type: 'buildLog'; log: string };
|
||||
|
||||
type ReducerState = {
|
||||
/**
|
||||
@@ -161,6 +162,11 @@ type ReducerState = {
|
||||
|
||||
contentError?: Error;
|
||||
syncError?: Error;
|
||||
|
||||
/**
|
||||
* A list of log messages that were emitted by the build process.
|
||||
*/
|
||||
buildLog: string[];
|
||||
};
|
||||
|
||||
export function reducer(
|
||||
@@ -171,6 +177,11 @@ export function reducer(
|
||||
|
||||
switch (action.type) {
|
||||
case 'sync':
|
||||
// reset the build log when a new check starts
|
||||
if (action.state === 'CHECKING') {
|
||||
newState.buildLog = [];
|
||||
}
|
||||
|
||||
newState.activeSyncState = action.state;
|
||||
newState.syncError = action.syncError;
|
||||
break;
|
||||
@@ -185,6 +196,10 @@ export function reducer(
|
||||
newState.path = action.path;
|
||||
break;
|
||||
|
||||
case 'buildLog':
|
||||
newState.buildLog = newState.buildLog.concat(action.log);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
@@ -195,6 +210,7 @@ export function reducer(
|
||||
['content', 'navigate'].includes(action.type)
|
||||
) {
|
||||
newState.activeSyncState = 'UP_TO_DATE';
|
||||
newState.buildLog = [];
|
||||
}
|
||||
|
||||
return newState;
|
||||
@@ -205,11 +221,17 @@ export function useReaderState(
|
||||
namespace: string,
|
||||
name: string,
|
||||
path: string,
|
||||
): { state: ContentStateTypes; content?: string; errorMessage?: string } {
|
||||
): {
|
||||
state: ContentStateTypes;
|
||||
content?: string;
|
||||
errorMessage?: string;
|
||||
buildLog: string[];
|
||||
} {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
activeSyncState: 'CHECKING',
|
||||
path,
|
||||
contentLoading: true,
|
||||
buildLog: [],
|
||||
});
|
||||
|
||||
const techdocsStorageApi = useApi(techdocsStorageApiRef);
|
||||
@@ -257,11 +279,16 @@ export function useReaderState(
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const result = await techdocsStorageApi.syncEntityDocs({
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
});
|
||||
const result = await techdocsStorageApi.syncEntityDocs(
|
||||
{
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
},
|
||||
log => {
|
||||
dispatch({ type: 'buildLog', log });
|
||||
},
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
case 'updated':
|
||||
@@ -317,5 +344,6 @@ export function useReaderState(
|
||||
state: displayState,
|
||||
content: state.content,
|
||||
errorMessage,
|
||||
buildLog: state.buildLog,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user