]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21445 migrate Marketplace page to the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 22 Jan 2024 09:57:00 +0000 (10:57 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 22 Jan 2024 20:02:35 +0000 (20:02 +0000)
28 files changed:
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/LicensePromptModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
server/sonar-web/src/main/js/apps/marketplace/Footer.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/Header.tsx
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx
server/sonar-web/src/main/js/apps/marketplace/Search.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/MarketplaceApp-it.tsx
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx [deleted file]
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/PluginLicense.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginOrganization.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.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/components/controls/SearchBox.tsx

index f08f3ccb9b1e991b826b3edf2ba307fac4b61899..a28ac860a19eae0019229c7bdc93249c2402d6dc 100644 (file)
@@ -79,6 +79,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/project/background_tasks',
   '/admin/background_tasks',
   '/admin/groups',
+  '/admin/marketplace',
   '/admin/system',
   '/admin/users',
   '/admin/settings/encryption',
diff --git a/server/sonar-web/src/main/js/app/components/LicensePromptModal.tsx b/server/sonar-web/src/main/js/app/components/LicensePromptModal.tsx
new file mode 100644 (file)
index 0000000..3ed3a5f
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { FormattedMessage } from 'react-intl';
+import Link from '../../components/common/Link';
+import Modal from '../../components/controls/Modal';
+import { ResetButtonLink } from '../../components/controls/buttons';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  onClose: () => void;
+}
+
+export default function LicensePromptModal({ onClose }: Readonly<Props>) {
+  const header = translate('license.prompt.title');
+  return (
+    <Modal contentLabel={header} onRequestClose={onClose}>
+      <header className="modal-head">
+        <h2>{header}</h2>
+      </header>
+      <div className="modal-body">
+        <FormattedMessage
+          defaultMessage={translate('license.prompt.description')}
+          id="license.prompt.description"
+          values={{
+            url: (
+              <Link onClick={onClose} to="/admin/extension/license/app">
+                {translate('license.prompt.link')}
+              </Link>
+            ),
+          }}
+        />
+      </div>
+      <footer className="modal-foot">
+        <ResetButtonLink onClick={onClose}>{translate('cancel')}</ResetButtonLink>
+      </footer>
+    </Modal>
+  );
+}
index 64077d76b91014ea67fa93d452eb2fec391fd473..192f4708c815b771fb473d45bb7fd613b982ccfa 100644 (file)
 import { differenceInDays } from 'date-fns';
 import * as React from 'react';
 import { showLicense } from '../../api/editions';
-import LicensePromptModal from '../../apps/marketplace/components/LicensePromptModal';
 import { parseDate, toShortISO8601String } from '../../helpers/dates';
 import { hasMessage } from '../../helpers/l10n';
 import { get, save } from '../../helpers/storage';
 import { AppState } from '../../types/appstate';
 import { EditionKey } from '../../types/editions';
 import { CurrentUser, isLoggedIn } from '../../types/users';
+import LicensePromptModal from './LicensePromptModal';
 import withAppStateContext from './app-state/withAppStateContext';
 import withCurrentUserContext from './current-user/withCurrentUserContext';
 
index f5ac54516d979583e43827cb75eeaa3079629fe8..3ed9b3b018b5d2c2eaaca2b2f9f56331133e6266 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  BasicSeparator,
+  FlagMessage,
+  LargeCenteredLayout,
+  PageContentFontWrapper,
+  Spinner,
+  SubTitle,
+} from 'design-system';
 import { sortBy, uniqBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { FormattedMessage } from 'react-intl';
 import { getAvailablePlugins, getInstalledPlugins } from '../../api/plugins';
 import { getValue, setSimpleSettingValue } from '../../api/settings';
-import DocLink from '../../components/common/DocLink';
+import DocumentationLink from '../../components/common/DocumentationLink';
+import ListFooter from '../../components/controls/ListFooter';
 import Suggestions from '../../components/embed-docs-modal/Suggestions';
 import { Location, Router, withRouter } from '../../components/hoc/withRouter';
-import { Alert } from '../../components/ui/Alert';
-import Spinner from '../../components/ui/Spinner';
 import { translate } from '../../helpers/l10n';
 import { EditionKey } from '../../types/editions';
 import { PendingPluginResult, Plugin, RiskConsent } from '../../types/plugins';
 import { SettingsKey } from '../../types/settings';
 import EditionBoxes from './EditionBoxes';
-import Footer from './Footer';
 import Header from './Header';
 import PluginsList from './PluginsList';
 import Search from './Search';
@@ -158,60 +164,73 @@ class App extends React.PureComponent<Props, State> {
       riskConsent === RiskConsent.Accepted;
 
     return (
-      <main className="page page-limited" id="marketplace-page">
-        <Suggestions suggestions="marketplace" />
-        <Helmet title={translate('marketplace.page')} />
-        <Header currentEdition={currentEdition} />
-        <EditionBoxes currentEdition={currentEdition} />
-        <header className="page-header">
-          <h2 className="page-title">{translate('marketplace.page.plugins')}</h2>
-          <div className="page-description">
-            <p>{translate('marketplace.page.plugins.description')}</p>
-            {currentEdition !== EditionKey.community && (
-              <Alert className="spacer-top" variant="info">
-                <FormattedMessage
-                  id="marketplace.page.plugins.description2"
-                  defaultMessage={translate('marketplace.page.plugins.description2')}
-                  values={{
-                    link: (
-                      <DocLink to="/instance-administration/marketplace/">
-                        {translate('marketplace.page.plugins.description2.link')}
-                      </DocLink>
-                    ),
-                  }}
-                />
-              </Alert>
-            )}
+      <LargeCenteredLayout as="main" id="marketplace-page">
+        <PageContentFontWrapper className="sw-body-sm sw-py-8">
+          <Suggestions suggestions="marketplace" />
+          <Helmet title={translate('marketplace.page')} />
+          <Header currentEdition={currentEdition} />
+          <EditionBoxes currentEdition={currentEdition} />
+
+          <BasicSeparator className="sw-my-6" />
+
+          <div>
+            <SubTitle>{translate('marketplace.page.plugins')}</SubTitle>
+            <div className="sw-mt-2 sw-max-w-abs-600 ">
+              <p>{translate('marketplace.page.plugins.description')}</p>
+              {currentEdition !== EditionKey.community && (
+                <FlagMessage className="sw-mt-2" variant="info">
+                  <p>
+                    <FormattedMessage
+                      id="marketplace.page.plugins.description2"
+                      defaultMessage={translate('marketplace.page.plugins.description2')}
+                      values={{
+                        link: (
+                          <DocumentationLink to="/instance-administration/marketplace/">
+                            {translate('marketplace.page.plugins.description2.link')}
+                          </DocumentationLink>
+                        ),
+                      }}
+                    />
+                  </p>
+                </FlagMessage>
+              )}
+            </div>
+          </div>
+
+          <PluginRiskConsentBox
+            acknowledgeRisk={this.acknowledgeRisk}
+            currentEdition={currentEdition}
+            riskConsent={riskConsent}
+          />
+
+          <Search
+            query={query}
+            updateCenterActive={this.props.updateCenterActive}
+            updateQuery={this.updateQuery}
+          />
+          <div className="sw-mt-4">
+            <Spinner loading={loadingPlugins}>
+              {filteredPlugins.length === 0 &&
+                translate('marketplace.plugin_list.no_plugins', query.filter)}
+              {filteredPlugins.length > 0 && (
+                <>
+                  <PluginsList
+                    pending={pendingPlugins}
+                    plugins={filteredPlugins}
+                    readOnly={!allowActions}
+                    refreshPending={this.props.fetchPendingPlugins}
+                  />
+                  <ListFooter
+                    useMIUIButtons
+                    count={filteredPlugins.length}
+                    total={plugins.length}
+                  />
+                </>
+              )}
+            </Spinner>
           </div>
-        </header>
-
-        <PluginRiskConsentBox
-          acknowledgeRisk={this.acknowledgeRisk}
-          currentEdition={currentEdition}
-          riskConsent={riskConsent}
-        />
-
-        <Search
-          query={query}
-          updateCenterActive={this.props.updateCenterActive}
-          updateQuery={this.updateQuery}
-        />
-        <Spinner loading={loadingPlugins}>
-          {filteredPlugins.length === 0 &&
-            translate('marketplace.plugin_list.no_plugins', query.filter)}
-          {filteredPlugins.length > 0 && (
-            <>
-              <PluginsList
-                pending={pendingPlugins}
-                plugins={filteredPlugins}
-                readOnly={!allowActions}
-                refreshPending={this.props.fetchPendingPlugins}
-              />
-              <Footer total={filteredPlugins.length} />
-            </>
-          )}
-        </Spinner>
-      </main>
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
index 135550963b0cb22a8ae4a338e7a97f9eed2f4f23..a22a23c2932717127303d1d0bf068579c33417aa 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Card, Link } from 'design-system';
 import * as React from 'react';
 import { getMarketplaceNavigation } from '../../api/navigation';
-import { getAllEditionsAbove } from '../../helpers/editions';
+import { getAllEditionsAbove, getEditionUrl } from '../../helpers/editions';
+import { translate } from '../../helpers/l10n';
 import { EditionKey } from '../../types/editions';
 import EditionBox from './components/EditionBox';
 
@@ -66,15 +68,20 @@ export default class EditionBoxes extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div className="spacer-bottom marketplace-editions">
+      <div className="sw-mt-4 sw-flex sw-gap-4">
         {visibleEditions.map((edition) => (
-          <EditionBox
-            currentEdition={currentEdition}
-            edition={edition}
+          <Card
             key={edition.key}
-            ncloc={ncloc}
-            serverId={serverId}
-          />
+            className="sw-max-w-1/2 sw-flex-1 sw-flex sw-flex-col sw-justify-between"
+          >
+            <EditionBox edition={edition} />
+
+            <div className="sw-mt-4">
+              <Link to={getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition })}>
+                {translate('marketplace.request_free_trial')}
+              </Link>
+            </div>
+          </Card>
         ))}
       </div>
     );
