Merge branch 'master' into techdocs-common/case-sensitive-migration

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-08-12 11:27:43 +02:00
635 changed files with 9792 additions and 6037 deletions
+15
View File
@@ -1,5 +1,20 @@
# @backstage/plugin-techdocs
## 0.10.3
### Patch Changes
- 260c053b9: Fix All Material UI Warnings
- db58cf06c: Avoid sanitize safe links in the header of document pages.
- 1d65bd490: Fix Techdocs feedback icon link for GitLab URLs with subgroup(s) in path
- Updated dependencies
- @backstage/core-components@0.3.0
- @backstage/config@0.1.6
- @backstage/core-plugin-api@0.1.5
- @backstage/integration@0.5.9
- @backstage/integration-react@0.1.6
- @backstage/plugin-catalog-react@0.4.1
## 0.10.2
### Patch Changes
+122 -3
View File
@@ -5,6 +5,7 @@
```ts
/// <reference types="react" />
import { Action } from '@material-table/core';
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
@@ -14,7 +15,65 @@ import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core-plugin-api';
import { LocationSpec } from '@backstage/catalog-model';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { TableColumn } from '@backstage/core-components';
import { TableProps } from '@backstage/core-components';
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
// Warning: (ae-missing-release-tag) "createCopyDocsUrlAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createCopyDocsUrlAction(copyToClipboard: Function): (
row: DocsTableRow,
) => {
icon: () => JSX.Element;
tooltip: string;
onClick: () => any;
};
// Warning: (ae-missing-release-tag) "createNameColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createNameColumn(): TableColumn<DocsTableRow>;
// Warning: (ae-missing-release-tag) "createOwnerColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createOwnerColumn(): TableColumn<DocsTableRow>;
// Warning: (ae-missing-release-tag) "createStarEntityAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createStarEntityAction(
isStarredEntity: Function,
toggleStarredEntity: Function,
): ({ entity }: DocsTableRow) => {
cellStyle: {
paddingLeft: string;
};
icon: () => JSX.Element;
tooltip: string;
onClick: () => any;
};
// Warning: (ae-missing-release-tag) "createTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createTypeColumn(): TableColumn<DocsTableRow>;
// Warning: (ae-missing-release-tag) "DefaultTechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const DefaultTechDocsHome: ({
initialFilter,
columns,
actions,
}: {
initialFilter?: UserListFilterKind | undefined;
columns?: TableColumn<DocsTableRow>[] | undefined;
actions?: TableProps<DocsTableRow>['actions'];
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -42,16 +101,58 @@ export const DocsResultListItem: ({
export const DocsTable: ({
entities,
title,
loading,
columns,
actions,
}: {
entities: Entity[] | undefined;
title?: string | undefined;
loading?: boolean | undefined;
columns?: TableColumn<DocsTableRow>[] | undefined;
actions?:
| (
| Action<DocsTableRow>
| {
action: (rowData: DocsTableRow) => Action<DocsTableRow>;
position: string;
}
| ((rowData: DocsTableRow) => Action<DocsTableRow>)
)[]
| undefined;
}) => JSX.Element | null;
// Warning: (ae-missing-release-tag) "DocsTableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type DocsTableRow = {
entity: Entity;
resolved: {
docsUrl: string;
ownedByRelationsTitle: string;
ownedByRelations: EntityName[];
};
};
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EmbeddedDocsRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EmbeddedDocsRouter: (_props: Props) => JSX.Element;
export const EmbeddedDocsRouter: (_props: Props_2) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityListDocsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityListDocsTable: {
({
columns,
actions,
}: {
columns?: TableColumn<DocsTableRow>[] | undefined;
actions?: TableProps<DocsTableRow>['actions'];
}): JSX.Element;
columns: typeof columnFactories;
actions: typeof actionFactories;
};
// Warning: (ae-missing-release-tag) "EntityTechdocsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -69,7 +170,7 @@ export type PanelType = 'DocsCardGrid' | 'DocsTable';
// Warning: (ae-missing-release-tag) "Reader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const Reader: ({ entityId, onReady }: Props_2) => JSX.Element;
export const Reader: ({ entityId, onReady }: Props_3) => JSX.Element;
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -143,11 +244,27 @@ export const TechDocsCustomHome: ({
tabsConfig: TabsConfig;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsIndexPage: () => JSX.Element;
// Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechdocsPage: () => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsPageWrapper: ({ children }: Props) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsPicker: () => null;
// Warning: (ae-missing-release-tag) "techdocsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -255,7 +372,9 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
// Warnings were encountered during analysis:
//
// src/plugin.d.ts:18:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts
// 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
// (No @packageDocumentation comment for this package)
```
+16 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-techdocs",
"version": "0.10.2",
"version": "0.10.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,42 +32,44 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.9.0",
"@backstage/config": "^0.1.5",
"@backstage/core-components": "^0.2.0",
"@backstage/core-plugin-api": "^0.1.4",
"@backstage/config": "^0.1.6",
"@backstage/core-components": "^0.3.0",
"@backstage/core-plugin-api": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.8",
"@backstage/integration-react": "^0.1.5",
"@backstage/plugin-catalog-react": "^0.4.0",
"@backstage/integration": "^0.5.9",
"@backstage/integration-react": "^0.1.6",
"@backstage/plugin-catalog": "^0.6.10",
"@backstage/plugin-catalog-react": "^0.4.1",
"@backstage/theme": "^0.2.9",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@material-ui/styles": "^4.10.0",
"@types/react": "*",
"dompurify": "^2.2.9",
"eventsource": "^1.1.0",
"event-source-polyfill": "^1.0.25",
"lodash": "^4.17.21",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.2",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4",
"react-text-truncate": "^0.16.0"
"react-text-truncate": "^0.16.0",
"git-url-parse": "~11.4.4"
},
"devDependencies": {
"@backstage/cli": "^0.7.6",
"@backstage/core-app-api": "^0.1.6",
"@backstage/dev-utils": "^0.2.4",
"@backstage/cli": "^0.7.7",
"@backstage/core-app-api": "^0.1.7",
"@backstage/dev-utils": "^0.2.5",
"@backstage/test-utils": "^0.1.16",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/user-event": "^13.1.8",
"@types/dompurify": "^2.2.2",
"@types/eventsource": "^1.1.5",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react": "^16.9",
"canvas": "^2.6.1",
"cross-fetch": "^3.0.6",
"msw": "^0.29.0"
+7 -4
View File
@@ -23,8 +23,8 @@ import {
rootDocsRouteRef,
rootCatalogDocsRouteRef,
} from './routes';
import { TechDocsHome } from './home/components/TechDocsHome';
import { TechDocsPage } from './reader/components/TechDocsPage';
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage';
import { EntityPageDocs } from './EntityPageDocs';
import { MissingAnnotationEmptyState } from '@backstage/core-components';
@@ -33,8 +33,11 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
export const Router = () => {
return (
<Routes>
<Route path={`/${rootRouteRef.path}`} element={<TechDocsHome />} />
<Route path={`/${rootDocsRouteRef.path}`} element={<TechDocsPage />} />
<Route path={`/${rootRouteRef.path}`} element={<TechDocsIndexPage />} />
<Route
path={`/${rootDocsRouteRef.path}`}
element={<TechDocsReaderPage />}
/>
</Routes>
);
};
+15 -19
View File
@@ -18,14 +18,14 @@ import { Config } from '@backstage/config';
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { NotFoundError } from '@backstage/errors';
import EventSource from 'eventsource';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { TechDocsStorageClient } from './client';
const MockedEventSource: jest.MockedClass<
typeof EventSource
> = EventSource as any;
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
typeof EventSourcePolyfill
>;
jest.mock('eventsource');
jest.mock('event-source-polyfill');
const mockEntity = {
kind: 'Component',
@@ -83,7 +83,7 @@ describe('TechDocsStorageClient', () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish') {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
@@ -91,9 +91,7 @@ describe('TechDocsStorageClient', () => {
await storageApi.syncEntityDocs(mockEntity);
expect(
MockedEventSource,
).toBeCalledWith(
expect(MockedEventSource).toBeCalledWith(
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
{ withCredentials: true, headers: {} },
);
@@ -109,7 +107,7 @@ describe('TechDocsStorageClient', () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish') {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
@@ -119,9 +117,7 @@ describe('TechDocsStorageClient', () => {
await storageApi.syncEntityDocs(mockEntity);
expect(
MockedEventSource,
).toBeCalledWith(
expect(MockedEventSource).toBeCalledWith(
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
{ withCredentials: true, headers: { Authorization: 'Bearer token' } },
);
@@ -137,7 +133,7 @@ describe('TechDocsStorageClient', () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish') {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
@@ -158,7 +154,7 @@ describe('TechDocsStorageClient', () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish') {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": true}' } as any);
}
},
@@ -179,11 +175,11 @@ describe('TechDocsStorageClient', () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'log') {
if (type === 'log' && typeof fn === 'function') {
fn({ data: '"A log message"' } as any);
}
if (type === 'finish') {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
@@ -215,7 +211,7 @@ describe('TechDocsStorageClient', () => {
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
instance.onerror({
instance.onerror?.({
status: 404,
message: 'Some not found warning',
} as any);
@@ -241,7 +237,7 @@ describe('TechDocsStorageClient', () => {
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
instance.onerror({
instance.onerror?.({
type: 'error',
data: 'Some other error',
} as any);
+3 -2
View File
@@ -18,7 +18,7 @@ import { EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { NotFoundError, ResponseError } from '@backstage/errors';
import EventSource from 'eventsource';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api';
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
@@ -214,7 +214,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
const token = await this.identityApi.getIdToken();
return new Promise((resolve, reject) => {
const source = new EventSource(url, {
// Polyfill is used to add support for custom headers and auth
const source = new EventSourcePolyfill(url, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
@@ -26,8 +26,7 @@ jest.mock('react-text-truncate', () => {
const validResult = {
location: 'https://backstage.io/docs',
title: 'Documentation',
text:
'Backstage is an open-source developer portal that puts the developer experience first.',
text: 'Backstage is an open-source developer portal that puts the developer experience first.',
kind: 'library',
namespace: '',
name: 'Backstage',
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
declare module 'event-source-polyfill' {
export class EventSourcePolyfill extends EventSource {
constructor(
url: string,
options?: {
withCredentials?: boolean;
headers?: HeadersInit;
},
);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { DefaultTechDocsHome } from './DefaultTechDocsHome';
import {
ApiProvider,
ApiRegistry,
ConfigReader,
} from '@backstage/core-app-api';
import {
ConfigApi,
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
return {
...actual,
useOwnUser: () => 'test-user',
};
});
const mockCatalogApi = {
getEntityByName: () => Promise.resolve(),
getEntities: async () => ({
items: [
{
apiVersion: 'version',
kind: 'User',
metadata: {
name: 'owned',
namespace: 'default',
},
},
],
}),
} as Partial<CatalogApi>;
describe('TechDocs Home', () => {
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
});
const apiRegistry = ApiRegistry.from([
[catalogApiRef, mockCatalogApi],
[configApiRef, configApi],
[storageApiRef, MockStorageApi.create()],
]);
it('should render a TechDocs home page', async () => {
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<DefaultTechDocsHome />
</ApiProvider>,
);
// Header
expect(await screen.findByText('Documentation')).toBeInTheDocument();
expect(
await screen.findByText(/Documentation available in My Company/i),
).toBeInTheDocument();
});
});
@@ -0,0 +1,75 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
Content,
ContentHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
import {
EntityListContainer,
FilterContainer,
FilteredEntityLayout,
} from '@backstage/plugin-catalog';
import {
EntityListProvider,
EntityOwnerPicker,
EntityTagPicker,
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { EntityListDocsTable } from './EntityListDocsTable';
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
import { TechDocsPicker } from './TechDocsPicker';
import { DocsTableRow } from './types';
export const DefaultTechDocsHome = ({
initialFilter = 'all',
columns,
actions,
}: {
initialFilter?: UserListFilterKind;
columns?: TableColumn<DocsTableRow>[];
actions?: TableProps<DocsTableRow>['actions'];
}) => {
return (
<TechDocsPageWrapper>
<Content>
<ContentHeader title="">
<SupportButton>
Discover documentation in your ecosystem.
</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<TechDocsPicker />
<UserListPicker initialFilter={initialFilter} />
<EntityOwnerPicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<EntityListDocsTable actions={actions} columns={columns} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</TechDocsPageWrapper>
);
};
@@ -18,25 +18,36 @@ import React from 'react';
import { useCopyToClipboard } from 'react-use';
import { generatePath } from 'react-router-dom';
import { IconButton, Tooltip } from '@material-ui/core';
import ShareIcon from '@material-ui/icons/Share';
import { Entity } from '@backstage/catalog-model';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import {
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { rootDocsRouteRef } from '../../routes';
import {
Table,
EmptyState,
Button,
SubvalueCell,
Link,
EmptyState,
Table,
TableColumn,
TableProps,
} from '@backstage/core-components';
import * as actionFactories from './actions';
import * as columnFactories from './columns';
import { DocsTableRow } from './types';
export const DocsTable = ({
entities,
title,
loading,
columns,
actions,
}: {
entities: Entity[] | undefined;
title?: string | undefined;
loading?: boolean | undefined;
columns?: TableColumn<DocsTableRow>[];
actions?: TableProps<DocsTableRow>['actions'];
}) => {
const [, copyToClipboard] = useCopyToClipboard();
// Lower-case entity triplets by default, but allow override.
@@ -49,67 +60,48 @@ export const DocsTable = ({
if (!entities) return null;
const documents = entities.map(entity => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
name: entity.metadata.name,
description: entity.metadata.description,
owner: entity?.spec?.owner,
type: entity?.spec?.type,
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
}),
entity,
resolved: {
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
}),
ownedByRelations,
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
},
};
});
const columns = [
{
title: 'Document',
field: 'name',
highlight: true,
render: (row: any): React.ReactNode => (
<SubvalueCell
value={<Link to={row.docsUrl}>{row.name}</Link>}
subvalue={row.description}
/>
),
},
{
title: 'Owner',
field: 'owner',
},
{
title: 'Type',
field: 'type',
},
{
title: 'Actions',
width: '10%',
render: (row: any) => (
<Tooltip title="Click to copy documentation link to clipboard">
<IconButton
onClick={() =>
copyToClipboard(`${window.location.href}/${row.docsUrl}`)
}
>
<ShareIcon />
</IconButton>
</Tooltip>
),
},
const defaultColumns: TableColumn<DocsTableRow>[] = [
columnFactories.createNameColumn(),
columnFactories.createOwnerColumn(),
columnFactories.createTypeColumn(),
];
const defaultActions: TableProps<DocsTableRow>['actions'] = [
actionFactories.createCopyDocsUrlAction(copyToClipboard),
];
return (
<>
{documents && documents.length > 0 ? (
<Table
{loading || (documents && documents.length > 0) ? (
<Table<DocsTableRow>
isLoading={loading}
options={{
paging: true,
pageSize: 20,
search: true,
actionsColumnIndex: -1,
}}
data={documents}
columns={columns}
columns={columns || defaultColumns}
actions={actions || defaultActions}
title={
title
? `${title} (${documents.length})`
@@ -0,0 +1,79 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { useCopyToClipboard } from 'react-use';
import { capitalize } from 'lodash';
import {
CodeSnippet,
TableColumn,
TableProps,
WarningPanel,
} from '@backstage/core-components';
import {
useEntityListProvider,
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import { DocsTable } from './DocsTable';
import * as actionFactories from './actions';
import * as columnFactories from './columns';
import { DocsTableRow } from './types';
export const EntityListDocsTable = ({
columns,
actions,
}: {
columns?: TableColumn<DocsTableRow>[];
actions?: TableProps<DocsTableRow>['actions'];
}) => {
const { loading, error, entities, filters } = useEntityListProvider();
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
const [, copyToClipboard] = useCopyToClipboard();
const title = capitalize(filters.user?.value ?? 'all');
const defaultActions = [
actionFactories.createCopyDocsUrlAction(copyToClipboard),
actionFactories.createStarEntityAction(
isStarredEntity,
toggleStarredEntity,
),
];
if (error) {
return (
<WarningPanel
severity="error"
title="Could not load available documentation."
>
<CodeSnippet language="text" text={error.toString()} />
</WarningPanel>
);
}
return (
<DocsTable
title={title}
entities={entities}
loading={loading}
actions={actions || defaultActions}
columns={columns}
/>
);
};
EntityListDocsTable.columns = columnFactories;
EntityListDocsTable.actions = actionFactories;
@@ -18,7 +18,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsHome } from './TechDocsHome';
import { LegacyTechDocsHome } from './LegacyTechDocsHome';
import {
ApiProvider,
@@ -51,7 +51,7 @@ const mockCatalogApi = {
}),
} as Partial<CatalogApi>;
describe('TechDocs Home', () => {
describe('Legacy TechDocs Home', () => {
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
@@ -66,7 +66,7 @@ describe('TechDocs Home', () => {
it('should render a TechDocs home page', async () => {
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsHome />
<LegacyTechDocsHome />
</ApiProvider>,
);
@@ -17,7 +17,7 @@
import React from 'react';
import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome';
export const TechDocsHome = () => {
export const LegacyTechDocsHome = () => {
const tabsConfig = [
{
label: 'Overview',
@@ -28,20 +28,19 @@ import {
import { Entity } from '@backstage/catalog-model';
import { DocsTable } from './DocsTable';
import { DocsCardGrid } from './DocsCardGrid';
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
import {
CodeSnippet,
Content,
Header,
HeaderTabs,
Page,
Progress,
WarningPanel,
SupportButton,
ContentHeader,
} from '@backstage/core-components';
import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
const panels = {
DocsTable: DocsTable,
@@ -122,9 +121,12 @@ export const TechDocsCustomHome = ({
}) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const configApi: ConfigApi = useApi(configApiRef);
const { value: entities, loading, error } = useAsync(async () => {
const {
value: entities,
loading,
error,
} = useAsync(async () => {
const response = await catalogApi.getEntities({
filter: {
'metadata.annotations.backstage.io/techdocs-ref': CATALOG_FILTER_EXISTS,
@@ -143,27 +145,21 @@ export const TechDocsCustomHome = ({
});
});
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
const currentTabConfig = tabsConfig[selectedTab];
if (loading) {
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsPageWrapper>
<Content>
<Progress />
</Content>
</Page>
</TechDocsPageWrapper>
);
}
if (error) {
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsPageWrapper>
<Content>
<WarningPanel
severity="error"
@@ -172,13 +168,12 @@ export const TechDocsCustomHome = ({
<CodeSnippet language="text" text={error.toString()} />
</WarningPanel>
</Content>
</Page>
</TechDocsPageWrapper>
);
}
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsPageWrapper>
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
@@ -197,6 +192,6 @@ export const TechDocsCustomHome = ({
/>
))}
</Content>
</Page>
</TechDocsPageWrapper>
);
};
@@ -0,0 +1,44 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { useOutlet } from 'react-router';
import { TechDocsIndexPage } from './TechDocsIndexPage';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useOutlet: jest.fn().mockReturnValue('Route Children'),
}));
jest.mock('./LegacyTechDocsHome', () => ({
LegacyTechDocsHome: jest.fn().mockReturnValue('LegacyTechDocsHomeMock'),
}));
describe('TechDocsIndexPage', () => {
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<TechDocsIndexPage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
it('renders legacy TechDocs home when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<TechDocsIndexPage />);
expect(getByText('LegacyTechDocsHomeMock')).toBeInTheDocument();
});
});
@@ -0,0 +1,25 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { useOutlet } from 'react-router';
import { LegacyTechDocsHome } from './LegacyTechDocsHome';
export const TechDocsIndexPage = () => {
const outlet = useOutlet();
return outlet || <LegacyTechDocsHome />;
};
@@ -0,0 +1,41 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { PageWithHeader } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
type Props = {
children?: React.ReactNode;
};
export const TechDocsPageWrapper = ({ children }: Props) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
return (
<PageWithHeader
title="Documentation"
subtitle={generatedSubtitle}
themeId="documentation"
>
{children}
</PageWithHeader>
);
};
@@ -0,0 +1,47 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { useEffect } from 'react';
import {
CATALOG_FILTER_EXISTS,
DefaultEntityFilters,
EntityFilter,
useEntityListProvider,
} from '@backstage/plugin-catalog-react';
class TechDocsFilter implements EntityFilter {
getCatalogFilters(): Record<string, string | symbol | (string | symbol)[]> {
return {
'metadata.annotations.backstage.io/techdocs-ref': CATALOG_FILTER_EXISTS,
};
}
}
type CustomFilters = DefaultEntityFilters & {
techdocs?: TechDocsFilter;
};
export const TechDocsPicker = () => {
const { updateFilters } = useEntityListProvider<CustomFilters>();
useEffect(() => {
updateFilters({
techdocs: new TechDocsFilter(),
});
}, [updateFilters]);
return null;
};
@@ -0,0 +1,54 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 ShareIcon from '@material-ui/icons/Share';
import {
favoriteEntityIcon,
favoriteEntityTooltip,
} from '@backstage/plugin-catalog-react';
import { DocsTableRow } from './types';
export function createCopyDocsUrlAction(copyToClipboard: Function) {
return (row: DocsTableRow) => {
return {
icon: () => <ShareIcon fontSize="small" />,
tooltip: 'Click to copy documentation link to clipboard',
onClick: () =>
copyToClipboard(
`${window.location.origin}${window.location.pathname.replace(
/\/?$/,
'/',
)}${row.resolved.docsUrl}`,
),
};
};
}
export function createStarEntityAction(
isStarredEntity: Function,
toggleStarredEntity: Function,
) {
return ({ entity }: DocsTableRow) => {
const isStarred = isStarredEntity(entity);
return {
cellStyle: { paddingLeft: '1em' },
icon: () => favoriteEntityIcon(isStarred),
tooltip: favoriteEntityTooltip(isStarred),
onClick: () => toggleStarredEntity(entity),
};
};
}
@@ -0,0 +1,56 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Link, SubvalueCell, TableColumn } from '@backstage/core-components';
import { EntityRefLinks } from '@backstage/plugin-catalog-react';
import { DocsTableRow } from './types';
export function createNameColumn(): TableColumn<DocsTableRow> {
return {
title: 'Document',
field: 'entity.metadata.name',
highlight: true,
render: (row: DocsTableRow) => (
<SubvalueCell
value={
<Link to={row.resolved.docsUrl}>{row.entity.metadata.name}</Link>
}
subvalue={row.entity.metadata.description}
/>
),
};
}
export function createOwnerColumn(): TableColumn<DocsTableRow> {
return {
title: 'Owner',
field: 'resolved.ownedByRelationsTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.ownedByRelations}
defaultKind="group"
/>
),
};
}
export function createTypeColumn(): TableColumn<DocsTableRow> {
return {
title: 'Type',
field: 'entity.spec.type',
};
}
@@ -0,0 +1,22 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { EntityListDocsTable } from './EntityListDocsTable';
export { DefaultTechDocsHome } from './DefaultTechDocsHome';
export { TechDocsPageWrapper } from './TechDocsPageWrapper';
export { TechDocsPicker } from './TechDocsPicker';
export type { PanelType } from './TechDocsCustomHome';
export type { DocsTableRow } from './types';
@@ -0,0 +1,26 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Entity, EntityName } from '@backstage/catalog-model';
export type DocsTableRow = {
entity: Entity;
resolved: {
docsUrl: string;
ownedByRelationsTitle: string;
ownedByRelations: EntityName[];
};
};
+8 -1
View File
@@ -18,13 +18,20 @@ export * from './api';
export { techdocsApiRef, techdocsStorageApiRef } from './api';
export type { TechDocsApi, TechDocsStorageApi } from './api';
export { TechDocsClient, TechDocsStorageClient } from './client';
export type { PanelType } from './home/components/TechDocsCustomHome';
export type { DocsTableRow, PanelType } from './home/components';
export {
EntityListDocsTable,
DefaultTechDocsHome,
TechDocsPageWrapper,
TechDocsPicker,
} from './home/components';
export * from './components/DocsResultListItem';
export {
DocsCardGrid,
DocsTable,
EntityTechdocsContent,
TechDocsCustomHome,
TechDocsIndexPage,
TechdocsPage,
techdocsPlugin as plugin,
techdocsPlugin,
+10
View File
@@ -113,6 +113,16 @@ export const TechDocsCustomHome = techdocsPlugin.provide(
}),
);
export const TechDocsIndexPage = techdocsPlugin.provide(
createRoutableExtension({
component: () =>
import('./home/components/TechDocsIndexPage').then(
m => m.TechDocsIndexPage,
),
mountPoint: rootRouteRef,
}),
);
export const TechDocsReaderPage = techdocsPlugin.provide(
createRoutableExtension({
component: () =>
@@ -34,9 +34,8 @@ jest.mock('react-router-dom', () => {
};
});
const { useParams }: { useParams: jest.Mock } = jest.requireMock(
'react-router-dom',
);
const { useParams }: { useParams: jest.Mock } =
jest.requireMock('react-router-dom');
describe('<Reader />', () => {
it('should render Reader content', async () => {
@@ -44,11 +43,12 @@ describe('<Reader />', () => {
entityId: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi = ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const scmIntegrationsApi: ScmIntegrationsApi =
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const techdocsStorageApi: Partial<TechDocsStorageApi> = {};
const apiRegistry = ApiRegistry.from([
@@ -23,9 +23,8 @@ type Props = {
};
export const TechDocsNotFound = ({ errorMessage }: Props) => {
const techdocsBuilder = useApi(configApiRef).getOptionalString(
'techdocs.builder',
);
const techdocsBuilder =
useApi(configApiRef).getOptionalString('techdocs.builder');
let additionalInfo = '';
if (techdocsBuilder !== 'local') {
@@ -45,9 +45,8 @@ jest.mock('./TechDocsPageHeader', () => {
};
});
const { useParams }: { useParams: jest.Mock } = jest.requireMock(
'react-router-dom',
);
const { useParams }: { useParams: jest.Mock } =
jest.requireMock('react-router-dom');
describe('<TechDocsPage />', () => {
it('should render techdocs page', async () => {
@@ -55,11 +54,12 @@ describe('<TechDocsPage />', () => {
entityId: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi = ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const scmIntegrationsApi: ScmIntegrationsApi =
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const techdocsApi: Partial<TechDocsApi> = {
getEntityMetadata: () =>
Promise.resolve({
@@ -35,7 +35,7 @@ describe('addGitFeedbackLink', () => {
<html>
<article class="md-content__inner">
<h1>HeaderText</h1>
<a title="Edit this page" href="https://gitlab.com/reponame/username/docs/TestDoc.md"></>
<a title="Edit this page" href="https://gitlab.com/groupname/reponame/-/blob/master/docs/docname.md"></>
</article>
</html>
`,
@@ -49,7 +49,32 @@ describe('addGitFeedbackLink', () => {
expect(
(shadowDom.querySelector('#git-feedback-link') as HTMLLinkElement)!.href,
).toEqual(
'https://gitlab.com/reponame/username/issues/new?issue[title]=Documentation%20Feedback%3A%20HeaderText&issue[description]=Page%20source%3A%0Ahttps%3A%2F%2Fgitlab.com%2Freponame%2Fusername%2Fdocs%2FTestDoc.md%0A%0AFeedback%3A',
'https://gitlab.com/groupname/reponame/issues/new?issue[title]=Documentation%20Feedback%3A%20HeaderText&issue[description]=Page%20source%3A%0Ahttps%3A%2F%2Fgitlab.com%2Fgroupname%2Freponame%2F-%2Fblob%2Fmaster%2Fdocs%2Fdocname.md%0A%0AFeedback%3A',
);
});
it('adds a feedback link correctly when a Gitlab source edit link is available and contains a subgroup', async () => {
const shadowDom = await createTestShadowDom(
`
<!DOCTYPE html>
<html>
<article class="md-content__inner">
<h1>HeaderText</h1>
<a title="Edit this page" href="https://gitlab.com/groupname/subgroupname/reponame/-/blob/master/docs/docname.md"></>
</article>
</html>
`,
{
preTransformers: [addGitFeedbackLink(integrations)],
postTransformers: [],
},
);
expect(shadowDom.querySelector('#git-feedback-link')).toBeTruthy();
expect(
(shadowDom.querySelector('#git-feedback-link') as HTMLLinkElement)!.href,
).toEqual(
'https://gitlab.com/groupname/subgroupname/reponame/issues/new?issue[title]=Documentation%20Feedback%3A%20HeaderText&issue[description]=Page%20source%3A%0Ahttps%3A%2F%2Fgitlab.com%2Fgroupname%2Fsubgroupname%2Freponame%2F-%2Fblob%2Fmaster%2Fdocs%2Fdocname.md%0A%0AFeedback%3A',
);
});
@@ -60,7 +85,7 @@ describe('addGitFeedbackLink', () => {
<html>
<article class="md-content__inner">
<h1>HeaderText</h1>
<a title="Edit this page" href="https://github.com/reponame/username/docs/TestDoc.md"></>
<a title="Edit this page" href="https://github.com/groupname/reponame/blob/master/docs/docname.md"></>
</article>
</html>
`,
@@ -74,7 +99,7 @@ describe('addGitFeedbackLink', () => {
expect(
(shadowDom.querySelector('#git-feedback-link') as HTMLLinkElement)!.href,
).toEqual(
'https://github.com/reponame/username/issues/new?title=Documentation%20Feedback%3A%20HeaderText&body=Page%20source%3A%0Ahttps%3A%2F%2Fgithub.com%2Freponame%2Fusername%2Fdocs%2FTestDoc.md%0A%0AFeedback%3A',
'https://github.com/groupname/reponame/issues/new?title=Documentation%20Feedback%3A%20HeaderText&body=Page%20source%3A%0Ahttps%3A%2F%2Fgithub.com%2Fgroupname%2Freponame%2Fblob%2Fmaster%2Fdocs%2Fdocname.md%0A%0AFeedback%3A',
);
});
@@ -124,7 +149,7 @@ describe('addGitFeedbackLink', () => {
<html>
<article class="md-content__inner">
<h1>HeaderText<a class="headerlink" href="http://headerlink.com"></a></h1>
<a title="Edit this page" href="https://self-hosted-git-hub-provider.com/reponame/username/docs/TestDoc.md"/>
<a title="Edit this page" href="https://self-hosted-git-hub-provider.com/groupname/reponame/blob/master/docs/docname.md"/>
</article>
</html>
`,
@@ -138,7 +163,7 @@ describe('addGitFeedbackLink', () => {
expect(
(shadowDom.querySelector('#git-feedback-link') as HTMLLinkElement)!.href,
).toEqual(
'https://self-hosted-git-hub-provider.com/reponame/username/issues/new?title=Documentation%20Feedback%3A%20HeaderText&body=Page%20source%3A%0Ahttps%3A%2F%2Fself-hosted-git-hub-provider.com%2Freponame%2Fusername%2Fdocs%2FTestDoc.md%0A%0AFeedback%3A',
'https://self-hosted-git-hub-provider.com/groupname/reponame/issues/new?title=Documentation%20Feedback%3A%20HeaderText&body=Page%20source%3A%0Ahttps%3A%2F%2Fself-hosted-git-hub-provider.com%2Fgroupname%2Freponame%2Fblob%2Fmaster%2Fdocs%2Fdocname.md%0A%0AFeedback%3A',
);
});
});
@@ -19,6 +19,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration';
import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined';
import React from 'react';
import ReactDOM from 'react-dom';
import parseGitUrl from 'git-url-parse';
// requires repo
export const addGitFeedbackLink = (
@@ -50,7 +51,8 @@ export const addGitFeedbackLink = (
const issueDesc = encodeURIComponent(
`Page source:\n${sourceAnchor.href}\n\nFeedback:`,
);
const repoPath = sourceURL.pathname.split('/').slice(0, 3).join('/');
const gitInfo = parseGitUrl(sourceURL.pathname);
const repoPath = `/${gitInfo.organization}/${gitInfo.name}`;
const feedbackLink = sourceAnchor.cloneNode() as HTMLAnchorElement;
switch (integration?.type) {