diff --git a/.changeset/techdocs-few-balloons-lie.md b/.changeset/techdocs-few-balloons-lie.md new file mode 100644 index 0000000000..4e94c117ac --- /dev/null +++ b/.changeset/techdocs-few-balloons-lie.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Show a "Refresh" button to if the content is stale. +This removes the need to do a full page-reload to display more recent TechDocs content. diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 80e81fe178..659a4e2caf 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -148,6 +148,11 @@ createDevApp() {createPage({ + entityDocs: ({ called, content }) => { + return called === 0 + ? content + : content.replace(/World/, 'New World'); + }, syncDocs: () => 'updated', syncDocsDelay: 2000, })} diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index f8938cf8d8..dcb53dfedc 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -19,7 +19,7 @@ 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 { CircularProgress, useTheme } from '@material-ui/core'; +import { Button, 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'; @@ -50,12 +50,13 @@ export const Reader = ({ entityId, onReady }: Props) => { const { '*': path } = useParams(); const theme = useTheme(); - const { state, content: rawPage, errorMessage, buildLog } = useReaderState( - kind, - namespace, - name, - path, - ); + const { + state, + contentReload, + content: rawPage, + errorMessage, + buildLog, + } = useReaderState(kind, namespace, name, path); const techdocsStorageApi = useApi(techdocsStorageApiRef); const [sidebars, setSidebars] = useState(); @@ -338,7 +339,15 @@ export const Reader = ({ entityId, onReady }: Props) => { )} {state === 'CONTENT_STALE_READY' && ( - + contentReload()}> + Refresh + + } + > A newer version of this documentation is now available, please refresh to view. diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index 3a26f91279..99ecf05bee 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -259,6 +259,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); await waitForValueToChange(() => result.current.state); @@ -268,6 +269,7 @@ describe('useReaderState', () => { content: 'my content', errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( @@ -312,6 +314,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); await waitForValueToChange(() => result.current.state); @@ -321,6 +324,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: ' Load error: NotFoundError: Page Not Found', buildLog: ['Line 1', 'Line 2'], + contentReload: expect.any(Function), }); await waitForValueToChange(() => result.current.state); @@ -330,6 +334,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); await waitForValueToChange(() => result.current.state); @@ -339,6 +344,7 @@ describe('useReaderState', () => { content: 'my content', errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); expect(techdocsStorageApi.getEntityDocs).toBeCalledTimes(2); @@ -359,7 +365,12 @@ describe('useReaderState', () => { }); it('should handle stale content', async () => { - techdocsStorageApi.getEntityDocs.mockResolvedValue('my content'); + techdocsStorageApi.getEntityDocs + .mockResolvedValueOnce('my content') + .mockImplementationOnce(async () => { + await new Promise(resolve => setTimeout(resolve, 1100)); + return 'my new content'; + }); techdocsStorageApi.syncEntityDocs.mockImplementation( async (_, logHandler) => { logHandler?.call(this, 'Line 1'); @@ -380,6 +391,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); // the content is returned but the sync is in progress @@ -389,6 +401,7 @@ describe('useReaderState', () => { content: 'my content', errorMessage: '', buildLog: ['Line 1', 'Line 2'], + contentReload: expect.any(Function), }); // the sync takes longer than 1 seconds so the refreshing state starts @@ -398,17 +411,43 @@ describe('useReaderState', () => { content: 'my content', errorMessage: '', buildLog: ['Line 1', 'Line 2'], + contentReload: expect.any(Function), }); - // the content is up-to-date + // the content is updated but not yet displayed await waitForValueToChange(() => result.current.state); expect(result.current).toEqual({ state: 'CONTENT_STALE_READY', content: 'my content', errorMessage: '', buildLog: ['Line 1', 'Line 2'], + contentReload: expect.any(Function), }); + // reload the content + result.current.contentReload(); + + // the new content refresh is triggered + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + buildLog: [], + contentReload: expect.any(Function), + }); + + // the new content is loaded + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_FRESH', + content: 'my new content', + errorMessage: '', + buildLog: [], + contentReload: expect.any(Function), + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledTimes(2); expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( { kind: 'Component', namespace: 'default', name: 'backstage' }, '/example', @@ -441,6 +480,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: '', buildLog: [], + contentReload: expect.any(Function), }); // the content loading threw an error @@ -450,6 +490,7 @@ describe('useReaderState', () => { content: undefined, errorMessage: ' Load error: NotFoundError: Some error description', buildLog: [], + contentReload: expect.any(Function), }); expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( diff --git a/plugins/techdocs/src/reader/components/useReaderState.ts b/plugins/techdocs/src/reader/components/useReaderState.ts index a2e70b5a38..f7201c84f7 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.ts +++ b/plugins/techdocs/src/reader/components/useReaderState.ts @@ -223,6 +223,7 @@ export function useReaderState( path: string, ): { state: ContentStateTypes; + contentReload: () => void; content?: string; errorMessage?: string; buildLog: string[]; @@ -342,6 +343,7 @@ export function useReaderState( return { state: displayState, + contentReload, content: state.content, errorMessage, buildLog: state.buildLog,