Browse Source

SONAR-13386 Don't show 'plugin will get installed' if it's already installed

tags/8.4.0.35506
Wouter Admiraal 4 years ago
parent
commit
b66618cf03
26 changed files with 838 additions and 126 deletions
  1. 11
    63
      server/sonar-web/src/main/js/api/plugins.ts
  2. 3
    2
      server/sonar-web/src/main/js/app/components/AdminContainer.tsx
  3. 2
    2
      server/sonar-web/src/main/js/app/components/AdminContext.tsx
  4. 3
    2
      server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx
  5. 2
    2
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
  6. 3
    4
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  7. 16
    9
      server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx
  8. 8
    8
      server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx
  9. 12
    6
      server/sonar-web/src/main/js/apps/marketplace/components/PluginAvailable.tsx
  10. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
  11. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
  12. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogItem.tsx
  13. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginDescription.tsx
  14. 2
    2
      server/sonar-web/src/main/js/apps/marketplace/components/PluginInstalled.tsx
  15. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginOrganization.tsx
  16. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginStatus.tsx
  17. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateButton.tsx
  18. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateItem.tsx
  19. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx
  20. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginUrls.tsx
  21. 3
    3
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginActions-test.tsx
  22. 64
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginAvailable-test.tsx
  23. 546
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginAvailable-test.tsx.snap
  24. 1
    13
      server/sonar-web/src/main/js/apps/marketplace/utils.ts
  25. 68
    0
      server/sonar-web/src/main/js/helpers/mocks/plugins.ts
  26. 84
    0
      server/sonar-web/src/main/js/types/plugins.ts

+ 11
- 63
server/sonar-web/src/main/js/api/plugins.ts View File

@@ -21,68 +21,16 @@ import { findLastIndex } from 'lodash';
import { getJSON, post } from 'sonar-ui-common/helpers/request';
import { isDefined } from 'sonar-ui-common/helpers/types';
import throwGlobalError from '../app/utils/throwGlobalError';

export interface Plugin {
key: string;
name: string;
category?: string;
description?: string;
editionBundled?: boolean;
license?: string;
organizationName?: string;
homepageUrl?: string;
organizationUrl?: string;
issueTrackerUrl?: string;
termsAndConditionsUrl?: string;
}

export interface Release {
version: string;
date: string;
description?: string;
changeLogUrl?: string;
}

export interface Update {
status: string;
release?: Release;
requires: Plugin[];
previousUpdates?: Update[];
}

export interface PluginPendingResult {
installing: PluginPending[];
updating: PluginPending[];
removing: PluginPending[];
}

export interface PluginAvailable extends Plugin {
release: Release;
update: Update;
}

export interface PluginPending extends Plugin {
version: string;
implementationBuild: string;
}

export interface PluginInstalled extends PluginPending {
documentationPath?: string;
filename: string;
hash: string;
sonarLintSupported: boolean;
updatedAt: number;
updates?: Update[];
}
import { AvailablePlugin, InstalledPlugin, PendingPluginResult, Update } from '../types/plugins';

export function getAvailablePlugins(): Promise<{
plugins: PluginAvailable[];
plugins: AvailablePlugin[];
updateCenterRefresh: string;
}> {
return getJSON('/api/plugins/available').catch(throwGlobalError);
}

