From ac6025f63afce4b964c528803543e414befe8692 Mon Sep 17 00:00:00 2001 From: "Chongyang Adrian, Ke" Date: Mon, 19 Apr 2021 18:07:49 +0800 Subject: [PATCH] techdocs-plugin: add feedback link for Github/Gitlab source repos Signed-off-by: Chongyang Adrian, Ke --- .changeset/techdocs-eight-camels-hear.md | 8 ++ .../techdocs/src/reader/components/Reader.tsx | 6 +- .../transformers/addGitFeedbackLink.test.ts | 115 ++++++++++++++++++ .../reader/transformers/addGitFeedbackLink.ts | 86 +++++++++++++ .../techdocs/src/reader/transformers/index.ts | 1 + 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 .changeset/techdocs-eight-camels-hear.md create mode 100644 plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts create mode 100644 plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts diff --git a/.changeset/techdocs-eight-camels-hear.md b/.changeset/techdocs-eight-camels-hear.md new file mode 100644 index 0000000000..0d6c16f725 --- /dev/null +++ b/.changeset/techdocs-eight-camels-hear.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Add feedback link icon in Techdocs Reader that directs to Gitlab or Github repo issue page with prefilled title and source link. +For link to appear, requires repo_url and edit_uri to be filled in mkdocs.yml, as per https://www.mkdocs.org/user-guide/configuration. edit_uri will need to be specified for self-hosted Gitlab/Github with different hostnames. +Gitlab or Github detection checks for 'gitlab' or 'github' in hostname of source in repo_url; +additionally 'techdocs.feedback.gitlab' and '.github' in app config can be used to specify hostnames that should be recognised as the respective Git hosting for the feedback link. diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index feed758caa..ff4f47dca0 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { EntityName } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core'; +import { useApi, configApiRef } from '@backstage/core'; import { BackstageTheme } from '@backstage/theme'; import { useTheme } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; @@ -31,6 +31,7 @@ import transformer, { rewriteDocLinks, sanitizeDOM, simplifyMkdocsFooter, + addGitFeedbackLink, } from '../transformers'; import { TechDocsNotFound } from './TechDocsNotFound'; import TechDocsProgressBar from './TechDocsProgressBar'; @@ -52,6 +53,7 @@ export const Reader = ({ entityId, onReady }: Props) => { const [loadedPath, setLoadedPath] = useState(''); const [atInitialLoad, setAtInitialLoad] = useState(true); const [newerDocsExist, setNewerDocsExist] = useState(false); + const configApi = useApi(configApiRef); const { value: isSynced, @@ -142,6 +144,7 @@ export const Reader = ({ entityId, onReady }: Props) => { rewriteDocLinks(), removeMkdocsHeader(), simplifyMkdocsFooter(), + addGitFeedbackLink(configApi), injectCss({ css: ` body { @@ -313,6 +316,7 @@ export const Reader = ({ entityId, onReady }: Props) => { name, newerDocsExist, isSynced, + configApi, ]); // docLoadError not considered an error state if sync request is still ongoing diff --git a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts new file mode 100644 index 0000000000..cc451c3a25 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTestShadowDom } from '../../test-utils'; +import { addGitFeedbackLink } from './addGitFeedbackLink'; + +const configApi = { + getOptionalString: function getOptionalString(key: string) { + return key === 'gitlab' ? 'gitlab.com' : 'github.com'; + }, +}; + +describe('addGitFeedbackLink', () => { + it('adds a feedback link when a Gitlab source edit link is available', () => { + const shadowDom = createTestShadowDom( + ` + + +
+

HeaderText

+ +
+ + `, + { + preTransformers: [addGitFeedbackLink(configApi)], + postTransformers: [], + }, + ); + + expect(shadowDom.querySelector('#git-feedback-link')).toBeTruthy(); + 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', + ); + }); + + it('adds a feedback link when a Github source edit link is available', () => { + const shadowDom = createTestShadowDom( + ` + + + + + `, + { + preTransformers: [addGitFeedbackLink(configApi)], + postTransformers: [], + }, + ); + + expect(shadowDom.querySelector('#git-feedback-link')).toBeTruthy(); + 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', + ); + }); + + it('does not add a feedback link when no source edit link is available', () => { + const shadowDom = createTestShadowDom( + ` + + + + + `, + { + preTransformers: [addGitFeedbackLink(configApi)], + postTransformers: [], + }, + ); + + expect(shadowDom.querySelector('#git-feedback-link')).toBeFalsy(); + }); + + it('does not add a feedback link when a Gitlab or Github source edit link is not available', () => { + const shadowDom = createTestShadowDom( + ` + + +
+

Conveyor Pipelines

+ +
+ + `, + { + preTransformers: [addGitFeedbackLink(configApi)], + postTransformers: [], + }, + ); + + expect(shadowDom.querySelector('#git-feedback-link')).toBeFalsy(); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts new file mode 100644 index 0000000000..d19d90228c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Transformer } from './index'; +import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +// requires repo +export const addGitFeedbackLink = (configApi: any): Transformer => { + return dom => { + // attempting to use selectors that are more likely to be static as MkDocs updates over time + const sourceAnchor = dom.querySelector( + '[title="Edit this page"]', + ) as HTMLAnchorElement; + + // don't show if edit link not available in raw page + if (!sourceAnchor || !sourceAnchor.href) { + return dom; + } + let gitHost = ''; + + const sourceURL = new URL(sourceAnchor.href); + const githubHosts = configApi + .getConfigArray('integrations.github') + .map((integration: any) => integration.data.host); + const gitlabHosts = configApi + .getConfigArray('integrations.gitlab') + .map((integration: any) => integration.data.host); + + // don't show if can't identify edit link hostname as a gitlab/github hosting + if ( + githubHosts.includes(sourceURL.hostname) || + sourceURL.origin.includes('github') + ) { + gitHost = 'github'; + } else if ( + gitlabHosts.includes(sourceURL.hostname) || + sourceURL.origin.includes('gitlab') + ) { + gitHost = 'gitlab'; + } else { + return dom; + } + + // topmost h1 only contains title for whole page + const title = (dom.querySelector('article>h1') as HTMLElement).childNodes[0] + .textContent; + const issueTitle = encodeURIComponent(`Documentation Feedback: ${title}`); + const issueDesc = encodeURIComponent( + `Page source:\n${sourceAnchor.href}\n\nFeedback:`, + ); + const repoPath = sourceURL.pathname.split('/').slice(0, 3).join('/'); + + const feedbackLink = sourceAnchor.cloneNode() as HTMLAnchorElement; + switch (gitHost) { + case 'gitlab': + feedbackLink.href = `${sourceURL.origin}${repoPath}/issues/new?issue[title]=${issueTitle}&issue[description]=${issueDesc}`; + break; + case 'github': + feedbackLink.href = `${sourceURL.origin}${repoPath}/issues/new?title=${issueTitle}&body=${issueDesc}`; + break; + default: + return dom; + } + ReactDOM.render(React.createElement(FeedbackOutlinedIcon), feedbackLink); + feedbackLink.style.paddingLeft = '5px'; + feedbackLink.title = 'Leave feedback for this page'; + feedbackLink.id = 'git-feedback-link'; + sourceAnchor?.insertAdjacentElement('beforebegin', feedbackLink); + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 4da4cb3add..a005ad470b 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -15,6 +15,7 @@ */ export * from './addBaseUrl'; +export * from './addGitFeedbackLink'; export * from './rewriteDocLinks'; export * from './addLinkClickListener'; export * from './removeMkdocsHeader';