Move GiveFeedbackAddon out of initial branch merge

Co-authored-by: Eric Peterson <iamEAP@users.noreply.github.com>
Co-authored-by: Anders Näsman <realandersn@users.noreply.github.com>
Signed-off-by: Otto Sichert <git@ottosichert.de>
This commit is contained in:
Otto Sichert
2022-04-11 15:38:50 +02:00
parent 85a3b082d1
commit 4e489d10db
15 changed files with 9 additions and 566 deletions
-1
View File
@@ -52,7 +52,6 @@
"@backstage/plugin-shortcuts": "^0.2.5-next.0",
"@backstage/plugin-tech-radar": "^0.5.11-next.1",
"@backstage/plugin-techdocs": "^1.0.1-next.1",
"@backstage/techdocs-addons": "^0.0.0",
"@backstage/plugin-todo": "^0.2.6-next.0",
"@backstage/plugin-user-settings": "^0.4.3-next.0",
"@backstage/plugin-tech-insights": "^0.1.14-next.0",
-5
View File
@@ -68,14 +68,12 @@ import {
TechDocsIndexPage,
TechDocsReaderPage,
techdocsPlugin,
GiveFeedbackAddon,
} from '@backstage/plugin-techdocs';
import {
UserSettingsPage,
UserSettingsTab,
} from '@backstage/plugin-user-settings';
import { AdvancedSettings } from './components/advancedSettings';
import { TechDocsAddons } from '@backstage/techdocs-addons';
import AlarmIcon from '@material-ui/icons/Alarm';
import React from 'react';
import { hot } from 'react-hot-loader/root';
@@ -181,9 +179,6 @@ const routes = (
element={<TechDocsReaderPage />}
>
{techDocsPage}
<TechDocsAddons>
<GiveFeedbackAddon />
</TechDocsAddons>
</Route>
<Route
path="/create"
@@ -104,10 +104,7 @@ import {
isRollbarAvailable,
} from '@backstage/plugin-rollbar';
import { EntitySentryContent } from '@backstage/plugin-sentry';
import {
EntityTechdocsContent,
GiveFeedbackAddon,
} from '@backstage/plugin-techdocs';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
import { EntityTodoContent } from '@backstage/plugin-todo';
import { Button, Grid } from '@material-ui/core';
import BadgeIcon from '@material-ui/icons/CallToAction';
@@ -141,7 +138,6 @@ import {
import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd';
import React, { ReactNode, useMemo, useState } from 'react';
import { TechDocsAddons } from '@backstage/techdocs-addons';
const customEntityFilterKind = ['Component', 'API', 'System'];
@@ -401,11 +397,7 @@ const serviceEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent>
<TechDocsAddons>
<GiveFeedbackAddon />
</TechDocsAddons>
</EntityTechdocsContent>
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route
@@ -472,11 +464,7 @@ const websiteEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent>
<TechDocsAddons>
<GiveFeedbackAddon />
</TechDocsAddons>
</EntityTechdocsContent>
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route
if={isNewRelicDashboardAvailable}
@@ -515,11 +503,7 @@ const defaultEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent>
<TechDocsAddons>
<GiveFeedbackAddon />
</TechDocsAddons>
</EntityTechdocsContent>
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
-21
View File
@@ -9,7 +9,6 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncState } from 'react-use/lib/useAsync';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CommonProps } from '@material-ui/core/OverridableComponent';
import { ComponentType } from 'react';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { CSSProperties } from '@material-ui/styles';
@@ -158,26 +157,6 @@ export const EntityTechdocsContent: (props: {
children?: ReactNode;
}) => JSX.Element;
// @public
export const GiveFeedbackAddon: ComponentType<GiveFeedbackProps>;
// @public (undocumented)
export type GiveFeedbackProps = {
debounceTime?: number;
templateBuilder?: GiveFeedbackTemplateBuilder;
};
// @public (undocumented)
export type GiveFeedbackTemplate = {
title: string;
body: string;
};
// @public (undocumented)
export type GiveFeedbackTemplateBuilder = (
selection: Selection,
) => GiveFeedbackTemplate;
// @public
export const isTechDocsAvailable: (entity: Entity) => boolean;
@@ -1,84 +0,0 @@
/*
* Copyright 2022 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 { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { analyticsApiRef } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
TestApiProvider,
wrapInTestApp,
} from '@backstage/test-utils';
import { FeedbackLink } from './FeedbackLink';
const defaultProps = {
repository: {
type: 'github',
name: 'backstage',
owner: 'backstage',
protocol: 'https',
resource: 'github.com',
},
template: {
title: 'Documentation feedback',
body: '## Documentation Feedback 📝',
},
};
describe('FeedbackLink', () => {
const apiSpy = new MockAnalyticsApi();
it('Should open new issue tab', () => {
render(
wrapInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<FeedbackLink {...defaultProps} />
</TestApiProvider>,
),
);
const link = screen.getByText(/Open new Github issue/);
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('target', '_blank');
const encodedTitle = encodeURIComponent(defaultProps.template.title);
const encodedBody = encodeURIComponent(defaultProps.template.body);
expect(link).toHaveAttribute(
'href',
`https://github.com/backstage/backstage/issues/new?title=${encodedTitle}&body=${encodedBody}`,
);
});
it('Should track click events', async () => {
render(
wrapInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<FeedbackLink {...defaultProps} />
</TestApiProvider>,
),
);
fireEvent.click(screen.getByText(/Open new Github issue/));
await waitFor(() => {
expect(apiSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Open new Github issue',
});
});
});
});
@@ -1,83 +0,0 @@
/*
* Copyright 2022 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 { makeStyles } from '@material-ui/core';
import BugReportIcon from '@material-ui/icons/BugReport';
import { Link, GitHubIcon } from '@backstage/core-components';
import { GiveFeedbackTemplate, Repository } from './types';
const useStyles = makeStyles(theme => ({
root: {
display: 'grid',
gridGap: theme.spacing(1),
gridAutoFlow: 'column',
justifyContent: 'center',
alignItems: 'center',
color: theme.palette.common.black,
fontSize: theme.typography.button.fontSize,
},
}));
type FeedbackLinkProps = {
template: GiveFeedbackTemplate;
repository: Repository;
};
const getIcon = ({ type }: Repository) => {
if (type === 'github') {
return GitHubIcon;
}
return BugReportIcon;
};
const getName = ({ type }: Repository) => {
if (type === 'github') {
return 'Github';
}
return 'Gitlab';
};
const getUrl = (repository: Repository, template: GiveFeedbackTemplate) => {
const { title, body } = template;
const encodedTitle = encodeURIComponent(title);
const encodedBody = encodeURIComponent(body);
const { protocol, resource, owner, name, type } = repository;
const encodedOwner = encodeURIComponent(owner);
const encodedName = encodeURIComponent(name);
const url = `${protocol}://${resource}/${encodedOwner}/${encodedName}`;
if (type === 'github') {
return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`;
}
return `${url}/issues/new?[title]=${encodedTitle}&[body]=${encodedBody}`;
};
export const FeedbackLink = ({ template, repository }: FeedbackLinkProps) => {
const classes = useStyles();
const Icon = getIcon(repository);
const url = getUrl(repository, template);
return (
<Link className={classes.root} to={url} target="_blank">
<Icon /> Open new {getName(repository)} issue
</Link>
);
};
@@ -1,132 +0,0 @@
/*
* Copyright 2022 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, { useState, useEffect } from 'react';
import { makeStyles, Portal, Paper } from '@material-ui/core';
import { useGitTemplate, useGitRepository } from './hooks';
import { GiveFeedbackTemplateBuilder } from './types';
import {
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
ADDON_FEEDBACK_CONTAINER_ID,
ADDON_FEEDBACK_CONTAINER_SELECTOR,
} from './constants';
import { FeedbackLink } from './FeedbackLink';
import {
useShadowRootElements,
useShadowRootSelection,
} from '../../reader/components/TechDocsReaderPage';
const useStyles = makeStyles(theme => ({
root: {
transform: 'translate(-100%, -100%)',
position: 'absolute',
padding: theme.spacing(1),
zIndex: theme.zIndex.tooltip,
background: theme.palette.common.white,
},
}));
type Style = {
top: string;
left: string;
};
/**
* @public
*/
export type GiveFeedbackProps = {
debounceTime?: number;
templateBuilder?: GiveFeedbackTemplateBuilder;
};
/**
* Show give feedback button when text is highlighted
*/
export const GiveFeedback = ({
debounceTime = 500,
templateBuilder: buildTemplate,
}: GiveFeedbackProps) => {
const classes = useStyles();
const [style, setStyle] = useState<Style>();
const repository = useGitRepository();
const defaultTemplate = useGitTemplate(debounceTime);
const selection = useShadowRootSelection(debounceTime);
const [mainContent, feedbackLink] = useShadowRootElements([
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
]);
let [feedbackContainer] = useShadowRootElements([
ADDON_FEEDBACK_CONTAINER_SELECTOR,
]);
if (feedbackLink) {
feedbackLink.style.display = 'none';
}
// calculates the position of the selected text to be able to set the position of the addon
useEffect(() => {
if (
// todo(backstage/techdocs-core) handle non-repo rendering
!repository ||
!selection ||
!selection.containsNode(mainContent!, true) ||
selection?.containsNode(feedbackContainer!, true)
) {
return;
}
const mainContentPosition = mainContent!.getBoundingClientRect();
const selectionPosition = selection.getRangeAt(0).getBoundingClientRect();
setStyle({
top: `${selectionPosition.top - mainContentPosition.top - 16}px`,
left: `${selectionPosition.left + selectionPosition.width / 2}px`,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection, mainContent, feedbackContainer]);
if (!selection || !repository) return null;
if (!feedbackContainer) {
feedbackContainer = document.createElement('div');
feedbackContainer.setAttribute('id', ADDON_FEEDBACK_CONTAINER_ID);
mainContent!.prepend(feedbackContainer);
}
return (
<Portal container={feedbackContainer}>
<Paper
data-testid="give-feedback-addon"
className={classes.root}
style={style}
>
<FeedbackLink
repository={repository}
template={buildTemplate ? buildTemplate(selection) : defaultTemplate}
/>
</Paper>
</Portal>
);
};
@@ -1,22 +0,0 @@
/*
* Copyright 2022 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 const ADDON_FEEDBACK_CONTAINER_ID = 'techdocs-give-feedback';
export const ADDON_FEEDBACK_CONTAINER_SELECTOR = `#${ADDON_FEEDBACK_CONTAINER_ID}`;
export const PAGE_EDIT_LINK_SELECTOR = '[title^="Edit this page"]';
export const PAGE_FEEDBACK_LINK_SELECTOR = '[title^="Leave feedback for"]';
export const PAGE_MAIN_CONTENT_SELECTOR =
'[data-md-component="main"] .md-content';
@@ -1,96 +0,0 @@
/*
* Copyright 2022 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 parseGitUrl from 'git-url-parse';
import { useApi } from '@backstage/core-plugin-api';
import {
replaceGitHubUrlType,
replaceGitLabUrlType,
} from '@backstage/integration';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
useShadowRootElements,
useShadowRootSelection,
} from '../../reader/components/TechDocsReaderPage';
import { PAGE_EDIT_LINK_SELECTOR } from './constants';
const resolveBlobUrl = (url: string, type: string) => {
if (type === 'github') {
return replaceGitHubUrlType(url, 'blob');
}
return replaceGitLabUrlType(url, 'blob');
};
export const getTitle = (selection: Selection) => {
const text = selection.toString().substring(0, 70);
const ellipsis = text.length === 70 ? '...' : '';
return `Documentation feedback: ${text}${ellipsis}`;
};
export const getBody = (selection: Selection, markdownUrl: string) => {
const title = '## Documentation Feedback 📝';
const subheading = '#### The highlighted text:';
const commentHeading = '#### The comment on the text:';
const commentPlaceholder = '_>replace this line with your comment<_';
const highlightedTextAsQuote = selection
.toString()
.trim()
.split('\n')
.map(line => `> ${line.trim()}`)
.join('\n');
const facts = [
`Backstage URL: <${window.location.href}> \nMarkdown URL: <${markdownUrl}>`,
];
return `${title}\n\n ${subheading} \n\n ${highlightedTextAsQuote}\n\n ${commentHeading} \n ${commentPlaceholder}\n\n ___\n${facts}`;
};
export const useGitTemplate = (debounceTime?: number) => {
const initialTemplate = { title: '', body: '' };
const selection = useShadowRootSelection(debounceTime);
const [editLink] = useShadowRootElements([PAGE_EDIT_LINK_SELECTOR]);
const url = (editLink as HTMLAnchorElement)?.href ?? '';
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
if (!selection || !url) return initialTemplate;
const type = scmIntegrationsApi.byUrl(url)?.type;
if (!type) return initialTemplate;
return {
title: getTitle(selection),
body: getBody(selection, resolveBlobUrl(url, type)),
};
};
export const useGitRepository = () => {
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const [editLink] = useShadowRootElements([PAGE_EDIT_LINK_SELECTOR]);
const url = (editLink as HTMLAnchorElement)?.href ?? '';
if (!url) return null;
const type = scmIntegrationsApi.byUrl(url)?.type;
if (!type) return null;
return { ...parseGitUrl(resolveBlobUrl(url, type)), type };
};
@@ -1,22 +0,0 @@
/*
* Copyright 2022 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 { GiveFeedback } from './GiveFeedback';
export type { GiveFeedbackProps } from './GiveFeedback';
export type {
GiveFeedbackTemplate,
GiveFeedbackTemplateBuilder,
} from './types';
@@ -1,38 +0,0 @@
/*
* Copyright 2022 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.
*/
/**
* @public
*/
export type GiveFeedbackTemplate = {
title: string;
body: string;
};
/**
* @public
*/
export type GiveFeedbackTemplateBuilder = (
selection: Selection,
) => GiveFeedbackTemplate;
export type Repository = {
type: string;
name: string;
owner: string;
protocol: string;
resource: string;
};
-21
View File
@@ -1,21 +0,0 @@
/*
* Copyright 2022 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 type {
GiveFeedbackProps,
GiveFeedbackTemplate,
GiveFeedbackTemplateBuilder,
} from './GiveFeedback';
-2
View File
@@ -24,7 +24,6 @@ export * from './types';
export * from './api';
export * from './client';
export * from './reader';
export * from './addons';
export * from './search';
export * from './home';
export {
@@ -35,6 +34,5 @@ export {
TechDocsReaderPage,
techdocsPlugin as plugin,
techdocsPlugin,
GiveFeedbackAddon,
} from './plugin';
export * from './Router';
-19
View File
@@ -30,11 +30,6 @@ import {
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { GiveFeedback, GiveFeedbackProps } from './addons/GiveFeedback';
import {
createTechDocsAddon,
TechDocsAddonLocations,
} from '@backstage/techdocs-addons';
/**
* The Backstage plugin that renders technical documentation for your components
@@ -155,17 +150,3 @@ export const TechDocsReaderPage = techdocsPlugin.provide(
mountPoint: rootDocsRouteRef,
}),
);
/**
* TechDocs addon that lets you select text and open GitHub/Gitlab issues
*
* @public
*/
export const GiveFeedbackAddon = techdocsPlugin.provide(
createTechDocsAddon<GiveFeedbackProps>({
name: 'GiveFeedback',
location: TechDocsAddonLocations.CONTENT,
component: GiveFeedback,
}),
);
+5
View File
@@ -12116,6 +12116,11 @@ event-source-polyfill@1.0.25:
resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8"
integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==
event-source-polyfill@^1.0.25:
version "1.0.26"
resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.26.tgz#86c04d088ef078279168eefa028f928fec5059a4"
integrity sha512-IwDLs9fUTcGAyacHBeS53T8wcEkDyDn0UP4tfQqJ4wQP8AyH0mszuQf2ULTylnpI0sMquzJ4usrNV7+uztwI9A==
event-stream@=3.3.4:
version "3.3.4"
resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"