]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13386 Don't show 'plugin will get installed' if it's already installed
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 15 Jun 2020 11:06:06 +0000 (13:06 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 19 Jun 2020 20:04:42 +0000 (20:04 +0000)
26 files changed:
server/sonar-web/src/main/js/api/plugins.ts
server/sonar-web/src/main/js/app/components/AdminContainer.tsx
server/sonar-web/src/main/js/app/components/AdminContext.tsx
server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginAvailable.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogItem.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginDescription.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginInstalled.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginOrganization.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginStatus.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateButton.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdateItem.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginUrls.tsx
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginActions-test.tsx
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginAvailable-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginAvailable-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/utils.ts
server/sonar-web/src/main/js/helpers/mocks/plugins.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/plugins.ts [new file with mode: 0644]

index 7a848e582ad47d8251848488bf5623b073c4dc9e..4e898155f52df484c3f67b7d82d136a5ae7c2c99 100644 (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,
index 43c2465e9e23432b6483b782d433acaf79193cb4..232a9fa97222eecd4fd7d28c2485afe305d8b30c 100644 (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;
 }
 
index 5f41575bbc608688433cef4dda807e3d0f93e962..9b251813e27f1da7d07d5fedf6f2276bb3f4c6ba 100644 (file)
  * 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;
 }
 
index 21db1cfde354f5470ca12f3fe35fb72d1a133b92..8b57882fab43983c34acdcb7cac0724c4ed12fc7 100644 (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;
 }
index 854e32fda3d61f21f7db8925cba3109886fdd5a1..5bbda188e0ed110588f5ff8f84edb1549a85f0f3 100644 (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;
 }
 
index 6f54f90f4c8f54228dcf2f7bddccdfadc1e4b413..be0278ceb20abb327fb2ca06606e73f3d9178605 100644 (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;
index 8d864fcdf59eea943a4b312ba30e85802b6d361e..4c1933ac58745b38aea9fc4072d39798e75748d3 100644 (file)
  * 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>
           ))}
index 57b237d965f826d4b843212ff64ac9ed3611ec2e..06319354ad5c6710d973f40af513bf777db42b1e 100644 (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)}
index 6b166a46ebaaef9450f8a4917c210b3adfe06d52..985e4212f9e6649f6b173dc7189cb6bca23cf104 100644 (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>
   );
index 95fff94e48e5e93404fa274017076bde7e7b31ae..9891560e82d7714e8c005105cd9a245db9d8cc5f 100644 (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 {
index 5824380100ab072863ba2201ccbce3ffe4faa54e..402a4665ed423404f8a54291b3a7d750f77c0e42 100644 (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 {
index 7e0da5e02ee259df6a72c822511b38fd90b74890..0e45bc31d72759de9e1237e6d7d572d513f5797f 100644 (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;
index e964a0a0e71dd1bdb172b0de7c1e9582758d10e9..a099350e7a74c9fb38e05b031cc0e493f39166fa 100644 (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;
index 7c3070e98049afdd61c84b1ca1c39ed94c154942..e3859f7c25b9e2fbedda2c585070584a159b88ff 100644 (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;
index 204fbb5c216389a7c21d95772118789169c35605..b889cbd82bd46cf23c96405ce78176db953e5505 100644 (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;
index 41b4ddf0ee784210b43cb00c2525669a9bb68e10..a7a603336d8a4158aced6b789889aa768d4428e6 100644 (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 {
index a7d094504b269b8a1862054c08c2dd0c4ea6e720..e34e3f37badfe2612044a6e91492432fb97aae31 100644 (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;
index a1e1e36cfcb88dc9bca2279700c3e6abbbcd6eee..f6ef8eff74e50d92acd37dee94e84db8ff543c04 100644 (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 {
index ab76582882a51a3f573e15a59c529c48733a82b3..0d6064d7e3382ab2ab105f9667e0e3d852181912 100644 (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 {
index 6a13802c3f9a858a27129498d85a2298d4e9da9d..f5e20c2fdc7451ff5979f09450aae3cd1cad16c9 100644 (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;
index b549e44431cf8db1358a2e58447a816f6eef1fef..2d2c245a9faa4d6edb5fccc3c1f2ec2addeee00d 100644 (file)
  */
 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' },
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginAvailable-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginAvailable-test.tsx
new file mode 100644 (file)
index 0000000..360592e
--- /dev/null
@@ -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}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginAvailable-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginAvailable-test.tsx.snap
new file mode 100644 (file)
index 0000000..9222f00
--- /dev/null
@@ -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>
+`;
index 4aa1ad8b878bd62034abcf386492ec9c569c0054..db6ed4743ee568956f80c05530240f39db75c846 100644 (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 => ({
diff --git a/server/sonar-web/src/main/js/helpers/mocks/plugins.ts b/server/sonar-web/src/main/js/helpers/mocks/plugins.ts
new file mode 100644 (file)
index 0000000..bdaf345
--- /dev/null
@@ -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
+  };
+}
diff --git a/server/sonar-web/src/main/js/types/plugins.ts b/server/sonar-web/src/main/js/types/plugins.ts
new file mode 100644 (file)
index 0000000..bea6452
--- /dev/null
@@ -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;
+}