Introduce techdocs metadata in techdocs-common
* Add TechdocsMetadata type in backend plugin endpoint * Introduce TechDocsMetadata type in frontend * Add changeset * Remove old thennable metadata resp * Address PR feedback - Remove explicit type annotation on TechDocsMetadata - Reintroduce res.send instead of throwing an error - Change logger info to logger error * Add TechDocsMetadata type in frontend plugin * Commit yarn.lock * Introduce JSON5 and remove parsing in local pub Signed-off-by: Matei David <matei.david.35@gmail.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Create type for TechDocsMetadata (#3716)
|
||||
|
||||
This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend).
|
||||
@@ -50,6 +50,7 @@
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"js-yaml": "^4.0.0",
|
||||
"json5": "^2.1.3",
|
||||
"mime-types": "^2.1.27",
|
||||
"mock-fs": "^4.13.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
|
||||
@@ -18,7 +18,7 @@ import path from 'path';
|
||||
import * as winston from 'winston';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { AwsS3Publish } from './awsS3';
|
||||
import { PublisherBase } from './types';
|
||||
import { PublisherBase, TechDocsMetadata } from './types';
|
||||
import type { Entity, EntityName } from '@backstage/catalog-model';
|
||||
|
||||
const createMockEntity = (annotations = {}): Entity => {
|
||||
@@ -159,13 +159,39 @@ describe('AwsS3Publish', () => {
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json': 'file-content',
|
||||
'techdocs_metadata.json':
|
||||
'{"site_name": "backstage", "site_description": "site_content"}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await publisher.fetchTechDocsMetadata(entityNameMock)).toBe(
|
||||
'file-content',
|
||||
);
|
||||
const expectedMetadata: TechDocsMetadata = {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
).toStrictEqual(expectedMetadata);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should return tech docs metadata when json encoded with single quotes', async () => {
|
||||
const entityNameMock = createMockEntityName();
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`,
|
||||
},
|
||||
});
|
||||
|
||||
const expectedMetadata: TechDocsMetadata = {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
).toStrictEqual(expectedMetadata);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,9 +20,10 @@ import { Logger } from 'winston';
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers';
|
||||
import { PublisherBase, PublishRequest } from './types';
|
||||
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
|
||||
import fs from 'fs-extra';
|
||||
import { Readable } from 'stream';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const streamToBuffer = (stream: Readable): Promise<Buffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -32,7 +33,7 @@ const streamToBuffer = (stream: Readable): Promise<Buffer> => {
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to parse the response data, ${e.message}`);
|
||||
throw new Error(`Unable to parse the response data ${e.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -162,9 +163,11 @@ export class AwsS3Publish implements PublisherBase {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(entityName: EntityName): Promise<string> {
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
): Promise<TechDocsMetadata> {
|
||||
try {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
return await new Promise<TechDocsMetadata>((resolve, reject) => {
|
||||
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
|
||||
this.storageClient
|
||||
@@ -182,8 +185,11 @@ export class AwsS3Publish implements PublisherBase {
|
||||
`Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`,
|
||||
);
|
||||
}
|
||||
const techdocsMetadata = JSON5.parse(
|
||||
techdocsMetadataJson.toString('utf-8'),
|
||||
);
|
||||
|
||||
resolve(techdocsMetadataJson.toString('utf-8'));
|
||||
resolve(techdocsMetadata);
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error(err.message);
|
||||
|
||||
@@ -24,7 +24,8 @@ import { Logger } from 'winston';
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers';
|
||||
import { PublisherBase, PublishRequest } from './types';
|
||||
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
export class GoogleGCSPublish implements PublisherBase {
|
||||
static async fromConfig(
|
||||
@@ -132,7 +133,7 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
});
|
||||
}
|
||||
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<string> {
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
|
||||
@@ -152,7 +153,7 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
const techdocsMetadataJson = Buffer.concat(
|
||||
fileStreamChunks,
|
||||
).toString();
|
||||
resolve(techdocsMetadataJson);
|
||||
resolve(JSON5.parse(techdocsMetadataJson));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Publisher } from './publish';
|
||||
export type { PublisherBase, PublisherType } from './types';
|
||||
export type { PublisherBase, PublisherType, TechDocsMetadata } from './types';
|
||||
|
||||
@@ -25,7 +25,12 @@ import {
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { PublisherBase, PublishRequest, PublishResponse } from './types';
|
||||
import {
|
||||
PublisherBase,
|
||||
PublishRequest,
|
||||
PublishResponse,
|
||||
TechDocsMetadata,
|
||||
} from './types';
|
||||
|
||||
// TODO: Use a more persistent storage than node_modules or /tmp directory.
|
||||
// Make it configurable with techdocs.publisher.local.publishDirectory
|
||||
@@ -102,7 +107,7 @@ export class LocalPublish implements PublisherBase {
|
||||
});
|
||||
}
|
||||
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<string> {
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => {
|
||||
const storageUrl = new URL(
|
||||
@@ -116,7 +121,7 @@ export class LocalPublish implements PublisherBase {
|
||||
.then(response =>
|
||||
response
|
||||
.json()
|
||||
.then(techdocsMetadataJson => resolve(techdocsMetadataJson))
|
||||
.then(techdocsMetadata => resolve(techdocsMetadata))
|
||||
.catch(err => {
|
||||
reject(
|
||||
`Unable to parse metadata JSON for ${entityRootDir}. Error: ${err}`,
|
||||
|
||||
@@ -32,6 +32,14 @@ export type PublishResponse = {
|
||||
remoteUrl?: string;
|
||||
} | void;
|
||||
|
||||
/**
|
||||
* Type to hold metadata found in techdocs_metadata.json and associated with each site
|
||||
*/
|
||||
export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for a TechDocs publisher (e.g. Local, Google GCS Bucket, AWS S3, etc.)
|
||||
* The publisher handles publishing of the generated static files after the prepare and generate steps of TechDocs.
|
||||
@@ -50,7 +58,7 @@ export interface PublisherBase {
|
||||
* Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc.
|
||||
* This API uses the techdocs_metadata.json file that co-exists along with the generated docs.
|
||||
*/
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<string>;
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata>;
|
||||
|
||||
/**
|
||||
* Route middleware to serve static documentation files for an entity.
|
||||
|
||||
@@ -58,14 +58,22 @@ export async function createRouter({
|
||||
const { '0': path } = req.params;
|
||||
const entityName = getEntityNameFromUrlPath(path);
|
||||
|
||||
publisher
|
||||
.fetchTechDocsMetadata(entityName)
|
||||
.then(techdocsMetadataJson => {
|
||||
res.send(techdocsMetadataJson);
|
||||
})
|
||||
.catch(reason => {
|
||||
res.status(500).send(`Unable to get Metadata. Reason: ${reason}`);
|
||||
});
|
||||
try {
|
||||
const techdocsMetadata = await publisher.fetchTechDocsMetadata(
|
||||
entityName,
|
||||
);
|
||||
|
||||
res.send(techdocsMetadata);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Unable to get metadata for ${entityName.namespace}/${entityName.name} with error ${err}`,
|
||||
);
|
||||
res
|
||||
.status(500)
|
||||
.send(
|
||||
`Unable to get metadata for $${entityName.namespace}/${entityName.name}, reason: ${err}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/metadata/entity/:namespace/:kind/:name', async (req, res) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { TechDocsMetadata } from './types';
|
||||
|
||||
export const techdocsStorageApiRef = createApiRef<TechDocsStorageApi>({
|
||||
id: 'plugin.techdocs.storageservice',
|
||||
@@ -33,7 +34,7 @@ export interface TechDocsStorage {
|
||||
}
|
||||
|
||||
export interface TechDocs {
|
||||
getTechDocsMetadata(entityId: EntityName): Promise<string>;
|
||||
getTechDocsMetadata(entityId: EntityName): Promise<TechDocsMetadata>;
|
||||
getEntityMetadata(entityId: EntityName): Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { Header, HeaderLabel, Link } from '@backstage/core';
|
||||
import { TechDocsMetadata } from '../../types';
|
||||
|
||||
type TechDocsPageHeaderProps = {
|
||||
entityId: EntityName;
|
||||
metadataRequest: {
|
||||
entity: AsyncState<any>;
|
||||
techdocs: AsyncState<any>;
|
||||
techdocs: AsyncState<TechDocsMetadata>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
};
|
||||
@@ -16734,7 +16734,7 @@ json3@^3.3.2:
|
||||
resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
|
||||
integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
|
||||
|
||||
json5@2.x, json5@^2.1.1, json5@^2.1.2:
|
||||
json5@2.x, json5@^2.1.1, json5@^2.1.2, json5@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
|
||||
integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
|
||||
|
||||
Reference in New Issue
Block a user