Merge branch 'master' into techdoc_title

This commit is contained in:
Aurelio Saraiva
2021-08-30 18:17:19 -03:00
582 changed files with 14919 additions and 4067 deletions
+1
View File
@@ -21,6 +21,7 @@ import { Reader } from './reader';
export const EntityPageDocs = ({ entity }: { entity: Entity }) => {
return (
<Reader
withSearch={false}
entityId={{
kind: entity.kind,
namespace: entity.metadata.namespace ?? 'default',
+3 -11
View File
@@ -18,11 +18,6 @@ import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Route, Routes } from 'react-router-dom';
import {
rootRouteRef,
rootDocsRouteRef,
rootCatalogDocsRouteRef,
} from './routes';
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage';
import { EntityPageDocs } from './EntityPageDocs';
@@ -33,9 +28,9 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
export const Router = () => {
return (
<Routes>
<Route path={`/${rootRouteRef.path}`} element={<TechDocsIndexPage />} />
<Route path="/" element={<TechDocsIndexPage />} />
<Route
path={`/${rootDocsRouteRef.path}`}
path="/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</Routes>
@@ -58,10 +53,7 @@ export const EmbeddedDocsRouter = (_props: Props) => {
return (
<Routes>
<Route
path={`/${rootCatalogDocsRouteRef.path}`}
element={<EntityPageDocs entity={entity} />}
/>
<Route path="/*" element={<EntityPageDocs entity={entity} />} />
</Routes>
);
};
@@ -46,4 +46,17 @@ describe('DocsResultListItem test', () => {
),
).toBeInTheDocument();
});
it('should use title if defined', async () => {
const { findByText } = render(
<DocsResultListItem result={validResult} title="Count Dookumentation" />,
);
expect(await findByText('Count Dookumentation')).toBeInTheDocument();
expect(
await findByText(
'Backstage is an open-source developer portal that puts the developer experience first.',
),
).toBeInTheDocument();
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { Divider, ListItem, ListItemText, makeStyles } from '@material-ui/core';
import { Link } from '@backstage/core-components';
import TextTruncate from 'react-text-truncate';
@@ -32,29 +32,53 @@ const useStyles = makeStyles({
export const DocsResultListItem = ({
result,
lineClamp = 5,
asListItem = true,
asLink = true,
title,
}: {
result: any;
lineClamp?: number;
asListItem?: boolean;
asLink?: boolean;
title?: string;
}) => {
const classes = useStyles();
return (
<Link to={result.location}>
<ListItem alignItems="flex-start" className={classes.flexContainer}>
<ListItemText
className={classes.itemText}
primaryTypographyProps={{ variant: 'h6' }}
primary={`${result.title} | ${result.name} docs `}
secondary={
<TextTruncate
line={lineClamp}
truncateText=""
text={result.text}
element="span"
/>
}
const TextItem = () => (
<ListItemText
className={classes.itemText}
primaryTypographyProps={{ variant: 'h6' }}
primary={title ? title : `${result.title} | ${result.name} docs`}
secondary={
<TextTruncate
line={lineClamp}
truncateText="…"
text={result.text}
element="span"
/>
</ListItem>
<Divider component="li" />
</Link>
}
/>
);
const LinkWrapper = ({ children }: PropsWithChildren<{}>) =>
asLink ? <Link to={result.location}>{children}</Link> : <>{children}</>;
const ListItemWrapper = ({ children }: PropsWithChildren<{}>) =>
asListItem ? (
<>
<ListItem alignItems="flex-start" className={classes.flexContainer}>
{children}
</ListItem>
<Divider component="li" />
</>
) : (
<>{children}</>
);
return (
<LinkWrapper>
<ListItemWrapper>
<TextItem />
</ListItemWrapper>
</LinkWrapper>
);
};
@@ -30,6 +30,7 @@ import {
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -73,6 +74,11 @@ describe('TechDocs Home', () => {
<ApiProvider apis={apiRegistry}>
<DefaultTechDocsHome />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
@@ -17,11 +17,36 @@
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { configApiRef } from '@backstage/core-plugin-api';
import { DocsCardGrid } from './DocsCardGrid';
import { rootDocsRouteRef } from '../../routes';
// Hacky way to mock a specific boolean config value.
const getOptionalBooleanMock = jest.fn().mockReturnValue(false);
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: (apiRef: any) => {
const actualUseApi = jest.requireActual(
'@backstage/core-plugin-api',
).useApi;
const actualApi = actualUseApi(apiRef);
if (apiRef === configApiRef) {
const configReader = actualApi;
configReader.getOptionalBoolean = getOptionalBooleanMock;
return configReader;
}
return actualApi;
},
}));
describe('Entity Docs Card Grid', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render all entities passed ot it', async () => {
const { findByText } = render(
const { findByText, findAllByRole } = render(
wrapInTestApp(
<DocsCardGrid
entities={[
@@ -47,9 +72,58 @@ describe('Entity Docs Card Grid', () => {
},
]}
/>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
expect(await findByText('testName')).toBeInTheDocument();
expect(await findByText('testName2')).toBeInTheDocument();
const [button1, button2] = await findAllByRole('button');
expect(button1.getAttribute('href')).toContain(
'/docs/default/testkind/testname',
);
expect(button2.getAttribute('href')).toContain(
'/docs/default/testkind2/testname2',
);
});
it('should fall back to case-sensitive links when configured', async () => {
getOptionalBooleanMock.mockReturnValue(true);
const { findByRole } = render(
wrapInTestApp(
<DocsCardGrid
entities={[
{
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
namespace: 'SomeNamespace',
},
spec: {
owner: 'techdocs@example.com',
},
},
]}
/>,
{
mountedRoutes: {
'/techdocs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
const button = await findByRole('button');
expect(getOptionalBooleanMock).toHaveBeenCalledWith(
'techdocs.legacyUseCaseSensitiveTripletPaths',
);
expect(button.getAttribute('href')).toContain(
'/techdocs/SomeNamespace/TestKind/testName',
);
});
});
@@ -15,9 +15,9 @@
*/
import React from 'react';
import { generatePath } from 'react-router-dom';
import { Entity } from '@backstage/catalog-model';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
import { rootDocsRouteRef } from '../../routes';
@@ -32,6 +32,15 @@ export const DocsCardGrid = ({
}: {
entities: Entity[] | undefined;
}) => {
const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
// Lower-case entity triplets by default, but allow override.
const toLowerMaybe = useApi(configApiRef).getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
)
? (str: string) => str
: (str: string) => str.toLocaleLowerCase();
if (!entities) return null;
return (
<ItemCardGrid data-testid="docs-explore">
@@ -45,10 +54,12 @@ export const DocsCardGrid = ({
<CardContent>{entity.metadata.description}</CardContent>
<CardActions>
<Button
to={generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
to={getRouteToReaderPageFor({
namespace: toLowerMaybe(
entity.metadata.namespace ?? 'default',
),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
})}
color="primary"
>
@@ -16,9 +16,34 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { configApiRef } from '@backstage/core-plugin-api';
import { DocsTable } from './DocsTable';
import { rootDocsRouteRef } from '../../routes';
// Hacky way to mock a specific boolean config value.
const getOptionalBooleanMock = jest.fn().mockReturnValue(false);
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: (apiRef: any) => {
const actualUseApi = jest.requireActual(
'@backstage/core-plugin-api',
).useApi;
const actualApi = actualUseApi(apiRef);
if (apiRef === configApiRef) {
const configReader = actualApi;
configReader.getOptionalBoolean = getOptionalBooleanMock;
return configReader;
}
return actualApi;
},
}));
describe('DocsTable test', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render documents passed', async () => {
const { findByText } = render(
wrapInTestApp(
@@ -66,15 +91,81 @@ describe('DocsTable test', () => {
},
]}
/>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
expect(await findByText('testName')).toBeInTheDocument();
expect(await findByText('testName2')).toBeInTheDocument();
const link1 = await findByText('testName');
const link2 = await findByText('testName2');
expect(link1).toBeInTheDocument();
expect(link1.getAttribute('href')).toContain(
'/docs/default/testkind/testname',
);
expect(link2).toBeInTheDocument();
expect(link2.getAttribute('href')).toContain(
'/docs/default/testkind2/testname2',
);
});
it('should fall back to case-sensitive links when configured', async () => {
getOptionalBooleanMock.mockReturnValue(true);
const { findByText } = render(
wrapInTestApp(
<DocsTable
entities={[
{
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
namespace: 'SomeNamespace',
},
spec: {
owner: 'user:owned',
},
relations: [
{
target: {
kind: 'user',
namespace: 'default',
name: 'owned',
},
type: 'ownedBy',
},
],
},
]}
/>,
{
mountedRoutes: {
'/techdocs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
),
);
const button = await findByText('testName');
expect(getOptionalBooleanMock).toHaveBeenCalledWith(
'techdocs.legacyUseCaseSensitiveTripletPaths',
);
expect(button.getAttribute('href')).toContain(
'/techdocs/SomeNamespace/TestKind/testName',
);
});
it('should render empty state if no owned documents exist', async () => {
const { findByText } = render(wrapInTestApp(<DocsTable entities={[]} />));
const { findByText } = render(
wrapInTestApp(<DocsTable entities={[]} />, {
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
}),
);
expect(await findByText('No documents to show')).toBeInTheDocument();
});
@@ -16,8 +16,8 @@
import React from 'react';
import { useCopyToClipboard } from 'react-use';
import { generatePath } from 'react-router-dom';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import {
formatEntityRefTitle,
@@ -49,6 +49,14 @@ export const DocsTable = ({
actions?: TableProps<DocsTableRow>['actions'];
}) => {
const [, copyToClipboard] = useCopyToClipboard();
const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
// Lower-case entity triplets by default, but allow override.
const toLowerMaybe = useApi(configApiRef).getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
)
? (str: string) => str
: (str: string) => str.toLocaleLowerCase();
if (!entities) return null;
@@ -58,10 +66,10 @@ export const DocsTable = ({
return {
entity,
resolved: {
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
docsUrl: getRouteToReaderPageFor({
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'),
kind: toLowerMaybe(entity.kind),
name: toLowerMaybe(entity.metadata.name),
}),
ownedByRelations,
ownedByRelationsTitle: ownedByRelations
@@ -26,6 +26,7 @@ import {
ConfigReader,
} from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -68,6 +69,11 @@ describe('Legacy TechDocs Home', () => {
<ApiProvider apis={apiRegistry}>
<LegacyTechDocsHome />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
@@ -20,6 +20,7 @@ import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -78,6 +79,11 @@ describe('TechDocsCustomHome', () => {
<ApiProvider apis={apiRegistry}>
<TechDocsCustomHome tabsConfig={tabsConfig} />
</ApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
},
},
);
// Header
+1
View File
@@ -65,6 +65,7 @@ export const techdocsPlugin = createPlugin({
],
routes: {
root: rootRouteRef,
docRoot: rootDocsRouteRef,
entityContent: rootCatalogDocsRouteRef,
},
});
@@ -25,6 +25,7 @@ import React from 'react';
import { TechDocsStorageApi, techdocsStorageApiRef } from '../../api';
import { Reader } from './Reader';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { searchApiRef } from '@backstage/plugin-search';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@@ -50,10 +51,16 @@ describe('<Reader />', () => {
}),
);
const techdocsStorageApi: Partial<TechDocsStorageApi> = {};
const searchApi = {
query: () =>
Promise.resolve({
results: [],
}),
};
const apiRegistry = ApiRegistry.from([
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
]);
await act(async () => {
@@ -22,6 +22,7 @@ import { BackstageTheme } from '@backstage/theme';
import {
Button,
CircularProgress,
Grid,
makeStyles,
useTheme,
} from '@material-ui/core';
@@ -43,23 +44,30 @@ import {
} from '../transformers';
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
import { TechDocsNotFound } from './TechDocsNotFound';
import { TechDocsSearch } from './TechDocsSearch';
import { useReaderState } from './useReaderState';
type Props = {
entityId: EntityName;
onReady?: () => void;
withSearch?: boolean;
};
const useStyles = makeStyles<BackstageTheme>(() => ({
const useStyles = makeStyles<BackstageTheme>(theme => ({
message: {
// `word-break: break-word` is deprecated, but gives legacy support to browsers not supporting `overflow-wrap` yet
// https://developer.mozilla.org/en-US/docs/Web/CSS/word-break
wordBreak: 'break-word',
overflowWrap: 'anywhere',
},
searchBar: {
marginLeft: '20rem',
maxWidth: 'calc(100% - 20rem * 2 - 3rem)',
marginTop: theme.spacing(1),
},
}));
export const Reader = ({ entityId, onReady }: Props) => {
export const Reader = ({ entityId, onReady, withSearch = true }: Props) => {
const { kind, namespace, name } = entityId;
const theme = useTheme<BackstageTheme>();
const classes = useStyles();
@@ -433,6 +441,12 @@ export const Reader = ({ entityId, onReady }: Props) => {
<TechDocsNotFound errorMessage={contentErrorMessage} />
</>
)}
{withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && (
<Grid container className={classes.searchBar}>
<TechDocsSearch entityId={entityId} />
</Grid>
)}
<div data-testid="techdocs-content-shadowroot" ref={shadowDomRef} />
</>
);
@@ -29,6 +29,7 @@ import {
TechDocsStorageApi,
} from '../../api';
import { ApiRegistry, ApiProvider } from '@backstage/core-app-api';
import { searchApiRef } from '@backstage/plugin-search';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@@ -47,6 +48,7 @@ jest.mock('./TechDocsPageHeader', () => {
const { useParams }: { useParams: jest.Mock } =
jest.requireMock('react-router-dom');
global.scroll = jest.fn();
describe('<TechDocsPage />', () => {
it('should render techdocs page', async () => {
@@ -75,16 +77,23 @@ describe('<TechDocsPage />', () => {
site_description: 'string',
}),
};
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
getBaseUrl: (): Promise<string> => Promise.resolve('String'),
getApiOrigin: (): Promise<string> => Promise.resolve('String'),
};
const searchApi = {
query: () =>
Promise.resolve({
results: [],
}),
};
const apiRegistry = ApiRegistry.from([
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsApiRef, techdocsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
]);
await act(async () => {
@@ -0,0 +1,122 @@
/*
* Copyright 2020 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 { TechDocsSearch } from './TechDocsSearch';
import {
act,
fireEvent,
render,
waitFor,
within,
} from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { searchApiRef } from '@backstage/plugin-search';
const entityId = {
name: 'test',
namespace: 'testspace',
kind: 'Testable',
};
const emptyResults = Promise.resolve({
results: [],
});
const singleResult = Promise.resolve({
results: [
{
type: 'testsearchresult',
document: {
text: 'text',
title: 'title',
location: '/something/something',
},
},
],
});
describe('<TechDocsPage />', () => {
it('should render techdocs search bar', async () => {
const query = () => emptyResults;
const querySpy = jest.fn(query);
const searchApi = { query: querySpy };
const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsSearch entityId={entityId} />
</ApiProvider>,
),
);
await emptyResults;
expect(querySpy).toBeCalled();
expect(rendered.getByTestId('techdocs-search-bar')).toBeInTheDocument();
});
});
it('should trigger query when autocomplete input changed', async () => {
const query = () => singleResult;
const querySpy = jest.fn(query);
const searchApi = { query: querySpy };
const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsSearch entityId={entityId} debounceTime={0} />
</ApiProvider>,
),
);
await singleResult;
expect(querySpy).toBeCalledWith({
filters: {
kind: 'Testable',
name: 'test',
namespace: 'testspace',
},
pageCursor: '',
term: '',
types: ['techdocs'],
});
const autocomplete = rendered.getByTestId('techdocs-search-bar');
const input = within(autocomplete).getByRole('textbox');
autocomplete.click();
autocomplete.focus();
fireEvent.change(input, { target: { value: 'asdf' } });
await singleResult;
await waitFor(() =>
expect(querySpy).toBeCalledWith({
filters: {
kind: 'Testable',
name: 'test',
namespace: 'testspace',
},
pageCursor: '',
term: 'asdf',
types: ['techdocs'],
}),
);
});
});
});
@@ -0,0 +1,177 @@
/*
* 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, { ChangeEvent, useEffect, useState } from 'react';
import {
Grid,
IconButton,
InputAdornment,
TextField,
CircularProgress,
} from '@material-ui/core';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { SearchContextProvider, useSearch } from '@backstage/plugin-search';
import { DocsResultListItem } from '../../components/DocsResultListItem';
import SearchIcon from '@material-ui/icons/Search';
import { useDebounce } from 'react-use';
import { useNavigate } from 'react-router';
type TechDocsSearchProps = {
entityId: {
name: string;
namespace: string;
kind: string;
};
debounceTime?: number;
};
type TechDocsDoc = {
namespace: string;
kind: string;
name: string;
path: string;
location: string;
title: string;
};
type TechDocsSearchResult = {
type: string;
document: TechDocsDoc;
};
const TechDocsSearchBar = ({
entityId,
debounceTime = 150,
}: TechDocsSearchProps) => {
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const {
term,
setTerm,
result: { loading, value: searchVal },
} = useSearch();
const [options, setOptions] = useState<any[]>([]);
useEffect(() => {
let mounted = true;
if (mounted && searchVal) {
// TODO: Change this into getting only subset of search results from the BE in the first place
// once pagination is implemented for search engines
// See: https://github.com/backstage/backstage/issues/6062
const searchResults = searchVal.results.slice(0, 10);
setOptions(searchResults);
}
return () => {
mounted = false;
};
}, [loading, searchVal]);
const [value, setValue] = useState<string>(term);
useDebounce(() => setTerm(value), debounceTime, [value]);
const handleQuery = (e: ChangeEvent<HTMLInputElement>) => {
if (!open) {
setOpen(true);
}
setValue(e.target.value);
};
const handleSelection = (_: any, selection: TechDocsSearchResult | null) => {
if (selection?.document) {
const { location } = selection.document;
navigate(location);
}
};
return (
<Grid item xs={12}>
<Autocomplete
data-testid="techdocs-search-bar"
size="small"
open={open}
getOptionLabel={() => ''}
filterOptions={x => {
return x; // This is needed to get renderOption to be called after options change. Bug in material-ui?
}}
onClose={() => {
setOpen(false);
}}
onFocus={() => {
setOpen(true);
}}
onChange={handleSelection}
blurOnSelect
noOptionsText="No results found"
value={null}
options={options}
renderOption={({ document }) => (
<DocsResultListItem
result={document}
lineClamp={3}
asListItem={false}
asLink={false}
title={document.title}
/>
)}
loading={loading}
renderInput={params => (
<TextField
{...params}
data-testid="techdocs-search-bar-input"
variant="outlined"
fullWidth
placeholder={`Search ${entityId.name} docs`}
value={value}
onChange={handleQuery}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<IconButton aria-label="Query term" disabled>
<SearchIcon />
</IconButton>
</InputAdornment>
),
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Grid>
);
};
const TechDocsSearch = (props: TechDocsSearchProps) => {
const initialState = {
term: '',
types: ['techdocs'],
pageCursor: '',
filters: props.entityId,
};
return (
<SearchContextProvider initialState={initialState}>
<TechDocsSearchBar {...props} />
</SearchContextProvider>
);
};
export { TechDocsSearch };
@@ -85,7 +85,7 @@ describe('addGitFeedbackLink', () => {
<html>
<article class="md-content__inner">
<h1>HeaderText</h1>
<a title="Edit this page" href="https://github.com/groupname/reponame/blob/master/docs/docname.md"></>
<a title="Edit this page" href="https://github.com/groupname/reponame/edit/master/docs/docname.md"></>
</article>
</html>
`,
@@ -99,7 +99,7 @@ describe('addGitFeedbackLink', () => {
expect(
(shadowDom.querySelector('#git-feedback-link') as HTMLLinkElement)!.href,
).toEqual(
'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',
'https://github.com/groupname/reponame/issues/new?title=Documentation%20Feedback%3A%20HeaderText&body=Page%20source%3A%0Ahttps%3A%2F%2Fgithub.com%2Fgroupname%2Freponame%2Fedit%2Fmaster%2Fdocs%2Fdocname.md%0A%0AFeedback%3A',
);
});
@@ -15,7 +15,10 @@
*/
import type { Transformer } from './index';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
replaceGitHubUrlType,
ScmIntegrationRegistry,
} from '@backstage/integration';
import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -51,7 +54,13 @@ export const addGitFeedbackLink = (
const issueDesc = encodeURIComponent(
`Page source:\n${sourceAnchor.href}\n\nFeedback:`,
);
const gitInfo = parseGitUrl(sourceURL.pathname);
// Convert GitHub edit url to blob type so it can be parsed by git-url-parse correctly
const gitUrl =
integration?.type === 'github'
? replaceGitHubUrlType(sourceURL.href, 'blob')
: sourceURL.href;
const gitInfo = parseGitUrl(gitUrl);
const repoPath = `/${gitInfo.organization}/${gitInfo.name}`;
const feedbackLink = sourceAnchor.cloneNode() as HTMLAnchorElement;
+4 -3
View File
@@ -17,16 +17,17 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
path: '',
id: 'techdocs-index-page',
title: 'TechDocs Landing Page',
});
export const rootDocsRouteRef = createRouteRef({
path: ':namespace/:kind/:name/*',
id: 'techdocs-reader-page',
title: 'Docs',
params: ['namespace', 'kind', 'name'],
});
export const rootCatalogDocsRouteRef = createRouteRef({
path: '*',
id: 'catalog-techdocs-reader-view',
title: 'Docs',
});