diff --git a/server/sonar-web/src/main/js/apps/marketplace/Footer.tsx b/server/sonar-web/src/main/js/apps/marketplace/Footer.tsx
deleted file mode 100644 (file)
index 6546dec..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { translateWithParameters } from '../../helpers/l10n';
-
-interface Props {
-  total: number;
-}
-
-export default function Footer({ total }: Props) {
-  return (
-    <footer className="spacer-top note text-center">
-      {translateWithParameters('x_show', total)}
-    </footer>
-  );
-}
index 1bf06a2d94c253459f64fd5cf64156e8f0f46cdb..9e02ad24d8d6a208628f2e3d28c12f001ca025cd 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Title } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { EditionKey } from '../../types/editions';
@@ -25,16 +26,16 @@ interface Props {
   currentEdition?: EditionKey;
 }
 
-export default function Header({ currentEdition }: Props) {
+export default function Header({ currentEdition }: Readonly<Props>) {
   return (
-    <header className="page-header" id="marketplace-header">
-      <h2 className="page-title">{translate('marketplace.page')}</h2>
+    <header id="marketplace-header">
+      <Title>{translate('marketplace.page')}</Title>
       {currentEdition && (
-        <h3 className="page-description">
+        <div className="sw-body-sm-highlight">
           {translate('marketplace.page.you_are_running', currentEdition)}
-        </h3>
+        </div>
       )}
-      <p className="page-description">
+      <p className="sw-mt-2">
         {currentEdition === 'datacenter'
           ? translate('marketplace.page.description_best_edition')
           : translate('marketplace.page.description')}
index 08f380c25e72b5f611f34b57caf48a8c84a943b1..8decba914331cfa66f7b54f8bdeb3384b9fd6c82 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Card, Table, TableRow } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
-import { isAvailablePlugin, isInstalledPlugin, PendingPlugin, Plugin } from '../../types/plugins';
+import { PendingPlugin, Plugin, isAvailablePlugin, isInstalledPlugin } from '../../types/plugins';
 import PluginAvailable from './components/PluginAvailable';
 import PluginInstalled from './components/PluginInstalled';
 
@@ -49,39 +50,42 @@ function getPluginStatus(plugin: Plugin, pending: PluginsListProps['pending']):
   return undefined;
 }
 
-export default function PluginsList(props: PluginsListProps) {
+export default function PluginsList(props: Readonly<PluginsListProps>) {
   const { pending, plugins, readOnly } = props;
   const installedPlugins = plugins.filter(isInstalledPlugin);
+
+  const columns = readOnly ? ['25%', 'auto', '20%'] : ['25%', 'auto', '20%', '20%'];
+
   return (
-    <div className="boxed-group boxed-group-inner" id="marketplace-plugins">
-      <ul aria-label={translate('marketplace.page.plugins')}>
+    <Card id="marketplace-plugins">
+      <Table
+        aria-label={translate('marketplace.page.plugins')}
+        columnCount={columns.length}
+        columnWidths={columns}
+      >
         {sortBy(plugins, ({ name }) => name).map((plugin) => (
-          <li className="panel panel-vertical" key={plugin.key}>
-            <table className="marketplace-plugin-table">
-              <tbody>
-                {isInstalledPlugin(plugin) && (
-                  <PluginInstalled
-                    plugin={plugin}
-                    readOnly={readOnly}
-                    refreshPending={props.refreshPending}
-                    status={getPluginStatus(plugin, pending)}
-                  />
-                )}
+          <TableRow key={plugin.key}>
+            {isInstalledPlugin(plugin) && (
+              <PluginInstalled
+                plugin={plugin}
+                readOnly={readOnly}
+                refreshPending={props.refreshPending}
+                status={getPluginStatus(plugin, pending)}
+              />
+            )}
 
-                {isAvailablePlugin(plugin) && (
-                  <PluginAvailable
-                    installedPlugins={installedPlugins}
-                    plugin={plugin}
-                    readOnly={readOnly}
-                    refreshPending={props.refreshPending}
-                    status={getPluginStatus(plugin, pending)}
-                  />
-                )}
-              </tbody>
-            </table>
-          </li>
+            {isAvailablePlugin(plugin) && (
+              <PluginAvailable
+                installedPlugins={installedPlugins}
+                plugin={plugin}
+                readOnly={readOnly}
+                refreshPending={props.refreshPending}
+                status={getPluginStatus(plugin, pending)}
+              />
+            )}
+          </TableRow>
         ))}
-      </ul>
-    </div>
+      </Table>
+    </Card>
   );
 }
index b9402cbda32fc3c6a6955cc042d1a9daf870ec23..8afebdcf2980aec0e568d90af5dbb2dc63cc9403 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputSearch, ToggleButton } from 'design-system';
 import * as React from 'react';
-import ButtonToggle from '../../components/controls/ButtonToggle';
-import SearchBox from '../../components/controls/SearchBox';
 import { translate } from '../../helpers/l10n';
 import { Query } from './utils';
 
@@ -49,17 +48,16 @@ export default class Search extends React.PureComponent<Props> {
       },
     ];
     return (
-      <div className="big-spacer-bottom" id="marketplace-search">
-        <div className="display-inline-block text-top nowrap abs-width-240 spacer-right">
-          <ButtonToggle
-            onCheck={this.handleFilterChange}
-            options={radioOptions}
-            value={query.filter}
-          />
-        </div>
-        <SearchBox
+      <div className="sw-mt-6 sw-flex sw-gap-6" id="marketplace-search">
+        <ToggleButton
+          onChange={this.handleFilterChange}
+          options={radioOptions}
+          value={query.filter}
+        />
+        <InputSearch
           onChange={this.handleSearch}
           placeholder={translate('marketplace.search')}
+          size="large"
           value={query.search}
         />
       </div>
index 4fdf0bf577e5821b18b50dd308439b4734e9c15a..54c5f8fdf5b651c906fa6d3ce648b4d0a3aab311 100644 (file)
@@ -42,10 +42,10 @@ const ui = {
   deTitle: byRole('heading', { name: 'SonarQube logo Developer Edition' }),
   eeTitle: byRole('heading', { name: 'SonarQube logo Enterprise Edition' }),
   dceTitle: byRole('heading', { name: 'SonarQube logo Data Center Edition' }),
-  pluginRow: byRole('list', { name: 'marketplace.page.plugins' }).byRole('table'),
-  filterAll: byRole('button', { name: 'marketplace.all' }),
-  filterInstalled: byRole('button', { name: 'marketplace.installed' }),
-  filterWithUpdates: byRole('button', { name: 'marketplace.updates_only' }),
+  pluginRow: byRole('table', { name: 'marketplace.page.plugins' }).byRole('row'),
+  filterAll: byRole('radio', { name: 'marketplace.all' }),
+  filterInstalled: byRole('radio', { name: 'marketplace.installed' }),
+  filterWithUpdates: byRole('radio', { name: 'marketplace.updates_only' }),
   search: byRole('searchbox', { name: 'marketplace.search' }),
   clearSearch: byRole('button', { name: 'clear' }),
   noPluginsText: byText('marketplace.plugin_list.no_plugins', { exact: false }),
@@ -115,7 +115,7 @@ it('should install, uninstall, update', async () => {
   expect(ui.updateButton.query()).not.toBeInTheDocument();
   expect(ui.riskConsentMessage.get()).toBeInTheDocument();
   expect(ui.riskConsentButton.get()).toBeInTheDocument();
-  await act(() => user.click(ui.riskConsentButton.get()));
+  await user.click(ui.riskConsentButton.get());
   expect(ui.riskConsentMessage.query()).not.toBeInTheDocument();
 
   expect(rows[0]).toHaveTextContent('ATest_install');
@@ -123,7 +123,7 @@ it('should install, uninstall, update', async () => {
   expect(ui.installButton.query(rows[0])).not.toBeInTheDocument();
   expect(ui.updateButton.query(rows[0])).not.toBeInTheDocument();
   expect(ui.uninstallPending.query(rows[0])).not.toBeInTheDocument();
-  await act(() => user.click(ui.uninstallButton.get(rows[0])));
+  await user.click(ui.uninstallButton.get(rows[0]));
   expect(await ui.uninstallPending.find(rows[0])).toBeInTheDocument();
   expect(ui.uninstallButton.query(rows[0])).not.toBeInTheDocument();
 
index 58a338754bc46d07cb3113841f525e6e221e648c..f845f0fd42934ff0af307e4c4320dd2612594a6d 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { SubHeading, UnorderedList } from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
-import { getEditionUrl } from '../../../helpers/editions';
-import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { Edition, EditionKey } from '../../../types/editions';
 
 interface Props {
-  currentEdition?: EditionKey;
   edition: Edition;
-  ncloc?: number;
-  serverId?: string;
 }
 
-export default function EditionBox({ edition, ncloc, serverId, currentEdition }: Props) {
-  return (
-    <div className="boxed-group boxed-group-inner marketplace-edition">
-      {edition.key === EditionKey.datacenter && (
-        <div className="markdown">
-          <div className="markdown-content">
-            <div>
-              <h3 id="data-center-edition">
-                <img
-                  alt="SonarQube logo"
-                  className="max-width-100 little-spacer-right"
-                  src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
-                />
-                Data Center Edition
-              </h3>
-              <p>
-                <em>Designed for High Availability and Scalability</em>
-              </p>
-              <p>Enterprise Edition functionality plus:</p>
-              <ul>
-                <li>Component redundancy</li>
-                <li>Data resiliency</li>
-                <li>Horizontal scalability</li>
-              </ul>
-            </div>
-          </div>
+export default function EditionBox({ edition }: Readonly<Props>) {
+  switch (edition.key) {
+    case EditionKey.datacenter:
+      return (
+        <div>
+          <SubHeading as="h2" id="data-center-edition">
+            <img
+              alt="SonarQube logo"
+              className="sw-mr-2"
+              width={16}
+              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+            />
+            <span>Data Center Edition</span>
+          </SubHeading>
+          <p className="sw-mt-4">
+            <em>Designed for High Availability and Scalability</em>
+          </p>
+          <p className="sw-mt-4">Enterprise Edition functionality plus:</p>
+          <UnorderedList className="sw-ml-8" ticks>
+            <li>Component redundancy</li>
+            <li>Data resiliency</li>
+            <li>Horizontal scalability</li>
+          </UnorderedList>
         </div>
-      )}
-      {edition.key === EditionKey.developer && (
-        <div className="markdown">
-          <div className="markdown-content">
-            <div>
-              <h3 id="developer-edition">
-                <img
-                  alt="SonarQube logo"
-                  className="max-width-100 little-spacer-right"
-                  src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
-                />
-                Developer Edition
-              </h3>
-              <p>
-                <em>Built for Developers by Developers</em>
-              </p>
-              <p>Community Edition functionality plus:</p>
-              <ul>
-                <li>
-                  PR / MR decoration &amp; Quality Gate
-                  <img
-                    alt="GitHub"
-                    className="little-spacer-left max-width-100"
-                    src={`${getBaseUrl()}/images/alm/github.svg`}
-                  />
-                  <img
-                    alt="GitLab"
-                    className="little-spacer-left max-width-100"
-                    src={`${getBaseUrl()}/images/alm/gitlab.svg`}
-                  />
-                  <img
-                    alt="Azure DevOps"
-                    className="little-spacer-left max-width-100"
-                    src={`${getBaseUrl()}/images/alm/azure.svg`}
-                  />
-                  <img
-                    alt="Bitbucket"
-                    className="little-spacer-left max-width-100"
-                    src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
-                  />
-                </li>
-                <li>
-                  Taint analysis / Injection flaw detection for Java, C#, PHP, Python, JS &amp; TS
-                </li>
-                <li>Branch analysis</li>
-                <li>Project aggregation</li>
-                <li>Additional languages: C, C++, Obj-C, PL/SQL, ABAP, TSQL &amp; Swift</li>
-              </ul>
-            </div>
-          </div>
+      );
+
+    case EditionKey.enterprise:
+      return (
+        <div>
+          <SubHeading as="h2" id="enterprise-edition">
+            <img
+              alt="SonarQube logo"
+              className="sw-mr-2"
+              width={16}
+              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+            />
+            <span>Enterprise Edition</span>
+          </SubHeading>
+          <p className="sw-mt-4">
+            <em>Designed to Meet Enterprise Requirements</em>
+          </p>
+          <p className="sw-mt-4">Developer Edition functionality plus:</p>
+          <UnorderedList className="sw-ml-8" ticks>
+            <li>Faster analysis with parallel processing</li>
+            <li>OWASP/CWE security reports</li>
+            <li>Portfolio management</li>
+            <li>Executive reporting</li>
+            <li>Project transfer</li>
+            <li>Additional languages: Apex, COBOL, PL/I, RPG &amp; VB6</li>
+          </UnorderedList>
         </div>
-      )}
-      {edition.key === EditionKey.enterprise && (
-        <div className="markdown">
-          <div className="markdown-content">
-            <div>
-              <h3 id="enterprise-edition">
-                <img
-                  alt="SonarQube logo"
-                  className="max-width-100 little-spacer-right"
-                  src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
-                />{' '}
-                Enterprise Edition
-              </h3>
-              <p>
-                <em>Designed to Meet Enterprise Requirements</em>
-              </p>
-              <p>Developer Edition functionality plus:</p>
-              <ul>
-                <li>Faster analysis with parallel processing</li>
-                <li>OWASP/CWE security reports</li>
-                <li>Portfolio management</li>
-                <li>Executive reporting</li>
-                <li>Project transfer</li>
-                <li>Additional languages: Apex, COBOL, PL/I, RPG &amp; VB6</li>
-              </ul>
-            </div>
-          </div>
+      );
+
+    case EditionKey.developer:
+      return (
+        <div>
+          <SubHeading as="h2" id="developer-edition">
+            <img
+              alt="SonarQube logo"
+              className="sw-mr-2"
+              width={16}
+              src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+            />
+            <span>Developer Edition</span>
+          </SubHeading>
+          <p className="sw-mt-4">
+            <em>Built for Developers by Developers</em>
+          </p>
+          <p className="sw-mt-4">Community Edition functionality plus:</p>
+          <UnorderedList className="sw-ml-8" ticks>
+            <li>
+              <span>PR / MR decoration &amp; Quality Gate</span>
+              <img
+                alt="GitHub"
+                className="sw-ml-2"
+                src={`${getBaseUrl()}/images/alm/github.svg`}
+                width={16}
+              />
+              <img
+                alt="GitLab"
+                className="sw-ml-2"
+                src={`${getBaseUrl()}/images/alm/gitlab.svg`}
+                width={16}
+              />
+              <img
+                alt="Azure DevOps"
+                className="sw-ml-2"
+                src={`${getBaseUrl()}/images/alm/azure.svg`}
+                width={16}
+              />
+              <img
+                alt="Bitbucket"
+                className="sw-ml-2"
+                src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
+                width={16}
+              />
+            </li>
+            <li>
+              Taint analysis / Injection flaw detection for Java, C#, PHP, Python, JS &amp; TS
+            </li>
+            <li>Branch analysis</li>
+            <li>Project aggregation</li>
+            <li>Additional languages: C, C++, Obj-C, PL/SQL, ABAP, TSQL &amp; Swift</li>
+          </UnorderedList>
         </div>
-      )}
-      <div className="marketplace-edition-action spacer-top">
-        <Link
-          to={getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition })}
-          target="_blank"
-        >
-          {translate('marketplace.request_free_trial')}
-        </Link>
-      </div>
-    </div>
-  );
+      );
+
+    default:
+      return null;
+  }
 }
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx
deleted file mode 100644 (file)
index 4deefed..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { FormattedMessage } from 'react-intl';
-import Link from '../../../components/common/Link';
-import { ResetButtonLink } from '../../../components/controls/buttons';
-import Modal from '../../../components/controls/Modal';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  onClose: () => void;
-}
-
-export default function LicensePromptModal({ onClose }: Props) {
-  const header = translate('license.prompt.title');
-  return (
-    <Modal contentLabel={header} onRequestClose={onClose}>
-      <header className="modal-head">
-        <h2>{header}</h2>
-      </header>
-      <div className="modal-body">
-        <FormattedMessage
-          defaultMessage={translate('license.prompt.description')}
-          id="license.prompt.description"
-          values={{
-            url: (
-              <Link onClick={onClose} to="/admin/extension/license/app">
-                {translate('license.prompt.link')}
-              </Link>
-            ),
-          }}
-        />
-      </div>
-      <footer className="modal-foot">
-        <ResetButtonLink onClick={onClose}>{translate('cancel')}</ResetButtonLink>
-      </footer>
-    </Modal>
-  );
-}
index 74d719fc36a59ce809bd1dc6511212104df6ca50..9f1344bd1a5a264fa8509ce67172715f9c2a703e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  ButtonSecondary,
+  CheckIcon,
+  Checkbox,
+  DangerButtonSecondary,
+  Link,
+  Spinner,
+} from 'design-system';
 import * as React from 'react';
 import { installPlugin, uninstallPlugin, updatePlugin } from '../../../api/plugins';
-import Link from '../../../components/common/Link';
-import Checkbox from '../../../components/controls/Checkbox';
 import Tooltip from '../../../components/controls/Tooltip';
-import { Button } from '../../../components/controls/buttons';
-import CheckIcon from '../../../components/icons/CheckIcon';
 import { translate } from '../../../helpers/l10n';
 import { Plugin, isAvailablePlugin, isInstalledPlugin } from '../../../types/plugins';
 import PluginUpdateButton from './PluginUpdateButton';
@@ -122,61 +126,48 @@ export default class PluginActions extends React.PureComponent<Props, State> {
     return (
       <div className="it__js-actions">
         {isAvailablePlugin(plugin) && plugin.termsAndConditionsUrl && (
-          <div className="little-spacer-bottom">
+          <div className="sw-flex sw-items-center sw-flex-wrap sw-mb-2">
             <Checkbox
               checked={this.state.acceptTerms}
-              className="js-terms"
               id={'plugin-terms-' + plugin.key}
               onCheck={this.handleTermsCheck}
             >
-              <label className="little-spacer-left" htmlFor={'plugin-terms-' + plugin.key}>
-                {translate('marketplace.i_accept_the')}
-              </label>
+              <span className="sw-ml-2">{translate('marketplace.i_accept_the')}</span>
             </Checkbox>
-            <a
-              className="js-plugin-terms nowrap little-spacer-left"
-              href={plugin.termsAndConditionsUrl}
-              target="_blank"
-              rel="noopener noreferrer"
-            >
+            <Link className="sw-whitespace-nowrap sw-ml-1" to={plugin.termsAndConditionsUrl}>
               {translate('marketplace.terms_and_conditions')}
-            </a>
+            </Link>
           </div>
         )}
-        {loading && <i className="spinner spacer-right little-spacer-top little-spacer-bottom" />}
+        <Spinner className="sw-my-2" loading={loading} />
         {isInstalledPlugin(plugin) && (
           <>
-            {plugin.updates &&
-              plugin.updates.map((update, idx) => (
+            {plugin.updates?.map((update, idx) => (
+              <div className="sw-inline-block sw-mr-2 sw-mb-2" key={idx}>
                 <PluginUpdateButton
                   disabled={loading}
-                  key={idx}
                   onClick={this.handleUpdate}
                   update={update}
                 />
-              ))}
+              </div>
+            ))}
             <Tooltip overlay={translate('marketplace.requires_restart')}>
-              <Button
-                className="js-uninstall button-red little-spacer-left"
-                disabled={loading}
-                onClick={this.handleUninstall}
-              >
+              <DangerButtonSecondary disabled={loading} onClick={this.handleUninstall}>
                 {translate('marketplace.uninstall')}
-              </Button>
+              </DangerButtonSecondary>
             </Tooltip>
           </>
         )}
         {isAvailablePlugin(plugin) && (
           <Tooltip overlay={translate('marketplace.requires_restart')}>
-            <Button
-              className="js-install"
+            <ButtonSecondary
               disabled={
                 loading || (plugin.termsAndConditionsUrl != null && !this.state.acceptTerms)
               }
               onClick={this.handleInstall}
             >
               {translate('marketplace.install')}
-            </Button>
+            </ButtonSecondary>
           </Tooltip>
         )}
       </div>
index 09e99894de6da3ff135ba388522505511c90dd81..1e0a0c7522eaf645fb46a20baedd2b63d6d2b2a7 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
+import { Badge, ContentCell, UnorderedList } from 'design-system';
 import * as React from 'react';
 import { translateWithParameters } from '../../../helpers/l10n';
 import { AvailablePlugin, InstalledPlugin } from '../../../types/plugins';
@@ -35,54 +37,58 @@ export interface PluginAvailableProps {
   status?: string;
 }
 
-export default function PluginAvailable(props: PluginAvailableProps) {
+export default function PluginAvailable(props: Readonly<PluginAvailableProps>) {
   const { installedPlugins, plugin, readOnly, status } = props;
   const installedPluginKeys = installedPlugins.map(({ key }) => key);
   return (
-    <tr>
+    <>
       <PluginDescription plugin={plugin} />
-      <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">{plugin.release.version}</span>
-            </div>
-            <div>
-              {plugin.release.description}
-              <PluginChangeLogButton
-                pluginName={plugin.name}
-                release={plugin.release}
-                update={plugin.update}
-              />
-              {plugin.update.requires.length > 0 && (
-                <p className="little-spacer-top">
-                  <strong>
-                    {translateWithParameters(
-                      'marketplace.installing_this_plugin_will_also_install_x',
-                      plugin.update.requires
-                        .filter(({ key }) => !installedPluginKeys.includes(key))
-                        .map((requiredPlugin) => requiredPlugin.name)
-                        .join(', '),
-                    )}
-                  </strong>
-                </p>
+      <ContentCell>
+        <div className="sw-mr-2">
+          <Badge variant="new">{plugin.release.version}</Badge>
+        </div>
+        <div className="sw-mr-2">{plugin.release.description}</div>
+        <PluginChangeLogButton
+          pluginName={plugin.name}
+          release={plugin.release}
+          update={plugin.update}
+        />
+        {plugin.update.requires.length > 0 && (
+          <p className="sw-mt-2">
+            <strong className="sw-body-sm-highlight">
+              {translateWithParameters(
+                'marketplace.installing_this_plugin_will_also_install_x',
+                plugin.update.requires
+                  .filter(({ key }) => !installedPluginKeys.includes(key))
+                  .map((requiredPlugin) => requiredPlugin.name)
+                  .join(', '),
               )}
-            </div>
-          </li>
-        </ul>
-      </td>
+            </strong>
+          </p>
+        )}
+      </ContentCell>
 
-      <td className="text-top width-20">
-        <ul>
+      <ContentCell>
+        <StyledUnorderedList>
           <PluginUrls plugin={plugin} />
           <PluginLicense license={plugin.license} />
           <PluginOrganization plugin={plugin} />
-        </ul>
-      </td>
+        </StyledUnorderedList>
+      </ContentCell>
 
       {!readOnly && (
-        <PluginStatus plugin={plugin} refreshPending={props.refreshPending} status={status} />
+        <ContentCell>
+          <PluginStatus plugin={plugin} refreshPending={props.refreshPending} status={status} />
+        </ContentCell>
       )}
-    </tr>
+    </>
   );
 }
+
+const StyledUnorderedList = styled(UnorderedList)`
+  margin-top: 0;
+
+  & li:first {
+    margin-top: 0;
+  }
+`;
index eb520c2ec130fca2e5d00b793455124af1976849..2985475d641fda61c62bda8b8eedf9b863fbc13b 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { UnorderedList } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
@@ -30,9 +31,9 @@ export interface Props {
 
 export default function PluginChangeLog({ release, update }: Props) {
   return (
-    <div className="abs-width-300">
-      <b className="sw-leading-6">{translate('changelog')}</b>
-      <ul className="js-plugin-changelog-list">
+    <div className="sw-p-4">
+      <span className="sw-body-md-highlight">{translate('changelog')}</span>
+      <UnorderedList>
         {update.previousUpdates &&
           sortBy(update.previousUpdates, (prevUpdate) => prevUpdate.release?.date).map(
             (previousUpdate) =>
@@ -45,7 +46,7 @@ export default function PluginChangeLog({ release, update }: Props) {
               ) : null,
           )}
         <PluginChangeLogItem release={release} update={update} />
-      </ul>
+      </UnorderedList>
     </div>
   );
 }
index 474146b4aea4c42ab20e186b29d56b15ecf72f1b..d2caa3a3c70f2dc03b46b3e64c887b93b73b1cf3 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonSecondary, DropdownToggler } from 'design-system';
 import * as React from 'react';
-import { ButtonLink } from '../../../components/controls/buttons';
-import Dropdown from '../../../components/controls/Dropdown';
-import EllipsisIcon from '../../../components/icons/EllipsisIcon';
-import { translateWithParameters } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Release, Update } from '../../../types/plugins';
 import PluginChangeLog from './PluginChangeLog';
 
@@ -31,22 +29,27 @@ interface Props {
   update: Update;
 }
 
-export default function PluginChangeLogButton({ pluginName, release, update }: Props) {
+export default function PluginChangeLogButton({ pluginName, release, update }: Readonly<Props>) {
+  const [open, setOpen] = React.useState(false);
+
   return (
-    <Dropdown
-      className="display-inline-block little-spacer-left"
+    <DropdownToggler
+      allowResizing
+      onRequestClose={() => setOpen(false)}
+      open={open}
+      id={`plugin-changelog-${pluginName}`}
       overlay={<PluginChangeLog release={release} update={update} />}
     >
-      <ButtonLink
-        className="js-changelog"
+      <ButtonSecondary
         aria-label={translateWithParameters(
           'marketplace.show_plugin_changelog',
           pluginName,
           release.version,
         )}
+        onClick={() => setOpen((open) => !open)}
       >
-        <EllipsisIcon />
-      </ButtonLink>
-    </Dropdown>
+        {translate('see_changelog')}
+      </ButtonSecondary>
+    </DropdownToggler>
   );
 }
index d7abc8698ff52e76ba8c762aa2379d044660b2a1..1775cf6aebcebd8baab4a7b1943a3f4ef5a5e4ba 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Badge, Link, ListItem, Note } from 'design-system';
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
 import DateFormatter from '../../../components/intl/DateFormatter';
@@ -30,34 +31,27 @@ interface Props {
 
 export default function PluginChangeLogItem({ release, update }: Props) {
   return (
-    <li className="big-spacer-bottom">
-      <div className="little-spacer-bottom">
+    <ListItem>
+      <div className="sw-mb-2">
         {update.status === 'COMPATIBLE' || !update.status ? (
-          <span className="js-plugin-changelog-version badge badge-success spacer-right">
+          <Badge variant="new" className="sw-mr-4">
             {release.version}
-          </span>
+          </Badge>
         ) : (
           <Tooltip overlay={translate('marketplace.update_status', update.status)}>
-            <span className="js-plugin-changelog-version badge badge-warning spacer-right">
-              {release.version}
+            <span>
+              <Badge className="sw-mr-4">{release.version}</Badge>
             </span>
           </Tooltip>
         )}
-        <span className="js-plugin-changelog-date note spacer-right">
+        <Note className="sw-mr-4">
           <DateFormatter date={release.date} />
-        </span>
+        </Note>
         {release.changeLogUrl && (
-          <a
-            className="js-plugin-changelog-link"
-            href={release.changeLogUrl}
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            {translate('marketplace.release_notes')}
-          </a>
+          <Link to={release.changeLogUrl}>{translate('marketplace.release_notes')}</Link>
         )}
       </div>
-      <div className="js-plugin-changelog-description">{release.description}</div>
-    </li>
+      <p>{release.description}</p>
+    </ListItem>
   );
 }
index 6858f2ad7833a359e0e7a29fc04757042132015f..e436531f096e670579c7702ccfe886373261c6b1 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Badge, CellComponent } from 'design-system';
 import * as React from 'react';
 import { Plugin } from '../../../types/plugins';
 
@@ -24,18 +25,12 @@ interface Props {
   plugin: Plugin;
 }
 
-const PluginDescription = (props: Props) => {
+export default function PluginDescription(props: Readonly<Props>) {
   return (
-    <td className="text-top width-25 big-spacer-right">
-      <div>
-        <strong className="js-plugin-name">{props.plugin.name}</strong>
-        {props.plugin.category && (
-          <span className="js-plugin-category badge spacer-left">{props.plugin.category}</span>
-        )}
-      </div>
-      <div className="js-plugin-description little-spacer-top">{props.plugin.description}</div>
-    </td>
+    <CellComponent>
+      <strong className="sw-body-sm-highlight">{props.plugin.name}</strong>
+      {props.plugin.category && <Badge className="sw-ml-2">{props.plugin.category}</Badge>}
+      {props.plugin.description && <div className="sw-mt-2">{props.plugin.description}</div>}
+    </CellComponent>
   );
-};
-
-export default PluginDescription;
+}
index 51ce958bfca822807ff769a266ba0b85b6b3fbac..7e1692ce31bd3c826afc0ac8d272e0fd775e20f9 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
+import { ContentCell, ListItem, UnorderedList } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { InstalledPlugin } from '../../../types/plugins';
@@ -34,33 +36,46 @@ interface Props {
   status?: string;
 }
 
-export default function PluginInstalled({ plugin, readOnly, refreshPending, status }: Props) {
+export default function PluginInstalled({
+  plugin,
+  readOnly,
+  refreshPending,
+  status,
+}: Readonly<Props>) {
   return (
-    <tr>
+    <>
       <PluginDescription plugin={plugin} />
-      <td className="text-top big-spacer-right">
-        <ul>
-          <li className="little-spacer-bottom">
-            <strong className="js-plugin-installed-version little-spacer-right">
-              {plugin.version}
-            </strong>
+      <ContentCell>
+        <StyledUnorderedList>
+          <ListItem className="sw-mt-0">
+            <strong className="sw-mr-1 sw-body-sm-highlight">{plugin.version}</strong>
             {translate('marketplace._installed')}
-          </li>
+          </ListItem>
           <PluginUpdates pluginName={plugin.name} updates={plugin.updates} />
-        </ul>
-      </td>
+        </StyledUnorderedList>
+      </ContentCell>
 
-      <td className="text-top width-20">
-        <ul>
+      <ContentCell>
+        <StyledUnorderedList>
           <PluginUrls plugin={plugin} />
           <PluginLicense license={plugin.license} />
           <PluginOrganization plugin={plugin} />
-        </ul>
-      </td>
+        </StyledUnorderedList>
+      </ContentCell>
 
       {!readOnly && (
-        <PluginStatus plugin={plugin} refreshPending={refreshPending} status={status} />
+        <ContentCell>
+          <PluginStatus plugin={plugin} refreshPending={refreshPending} status={status} />
+        </ContentCell>
       )}
-    </tr>
+    </>
   );
 }
+
+const StyledUnorderedList = styled(UnorderedList)`
+  margin-top: 0;
+
+  & li:first {
+    margin-top: 0;
+  }
+`;
index 0ce8014350246e16c641625e4419493b0dcbd67c..aa5ac6ccacbe55b6538eadb493c8389c5accd7e9 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ListItem } from 'design-system';
+import { isEmpty } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
 
 interface Props {
   license?: string;
 }
 
-export default function PluginLicense({ license }: Props) {
-  if (!license) {
+export default function PluginLicense({ license }: Readonly<Props>) {
+  if (isEmpty(license)) {
     return null;
   }
   return (
-    <Tooltip overlay={license}>
-      <li className="little-spacer-bottom marketplace-plugin-license">
-        <FormattedMessage
-          defaultMessage={translate('marketplace.licensed_under_x')}
-          id="marketplace.licensed_under_x"
-          values={{
-            license: <span className="js-plugin-license">{license}</span>,
-          }}
-        />
-      </li>
-    </Tooltip>
+    <ListItem>
+      <Tooltip overlay={license}>
+        <div>
+          <FormattedMessage
+            id="marketplace.licensed_under_x"
+            values={{
+              license,
+            }}
+          />
+        </div>
+      </Tooltip>
+    </ListItem>
   );
 }
index fa387126b4e42d66a78a684ae5e98b6096131b62..ac97e348544442817564093b0e549553802c9a04 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Link, ListItem } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { translate } from '../../../helpers/l10n';
 import { Plugin } from '../../../types/plugins';
 
 export interface PluginOrganizationProps {
   plugin: Plugin;
 }
 
-export default function PluginOrganization({ plugin }: PluginOrganizationProps) {
+export default function PluginOrganization({ plugin }: Readonly<PluginOrganizationProps>) {
   if (!plugin.organizationName) {
     return null;
   }
   return (
-    <li className="little-spacer-bottom">
+    <ListItem>
       <FormattedMessage
-        defaultMessage={translate('marketplace.developed_by_x')}
         id="marketplace.developed_by_x"
         values={{
           organization: plugin.organizationUrl ? (
-            <a
-              className="js-plugin-organization"
-              href={plugin.organizationUrl}
-              target="_blank"
-              rel="noopener noreferrer"
-            >
-              {plugin.organizationName}
-            </a>
+            <Link to={plugin.organizationUrl}>{plugin.organizationName}</Link>
           ) : (
-            <span className="js-plugin-organization">{plugin.organizationName}</span>
+            <span>{plugin.organizationName}</span>
           ),
         }}
       />
-    </li>
+    </ListItem>
   );
 }
index ae01172fd74b443776386d9e562a385a4eb7a21b..ade15440de3157167db8f234191d3adceba7aa8f 100644 (file)
@@ -17,8 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonPrimary, Card, DarkLabel } from 'design-system';
 import * as React from 'react';
-import { Button } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import { EditionKey } from '../../../types/editions';
 import { RiskConsent } from '../../../types/plugins';
@@ -29,7 +29,7 @@ export interface PluginRiskConsentBoxProps {
   riskConsent?: RiskConsent;
 }
 
-export default function PluginRiskConsentBox(props: PluginRiskConsentBoxProps) {
+export default function PluginRiskConsentBox(props: Readonly<PluginRiskConsentBoxProps>) {
   const { currentEdition, riskConsent } = props;
 
   if (riskConsent === RiskConsent.Accepted) {
@@ -37,20 +37,16 @@ export default function PluginRiskConsentBox(props: PluginRiskConsentBoxProps) {
   }
 
   return (
-    <div className="boxed-group it__plugin_risk_consent_box">
-      <h2>{translate('marketplace.risk_consent.title')}</h2>
-      <div className="boxed-group-inner">
-        <p>{translate('marketplace.risk_consent.description')}</p>
-        {currentEdition === EditionKey.community && (
-          <p className="spacer-top">{translate('marketplace.risk_consent.installation')}</p>
-        )}
-        <Button
-          className="display-block big-spacer-top button-primary"
-          onClick={props.acknowledgeRisk}
-        >
-          {translate('marketplace.risk_consent.action')}
-        </Button>
-      </div>
-    </div>
+    <Card className="sw-mt-6 it__plugin_risk_consent_box">
+      <DarkLabel>{translate('marketplace.risk_consent.title')}</DarkLabel>
+
+      <p className="sw-mt-2">{translate('marketplace.risk_consent.description')}</p>
+      {currentEdition === EditionKey.community && (
+        <p className="sw-mt-2">{translate('marketplace.risk_consent.installation')}</p>
+      )}
+      <ButtonPrimary className="sw-mt-4" onClick={props.acknowledgeRisk}>
+        {translate('marketplace.risk_consent.action')}
+      </ButtonPrimary>
+    </Card>
   );
 }
index 2160961678b0dea4418a6a07819787e6754525c5..e543fa1421aa2ec7bd9d383f7b8ce6ea3791dd63 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { Plugin } from '../../../types/plugins';
@@ -28,22 +29,18 @@ interface Props {
   status?: string;
 }
 
-export default function PluginStatus({ plugin, refreshPending, status }: Props) {
-  return (
-    <td className="text-top text-right width-20 little-spacer-left">
-      {status === 'installing' && (
-        <p className="text-success">{translate('marketplace.install_pending')}</p>
-      )}
+export default function PluginStatus({ plugin, refreshPending, status }: Readonly<Props>) {
+  switch (status) {
+    case 'installing':
+      return <FlagMessage variant="info">{translate('marketplace.install_pending')}</FlagMessage>;
 
-      {status === 'updating' && (
-        <p className="text-success">{translate('marketplace.update_pending')}</p>
-      )}
+    case 'updating':
+      return <FlagMessage variant="info">{translate('marketplace.update_pending')}</FlagMessage>;
 
-      {status === 'removing' && (
-        <p className="text-danger">{translate('marketplace.uninstall_pending')}</p>
-      )}
+    case 'removing':
+      return <FlagMessage variant="info">{translate('marketplace.uninstall_pending')}</FlagMessage>;
 
-      {status == null && <PluginActions plugin={plugin} refreshPending={refreshPending} />}
-    </td>
-  );
+    default:
+      return <PluginActions plugin={plugin} refreshPending={refreshPending} />;
+  }
 }
index f36f2acc954698992d39672ae324846bbb6a04e6..bfa202e0daf1cf592e783f34ad17b5a81c2479b9 100644 (file)
@@ -17,8 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonSecondary } from 'design-system';
 import * as React from 'react';
-import { Button } from '../../../components/controls/buttons';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Update } from '../../../types/plugins';
@@ -29,26 +29,21 @@ interface Props {
   update: Update;
 }
 
-export default class PluginUpdateButton extends React.PureComponent<Props> {
-  handleClick = () => {
-    this.props.onClick(this.props.update);
-  };
+export default function PluginUpdateButton(props: Readonly<Props>) {
+  const { disabled, onClick, update } = props;
 
-  render() {
-    const { disabled, update } = this.props;
-    if (update.status !== 'COMPATIBLE' || !update.release) {
-      return null;
-    }
-    return (
-      <Tooltip overlay={translate('marketplace.requires_restart')}>
-        <Button
-          className="js-update little-spacer-bottom"
-          disabled={disabled}
-          onClick={this.handleClick}
-        >
-          {translateWithParameters('marketplace.update_to_x', update.release.version)}
-        </Button>
-      </Tooltip>
-    );
+  const handleClick = React.useCallback(() => {
+    onClick(update);
+  }, [onClick, update]);
+
+  if (update.status !== 'COMPATIBLE' || !update.release) {
+    return null;
   }
+  return (
+    <Tooltip overlay={translate('marketplace.requires_restart')}>
+      <ButtonSecondary disabled={disabled} onClick={handleClick}>
+        {translateWithParameters('marketplace.update_to_x', update.release.version)}
+      </ButtonSecondary>
+    </Tooltip>
+  );
 }
index 5b26c1cf62e43781583dca812f191d2f48238dba..9c6923f8bb2b1854c13d7b89053cb438d04673fc 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Badge, ListItem } from 'design-system';
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
@@ -29,15 +30,17 @@ interface Props {
   release: Release;
 }
 
-export default function PluginUpdateItem({ release, update, pluginName }: Props) {
+export default function PluginUpdateItem({ release, update, pluginName }: Readonly<Props>) {
   return (
-    <li className="display-flex-row little-spacer-bottom" key={release.version}>
-      <div className="pull-left spacer-right">
+    <ListItem className="sw-flex sw-items-center" key={release.version}>
+      <div className="sw-mr-2">
         {update.status === 'COMPATIBLE' ? (
-          <span className="js-update-version badge badge-success">{release.version}</span>
+          <Badge variant="new">{release.version}</Badge>
         ) : (
           <Tooltip overlay={translate('marketplace.update_status', update.status)}>
-            <span className="js-update-version badge badge-warning">{release.version}</span>
+            <span>
+              <Badge>{release.version}</Badge>
+            </span>
           </Tooltip>
         )}
       </div>
@@ -45,6 +48,6 @@ export default function PluginUpdateItem({ release, update, pluginName }: Props)
         {release.description}
         <PluginChangeLogButton pluginName={pluginName} release={release} update={update} />
       </div>
-    </li>
+    </ListItem>
   );
 }
index 1c9bc33e814d288a62234cf78a7895378d2cec1c..5457c590c68971998083ff6d3036dfe0d30b9ad8 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ListItem, UnorderedList } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { Update } from '../../../types/plugins';
@@ -27,14 +28,14 @@ export interface PluginUpdatesProps {
   updates?: Update[];
 }
 
-export default function PluginUpdates({ pluginName, updates }: PluginUpdatesProps) {
+export default function PluginUpdates({ pluginName, updates }: Readonly<PluginUpdatesProps>) {
   if (!updates || updates.length <= 0) {
     return null;
   }
   return (
-    <li className="spacer-top">
-      <strong>{translate('marketplace.updates')}:</strong>
-      <ul className="little-spacer-top">
+    <ListItem>
+      <strong className="sw-body-sm-highlight">{translate('marketplace.updates')}:</strong>
+      <UnorderedList className="sw-mt-2">
         {updates.map((update) =>
           update.release ? (
             <PluginUpdateItem
@@ -45,7 +46,7 @@ export default function PluginUpdates({ pluginName, updates }: PluginUpdatesProp
             />
           ) : null,
         )}
-      </ul>
-    </li>
+      </UnorderedList>
+    </ListItem>
   );
 }
index c6a69cb59572503118b33de24f45cd37882224b7..a17788db2d62178ccd70374f2fb83475e341fe79 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Link, ListItem } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { Plugin } from '../../../types/plugins';
@@ -25,38 +26,22 @@ interface Props {
   plugin: Plugin;
 }
 
-export default function PluginUrls({ plugin }: Props) {
+export default function PluginUrls({ plugin }: Readonly<Props>) {
   if (!plugin.homepageUrl && !plugin.issueTrackerUrl) {
     return null;
   }
   return (
-    <li className="little-spacer-bottom">
-      <ul className="list-inline">
-        {plugin.homepageUrl && (
-          <li>
-            <a
-              className="js-plugin-homepage"
-              href={plugin.homepageUrl}
-              target="_blank"
-              rel="noopener noreferrer"
-            >
-              {translate('marketplace.homepage')}
-            </a>
-          </li>
-        )}
-        {plugin.issueTrackerUrl && (
-          <li>
-            <a
-              className="js-plugin-issues"
-              href={plugin.issueTrackerUrl}
-              target="_blank"
-              rel="noopener noreferrer"
-            >
-              {translate('marketplace.issue_tracker')}
-            </a>
-          </li>
-        )}
-      </ul>
-    </li>
+    <ListItem className="sw-flex sw-flex-wrap sw-gap-4">
+      {plugin.homepageUrl && (
+        <Link className="sw-whitespace-nowrap" to={plugin.homepageUrl}>
+          {translate('marketplace.homepage')}
+        </Link>
+      )}
+      {plugin.issueTrackerUrl && (
+        <Link className="sw-whitespace-nowrap" to={plugin.issueTrackerUrl}>
+          {translate('marketplace.issue_tracker')}
+        </Link>
+      )}
+    </ListItem>
   );
 }
index 36d14bd7f4870815176272a734a74da676df9622..d5ce452ffa46f5abf4ec28ce182280dd06aa92e9 100644 (file)
@@ -56,7 +56,7 @@ export default class SearchBox extends React.PureComponent<Props, State> {
 
   constructor(props: Props) {
     super(props);
-    this.state = { value: props.value || '' };
+    this.state = { value: props.value ?? '' };
     this.debouncedOnChange = debounce(this.props.onChange, DEBOUNCE_DELAY);
   }