diff options
Diffstat (limited to 'server')
11 files changed, 688 insertions, 0 deletions
diff --git a/server/sonar-docs/README.md b/server/sonar-docs/README.md index 89646417296..11400efc5c2 100644 --- a/server/sonar-docs/README.md +++ b/server/sonar-docs/README.md @@ -307,3 +307,21 @@ Note that an iframe is **not** a self-closing tag. This means that the following <iframe src="http://www.sonarsource.com" /> ``` + +#### Dynamic Plugin Version Info + +_Note: at this time, this is only supported for the static documentation, and will be stripped from the embedded documentation._ + +You can dynamically include a plugin version block to any page, using the following special tag: + +```html +<!-- update_center:PLUGIN_KEY --> +``` + +For example, for Sonar Java, use: + +```html +<!-- update_center:java --> +``` + +You can include multiple boxes per page, if needed. diff --git a/server/sonar-docs/src/@types/types.d.ts b/server/sonar-docs/src/@types/types.d.ts index 67db6d2ad17..289ad620c2a 100644 --- a/server/sonar-docs/src/@types/types.d.ts +++ b/server/sonar-docs/src/@types/types.d.ts @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +export type Dict<T> = { [key: string]: T }; + export interface DocVersion { current: boolean; value: string; @@ -35,6 +37,31 @@ export interface DocsNavigationExternalLink { url: string; } +export type PluginMetaDataInfo = { + category?: string; + isSonarSourceCommercial: boolean; + issueTrackerURL?: string; + key?: string; + license?: string; + name: string; + organization?: { + name: string; + url?: string; + }; + sourcesURL?: string; + versions?: PluginVersionInfo[]; +}; + +export type PluginVersionInfo = { + archived?: boolean; + changeLogUrl?: string; + compatibility?: string; + date?: string; + description?: string; + downloadURL?: string; + version: string; +}; + export interface SearchResult { exactMatch?: boolean; highlights: { [field: string]: [number, number][] }; diff --git a/server/sonar-docs/src/components/PluginMetaData.tsx b/server/sonar-docs/src/components/PluginMetaData.tsx new file mode 100644 index 00000000000..80fd7234413 --- /dev/null +++ b/server/sonar-docs/src/components/PluginMetaData.tsx @@ -0,0 +1,162 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import PluginVersionMetaData from './PluginVersionMetaData'; +import { getPluginMetaData } from './utils'; +import { Dict, PluginMetaDataInfo } from '../@types/types'; + +interface Props { + location: Pick<Location, 'pathname'>; +} + +interface State { + data: Dict<PluginMetaDataInfo>; + wrappers: Dict<HTMLDivElement>; +} + +export default class PluginMetaData extends React.Component<Props, State> { + state: State = { + data: {}, + wrappers: {} + }; + + componentDidMount() { + this.searchForCommentNodes(); + } + + componentDidUpdate({ location }: Props) { + if (location.pathname !== this.props.location.pathname) { + this.clearWrapperNodes(); + this.searchForCommentNodes(); + } + } + + clearWrapperNodes = () => { + const { wrappers } = this.state; + + Object.keys(wrappers).forEach(key => { + const node = wrappers[key]; + const { parentNode } = node; + if (parentNode) { + parentNode.removeChild(node); + } + + delete wrappers[key]; + }); + + this.setState({ data: {}, wrappers: {} }); + }; + + fetchAndRender = () => { + const { wrappers } = this.state; + + Object.keys(wrappers).forEach(key => { + getPluginMetaData(key).then( + (payload: PluginMetaDataInfo) => { + this.setState(({ data }) => ({ data: { ...data, [key]: payload } })); + }, + () => {} + ); + }); + }; + + searchForCommentNodes = () => { + const pageContainer = document.querySelector('.page-container'); + + if (pageContainer) { + const iterator = document.createNodeIterator(pageContainer, NodeFilter.SHOW_COMMENT, { + acceptNode: (_: Node) => NodeFilter.FILTER_ACCEPT + }); + + let node; + const wrappers: Dict<HTMLDivElement> = {}; + while ((node = iterator.nextNode())) { + if (node.nodeValue && /update_center\s*:/.test(node.nodeValue)) { + let [, key] = node.nodeValue.split(':'); + key = key.trim(); + + const wrapper = document.createElement('div'); + wrapper.className = 'plugin-meta-data-wrapper'; + wrappers[key] = wrapper; + + node.parentNode!.insertBefore(wrapper, node); + } + } + this.setState({ wrappers }, this.fetchAndRender); + } + }; + + renderMetaData = ({ + isSonarSourceCommercial, + issueTrackerURL, + license, + organization, + versions + }: PluginMetaDataInfo) => { + let vendor; + if (organization) { + vendor = organization.name; + if (organization.url) { + vendor = ( + <a href={organization.url} rel="noopener noreferrer" target="_blank"> + {vendor} + </a> + ); + } + } + return ( + <div className="plugin-meta-data"> + <div className="plugin-meta-data-header"> + {vendor && <span className="plugin-meta-data-vendor">By {vendor}</span>} + {license && <span className="plugin-meta-data-license">{license}</span>} + {issueTrackerURL && ( + <span className="plugin-meta-data-issue-tracker"> + <a href={issueTrackerURL} rel="noopener noreferrer" target="_blank"> + Issue Tracker + </a> + </span> + )} + {isSonarSourceCommercial && ( + <span className="plugin-meta-data-supported">Supported by SonarSource</span> + )} + </div> + {versions && versions.length > 0 && <PluginVersionMetaData versions={versions} />} + </div> + ); + }; + + render() { + const { data, wrappers } = this.state; + const keys = Object.keys(data); + + if (keys.length === 0) { + return null; + } + + return keys.map(key => { + if (wrappers[key] !== undefined && data[key] !== undefined) { + return createPortal(this.renderMetaData(data[key]), wrappers[key]); + } else { + return null; + } + }); + } +} diff --git a/server/sonar-docs/src/components/PluginVersionMetaData.tsx b/server/sonar-docs/src/components/PluginVersionMetaData.tsx new file mode 100644 index 00000000000..e71fae755eb --- /dev/null +++ b/server/sonar-docs/src/components/PluginVersionMetaData.tsx @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import classNames from 'classnames'; +import { PluginVersionInfo } from '../@types/types'; + +interface Props { + versions: PluginVersionInfo[]; +} + +interface State { + collapsed: boolean; +} + +export default class PluginVersionMetaData extends React.Component<Props, State> { + state: State = { + collapsed: true + }; + + handleClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState(({ collapsed }) => ({ collapsed: !collapsed })); + }; + + renderVersion = ({ + archived, + changeLogUrl, + compatibility, + date, + description, + downloadURL, + version + }: PluginVersionInfo) => { + return ( + <div + className={classNames('plugin-meta-data-version', { + 'plugin-meta-data-version-archived': archived + })} + key={version}> + <div className="plugin-meta-data-version-version">{version}</div> + + <div className="plugin-meta-data-version-release-info"> + {date && <time className="plugin-meta-data-version-release-date">{date}</time>} + + {compatibility && ( + <span className="plugin-meta-data-version-compatibility">{compatibility}</span> + )} + </div> + + {description && ( + <div className="plugin-meta-data-version-release-description">{description}</div> + )} + + {(downloadURL || changeLogUrl) && ( + <div className="plugin-meta-data-version-release-links"> + {downloadURL && ( + <span className="plugin-meta-data-version-download"> + <a href={downloadURL} rel="noopener noreferrer" target="_blank"> + Download + </a> + </span> + )} + + {changeLogUrl && ( + <span className="plugin-meta-data-version-release-notes"> + <a href={changeLogUrl} rel="noopener noreferrer" target="_blank"> + Release notes + </a> + </span> + )} + </div> + )} + </div> + ); + }; + + render() { + const { versions } = this.props; + const { collapsed } = this.state; + + const archivedVersions = versions.filter(version => version.archived); + const currentVersions = versions.filter(version => !version.archived); + return ( + <div className="plugin-meta-data-versions"> + {archivedVersions.length > 0 && ( + <button + className="plugin-meta-data-versions-show-more" + onClick={this.handleClick} + type="button"> + {collapsed ? 'Show more versions' : 'Show fewer version'} + </button> + )} + + {currentVersions.map(version => this.renderVersion(version))} + + {!collapsed && archivedVersions.map(version => this.renderVersion(version))} + </div> + ); + } +} diff --git a/server/sonar-docs/src/components/__tests__/PluginMetaData-test.tsx b/server/sonar-docs/src/components/__tests__/PluginMetaData-test.tsx new file mode 100644 index 00000000000..78c9ca927d3 --- /dev/null +++ b/server/sonar-docs/src/components/__tests__/PluginMetaData-test.tsx @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { mount } from 'enzyme'; +import PluginMetaData from '../PluginMetaData'; +import { getPluginMetaData } from '../utils'; + +jest.mock('../utils', () => ({ + getPluginMetaData: jest.fn().mockResolvedValue({ + name: 'SonarJava', + key: 'java', + isSonarSourceCommercial: true, + organization: { + name: 'SonarSource', + url: 'http://www.sonarsource.com/' + }, + category: 'Languages', + license: 'SonarSource', + issueTrackerURL: 'https://jira.sonarsource.com/browse/SONARJAVA', + sourcesURL: 'https://github.com/SonarSource/sonar-java', + versions: [ + { + version: '4.2', + compatibilityRange: { minimum: '6.0', maximum: '6.6' }, + archived: false, + downloadURL: 'https://example.com/sonar-java-plugin-5.13.0.18197.jar' + }, + { + version: '3.2', + date: '2015-04-30', + compatibilityRange: { maximum: '6.0' }, + archived: true, + changeLogUrl: 'https://example.com/sonar-java-plugin/release', + downloadURL: 'https://example.com/sonar-java-plugin-3.2.jar' + } + ] + }) +})); + +beforeAll(() => { + (global as any).document.body.innerHTML = ` +<div class="page-container"> + <p>Lorem ipsum</p> + <!-- update_center:java --> + <p>Dolor sit amet</p> + <!-- update_center : python --> + <p>Foo Bar</p> + <!--update_center : abap--> +</div> +`; +}); + +it('should render correctly', async () => { + const wrapper = shallowRender(); + await new Promise(setImmediate); + expect(wrapper).toMatchSnapshot(); + expect(getPluginMetaData).toBeCalledWith('java'); + expect(getPluginMetaData).toBeCalledWith('python'); + expect(getPluginMetaData).toBeCalledWith('abap'); +}); + +function shallowRender(props: Partial<PluginMetaData['props']> = {}) { + return mount(<PluginMetaData location={{ pathname: 'foo' }} {...props} />); +} diff --git a/server/sonar-docs/src/components/__tests__/PluginVersionMetaData-test.tsx b/server/sonar-docs/src/components/__tests__/PluginVersionMetaData-test.tsx new file mode 100644 index 00000000000..699599a3efb --- /dev/null +++ b/server/sonar-docs/src/components/__tests__/PluginVersionMetaData-test.tsx @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import PluginVersionMetaData from '../PluginVersionMetaData'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should correctly show all versions', () => { + const wrapper = shallowRender(); + expect(wrapper.find('.plugin-meta-data-version').length).toBe(2); + wrapper.instance().setState({ collapsed: false }); + expect(wrapper.find('.plugin-meta-data-version').length).toBe(5); +}); + +function shallowRender(props: Partial<PluginVersionMetaData['props']> = {}) { + return shallow( + <PluginVersionMetaData + versions={[ + { + version: '5.13', + date: '2019-05-31', + compatibility: '6.7', + archived: false, + downloadURL: 'https://example.com/sonar-java-plugin-5.13.0.18197.jar', + changeLogUrl: 'https://example.com/sonar-java-plugin/release' + }, + { + version: '4.2', + archived: false, + downloadURL: 'https://example.com/sonar-java-plugin-5.13.0.18197.jar' + }, + { + version: '3.2', + date: '2015-04-30', + compatibility: '6.0 to 7.1', + archived: true, + changeLogUrl: 'https://example.com/sonar-java-plugin/release', + downloadURL: 'https://example.com/sonar-java-plugin-3.2.jar' + }, + { + version: '3.1', + description: 'Lorem ipsum dolor sit amet', + archived: true, + changeLogUrl: 'https://example.com/sonar-java-plugin/release', + downloadURL: 'https://example.com/sonar-java-plugin-3.1.jar' + }, + { + version: '2.1', + archived: true, + downloadURL: 'https://example.com/sonar-java-plugin-2.1.jar' + } + ]} + {...props} + /> + ); +} diff --git a/server/sonar-docs/src/components/__tests__/__snapshots__/PluginMetaData-test.tsx.snap b/server/sonar-docs/src/components/__tests__/__snapshots__/PluginMetaData-test.tsx.snap new file mode 100644 index 00000000000..e98eab29c96 --- /dev/null +++ b/server/sonar-docs/src/components/__tests__/__snapshots__/PluginMetaData-test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<PluginMetaData + location={ + Object { + "pathname": "foo", + } + } +/> +`; diff --git a/server/sonar-docs/src/components/__tests__/__snapshots__/PluginVersionMetaData-test.tsx.snap b/server/sonar-docs/src/components/__tests__/__snapshots__/PluginVersionMetaData-test.tsx.snap new file mode 100644 index 00000000000..e8c0a1238f8 --- /dev/null +++ b/server/sonar-docs/src/components/__tests__/__snapshots__/PluginVersionMetaData-test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="plugin-meta-data-versions" +> + <button + className="plugin-meta-data-versions-show-more" + onClick={[Function]} + type="button" + > + Show more versions + </button> + <div + className="plugin-meta-data-version" + key="5.13" + > + <div + className="plugin-meta-data-version-version" + > + 5.13 + </div> + <div + className="plugin-meta-data-version-release-info" + > + <time + className="plugin-meta-data-version-release-date" + > + 2019-05-31 + </time> + <span + className="plugin-meta-data-version-compatibility" + > + 6.7 + </span> + </div> + <div + className="plugin-meta-data-version-release-links" + > + <span + className="plugin-meta-data-version-download" + > + <a + href="https://example.com/sonar-java-plugin-5.13.0.18197.jar" + rel="noopener noreferrer" + target="_blank" + > + Download + </a> + </span> + <span + className="plugin-meta-data-version-release-notes" + > + <a + href="https://example.com/sonar-java-plugin/release" + rel="noopener noreferrer" + target="_blank" + > + Release notes + </a> + </span> + </div> + </div> + <div + className="plugin-meta-data-version" + key="4.2" + > + <div + className="plugin-meta-data-version-version" + > + 4.2 + </div> + <div + className="plugin-meta-data-version-release-info" + /> + <div + className="plugin-meta-data-version-release-links" + > + <span + className="plugin-meta-data-version-download" + > + <a + href="https://example.com/sonar-java-plugin-5.13.0.18197.jar" + rel="noopener noreferrer" + target="_blank" + > + Download + </a> + </span> + </div> + </div> +</div> +`; diff --git a/server/sonar-docs/src/components/utils.tsx b/server/sonar-docs/src/components/utils.tsx index f39e3c331d4..471e35fa48b 100644 --- a/server/sonar-docs/src/components/utils.tsx +++ b/server/sonar-docs/src/components/utils.tsx @@ -19,6 +19,7 @@ */ import { sortBy } from 'lodash'; import { MarkdownRemark } from '../@types/graphql-types'; +import { PluginMetaDataInfo } from '../@types/types'; const WORDS = 6; @@ -126,3 +127,18 @@ export function highlightMarks(str: string, marks: Array<{ from: number; to: num export function isDefined<T>(x: T | undefined | null): x is T { return x !== undefined && x !== null; } + +export function getPluginMetaData(key: string): Promise<PluginMetaDataInfo> { + return ( + window + .fetch(`https://update.sonarsource.org/${key}.json`) + .then((response: Response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + return Promise.reject(response); + }) + /* eslint-disable no-console */ + .catch(console.error) + ); +} diff --git a/server/sonar-docs/src/layouts/index.tsx b/server/sonar-docs/src/layouts/index.tsx index e5c2dbf86bb..f6d0923d4f8 100644 --- a/server/sonar-docs/src/layouts/index.tsx +++ b/server/sonar-docs/src/layouts/index.tsx @@ -22,6 +22,7 @@ import { StaticQuery, graphql } from 'gatsby'; import Footer from '../components/Footer'; import HeaderListProvider from '../components/HeaderListProvider'; import HeadingsLink from '../components/HeadingsLink'; +import PluginMetaData from '../components/PluginMetaData'; import Sidebar from '../components/Sidebar'; import { MarkdownRemarkConnection, MarkdownRemark } from '../@types/graphql-types'; import './layout.css'; @@ -94,6 +95,7 @@ export default function Layout({ children, location }: Props) { <div className="markdown-container">{children}</div> </div> <Footer /> + <PluginMetaData location={location} /> </div> </div> )} diff --git a/server/sonar-docs/src/layouts/layout.css b/server/sonar-docs/src/layouts/layout.css index 0fa37ad5a93..8a8f25594c2 100644 --- a/server/sonar-docs/src/layouts/layout.css +++ b/server/sonar-docs/src/layouts/layout.css @@ -708,3 +708,86 @@ img[src$='/images/info.svg'] { margin-bottom: 0; top: 0 !important; } + +.plugin-meta-data { + margin: 16px 0; + padding: 16px 16px 8px 16px; + background: #f9f9fb; + border: 1px solid #e6e6e6; + border-radius: 3px; +} + +.plugin-meta-data a svg { + margin-right: 8px; +} + +.plugin-meta-data-header { + border-bottom: 1px solid #cfd3d7; + padding-bottom: 16px; +} + +.plugin-meta-data-header, +.plugin-meta-data-version-release-info, +.plugin-meta-data-version-links { + display: flex; +} + +.plugin-meta-data-header > * + *, +.plugin-meta-data-version-release-info > * + *, +.plugin-meta-data-version-release-links > * + * { + margin-left: 16px; +} + +.plugin-meta-data-header > * + * { + padding-left: 16px; + border-left: 1px solid #cfd3d7; +} + +.plugin-meta-data-versions { + margin-top: 16px; +} + +.plugin-meta-data-versions-show-more { + font-size: 14px; + float: right; + color: #51575a; + border-color: #7b8184; + border-width: 0 0 1px 0; + padding-left: 0; + padding-right: 0; + background: transparent; + cursor: pointer; +} + +.plugin-meta-data-versions-show-more:hover { + color: #2d3032; + border-color: #2d3032; +} + +.plugin-meta-data-version { + margin-bottom: 16px; +} + +.plugin-meta-data-version + .plugin-meta-data-version { + padding-top: 8px; + padding-top: 8px; + border-top: 1px dashed #cfd3d7; +} + +.plugin-meta-data-version-version { + font-weight: bold; + font-size: 18px; +} + +.plugin-meta-data-version-release-info { + margin-top: 8px; + font-style: italic; +} + +.plugin-meta-data-version-release-description { + margin-top: 8px; +} + +.plugin-meta-data-version-release-links { + margin-top: 8px; +} |