Merge pull request #6709 from backstage/techdocs-common/case-sensitive-migration

[TechDocs] Beta!
This commit is contained in:
Eric Peterson
2021-08-25 13:06:31 +02:00
committed by GitHub
44 changed files with 1506 additions and 864 deletions
+6 -1
View File
@@ -271,6 +271,11 @@ export const TechDocsPicker: () => null;
const techdocsPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
docRoot: RouteRef<{
name: string;
kind: string;
namespace: string;
}>;
entityContent: RouteRef<undefined>;
},
{}
@@ -374,7 +379,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
//
// src/home/components/EntityListDocsTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts
// src/home/components/EntityListDocsTable.d.ts:12:5 - (ae-forgotten-export) The symbol "actionFactories" needs to be exported by the entry point index.d.ts
// src/plugin.d.ts:24:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts
// src/plugin.d.ts:29:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
```
+7
View File
@@ -26,6 +26,13 @@ export interface Config {
*/
builder: 'local' | 'external';
/**
* Allows fallback to case-sensitive triplets in case of migration issues.
* @visibility frontend
* @see https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta
*/
legacyUseCaseSensitiveTripletPaths?: boolean;
/**
* @example http://localhost:7000/api/techdocs
* @visibility frontend
+3 -11
View File
@@ -18,11 +18,6 @@ import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Route, Routes } from 'react-router-dom';
import {
rootRouteRef,
rootDocsRouteRef,
rootCatalogDocsRouteRef,
} from './routes';
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage';
import { EntityPageDocs } from './EntityPageDocs';
@@ -33,9 +28,9 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
export const Router = () => {
return (
<Routes>
<Route path={`/${rootRouteRef.path}`} element={<TechDocsIndexPage />} />
<Route path="/" element={<TechDocsIndexPage />} />
<Route
path={`/${rootDocsRouteRef.path}`}
path="/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</Routes>
@@ -58,10 +53,7 @@ export const EmbeddedDocsRouter = (_props: Props) => {
return (
<Routes>
<Route
path={`/${rootCatalogDocsRouteRef.path}`}
element={<EntityPageDocs entity={entity} />}
/>
<Route path="/*" element={<EntityPageDocs entity={entity} />} />
</Routes>
);
};
@@ -30,6 +30,7 @@ import {
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -73,6 +74,11 @@ describe('TechDocs Home', () => {
<ApiProvider apis={apiRegistry}>
<DefaultTechDocsHome />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
@@ -17,11 +17,36 @@
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { configApiRef } from '@backstage/core-plugin-api';
import { DocsCardGrid } from './DocsCardGrid';
import { rootDocsRouteRef } from '../../routes';
// Hacky way to mock a specific boolean config value.
const getOptionalBooleanMock = jest.fn().mockReturnValue(false);
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: (apiRef: any) => {
const actualUseApi = jest.requireActual(
'@backstage/core-plugin-api',
).useApi;
const actualApi = actualUseApi(apiRef);
if (apiRef === configApiRef) {
const configReader = actualApi;
configReader.getOptionalBoolean = getOptionalBooleanMock;
return configReader;
}
return actualApi;
},
}));
describe('Entity Docs Card Grid', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render all entities passed ot it', async () => {
const { findByText } = render(
const { findByText, findAllByRole } = render(
wrapInTestApp(
<DocsCardGrid
entities={[
@@ -47,9 +72,58 @@ describe('Entity Docs Card Grid', () => {
},
]}
/>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
expect(await findByText('testName')).toBeInTheDocument();
expect(await findByText('testName2')).toBeInTheDocument();
const [button1, button2] = await findAllByRole('button');
expect(button1.getAttribute('href')).toContain(
'/docs/default/testkind/testname',
);
expect(button2.getAttribute('href')).toContain(
'/docs/default/testkind2/testname2',
);
});
it('should fall back to case-sensitive links when configured', async () => {
getOptionalBooleanMock.mockReturnValue(true);
const { findByRole } = render(
wrapInTestApp(
<DocsCardGrid
entities={[
{
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
namespace: 'SomeNamespace',
},
spec: {
owner: 'techdocs@example.com',
},
},
]}
/>,
{
mountedRoutes: {
'/techdocs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
const button = await findByRole('button');
expect(getOptionalBooleanMock).toHaveBeenCalledWith(
'techdocs.legacyUseCaseSensitiveTripletPaths',
);
expect(button.getAttribute('href')).toContain(
'/techdocs/SomeNamespace/TestKind/testName',
);
});
});
@@ -15,9 +15,9 @@
*/
import React from 'react';
import { generatePath } from 'react-router-dom';
import { Entity } from '@backstage/catalog-model';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
import { rootDocsRouteRef } from '../../routes';
@@ -32,6 +32,15 @@ export const DocsCardGrid = ({
}: {
entities: Entity[] | undefined;
}) => {
const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
// Lower-case entity triplets by default, but allow override.
const toLowerMaybe = useApi(configApiRef).getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
)
? (str: string) => str
: (str: string) => str.toLocaleLowerCase();
if (!entities) return null;
return (
<ItemCardGrid data-testid="docs-explore">
@@ -45,10 +54,12 @@ export const DocsCardGrid = ({
<CardContent>{entity.metadata.description}</CardContent>
<CardActions>
<Button
to={generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
to={getRouteToReaderPageFor({
namespace: toLowerMaybe(
entity.metadata.namespace ?? 'default',
),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
})}
color="primary"
>
@@ -16,9 +16,34 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { configApiRef } from '@backstage/core-plugin-api';
import { DocsTable } from './DocsTable';
import { rootDocsRouteRef } from '../../routes';
// Hacky way to mock a specific boolean config value.
const getOptionalBooleanMock = jest.fn().mockReturnValue(false);
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: (apiRef: any) => {
const actualUseApi = jest.requireActual(
'@backstage/core-plugin-api',
).useApi;
const actualApi = actualUseApi(apiRef);
if (apiRef === configApiRef) {
const configReader = actualApi;
configReader.getOptionalBoolean = getOptionalBooleanMock;
return configReader;
}
return actualApi;
},
}));
describe('DocsTable test', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render documents passed', async () => {
const { findByText } = render(
wrapInTestApp(
@@ -66,15 +91,81 @@ describe('DocsTable test', () => {
},
]}
/>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
expect(await findByText('testName')).toBeInTheDocument();
expect(await findByText('testName2')).toBeInTheDocument();
const link1 = await findByText('testName');
const link2 = await findByText('testName2');
expect(link1).toBeInTheDocument();
expect(link1.getAttribute('href')).toContain(
'/docs/default/testkind/testname',
);
expect(link2).toBeInTheDocument();
expect(link2.getAttribute('href')).toContain(
'/docs/default/testkind2/testname2',
);
});
it('should fall back to case-sensitive links when configured', async () => {
getOptionalBooleanMock.mockReturnValue(true);
const { findByText } = render(
wrapInTestApp(
<DocsTable
entities={[
{
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
namespace: 'SomeNamespace',
},
spec: {
owner: 'user:owned',
},
relations: [
{
target: {
kind: 'user',
namespace: 'default',
name: 'owned',
},
type: 'ownedBy',
},
],
},
]}
/>,
{
mountedRoutes: {
'/techdocs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
const button = await findByText('testName');
expect(getOptionalBooleanMock).toHaveBeenCalledWith(
'techdocs.legacyUseCaseSensitiveTripletPaths',
);
expect(button.getAttribute('href')).toContain(
'/techdocs/SomeNamespace/TestKind/testName',
);
});
it('should render empty state if no owned documents exist', async () => {
const { findByText } = render(wrapInTestApp(<DocsTable entities={[]} />));
const { findByText } = render(
wrapInTestApp(<DocsTable entities={[]} />, {
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
}),
);
expect(await findByText('No documents to show')).toBeInTheDocument();
});
@@ -16,8 +16,8 @@
import React from 'react';
import { useCopyToClipboard } from 'react-use';
import { generatePath } from 'react-router-dom';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import {
formatEntityRefTitle,
@@ -49,6 +49,14 @@ export const DocsTable = ({
actions?: TableProps<DocsTableRow>['actions'];
}) => {
const [, copyToClipboard] = useCopyToClipboard();
const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
// Lower-case entity triplets by default, but allow override.
const toLowerMaybe = useApi(configApiRef).getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
)
? (str: string) => str
: (str: string) => str.toLocaleLowerCase();
if (!entities) return null;
@@ -58,10 +66,10 @@ export const DocsTable = ({
return {
entity,
resolved: {
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
docsUrl: getRouteToReaderPageFor({
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
}),
ownedByRelations,
ownedByRelationsTitle: ownedByRelations
@@ -26,6 +26,7 @@ import {
ConfigReader,
} from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -68,6 +69,11 @@ describe('Legacy TechDocs Home', () => {
<ApiProvider apis={apiRegistry}>
<LegacyTechDocsHome />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
@@ -20,6 +20,7 @@ import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -78,6 +79,11 @@ describe('TechDocsCustomHome', () => {
<ApiProvider apis={apiRegistry}>
<TechDocsCustomHome tabsConfig={tabsConfig} />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
+1
View File
@@ -65,6 +65,7 @@ export const techdocsPlugin = createPlugin({
],
routes: {
root: rootRouteRef,
docRoot: rootDocsRouteRef,
entityContent: rootCatalogDocsRouteRef,
},
});
+4 -3
View File
@@ -17,16 +17,17 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
path: '',
id: 'techdocs-index-page',
title: 'TechDocs Landing Page',
});
export const rootDocsRouteRef = createRouteRef({
path: ':namespace/:kind/:name/*',
id: 'techdocs-reader-page',
title: 'Docs',
params: ['namespace', 'kind', 'name'],
});
export const rootCatalogDocsRouteRef = createRouteRef({
path: '*',
id: 'catalog-techdocs-reader-view',
title: 'Docs',
});