diff --git a/app-config.yaml b/app-config.yaml
index 03d13443dd..88df40813e 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -42,7 +42,7 @@ organization:
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
- requestUrl: http://localhost:7000/api/techdocs/docs
+ requestUrl: http://localhost:7000/api/techdocs
generators:
techdocs: 'docker'
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx b/plugins/techdocs-backend/src/service/metadata.ts
similarity index 53%
rename from plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx
rename to plugins/techdocs-backend/src/service/metadata.ts
index b511fde1e7..671fcdda17 100644
--- a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx
+++ b/plugins/techdocs-backend/src/service/metadata.ts
@@ -1,3 +1,4 @@
+import fetch from 'node-fetch';
/*
* Copyright 2020 Spotify AB
*
@@ -14,24 +15,26 @@
* limitations under the License.
*/
-import React from 'react';
-import { Header, Content, Page, pageTheme } from '@backstage/core';
+export class TechDocsMetadata {
+ private async getMetadataFile(docsUrl: String) {
+ const metadataURL = `${docsUrl}/techdocs_metadata.json`;
-type TechDocsPageWrapperProps = {
- title: string;
- subtitle: string;
- children: any;
-};
+ try {
+ const req = await fetch(metadataURL);
-export const TechDocsPageWrapper = ({
- children,
- title,
- subtitle,
-}: TechDocsPageWrapperProps) => {
- return (
-
-
- {children}
-
- );
-};
+ return await req.json();
+ } catch (error) {
+ throw new Error(error);
+ }
+ }
+
+ public async getMkDocsMetaData(docsUrl: any) {
+ const mkDocsMetadata = await this.getMetadataFile(docsUrl);
+
+ if (!mkDocsMetadata) return null;
+
+ return {
+ ...mkDocsMetadata,
+ };
+ }
+}
diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts
index bd9e71cb8b..365534b9d0 100644
--- a/plugins/techdocs-backend/src/service/router.ts
+++ b/plugins/techdocs-backend/src/service/router.ts
@@ -32,6 +32,7 @@ import {
} from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { DocsBuilder } from './helpers';
+import { getLocationForEntity } from '../helpers';
type RouterOptions = {
preparers: PreparerBuilder;
@@ -60,6 +61,46 @@ export async function createRouter({
}: RouterOptions): Promise {
const router = Router();
+ router.get('/metadata/mkdocs/*', async (req, res) => {
+ const storageUrl = config.getString('techdocs.storageUrl');
+ const { '0': path } = req.params;
+
+ const metadataURL = `${storageUrl}/${path}/techdocs_metadata.json`;
+
+ try {
+ const mkDocsMetadata = await (await fetch(metadataURL)).json();
+ res.send(mkDocsMetadata);
+ } catch (err) {
+ logger.info(
+ `[TechDocs] Unable to get metadata for ${path} with error ${err}`,
+ );
+ throw new Error(`Unable to get metadata for ${path} with error ${err}`);
+ }
+ });
+
+ router.get('/metadata/entity/:kind/:namespace/:name', async (req, res) => {
+ const baseUrl = config.getString('backend.baseUrl');
+ const { kind, namespace, name } = req.params;
+
+ try {
+ const entity = (await (
+ await fetch(
+ `${baseUrl}/api/catalog/entities/by-name/${kind}/${namespace}/${name}`,
+ )
+ ).json()) as Entity;
+
+ const locationMetadata = getLocationForEntity(entity);
+ res.send({ ...entity, locationMetadata });
+ } catch (err) {
+ logger.info(
+ `[TechDocs] Unable to get metadata for ${kind}/${namespace}/${name} with error ${err}`,
+ );
+ throw new Error(
+ `Unable to get metadata for ${kind}/${namespace}/${name} with error ${err}`,
+ );
+ }
+ });
+
router.get('/docs/:kind/:namespace/:name/*', async (req, res) => {
const storageUrl = config.getString('techdocs.storageUrl');
diff --git a/plugins/techdocs/src/api.test.ts b/plugins/techdocs/src/api.test.ts
index 0ab05d5ecd..f7c52f8a7d 100644
--- a/plugins/techdocs/src/api.test.ts
+++ b/plugins/techdocs/src/api.test.ts
@@ -28,7 +28,7 @@ describe('TechDocsStorageApi', () => {
const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL });
expect(storageApi.getBaseUrl('test.js', mockEntity, '')).toEqual(
- `${DOC_STORAGE_URL}/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test.js`,
+ `${DOC_STORAGE_URL}/docs/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test.js`,
);
});
@@ -36,7 +36,7 @@ describe('TechDocsStorageApi', () => {
const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL });
expect(storageApi.getBaseUrl('test/', mockEntity, '')).toEqual(
- `${DOC_STORAGE_URL}/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test/`,
+ `${DOC_STORAGE_URL}/docs/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test/`,
);
});
});
diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts
index 6fc913e5df..87a0a56a01 100644
--- a/plugins/techdocs/src/api.ts
+++ b/plugins/techdocs/src/api.ts
@@ -23,6 +23,11 @@ export const techdocsStorageApiRef = createApiRef({
description: 'Used to make requests towards the techdocs storage',
});
+export const techdocsApiRef = createApiRef({
+ id: 'plugin.techdocs.service',
+ description: 'Used to make requests towards techdocs API',
+});
+
export interface TechDocsStorage {
getEntityDocs(entityId: ParsedEntityId, path: string): Promise;
getBaseUrl(
@@ -32,6 +37,31 @@ export interface TechDocsStorage {
): string;
}
+export interface TechDocs {
+ getMetadata(metadataType: string, entityId: ParsedEntityId): Promise;
+}
+
+export class TechDocsApi implements TechDocs {
+ public apiOrigin: string;
+
+ constructor({ apiOrigin }: { apiOrigin: string }) {
+ this.apiOrigin = apiOrigin;
+ }
+
+ async getMetadata(metadataType: string, entityId: ParsedEntityId) {
+ const { kind, namespace, name } = entityId;
+
+ const requestUrl = `${this.apiOrigin}/metadata/${metadataType}/${kind}/${
+ namespace ? namespace : 'default'
+ }/${name}`;
+
+ const request = await fetch(`${requestUrl}`);
+ const res = await request.json();
+
+ return res;
+ }
+}
+
export class TechDocsStorageApi implements TechDocsStorage {
public apiOrigin: string;
@@ -42,7 +72,7 @@ export class TechDocsStorageApi implements TechDocsStorage {
async getEntityDocs(entityId: ParsedEntityId, path: string) {
const { kind, namespace, name } = entityId;
- const url = `${this.apiOrigin}/${kind}/${
+ const url = `${this.apiOrigin}/docs/${kind}/${
namespace ? namespace : 'default'
}/${name}/${path}`;
@@ -66,7 +96,7 @@ export class TechDocsStorageApi implements TechDocsStorage {
return new URL(
oldBaseUrl,
- `${this.apiOrigin}/${kind}/${
+ `${this.apiOrigin}/docs/${kind}/${
namespace ? namespace : 'default'
}/${name}/${path}`,
).toString();
diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts
index 1f15e9ebd6..b197bdfb76 100644
--- a/plugins/techdocs/src/plugin.ts
+++ b/plugins/techdocs/src/plugin.ts
@@ -35,7 +35,12 @@ import {
createApiFactory,
configApiRef,
} from '@backstage/core';
-import { techdocsStorageApiRef, TechDocsStorageApi } from './api';
+import {
+ techdocsStorageApiRef,
+ TechDocsStorageApi,
+ techdocsApiRef,
+ TechDocsApi,
+} from './api';
export const rootRouteRef = createRouteRef({
path: '',
@@ -63,5 +68,13 @@ export const plugin = createPlugin({
apiOrigin: configApi.getString('techdocs.requestUrl'),
}),
}),
+ createApiFactory({
+ api: techdocsApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) =>
+ new TechDocsApi({
+ apiOrigin: configApi.getString('techdocs.requestUrl'),
+ }),
+ }),
],
});
diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx
index f2c6d1e2da..550b8df923 100644
--- a/plugins/techdocs/src/reader/components/Reader.tsx
+++ b/plugins/techdocs/src/reader/components/Reader.tsx
@@ -39,9 +39,10 @@ import { TechDocsNotFound } from './TechDocsNotFound';
type Props = {
entityId: ParsedEntityId;
+ onReady?: () => void;
};
-export const Reader = ({ entityId }: Props) => {
+export const Reader = ({ entityId, onReady }: Props) => {
const { kind, namespace, name } = entityId;
const { '*': path } = useParams();
const theme = useTheme();
@@ -58,7 +59,9 @@ export const Reader = ({ entityId }: Props) => {
if (!shadowRoot || loading || error) {
return; // Shadow DOM isn't ready / It's not ready / Docs was not found
}
-
+ if (onReady) {
+ onReady();
+ }
// Pre-render
const transformedElement = transformer(value as string, [
sanitizeDOM(),
@@ -143,6 +146,7 @@ export const Reader = ({ entityId }: Props) => {
navigate,
techdocsStorageApi,
theme,
+ onReady,
]);
if (error) {
diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx
index e51de8f57f..c478a52127 100644
--- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx
@@ -18,8 +18,15 @@ import React from 'react';
import { useAsync } from 'react-use';
import { useNavigate, generatePath } from 'react-router-dom';
import { Grid } from '@material-ui/core';
-import { ItemCard, Progress, useApi } from '@backstage/core';
-import { TechDocsPageWrapper } from './TechDocsPageWrapper';
+import {
+ ItemCard,
+ Progress,
+ useApi,
+ Content,
+ Page,
+ pageTheme,
+ Header,
+} from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog';
import { rootDocsRouteRef } from '../../plugin';
@@ -36,53 +43,62 @@ export const TechDocsHome = () => {
if (loading) {
return (
-
-
-
+
+
+
+
+
+
);
}
if (error) {
return (
-
- {error.message}
-
+
+
+
+ {error.message}
+
+
);
}
return (
-
-
- {value?.length
- ? value.map((entity, index: number) => (
-
-
- navigate(
- generatePath(rootDocsRouteRef.path, {
- entityId: `${entity.kind}:${
- entity.metadata.namespace ?? ''
- }:${entity.metadata.name}`,
- }),
- )
- }
- title={entity.metadata.name}
- label="Read Docs"
- description={entity.metadata.description}
- />
-
- ))
- : null}
-
-
+
+
+
+
+ {value?.length
+ ? value.map((entity, index: number) => (
+
+
+ navigate(
+ generatePath(rootDocsRouteRef.path, {
+ entityId: `${entity.kind}:${
+ entity.metadata.namespace ?? ''
+ }:${entity.metadata.name}`,
+ }),
+ )
+ }
+ title={entity.metadata.name}
+ label="Read Docs"
+ description={entity.metadata.description}
+ />
+
+ ))
+ : null}
+
+
+
);
};
diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx
new file mode 100644
index 0000000000..464115bab7
--- /dev/null
+++ b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 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.
+ */
+import React from 'react';
+import { TechDocsPage } from './TechDocsPage';
+import { render, act } from '@testing-library/react';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { ApiRegistry, ApiProvider } from '@backstage/core-api';
+import {
+ techdocsApiRef,
+ TechDocsApi,
+ techdocsStorageApiRef,
+ TechDocsStorageApi,
+} from '../../api';
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return {
+ ...actual,
+ useParams: jest.fn(),
+ };
+});
+
+jest.mock('./TechDocsPageHeader', () => {
+ return {
+ __esModule: true,
+ TechDocsPageHeader: () => ,
+ };
+});
+
+const { useParams }: { useParams: jest.Mock } = jest.requireMock(
+ 'react-router-dom',
+);
+
+describe('', () => {
+ it('should render techdocs page', async () => {
+ useParams.mockReturnValue({
+ entityId: 'Component::backstage',
+ });
+
+ const techDocsApi: Partial = {
+ getMetadata: () => Promise.resolve([]),
+ };
+ const techDocsStorageApi: Partial = {
+ getEntityDocs: (): Promise => Promise.resolve('String'),
+ getBaseUrl: (): string => '',
+ };
+
+ const apiRegistry = ApiRegistry.from([
+ [techdocsApiRef, techDocsApi],
+ [techdocsStorageApiRef, techDocsStorageApi],
+ ]);
+
+ await act(async () => {
+ const rendered = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ expect(rendered.getByTestId('techdocs-content')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.tsx
index 1328d13ee9..a70ce76191 100644
--- a/plugins/techdocs/src/reader/components/TechDocsPage.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsPage.tsx
@@ -14,26 +14,60 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
-
-import { TechDocsPageWrapper } from './TechDocsPageWrapper';
+import { Content, Page, pageTheme, useApi } from '@backstage/core';
import { Reader } from './Reader';
+import { useAsync } from 'react-use';
+import { TechDocsPageHeader } from './TechDocsPageHeader';
+import { techdocsApiRef } from '../../api';
export const TechDocsPage = () => {
+ const [documentReady, setDocumentReady] = useState(false);
const { entityId } = useParams();
-
const [kind, namespace, name] = entityId.split(':');
+ const techDocsApi = useApi(techdocsApiRef);
+
+ const mkdocsMetadataRequest = useAsync(() => {
+ if (documentReady) {
+ return techDocsApi.getMetadata('mkdocs', { kind, namespace, name });
+ }
+
+ return Promise.resolve({ loading: true });
+ }, [kind, namespace, name, techDocsApi, documentReady]);
+
+ const entityMetadataRequest = useAsync(() => {
+ return techDocsApi.getMetadata('entity', { kind, namespace, name });
+ }, [kind, namespace, name, techDocsApi]);
+
+ const onReady = () => {
+ setDocumentReady(true);
+ };
+
return (
-
-
+
-
+
+
+
+
);
};
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx
new file mode 100644
index 0000000000..70388e81c2
--- /dev/null
+++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 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.
+ */
+import React from 'react';
+import { TechDocsPageHeader } from './TechDocsPageHeader';
+import { render, act } from '@testing-library/react';
+import { wrapInTestApp } from '@backstage/test-utils';
+
+describe('', () => {
+ it('should render a techdocs page header', async () => {
+ await act(async () => {
+ const rendered = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+ expect(rendered.container.innerHTML).toContain('header');
+ expect(rendered.getByText('test-site-name')).toBeDefined();
+ expect(rendered.getByText('test-site-desc')).toBeDefined();
+ });
+ });
+
+ it('should render a techdocs page header even if metadata is missing', async () => {
+ await act(async () => {
+ const rendered = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+
+ expect(rendered.container.innerHTML).toContain('header');
+ });
+ });
+});
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
new file mode 100644
index 0000000000..b01ace4994
--- /dev/null
+++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 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.
+ */
+
+import React from 'react';
+import GitHubIcon from '@material-ui/icons/GitHub';
+import { Header, HeaderLabel, Link } from '@backstage/core';
+import { CircularProgress } from '@material-ui/core';
+import { ParsedEntityId } from '../../types';
+import { AsyncState } from 'react-use/lib/useAsync';
+
+type TechDocsPageHeaderProps = {
+ entityId: ParsedEntityId;
+ metadataRequest: {
+ entity: AsyncState;
+ mkdocs: AsyncState;
+ };
+};
+
+export const TechDocsPageHeader = ({
+ entityId,
+ metadataRequest,
+}: TechDocsPageHeaderProps) => {
+ const { mkdocs: mkdocsMetadata, entity: entityMetadata } = metadataRequest;
+
+ const { value: mkDocsMetadataValues } = mkdocsMetadata;
+ const { value: entityMetadataValues } = entityMetadata;
+
+ const { kind, name } = entityId;
+
+ const { site_name: siteName, site_description: siteDescription } =
+ mkDocsMetadataValues || {};
+
+ const {
+ locationMetadata,
+ spec: { owner, lifecycle },
+ } = entityMetadataValues || { spec: {} };
+
+ const labels = (
+ <>
+
+ {name}
+
+ }
+ />
+ {owner ? : null}
+ {lifecycle ? : null}
+ {locationMetadata &&
+ locationMetadata.type !== 'dir' &&
+ locationMetadata.type !== 'file' ? (
+
+
+
+ }
+ />
+ ) : null}
+ >
+ );
+
+ return (
+ }
+ subtitle={
+ siteDescription && siteDescription !== 'None' ? siteDescription : ''
+ }
+ >
+ {labels}
+
+ );
+};
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx
deleted file mode 100644
index 5265492fb3..0000000000
--- a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-import { TechDocsPageWrapper } from './TechDocsPageWrapper';
-import React from 'react';
-import { render } from '@testing-library/react';
-import { wrapInTestApp } from '@backstage/test-utils';
-
-describe('TechDocs Page Wrapper', () => {
- it('should render a TechDocs Page Wrapper', async () => {
- const { queryByText } = render(
- wrapInTestApp(
-
- Test
- ,
- ),
- );
- expect(queryByText(/test-title/i)).toBeInTheDocument();
- expect(queryByText(/test-subtitle/i)).toBeInTheDocument();
- });
-});