Merge pull request #7062 from RoadieHQ/legacy-paths-to-tech-docs-collator
Modify TechDocsCollator and Search to be aware of new urls
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Modify TechDocsCollator to be aware of new TechDocs URL pattern. Modify tech docs in context search to use correct casing when creating initial filter.
|
||||
@@ -73,7 +73,10 @@ export default async function createPlugin({
|
||||
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: new DefaultTechDocsCollator({ discovery, logger }),
|
||||
collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
}),
|
||||
});
|
||||
|
||||
// The scheduler controls when documents are gathered from collators and sent
|
||||
|
||||
@@ -25,19 +25,15 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
// @deprecated
|
||||
constructor({
|
||||
discovery,
|
||||
locationTemplate,
|
||||
logger,
|
||||
catalogClient,
|
||||
parallelismLimit,
|
||||
}: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
logger: Logger_2;
|
||||
locationTemplate?: string;
|
||||
catalogClient?: CatalogApi;
|
||||
parallelismLimit?: number;
|
||||
});
|
||||
legacyPathCasing,
|
||||
}: TechDocsCollatorOptions);
|
||||
// (undocumented)
|
||||
protected applyArgsToFormat(
|
||||
format: string,
|
||||
@@ -48,11 +44,28 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
// (undocumented)
|
||||
execute(): Promise<TechDocsDocument[]>;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: TechDocsCollatorOptions,
|
||||
): DefaultTechDocsCollator;
|
||||
// (undocumented)
|
||||
protected locationTemplate: string;
|
||||
// (undocumented)
|
||||
readonly type: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "TechDocsCollatorOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type TechDocsCollatorOptions = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
logger: Logger_2;
|
||||
locationTemplate?: string;
|
||||
catalogClient?: CatalogApi;
|
||||
parallelismLimit?: number;
|
||||
legacyPathCasing?: boolean;
|
||||
};
|
||||
|
||||
export { TechDocsDocument };
|
||||
|
||||
export * from '@backstage/techdocs-common';
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DefaultTechDocsCollator } from './DefaultTechDocsCollator';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -83,7 +84,7 @@ const expectedEntities: Entity[] = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('DefaultTechDocsCollator', () => {
|
||||
describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => {
|
||||
let mockDiscoveryApi: jest.Mocked<PluginEndpointDiscovery>;
|
||||
let collator: DefaultTechDocsCollator;
|
||||
|
||||
@@ -94,9 +95,15 @@ describe('DefaultTechDocsCollator', () => {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
collator = new DefaultTechDocsCollator({
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
},
|
||||
});
|
||||
collator = DefaultTechDocsCollator.fromConfig(mockConfig, {
|
||||
discovery: mockDiscoveryApi,
|
||||
logger,
|
||||
legacyPathCasing: true,
|
||||
});
|
||||
|
||||
worker.use(
|
||||
@@ -132,6 +139,50 @@ describe('DefaultTechDocsCollator', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DefaultTechDocsCollator', () => {
|
||||
let mockDiscoveryApi: jest.Mocked<PluginEndpointDiscovery>;
|
||||
let collator: DefaultTechDocsCollator;
|
||||
|
||||
const worker = setupServer();
|
||||
msw.setupDefaultHandlers(worker);
|
||||
beforeEach(() => {
|
||||
mockDiscoveryApi = {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
collator = DefaultTechDocsCollator.fromConfig(new ConfigReader({}), {
|
||||
discovery: mockDiscoveryApi,
|
||||
logger,
|
||||
});
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://test-backend/static/docs/default/component/test-entity-with-docs/search/search_index.json',
|
||||
(_, res, ctx) => res(ctx.status(200), ctx.json(mockSearchDocIndex)),
|
||||
),
|
||||
rest.get('http://test-backend/entities', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(expectedEntities)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create documents for each tech docs search index', async () => {
|
||||
const documents = await collator.execute();
|
||||
const entity = expectedEntities[0];
|
||||
documents.forEach((document, idx) => {
|
||||
expect(document).toMatchObject({
|
||||
title: mockSearchDocIndex.docs[idx].title,
|
||||
location: `/docs/default/component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`,
|
||||
text: mockSearchDocIndex.docs[idx].text,
|
||||
namespace: 'default',
|
||||
componentType: entity!.spec!.type,
|
||||
lifecycle: entity!.spec!.lifecycle,
|
||||
owner: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a returned entity with a custom locationTemplate', async () => {
|
||||
// Provide an alternate location template.
|
||||
|
||||
@@ -21,6 +21,7 @@ import fetch from 'cross-fetch';
|
||||
import unescape from 'lodash/unescape';
|
||||
import { Logger } from 'winston';
|
||||
import pLimit from 'p-limit';
|
||||
import { Config } from '@backstage/config';
|
||||
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
|
||||
import { TechDocsDocument } from '@backstage/techdocs-common';
|
||||
|
||||
@@ -30,27 +31,41 @@ interface MkSearchIndexDoc {
|
||||
location: string;
|
||||
}
|
||||
|
||||
export type TechDocsCollatorOptions = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
logger: Logger;
|
||||
locationTemplate?: string;
|
||||
catalogClient?: CatalogApi;
|
||||
parallelismLimit?: number;
|
||||
legacyPathCasing?: boolean;
|
||||
};
|
||||
|
||||
type EntityInfo = {
|
||||
name: string;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
};
|
||||
|
||||
export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
protected discovery: PluginEndpointDiscovery;
|
||||
protected locationTemplate: string;
|
||||
private readonly logger: Logger;
|
||||
private readonly catalogClient: CatalogApi;
|
||||
private readonly parallelismLimit: number;
|
||||
private readonly legacyPathCasing: boolean;
|
||||
public readonly type: string = 'techdocs';
|
||||
|
||||
/**
|
||||
* @deprecated use static fromConfig method instead.
|
||||
*/
|
||||
constructor({
|
||||
discovery,
|
||||
locationTemplate,
|
||||
logger,
|
||||
catalogClient,
|
||||
parallelismLimit = 10,
|
||||
}: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
logger: Logger;
|
||||
locationTemplate?: string;
|
||||
catalogClient?: CatalogApi;
|
||||
parallelismLimit?: number;
|
||||
}) {
|
||||
legacyPathCasing = false,
|
||||
}: TechDocsCollatorOptions) {
|
||||
this.discovery = discovery;
|
||||
this.locationTemplate =
|
||||
locationTemplate || '/docs/:namespace/:kind/:name/:path';
|
||||
@@ -58,6 +73,15 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
this.catalogClient =
|
||||
catalogClient || new CatalogClient({ discoveryApi: discovery });
|
||||
this.parallelismLimit = parallelismLimit;
|
||||
this.legacyPathCasing = legacyPathCasing;
|
||||
}
|
||||
|
||||
static fromConfig(config: Config, options: TechDocsCollatorOptions) {
|
||||
const legacyPathCasing =
|
||||
config.getOptionalBoolean(
|
||||
'techdocs.legacyUseCaseSensitiveTripletPaths',
|
||||
) || false;
|
||||
return new DefaultTechDocsCollator({ ...options, legacyPathCasing });
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -79,11 +103,14 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
.filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref'])
|
||||
.map((entity: Entity) =>
|
||||
limit(async (): Promise<TechDocsDocument[]> => {
|
||||
const entityInfo = {
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
name: entity.metadata.name,
|
||||
};
|
||||
const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing(
|
||||
this.legacyPathCasing,
|
||||
{
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
name: entity.metadata.name,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const searchIndexResponse = await fetch(
|
||||
@@ -138,4 +165,15 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
) {
|
||||
return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`;
|
||||
}
|
||||
|
||||
private static handleEntityInfoCasing(
|
||||
legacyPaths: boolean,
|
||||
entityInfo: EntityInfo,
|
||||
): EntityInfo {
|
||||
return legacyPaths
|
||||
? entityInfo
|
||||
: Object.entries(entityInfo).reduce((acc, [key, value]) => {
|
||||
return { ...acc, [key]: value.toLocaleLowerCase('en-US') };
|
||||
}, {} as EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
export { DefaultTechDocsCollator } from './DefaultTechDocsCollator';
|
||||
|
||||
export type { TechDocsCollatorOptions } from './DefaultTechDocsCollator';
|
||||
|
||||
/**
|
||||
* @deprecated Use directly from @backstage/techdocs-common
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { TechDocsSearch } from './TechDocsSearch';
|
||||
import { buildInitialFilters, TechDocsSearch } from './TechDocsSearch';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
@@ -89,7 +89,7 @@ describe('<TechDocsPage />', () => {
|
||||
await singleResult;
|
||||
expect(querySpy).toBeCalledWith({
|
||||
filters: {
|
||||
kind: 'Testable',
|
||||
kind: 'testable',
|
||||
name: 'test',
|
||||
namespace: 'testspace',
|
||||
},
|
||||
@@ -108,7 +108,7 @@ describe('<TechDocsPage />', () => {
|
||||
await waitFor(() =>
|
||||
expect(querySpy).toBeCalledWith({
|
||||
filters: {
|
||||
kind: 'Testable',
|
||||
kind: 'testable',
|
||||
name: 'test',
|
||||
namespace: 'testspace',
|
||||
},
|
||||
@@ -120,3 +120,23 @@ describe('<TechDocsPage />', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInitialFilters', () => {
|
||||
const filterEnt = {
|
||||
name: 'Test',
|
||||
kind: 'TestKind',
|
||||
namespace: 'TeStNaMeSpAcE',
|
||||
};
|
||||
it('should use filters as is when legacy path', () => {
|
||||
const filters = buildInitialFilters(true, filterEnt);
|
||||
expect(filters).toStrictEqual(filterEnt);
|
||||
});
|
||||
it('should lowercase all filters for new approach', () => {
|
||||
const filters = buildInitialFilters(false, filterEnt);
|
||||
expect(filters).toStrictEqual({
|
||||
name: 'test',
|
||||
kind: 'testkind',
|
||||
namespace: 'testnamespace',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import {
|
||||
CircularProgress,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { SearchContextProvider, useSearch } from '@backstage/plugin-search';
|
||||
@@ -28,13 +28,15 @@ import { DocsResultListItem } from '../../components/DocsResultListItem';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
type EntityId = {
|
||||
name: string;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
};
|
||||
type TechDocsSearchProps = {
|
||||
entityId: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
};
|
||||
entityId: EntityId;
|
||||
debounceTime?: number;
|
||||
};
|
||||
|
||||
@@ -52,6 +54,17 @@ type TechDocsSearchResult = {
|
||||
document: TechDocsDoc;
|
||||
};
|
||||
|
||||
export const buildInitialFilters = (
|
||||
legacyPaths: boolean,
|
||||
entityId: EntityId,
|
||||
) => {
|
||||
return legacyPaths
|
||||
? entityId
|
||||
: Object.entries(entityId).reduce((acc, [key, value]) => {
|
||||
return { ...acc, [key]: value.toLocaleLowerCase('en-US') };
|
||||
}, {});
|
||||
};
|
||||
|
||||
const TechDocsSearchBar = ({
|
||||
entityId,
|
||||
debounceTime = 150,
|
||||
@@ -161,12 +174,17 @@ const TechDocsSearchBar = ({
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const TechDocsSearch = (props: TechDocsSearchProps) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const legacyPaths = configApi.getOptionalBoolean(
|
||||
'techdocs.legacyUseCaseSensitiveTripletPaths',
|
||||
);
|
||||
const initialState = {
|
||||
term: '',
|
||||
types: ['techdocs'],
|
||||
pageCursor: '',
|
||||
filters: props.entityId,
|
||||
filters: buildInitialFilters(legacyPaths || false, props.entityId),
|
||||
};
|
||||
return (
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
|
||||
Reference in New Issue
Block a user