export function getPendingPlugins(): Promise<PluginPendingResult> {
export function getPendingPlugins(): Promise<PendingPluginResult> {
return getJSON('/api/plugins/pending').catch(throwGlobalError);
}

@@ -108,22 +56,22 @@ function addChangelog(update: Update, updates?: Update[]) {
return { ...update, previousUpdates };
}

export function getInstalledPlugins(): Promise<PluginInstalled[]> {
export function getInstalledPlugins(): Promise<InstalledPlugin[]> {
return getJSON('/api/plugins/installed', { f: 'category' }).then(
({ plugins }) => plugins,
throwGlobalError
);
}

export function getInstalledPluginsWithUpdates(): Promise<PluginInstalled[]> {
export function getInstalledPluginsWithUpdates(): Promise<InstalledPlugin[]> {
return Promise.all([
getJSON('/api/plugins/installed', { f: 'category' }),
getJSON('/api/plugins/updates')
])
.then(([installed, updates]) =>
installed.plugins.map((plugin: PluginInstalled) => {
const updatePlugin: PluginInstalled = updates.plugins.find(
(p: PluginInstalled) => p.key === plugin.key
installed.plugins.map((plugin: InstalledPlugin) => {
const updatePlugin: InstalledPlugin = updates.plugins.find(
(p: InstalledPlugin) => p.key === plugin.key
);
if (updatePlugin) {
return {
@@ -140,14 +88,14 @@ export function getInstalledPluginsWithUpdates(): Promise<PluginInstalled[]> {
.catch(throwGlobalError);
}

export function getPluginUpdates(): Promise<PluginInstalled[]> {
export function getPluginUpdates(): Promise<InstalledPlugin[]> {
return Promise.all([getJSON('/api/plugins/updates'), getJSON('/api/plugins/installed')])
.then(([updates, installed]) =>
updates.plugins.map((updatePlugin: PluginInstalled) => {
updates.plugins.map((updatePlugin: InstalledPlugin) => {
const updates = getLastUpdates(updatePlugin.updates).map(update =>
addChangelog(update, updatePlugin.updates)
);
const plugin = installed.plugins.find((p: PluginInstalled) => p.key === updatePlugin.key);
const plugin = installed.plugins.find((p: InstalledPlugin) => p.key === updatePlugin.key);
if (plugin) {
return {
...plugin,

+ 3
- 2
server/sonar-web/src/main/js/app/components/AdminContainer.tsx View File

@@ -22,11 +22,12 @@ import { Helmet } from 'react-helmet-async';
import { connect } from 'react-redux';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getSettingsNavigation } from '../../api/nav';
import { getPendingPlugins, PluginPendingResult } from '../../api/plugins';
import { getPendingPlugins } from '../../api/plugins';
import { getSystemStatus, waitSystemUPStatus } from '../../api/system';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
import { setAdminPages } from '../../store/appState';
import { getAppState, Store } from '../../store/rootReducer';
import { PendingPluginResult } from '../../types/plugins';
import AdminContext, { defaultPendingPlugins, defaultSystemStatus } from './AdminContext';
import SettingsNav from './nav/settings/SettingsNav';

@@ -37,7 +38,7 @@ interface Props {
}

interface State {
pendingPlugins: PluginPendingResult;
pendingPlugins: PendingPluginResult;
systemStatus: T.SysStatus;
}


+ 2
- 2
server/sonar-web/src/main/js/app/components/AdminContext.tsx View File

@@ -18,12 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { PluginPendingResult } from '../../api/plugins';
import { PendingPluginResult } from '../../types/plugins';

export interface AdminContextInterface {
fetchSystemStatus: () => void;
fetchPendingPlugins: () => void;
pendingPlugins: PluginPendingResult;
pendingPlugins: PendingPluginResult;
systemStatus: T.SysStatus;
}


+ 3
- 2
server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx View File

@@ -22,13 +22,14 @@ import { FormattedMessage } from 'react-intl';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { cancelPendingPlugins, PluginPendingResult } from '../../../../api/plugins';
import { cancelPendingPlugins } from '../../../../api/plugins';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import RestartButton from '../../../../components/common/RestartButton';
import { PendingPluginResult } from '../../../../types/plugins';

interface Props {
fetchSystemStatus: () => void;
pending: PluginPendingResult;
pending: PendingPluginResult;
refreshPending: () => void;
systemStatus: T.SysStatus;
}

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx View File

@@ -26,7 +26,7 @@ import ContextNavBar from 'sonar-ui-common/components/ui/ContextNavBar';
import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { PluginPendingResult } from '../../../../api/plugins';
import { PendingPluginResult } from '../../../../types/plugins';
import { rawSizes } from '../../../theme';
import PendingPluginsActionNotif from './PendingPluginsActionNotif';
import SystemRestartNotif from './SystemRestartNotif';
@@ -37,7 +37,7 @@ interface Props {
fetchSystemStatus: () => void;
location: {};
organizationsEnabled?: boolean;
pendingPlugins: PluginPendingResult;
pendingPlugins: PendingPluginResult;
systemStatus: T.SysStatus;
}


+ 3
- 4
server/sonar-web/src/main/js/apps/marketplace/App.tsx View File

@@ -25,13 +25,12 @@ import {
getAvailablePlugins,
getInstalledPlugins,
getInstalledPluginsWithUpdates,
getPluginUpdates,
Plugin,
PluginPendingResult
getPluginUpdates
} from '../../api/plugins';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { Location, Router, withRouter } from '../../components/hoc/withRouter';
import { EditionKey } from '../../types/editions';
import { PendingPluginResult, Plugin } from '../../types/plugins';
import EditionBoxes from './EditionBoxes';
import Footer from './Footer';
import Header from './Header';
@@ -43,7 +42,7 @@ import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
interface Props {
currentEdition?: EditionKey;
fetchPendingPlugins: () => void;
pendingPlugins: PluginPendingResult;
pendingPlugins: PendingPluginResult;
location: Location;
router: Pick<Router, 'push'>;
standaloneMode?: boolean;

+ 16
- 9
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx View File

@@ -18,17 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Plugin, PluginPending } from '../../api/plugins';
import {
InstalledPlugin,
isAvailablePlugin,
isInstalledPlugin,
PendingPlugin,
Plugin
} from '../../types/plugins';
import PluginAvailable from './components/PluginAvailable';
import PluginInstalled from './components/PluginInstalled';
import { isPluginAvailable, isPluginInstalled } from './utils';

interface Props {
plugins: Plugin[];
pending: {
installing: PluginPending[];
updating: PluginPending[];
removing: PluginPending[];
installing: PendingPlugin[];
updating: PendingPlugin[];
removing: PendingPlugin[];
};
readOnly: boolean;
refreshPending: () => void;
@@ -49,9 +54,9 @@ export default class PluginsList extends React.PureComponent<Props> {
return undefined;
};

renderPlugin = (plugin: Plugin) => {
renderPlugin = (plugin: Plugin, installedPlugins: InstalledPlugin[]) => {
const status = this.getPluginStatus(plugin);
if (isPluginInstalled(plugin)) {
if (isInstalledPlugin(plugin)) {
return (
<PluginInstalled
plugin={plugin}
@@ -61,9 +66,10 @@ export default class PluginsList extends React.PureComponent<Props> {
/>
);
}
if (isPluginAvailable(plugin)) {
if (isAvailablePlugin(plugin)) {
return (
<PluginAvailable
installedPlugins={installedPlugins}
plugin={plugin}
readOnly={this.props.readOnly}
refreshPending={this.props.refreshPending}
@@ -75,13 +81,14 @@ export default class PluginsList extends React.PureComponent<Props> {
};

render() {
const installedPlugins = this.props.plugins.filter(isInstalledPlugin);
return (
<div className="boxed-group boxed-group-inner" id="marketplace-plugins">
<ul>
{this.props.plugins.map(plugin => (
<li className="panel panel-vertical" key={plugin.key}>
<table className="marketplace-plugin-table">
<tbody>{this.renderPlugin(plugin)}</tbody>
<tbody>{this.renderPlugin(plugin, installedPlugins)}</tbody>
</table>
</li>
))}

+ 8
- 8
server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx View File

@@ -22,8 +22,8 @@ import { Button } from 'sonar-ui-common/components/controls/buttons';
import Checkbox from 'sonar-ui-common/components/controls/Checkbox';
import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { installPlugin, Plugin, uninstallPlugin, updatePlugin } from '../../../api/plugins';
import { isPluginAvailable, isPluginInstalled } from '../utils';
import { installPlugin, uninstallPlugin, updatePlugin } from '../../../api/plugins';
import { isAvailablePlugin, isInstalledPlugin, Plugin } from '../../../types/plugins';
import PluginUpdateButton from './PluginUpdateButton';

interface Props {
@@ -75,7 +75,7 @@ export default class PluginActions extends React.PureComponent<Props, State> {

return (
<div className="js-actions">
{isPluginAvailable(plugin) && (
{isAvailablePlugin(plugin) && (
<div>
<p className="little-spacer-bottom">
{translate('marketplace.available_under_commercial_license')}
@@ -85,13 +85,13 @@ export default class PluginActions extends React.PureComponent<Props, State> {
</a>
</div>
)}
{isPluginInstalled(plugin) && (
{isInstalledPlugin(plugin) && (
<p>
<CheckIcon className="little-spacer-right" />
{translate('marketplace.installed')}
</p>
)}
{isPluginInstalled(plugin) && plugin.updates && plugin.updates.length > 0 && (
{isInstalledPlugin(plugin) && plugin.updates && plugin.updates.length > 0 && (
<div className="spacer-top">
{plugin.updates.map((update, idx) => (
<PluginUpdateButton
@@ -117,7 +117,7 @@ export default class PluginActions extends React.PureComponent<Props, State> {
const { loading } = this.state;
return (
<div className="js-actions">
{isPluginAvailable(plugin) && plugin.termsAndConditionsUrl && (
{isAvailablePlugin(plugin) && plugin.termsAndConditionsUrl && (
<p className="little-spacer-bottom">
<Checkbox
checked={this.state.acceptTerms}
@@ -138,7 +138,7 @@ export default class PluginActions extends React.PureComponent<Props, State> {
</p>
)}
{loading && <i className="spinner spacer-right little-spacer-top little-spacer-bottom" />}
{isPluginInstalled(plugin) && (
{isInstalledPlugin(plugin) && (
<div className="display-inlin-block">
{plugin.updates &&
plugin.updates.map((update, idx) => (
@@ -157,7 +157,7 @@ export default class PluginActions extends React.PureComponent<Props, State> {
</Button>
</div>
)}
{isPluginAvailable(plugin) && (
{isAvailablePlugin(plugin) && (
<Button
className="js-install"
disabled={loading || (plugin.termsAndConditionsUrl != null && !this.state.acceptTerms)}

+ 12
- 6
server/sonar-web/src/main/js/apps/marketplace/components/PluginAvailable.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { PluginAvailable as IPluginAvailable } from '../../../api/plugins';
import { AvailablePlugin, InstalledPlugin } from '../../../types/plugins';
import PluginChangeLogButton from './PluginChangeLogButton';
import PluginDescription from './PluginDescription';
import PluginLicense from './PluginLicense';
@@ -27,14 +27,17 @@ import PluginOrganization from './PluginOrganization';
import PluginStatus from './PluginStatus';
import PluginUrls from './PluginUrls';

interface Props {
plugin: IPluginAvailable;
export interface PluginAvailableProps {
installedPlugins: InstalledPlugin[];
plugin: AvailablePlugin;
readOnly: boolean;
refreshPending: () => void;
status?: string;
}

export default function PluginAvailable({ plugin, readOnly, refreshPending, status }: Props) {
export default function PluginAvailable(props: PluginAvailableProps) {
const { installedPlugins, plugin, readOnly, status } = props;
const installedPluginKeys = installedPlugins.map(({ key }) => key);
return (
<tr>
<PluginDescription plugin={plugin} />
@@ -52,7 +55,10 @@ export default function PluginAvailable({ plugin, readOnly, refreshPending, stat
<strong>
{translateWithParameters(
'marketplace.installing_this_plugin_will_also_install_x',
plugin.update.requires.map(requiredPlugin => requiredPlugin.name).join(', ')
plugin.update.requires
.filter(({ key }) => !installedPluginKeys.includes(key))
.map(requiredPlugin => requiredPlugin.name)
.join(', ')
)}
</strong>
</p>
@@ -71,7 +77,7 @@ export default function PluginAvailable({ plugin, readOnly, refreshPending, stat
</td>

{!readOnly && (
<PluginStatus plugin={plugin} refreshPending={refreshPending} status={status} />
<PluginStatus plugin={plugin} refreshPending={props.refreshPending} status={status} />
)}
</tr>
);

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Release, Update } from '../../../api/plugins';
import { Release, Update } from '../../../types/plugins';
import PluginChangeLogItem from './PluginChangeLogItem';

export interface Props {

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import EllipsisIcon from 'sonar-ui-common/components/icons/EllipsisIcon';
import { Release, Update } from '../../../api/plugins';
import { Release, Update } from '../../../types/plugins';
import PluginChangeLog from './PluginChangeLog';

interface Props {

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogItem.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Release, Update } from '../../../api/plugins';
import { Release, Update } from '../../../types/plugins';

interface Props {
release: Release;

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginDescription.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Plugin } from '../../../api/plugins';
import { Plugin } from '../../../types/plugins';

interface Props {
plugin: Plugin;

+ 2
- 2
server/sonar-web/src/main/js/apps/marketplace/components/PluginInstalled.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { PluginInstalled as IPluginInstalled } from '../../../api/plugins';
import { InstalledPlugin } from '../../../types/plugins';
import PluginDescription from './PluginDescription';
import PluginLicense from './PluginLicense';
import PluginOrganization from './PluginOrganization';
@@ -28,7 +28,7 @@ import PluginUpdates from './PluginUpdates';
import PluginUrls from './PluginUrls';

interface Props {
plugin: IPluginInstalled;
plugin: InstalledPlugin;
readOnly: boolean;
refreshPending: () => void;
status?: string;

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginOrganization.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Plugin } from '../../../api/plugins';
import { Plugin } from '../../../types/plugins';

export interface PluginOrganizationProps {
plugin: Plugin;

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginStatus.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Plugin } from '../../../api/plugins';
import { Plugin } from '../../../types/plugins';
import PluginActions from './PluginActions';

interface Props {

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateButton.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { Update } from '../../../api/plugins';
import { Update } from '../../../types/plugins';

interface Props {
disabled: boolean;

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateItem.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Release, Update } from '../../../api/plugins';
import { Release, Update } from '../../../types/plugins';
import PluginChangeLogButton from './PluginChangeLogButton';

interface Props {

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Update } from '../../../api/plugins';
import { Update } from '../../../types/plugins';
import PluginUpdateItem from './PluginUpdateItem';

interface Props {

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginUrls.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Plugin } from '../../../api/plugins';
import { Plugin } from '../../../types/plugins';

interface Props {
plugin: Plugin;

+ 3
- 3
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginActions-test.tsx View File

@@ -19,10 +19,10 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { PluginAvailable, PluginInstalled } from '../../../../api/plugins';
import { AvailablePlugin, InstalledPlugin } from '../../../../types/plugins';
import PluginActions from '../PluginActions';

const installedPlugin: PluginInstalled = {
const installedPlugin: InstalledPlugin = {
key: 'foo',
name: 'Foo',
filename: 'foo.zip',
@@ -35,7 +35,7 @@ const installedPlugin: PluginInstalled = {
version: '7.7'
};

const availablePlugin: PluginAvailable = {
const availablePlugin: AvailablePlugin = {
key: 'foo',
name: 'Foo',
release: { version: '7.7', date: 'date' },

+ 64
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginAvailable-test.tsx View File

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import {
mockAvailablePlugin,
mockInstalledPlugin,
mockPlugin,
mockUpdate
} from '../../../../helpers/mocks/plugins';
import PluginAvailable, { PluginAvailableProps } from '../PluginAvailable';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ readOnly: true })).toMatchSnapshot('read only');
expect(
shallowRender({
plugin: mockAvailablePlugin({
update: mockUpdate({ requires: [mockPlugin()] })
})
})
).toMatchSnapshot('has requirements');
const installed = mockInstalledPlugin({ key: 'sonar-bar', name: 'Sonar Bar' });
expect(
shallowRender({
installedPlugins: [installed],
plugin: mockAvailablePlugin({
update: mockUpdate({
requires: [mockPlugin(), installed]
})
})
})
).toMatchSnapshot('has requirements, some of them already met');
});

function shallowRender(props: Partial<PluginAvailableProps> = {}) {
return shallow<PluginAvailableProps>(
<PluginAvailable
installedPlugins={[]}
plugin={mockAvailablePlugin()}
readOnly={false}
refreshPending={jest.fn()}
{...props}
/>
);
}

+ 546
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginAvailable-test.tsx.snap View File

@@ -0,0 +1,546 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<tr>
<PluginDescription
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
<td
className="text-top big-spacer-right"
>
<ul>
<li
className="display-flex-row little-spacer-bottom"
>
<div
className="pull-left spacer-right"
>
<span
className="badge badge-success"
>
8.2
</span>
</div>
<div>
<PluginChangeLogButton
release={
Object {
"date": "2020-01-01",
"version": "8.2",
}
}
update={
Object {
"requires": Array [],
"status": "available",
}
}
/>
</div>
</li>
</ul>
</td>
<td
className="text-top width-20"
>
<ul>
<PluginUrls
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
<PluginLicense />
<PluginOrganization
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
</ul>
</td>
<PluginStatus
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
refreshPending={[MockFunction]}
/>
</tr>
`;

exports[`should render correctly: has requirements 1`] = `
<tr>
<PluginDescription
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
],
"status": "available",
},
}
}
/>
<td
className="text-top big-spacer-right"
>
<ul>
<li
className="display-flex-row little-spacer-bottom"
>
<div
className="pull-left spacer-right"
>
<span
className="badge badge-success"
>
8.2
</span>
</div>
<div>
<PluginChangeLogButton
release={
Object {
"date": "2020-01-01",
"version": "8.2",
}
}
update={
Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
],
"status": "available",
}
}
/>
<p
className="little-spacer-top"
>
<strong>
marketplace.installing_this_plugin_will_also_install_x.Sonar Foo
</strong>
</p>
</div>
</li>
</ul>
</td>
<td
className="text-top width-20"
>
<ul>
<PluginUrls
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
],
"status": "available",
},
}
}
/>
<PluginLicense />
<PluginOrganization
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
],
"status": "available",
},
}
}
/>
</ul>
</td>
<PluginStatus
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
],
"status": "available",
},
}
}
refreshPending={[MockFunction]}
/>
</tr>
`;

exports[`should render correctly: has requirements, some of them already met 1`] = `
<tr>
<PluginDescription
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
Object {
"filename": "sonar-bar-1.0.jar",
"hash": "hash",
"implementationBuild": "1.0.0.1234",
"key": "sonar-bar",
"name": "Sonar Bar",
"sonarLintSupported": false,
"updatedAt": 100,
"version": "1.0",
},
],
"status": "available",
},
}
}
/>
<td
className="text-top big-spacer-right"
>
<ul>
<li
className="display-flex-row little-spacer-bottom"
>
<div
className="pull-left spacer-right"
>
<span
className="badge badge-success"
>
8.2
</span>
</div>
<div>
<PluginChangeLogButton
release={
Object {
"date": "2020-01-01",
"version": "8.2",
}
}
update={
Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
Object {
"filename": "sonar-bar-1.0.jar",
"hash": "hash",
"implementationBuild": "1.0.0.1234",
"key": "sonar-bar",
"name": "Sonar Bar",
"sonarLintSupported": false,
"updatedAt": 100,
"version": "1.0",
},
],
"status": "available",
}
}
/>
<p
className="little-spacer-top"
>
<strong>
marketplace.installing_this_plugin_will_also_install_x.Sonar Foo
</strong>
</p>
</div>
</li>
</ul>
</td>
<td
className="text-top width-20"
>
<ul>
<PluginUrls
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
Object {
"filename": "sonar-bar-1.0.jar",
"hash": "hash",
"implementationBuild": "1.0.0.1234",
"key": "sonar-bar",
"name": "Sonar Bar",
"sonarLintSupported": false,
"updatedAt": 100,
"version": "1.0",
},
],
"status": "available",
},
}
}
/>
<PluginLicense />
<PluginOrganization
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
Object {
"filename": "sonar-bar-1.0.jar",
"hash": "hash",
"implementationBuild": "1.0.0.1234",
"key": "sonar-bar",
"name": "Sonar Bar",
"sonarLintSupported": false,
"updatedAt": 100,
"version": "1.0",
},
],
"status": "available",
},
}
}
/>
</ul>
</td>
<PluginStatus
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
},
Object {
"filename": "sonar-bar-1.0.jar",
"hash": "hash",
"implementationBuild": "1.0.0.1234",
"key": "sonar-bar",
"name": "Sonar Bar",
"sonarLintSupported": false,
"updatedAt": 100,
"version": "1.0",
},
],
"status": "available",
},
}
}
refreshPending={[MockFunction]}
/>
</tr>
`;

exports[`should render correctly: read only 1`] = `
<tr>
<PluginDescription
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
<td
className="text-top big-spacer-right"
>
<ul>
<li
className="display-flex-row little-spacer-bottom"
>
<div
className="pull-left spacer-right"
>
<span
className="badge badge-success"
>
8.2
</span>
</div>
<div>
<PluginChangeLogButton
release={
Object {
"date": "2020-01-01",
"version": "8.2",
}
}
update={
Object {
"requires": Array [],
"status": "available",
}
}
/>
</div>
</li>
</ul>
</td>
<td
className="text-top width-20"
>
<ul>
<PluginUrls
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
<PluginLicense />
<PluginOrganization
plugin={
Object {
"key": "sonar-foo",
"name": "Sonar Foo",
"release": Object {
"date": "2020-01-01",
"version": "8.2",
},
"update": Object {
"requires": Array [],
"status": "available",
},
}
}
/>
</ul>
</td>
</tr>
`;

+ 1
- 13
server/sonar-web/src/main/js/apps/marketplace/utils.ts View File

@@ -19,7 +19,7 @@
*/
import { memoize } from 'lodash';
import { cleanQuery, parseAsString, serializeString } from 'sonar-ui-common/helpers/query';
import { Plugin, PluginAvailable, PluginInstalled, PluginPending } from '../../api/plugins';
import { Plugin } from '../../types/plugins';

export interface Query {
filter: string;
@@ -43,18 +43,6 @@ export function filterPlugins(plugins: Plugin[], search?: string): Plugin[] {
});
}

export function isPluginAvailable(plugin: Plugin): plugin is PluginAvailable {
return (plugin as any).release !== undefined;
}

export function isPluginInstalled(plugin: Plugin): plugin is PluginInstalled {
return isPluginPending(plugin) && (plugin as any).updatedAt !== undefined;
}

export function isPluginPending(plugin: Plugin): plugin is PluginPending {
return (plugin as any).version !== undefined;
}

export const DEFAULT_FILTER = 'all';
export const parseQuery = memoize(
(urlQuery: T.RawQuery): Query => ({

+ 68
- 0
server/sonar-web/src/main/js/helpers/mocks/plugins.ts View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { AvailablePlugin, InstalledPlugin, Plugin, Release, Update } from '../../types/plugins';

export function mockPlugin(overrides: Partial<Plugin> = {}): Plugin {
return {
key: 'sonar-foo',
name: 'Sonar Foo',
...overrides
};
}

export function mockInstalledPlugin(overrides: Partial<InstalledPlugin> = {}): InstalledPlugin {
return {
key: 'sonar-bar',
name: 'Sonar Bar',
version: '1.0',
implementationBuild: '1.0.0.1234',
filename: 'sonar-bar-1.0.jar',
hash: 'hash',
sonarLintSupported: false,
updatedAt: 100,
...overrides
};
}

export function mockAvailablePlugin(overrides: Partial<AvailablePlugin> = {}): AvailablePlugin {
return {
release: mockRelease(),
update: mockUpdate(),
...mockPlugin(),
...overrides
};
}

export function mockRelease(overrides: Partial<Release> = {}): Release {
return {
date: '2020-01-01',
version: '8.2',
...overrides
};
}

export function mockUpdate(overrides: Partial<Update> = {}): Update {
return {
status: 'available',
requires: [],
...overrides
};
}

+ 84
- 0
server/sonar-web/src/main/js/types/plugins.ts View File

@@ -0,0 +1,84 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/

export interface Plugin {
key: string;
name: string;
category?: string;
description?: string;
editionBundled?: boolean;
license?: string;
organizationName?: string;
homepageUrl?: string;
organizationUrl?: string;
issueTrackerUrl?: string;
termsAndConditionsUrl?: string;
}

export interface PendingPluginResult {
installing: PendingPlugin[];
updating: PendingPlugin[];
removing: PendingPlugin[];
}

export interface AvailablePlugin extends Plugin {
release: Release;
update: Update;
}

export interface PendingPlugin extends Plugin {
version: string;
implementationBuild: string;
}

export interface InstalledPlugin extends PendingPlugin {
documentationPath?: string;
filename: string;
hash: string;
sonarLintSupported: boolean;
updatedAt: number;
updates?: Update[];
}

export interface Release {
version: string;
date: string;
description?: string;
changeLogUrl?: string;
}

export interface Update {
status: string;
release?: Release;
requires: Plugin[];
previousUpdates?: Update[];
}

export function isAvailablePlugin(plugin: Plugin): plugin is AvailablePlugin {
return (plugin as any).release !== undefined;
}

export function isInstalledPlugin(plugin: Plugin): plugin is InstalledPlugin {
return isPendingPlugin(plugin) && (plugin as any).updatedAt !== undefined;
}

export function isPendingPlugin(plugin: Plugin): plugin is PendingPlugin {
return (plugin as any).version !== undefined;
}

Loading…
Cancel
Save