]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21422 Migrating projects management page to adopt new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 23 Jan 2024 10:01:29 +0000 (11:01 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 24 Jan 2024 20:03:33 +0000 (20:03 +0000)
19 files changed:
server/sonar-web/design-system/src/components/input/InputSearch.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/index.ts
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.css [deleted file]
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
server/sonar-web/src/main/js/components/controls/DateInput.css [deleted file]
server/sonar-web/src/main/js/components/controls/DateInput.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx [deleted file]

index c5b47c07fde8de164a7dbd4d201ccfaf2521d910..e3288bbc497c498656f63fa6f6300c1c1ceb8ea9 100644 (file)
@@ -75,7 +75,6 @@ export function InputSearch(props: PropsWithChildren<Props>) {
     value: parentValue,
     searchInputAriaLabel,
   } = props;
-
   const intl = useIntl();
   const input = useRef<null | HTMLElement>(null);
   const [value, setValue] = useState(parentValue ?? '');
@@ -84,6 +83,7 @@ export function InputSearch(props: PropsWithChildren<Props>) {
     () =>
       debounce((val: string) => {
         onChange(val);
+        setDirty(false);
       }, DEBOUNCE_DELAY),
     [onChange],
   );
index a28ac860a19eae0019229c7bdc93249c2402d6dc..e7e087470038811558177b252cd7d368650ca402 100644 (file)
@@ -85,6 +85,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/admin/settings/encryption',
   '/admin/extension/license/support',
   '/admin/audit',
+  '/admin/projects_management',
 ];
 
 export default function GlobalContainer() {
index c2f82a83d4e74c584be0044b4b7b4d825918c634..9f8575a64ef8e963d1470c4f6d151e076c167dbd 100644 (file)
@@ -23,6 +23,7 @@
 import 'core-js/stable';
 /*                                                                                              */
 import axios from 'axios';
+import 'react-day-picker/dist/style.css';
 import { getAvailableFeatures } from '../api/features';
 import { getGlobalNavigation } from '../api/navigation';
 import { getCurrentUser } from '../api/users';
index 2181a978e133e830365d5b8afe2a47ff34ee8e74..78b18822c402b2ff74510b90210131e8d5cb13d3 100644 (file)
@@ -27,7 +27,6 @@ import {
 } from 'design-system';
 import * as React from 'react';
 import { applyTemplateToProject, getPermissionTemplates } from '../../../../api/permissions';
-import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { PermissionTemplate } from '../../../../types/types';
 
@@ -136,25 +135,22 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
               )}
 
               {!this.state.done && !this.state.loading && (
-                <>
-                  <MandatoryFieldsExplanation className="sw-mb-4" />
-                  <FormField
-                    label={translate('template')}
-                    required
-                    htmlFor="project-permissions-template-input"
-                  >
-                    {this.state.permissionTemplates && (
-                      <InputSelect
-                        size="full"
-                        id="project-permissions-template"
-                        inputId="project-permissions-template-input"
-                        onChange={this.handlePermissionTemplateChange}
-                        options={options}
-                        value={options.filter((o) => o.value === this.state.permissionTemplate)}
-                      />
-                    )}
-                  </FormField>
-                </>
+                <FormField
+                  label={translate('template')}
+                  required
+                  htmlFor="project-permissions-template-input"
+                >
+                  {this.state.permissionTemplates && (
+                    <InputSelect
+                      size="full"
+                      id="project-permissions-template"
+                      inputId="project-permissions-template-input"
+                      onChange={this.handlePermissionTemplateChange}
+                      options={options}
+                      value={options.filter((o) => o.value === this.state.permissionTemplate)}
+                    />
+                  )}
+                </FormField>
               )}
             </div>
           </form>
index f174dcdcb74878a82408f3b366bf4fbce13b2cfb..bf45c9e1962025172c655b11271f071269f4d5ea 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 {
+  ButtonPrimary,
+  FlagMessage,
+  FormField,
+  InputSelect,
+  LabelValueSelectOption,
+  Modal,
+  Spinner,
+} from 'design-system';
 import * as React from 'react';
 import { bulkApplyTemplate, getPermissionTemplates } from '../../api/permissions';
 import { Project } from '../../api/project-management';
-import Modal from '../../components/controls/Modal';
-import Select from '../../components/controls/Select';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
-import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
 import { toISO8601WithOffsetString } from '../../helpers/dates';
 import { addGlobalErrorMessageFromAPI } from '../../helpers/globalMessages';
@@ -49,6 +53,8 @@ interface State {
   submitting: boolean;
 }
 
+const FORM_ID = 'bulk-apply-template-form';
+
 export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = { done: false, loading: true, submitting: false };
@@ -83,7 +89,8 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     );
   }
 
-  handleConfirmClick = () => {
+  handleConfirmClick = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
     const { analyzedBefore } = this.props;
     const { permissionTemplate } = this.state;
     if (permissionTemplate) {
@@ -118,7 +125,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     }
   };
 
-  handlePermissionTemplateChange = ({ value }: { value: string }) => {
+  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
     this.setState({ permissionTemplate: value });
   };
 
@@ -132,15 +139,15 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
 
     if (isSelectionOnlyManaged) {
       return (
-        <Alert variant="error">
+        <FlagMessage variant="error" className="sw-my-2">
           {translate(
             'permission_templates.bulk_apply_permission_template.apply_to_only_github_projects',
           )}
-        </Alert>
+        </FlagMessage>
       );
     } else if (isSelectionOnlyLocal) {
       return (
-        <Alert variant="warning">
+        <FlagMessage variant="warning" className="sw-my-2">
           {this.props.selection.length
             ? translateWithParameters(
                 'permission_templates.bulk_apply_permission_template.apply_to_selected',
@@ -150,11 +157,11 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
                 'permission_templates.bulk_apply_permission_template.apply_to_all',
                 this.props.total,
               )}
-        </Alert>
+        </FlagMessage>
       );
     }
     return (
-      <Alert variant="warning">
+      <FlagMessage variant="warning" className="sw-my-2">
         {translateWithParameters(
           'permission_templates.bulk_apply_permission_template.apply_to_selected',
           localProjects.length,
@@ -164,7 +171,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
           'permission_templates.bulk_apply_permission_template.apply_to_github_projects',
           managedProjects.length,
         )}
-      </Alert>
+      </FlagMessage>
     );
   };
 
@@ -174,20 +181,17 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
         ? this.state.permissionTemplates.map((t) => ({ label: t.name, value: t.id }))
         : [];
     return (
-      <div className="modal-field">
-        <label htmlFor="bulk-apply-template-input">
-          {translate('template')}
-          <MandatoryFieldMarker />
-        </label>
-        <Select
+      <FormField htmlFor="bulk-apply-template-input" label={translate('template')} required>
+        <InputSelect
           id="bulk-apply-template"
           inputId="bulk-apply-template-input"
           isDisabled={this.state.submitting || isSelectionOnlyManaged}
           onChange={this.handlePermissionTemplateChange}
           options={options}
           value={options.find((option) => option.value === this.state.permissionTemplate)}
+          size="auto"
         />
-      </div>
+      </FormField>
     );
   };
 
@@ -196,44 +200,49 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     const header = translate('permission_templates.bulk_apply_permission_template');
 
     const isSelectionOnlyManaged = this.props.selection.every((s) => s.managed === true);
+    const body = (
+      <form id={FORM_ID} onSubmit={this.handleConfirmClick}>
+        {done && (
+          <FlagMessage variant="success">
+            {translate('projects_role.apply_template.success')}
+          </FlagMessage>
+        )}
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-
-        <div className="modal-body">
-          {done && (
-            <Alert variant="success">{translate('projects_role.apply_template.success')}</Alert>
-          )}
-
-          {loading && <i className="spinner" />}
-
-          {!loading && !done && permissionTemplates && (
-            <>
-              <MandatoryFieldsExplanation className="spacer-bottom" />
-              {this.renderWarning()}
-              {this.renderSelect(isSelectionOnlyManaged)}
-            </>
-          )}
-        </div>
+        <Spinner loading={loading} />
 
-        <footer className="modal-foot">
-          {submitting && <i className="spinner spacer-right" />}
-          {!loading && !done && permissionTemplates && (
-            <SubmitButton
+        {!loading && !done && permissionTemplates && (
+          <>
+            <MandatoryFieldsExplanation className="sw-mb-2" />
+            {this.renderWarning()}
+            {this.renderSelect(isSelectionOnlyManaged)}
+          </>
+        )}
+      </form>
+    );
+    return (
+      <Modal
+        isScrollable={false}
+        isOverflowVisible
+        headerTitle={header}
+        onClose={this.props.onClose}
+        loading={submitting}
+        body={body}
+        primaryButton={
+          !loading &&
+          !done &&
+          permissionTemplates && (
+            <ButtonPrimary
+              autoFocus
               disabled={submitting || isSelectionOnlyManaged}
-              onClick={this.handleConfirmClick}
+              form={FORM_ID}
+              type="submit"
             >
               {translate('apply')}
-            </SubmitButton>
-          )}
-          <ResetButtonLink onClick={this.props.onClose}>
-            {done ? translate('close') : translate('cancel')}
-          </ResetButtonLink>
-        </footer>
-      </Modal>
+            </ButtonPrimary>
+          )
+        }
+        secondaryButtonLabel={done ? translate('close') : translate('cancel')}
+      />
     );
   }
 }
index 5ee867fb7db8f049c0add863e03b3368738369d6..31d075ba7a1d6c1cfbdb6eb8e31f6ccffae48ad7 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 { ButtonPrimary, FlagMessage, Modal, RadioButton, TextSubdued } from 'design-system';
 import React, { useState } from 'react';
-import Modal from '../../components/controls/Modal';
-import Radio from '../../components/controls/Radio';
-import { Button, ResetButtonLink } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
 import { translate } from '../../helpers/l10n';
 import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { Visibility } from '../../types/component';
@@ -32,11 +29,14 @@ export interface Props {
   onConfirm: (visiblity: Visibility) => void;
 }
 
+const FORM_ID = 'change-default-visibility-form';
+
 export default function ChangeDefaultVisibilityForm(props: Props) {
   const [visibility, setVisibility] = useState(props.defaultVisibility);
   const { data: githubProbivisioningEnabled } = useGithubProvisioningEnabledQuery();
 
-  const handleConfirmClick = () => {
+  const handleConfirmClick = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
     props.onConfirm(visibility);
     props.onClose();
   };
@@ -47,47 +47,47 @@ export default function ChangeDefaultVisibilityForm(props: Props) {
 
   const header = translate('settings.projects.change_visibility_form.header');
 
-  return (
-    <Modal contentLabel={header} onRequestClose={props.onClose}>
-      <header className="modal-head">
-        <h2>{header}</h2>
-      </header>
-
-      <div className="modal-body">
-        {Object.values(Visibility).map((visibilityValue) => (
-          <div className="big-spacer-bottom" key={visibilityValue}>
-            <Radio
-              value={visibilityValue}
-              checked={visibility === visibilityValue}
-              onCheck={handleVisibilityChange}
-            >
-              <div>
-                {translate('visibility', visibilityValue)}
-                <p className="text-muted spacer-top">
-                  {translate('visibility', visibilityValue, 'description.short')}
-                </p>
-              </div>
-            </Radio>
-          </div>
-        ))}
-
-        <Alert variant="warning">
-          {translate(
-            `settings.projects.change_visibility_form.warning${
-              githubProbivisioningEnabled ? '.github' : ''
-            }`,
-          )}
-        </Alert>
-      </div>
+  const body = (
+    <form id={FORM_ID} onSubmit={handleConfirmClick}>
+      {Object.values(Visibility).map((visibilityValue) => (
+        <div className="sw-mb-4" key={visibilityValue}>
+          <RadioButton
+            value={visibilityValue}
+            checked={visibility === visibilityValue}
+            onCheck={handleVisibilityChange}
+          >
+            <div>
+              {translate('visibility', visibilityValue)}
+              <TextSubdued as="p" className="sw-mt-2">
+                {translate('visibility', visibilityValue, 'description.short')}
+              </TextSubdued>
+            </div>
+          </RadioButton>
+        </div>
+      ))}
+      <FlagMessage variant="warning">
+        {translate(
+          `settings.projects.change_visibility_form.warning${
+            githubProbivisioningEnabled ? '.github' : ''
+          }`,
+        )}
+      </FlagMessage>
+    </form>
+  );
 
-      <footer className="modal-foot">
-        <Button className="js-confirm" type="submit" onClick={handleConfirmClick}>
+  return (
+    <Modal
+      isScrollable={false}
+      isOverflowVisible
+      headerTitle={header}
+      onClose={props.onClose}
+      body={body}
+      primaryButton={
+        <ButtonPrimary form={FORM_ID} autoFocus type="submit">
           {translate('settings.projects.change_visibility_form.submit')}
-        </Button>
-        <ResetButtonLink className="js-modal-close" onClick={props.onClose}>
-          {translate('cancel')}
-        </ResetButtonLink>
-      </footer>
-    </Modal>
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
   );
 }
index c33dfa87bf599b5f1b82b3307d7010173797fbfd..81fb8629f6f226e33f69792e2df66f13f31eca08 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 { DangerButtonPrimary, FlagMessage, Modal } from 'design-system';
 import * as React from 'react';
 import { Project, bulkDeleteProjects } from '../../api/project-management';
-import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
 import { toISO8601WithOffsetString } from '../../helpers/dates';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 
@@ -80,42 +78,43 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
   };
 
   renderWarning = () => (
-    <Alert variant="warning">
+    <FlagMessage variant="warning">
       {this.props.selection.length
         ? translateWithParameters(
             'projects_management.delete_selected_warning',
             this.props.selection.length,
           )
         : translateWithParameters('projects_management.delete_all_warning', this.props.total)}
-    </Alert>
+    </FlagMessage>
   );
 
   render() {
     const header = translate('qualifiers.delete', this.props.qualifier);
 
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-
-        <div className="modal-body">
-          {this.renderWarning()}
-          {translate('qualifiers.delete_confirm', this.props.qualifier)}
-        </div>
-
-        <footer className="modal-foot">
-          {this.state.loading && <i className="spinner spacer-right" />}
-          <SubmitButton
-            className="button-red"
+      <Modal
+        headerTitle={header}
+        onClose={this.props.onClose}
+        body={
+          <>
+            {this.renderWarning()}
+            <p className="sw-mt-2">
+              {translate('qualifiers.delete_confirm', this.props.qualifier)}
+            </p>
+          </>
+        }
+        primaryButton={
+          <DangerButtonPrimary
+            autoFocus
             disabled={this.state.loading}
             onClick={this.handleConfirmClick}
+            type="submit"
           >
             {translate('delete')}
-          </SubmitButton>
-          <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-        </footer>
-      </Modal>
+          </DangerButtonPrimary>
+        }
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 }
index b9076134e046772776908ee00f86ef1f45e9f594..18673aae3f6ae3b98082a10c6b1ce46f7c6a8a93 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 { ButtonPrimary, InteractiveIcon, PencilIcon, Title } from 'design-system';
 import * as React from 'react';
 import { useState } from 'react';
 import { useLocation, useNavigate } from 'react-router-dom';
-import { Button, EditButton } from '../../components/controls/buttons';
 import { translate } from '../../helpers/l10n';
 import { Visibility } from '../../types/component';
 import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm';
@@ -39,39 +39,41 @@ export default function Header(props: Readonly<Props>) {
   const { defaultProjectVisibility, hasProvisionPermission } = props;
 
   return (
-    <header className="page-header">
-      <h1 className="page-title">{translate('projects_management')}</h1>
+    <header className="sw-mb-5">
+      <div className="sw-flex sw-items-center sw-justify-between">
+        <Title className="sw-m-0">{translate('projects_management')}</Title>
+        <div className="sw-flex sw-items-center it__page-actions">
+          <div className="sw-mr-2">
+            <span className="sw-mr-1">
+              {translate('settings.projects.default_visibility_of_new_projects')}{' '}
+              <strong className="sw-body-sm-highlight">
+                {defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
+              </strong>
+            </span>
+            <InteractiveIcon
+              className="it__change-visibility"
+              Icon={PencilIcon}
+              onClick={() => setVisibilityForm(true)}
+              aria-label={translate('settings.projects.change_visibility_form.label')}
+            />
+          </div>
 
-      <div className="page-actions">
-        <span className="big-spacer-right">
-          <span className="text-middle">
-            {translate('settings.projects.default_visibility_of_new_projects')}{' '}
-            <strong>
-              {defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
-            </strong>
-          </span>
-          <EditButton
-            className="js-change-visibility spacer-left button-small"
-            onClick={() => setVisibilityForm(true)}
-            aria-label={translate('settings.projects.change_visibility_form.label')}
-          />
-        </span>
-
-        {hasProvisionPermission && (
-          <Button
-            id="create-project"
-            onClick={() =>
-              navigate('/projects/create?mode=manual', {
-                state: { from: location.pathname },
-              })
-            }
-          >
-            {translate('qualifiers.create.TRK')}
-          </Button>
-        )}
+          {hasProvisionPermission && (
+            <ButtonPrimary
+              id="create-project"
+              onClick={() =>
+                navigate('/projects/create?mode=manual', {
+                  state: { from: location.pathname },
+                })
+              }
+            >
+              {translate('qualifiers.create.TRK')}
+            </ButtonPrimary>
+          )}
+        </div>
       </div>
 
-      <p className="page-description">{translate('projects_management.page.description')}</p>
+      <p className="sw-mt-4">{translate('projects_management.page.description')}</p>
 
       {visibilityForm && (
         <ChangeDefaultVisibilityForm
index baf0a3bccdab6a2bc22a7cd5a91d16dc53fdfb22..e75ef5ff0a2f78cd16e2b88993d68160a242d7fb 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 { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import { debounce, uniq } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -202,52 +203,55 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
     const { currentUser } = this.props;
     const { defaultProjectVisibility } = this.state;
     return (
-      <main className="page page-limited" id="projects-management-page">
-        <Suggestions suggestions="projects_management" />
-        <Helmet defer={false} title={translate('projects_management')} />
-
-        <Header
-          defaultProjectVisibility={defaultProjectVisibility}
-          hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
-          onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
-        />
-
-        <Search
-          analyzedBefore={this.state.analyzedBefore}
-          onAllDeselected={this.onAllDeselected}
-          onAllSelected={this.onAllSelected}
-          onDateChanged={this.handleDateChanged}
-          onDeleteProjects={this.requestProjects}
-          onProvisionedChanged={this.onProvisionedChanged}
-          onQualifierChanged={this.onQualifierChanged}
-          onSearch={this.onSearch}
-          onVisibilityChanged={this.onVisibilityChanged}
-          projects={this.state.projects}
-          provisioned={this.state.provisioned}
-          qualifiers={this.state.qualifiers}
-          query={this.state.query}
-          ready={this.state.ready}
-          selection={this.state.selection}
-          total={this.state.total}
-          visibility={this.state.visibility}
-        />
-
-        <Projects
-          currentUser={this.props.currentUser}
-          onProjectDeselected={this.onProjectDeselected}
-          onProjectSelected={this.onProjectSelected}
-          projects={this.state.projects}
-          ready={this.state.ready}
-          selection={this.state.selection}
-        />
-
-        <ListFooter
-          count={this.state.projects.length}
-          loadMore={this.loadMore}
-          ready={this.state.ready}
-          total={this.state.total}
-        />
-      </main>
+      <LargeCenteredLayout as="main" id="projects-management-page">
+        <PageContentFontWrapper className="sw-body-sm sw-my-8">
+          <Suggestions suggestions="projects_management" />
+          <Helmet defer={false} title={translate('projects_management')} />
+
+          <Header
+            defaultProjectVisibility={defaultProjectVisibility}
+            hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
+            onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
+          />
+
+          <Search
+            analyzedBefore={this.state.analyzedBefore}
+            onAllDeselected={this.onAllDeselected}
+            onAllSelected={this.onAllSelected}
+            onDateChanged={this.handleDateChanged}
+            onDeleteProjects={this.requestProjects}
+            onProvisionedChanged={this.onProvisionedChanged}
+            onQualifierChanged={this.onQualifierChanged}
+            onSearch={this.onSearch}
+            onVisibilityChanged={this.onVisibilityChanged}
+            projects={this.state.projects}
+            provisioned={this.state.provisioned}
+            qualifiers={this.state.qualifiers}
+            query={this.state.query}
+            ready={this.state.ready}
+            selection={this.state.selection}
+            total={this.state.total}
+            visibility={this.state.visibility}
+          />
+
+          <Projects
+            currentUser={this.props.currentUser}
+            onProjectDeselected={this.onProjectDeselected}
+            onProjectSelected={this.onProjectSelected}
+            projects={this.state.projects}
+            ready={this.state.ready}
+            selection={this.state.selection}
+          />
+
+          <ListFooter
+            count={this.state.projects.length}
+            loadMore={this.loadMore}
+            ready={this.state.ready}
+            total={this.state.total}
+            useMIUIButtons
+          />
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.css b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.css
deleted file mode 100644 (file)
index 439591f..0000000
+++ /dev/null
@@ -1,43 +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.
- */
-.project-row-text-cell {
-  max-width: 20em;
-}
-
-.projects-management-search {
-  display: flex;
-  gap: 20px;
-  padding: 8px 10px;
-  flex-wrap: wrap;
-}
-
-.projects-management-search > * {
-  display: flex;
-  white-space: nowrap;
-}
-
-.projects-management-search > *:not(.bulk-actions) {
-  flex-direction: column;
-  justify-content: center;
-}
-
-.projects-management-search > .bulk-actions {
-  justify-content: end;
-}
index 262f7401ae5ab867aa1c8fccbab4453c71baf3bb..3c1e3d0fea265f1f10cd8682f15c0dbdf77e5aed 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 { ActionCell, Badge, Checkbox, ContentCell, HoverLink, Note, TableRow } from 'design-system';
 import * as React from 'react';
 import { Project } from '../../api/project-management';
-import Link from '../../components/common/Link';
 import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
-import Checkbox from '../../components/controls/Checkbox';
 import Tooltip from '../../components/controls/Tooltip';
-import QualifierIcon from '../../components/icons/QualifierIcon';
 import DateFormatter from '../../components/intl/DateFormatter';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { getComponentOverviewUrl } from '../../helpers/urls';
 import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { ComponentQualifier } from '../../types/component';
 import { LoggedInUser } from '../../types/users';
-import './ProjectRow.css';
 import ProjectRowActions from './ProjectRowActions';
 
 interface Props {
@@ -49,52 +46,42 @@ export default function ProjectRow(props: Props) {
   };
 
   return (
-    <tr data-project-key={project.key}>
-      <td className="thin">
+    <TableRow data-project-key={project.key}>
+      <ContentCell>
         <Checkbox
           label={translateWithParameters('projects_management.select_project', project.name)}
           checked={selected}
           onCheck={handleProjectCheck}
         />
-      </td>
-
-      <td className="nowrap hide-overflow project-row-text-cell">
-        <Link
-          className="link-no-underline"
-          to={getComponentOverviewUrl(project.key, project.qualifier)}
-        >
-          <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} />
-
+      </ContentCell>
+      <ContentCell className="it__project-row-text-cell">
+        <HoverLink to={getComponentOverviewUrl(project.key, project.qualifier)}>
           <Tooltip overlay={project.name} placement="left">
             <span>{project.name}</span>
           </Tooltip>
-        </Link>
+        </HoverLink>
         {project.qualifier === ComponentQualifier.Project &&
           githubProvisioningEnabled &&
-          !project.managed && <span className="badge sw-ml-1">{translate('local')}</span>}
-      </td>
-
-      <td className="thin nowrap">
+          !project.managed && <Badge className="sw-ml-1">{translate('local')}</Badge>}
+      </ContentCell>
+      <ContentCell>
         <PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} />
-      </td>
-
-      <td className="nowrap hide-overflow project-row-text-cell">
+      </ContentCell>
+      <ContentCell className="it__project-row-text-cell">
         <Tooltip overlay={project.key} placement="left">
-          <span className="note">{project.key}</span>
+          <Note>{project.key}</Note>
         </Tooltip>
-      </td>
-
-      <td className="thin nowrap text-right">
+      </ContentCell>
+      <ContentCell>
         {project.lastAnalysisDate ? (
           <DateFormatter date={project.lastAnalysisDate} />
         ) : (
-          <span className="note">—</span>
+          <Note>—</Note>
         )}
-      </td>
-
-      <td className="thin nowrap">
+      </ContentCell>
+      <ActionCell>
         <ProjectRowActions currentUser={currentUser} project={project} />
-      </td>
-    </tr>
+      </ActionCell>
+    </TableRow>
   );
 }
index 0d7d7405ca6a455ab7165b010b5038f4f9b31b01..aedbd1db09a8257cd022d5a945e746f83d29e47b 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 { ActionsDropdown, ItemButton, ItemLink, PopupZLevel, Spinner } from 'design-system';
 import React, { useState } from 'react';
 import { getComponentNavigation } from '../../api/navigation';
 import { Project } from '../../api/project-management';
-import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown';
-import Spinner from '../../components/ui/Spinner';
 import { throwGlobalError } from '../../helpers/error';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { getComponentPermissionsUrl } from '../../helpers/urls';
@@ -71,43 +70,37 @@ export default function ProjectRowActions({ currentUser, project }: Props) {
   return (
     <>
       <ActionsDropdown
-        label={translateWithParameters('projects_management.show_actions_for_x', project.name)}
+        id="project-management-action-dropdown"
+        toggleClassName="it__user-actions-toggle"
         onOpen={handleDropdownOpen}
+        allowResizing
+        ariaLabel={translateWithParameters('projects_management.show_actions_for_x', project.name)}
+        zLevel={PopupZLevel.Global}
       >
-        {loading ? (
-          <ActionsDropdownItem>
-            <Spinner />
-          </ActionsDropdownItem>
-        ) : (
+        <Spinner loading={loading} className="sw-flex sw-ml-3">
           <>
             {hasAccess === true && (
-              <ActionsDropdownItem
-                className="js-edit-permissions"
-                to={getComponentPermissionsUrl(project.key)}
-              >
+              <ItemLink to={getComponentPermissionsUrl(project.key)}>
                 {translate(project.managed ? 'show_permissions' : 'edit_permissions')}
-              </ActionsDropdownItem>
+              </ItemLink>
             )}
 
             {hasAccess === false &&
               (!project.managed || currentUser.local || !githubProvisioningEnabled) && (
-                <ActionsDropdownItem
-                  className="js-restore-access"
+                <ItemButton
+                  className="it__restore-access"
                   onClick={() => setRestoreAccessModal(true)}
                 >
                   {translate('global_permissions.restore_access')}
-                </ActionsDropdownItem>
+                </ItemButton>
               )}
           </>
-        )}
+        </Spinner>
 
         {!project.managed && (
-          <ActionsDropdownItem
-            className="js-apply-template"
-            onClick={() => setApplyTemplateModal(true)}
-          >
+          <ItemButton className="it__apply-template" onClick={() => setApplyTemplateModal(true)}>
             {translate('projects_role.apply_template')}
-          </ActionsDropdownItem>
+          </ItemButton>
         )}
       </ActionsDropdown>
 
index 07a0e0e4a8cba32d30fc9efb5a956cf1a015c9a1..bc6b43364691f48f57b2a6f2aff781c809dacc9b 100644 (file)
@@ -17,7 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import classNames from 'classnames';
+import { ActionCell, ContentCell, Table, TableRow } from 'design-system';
 import * as React from 'react';
 import { Project } from '../../api/project-management';
 import { translate } from '../../helpers/l10n';
@@ -44,34 +46,33 @@ export default function Projects(props: Readonly<Props>) {
     }
   };
 
+  const header = (
+    <TableRow>
+      <ContentCell>&nbsp;</ContentCell>
+      <ContentCell>{translate('name')}</ContentCell>
+      <ContentCell>{translate('visibility')}</ContentCell>
+      <ContentCell>{translate('key')}</ContentCell>
+      <ContentCell>{translate('last_analysis')}</ContentCell>
+      <ActionCell>{translate('actions')}</ActionCell>
+    </TableRow>
+  );
+
   return (
-    <div className="boxed-group boxed-group-inner">
-      <table
-        className={classNames('data', 'zebra', { 'new-loading': !ready })}
-        id="projects-management-page-projects"
-      >
-        <thead>
-          <tr>
-            <th />
-            <th>{translate('name')}</th>
-            <th />
-            <th>{translate('key')}</th>
-            <th className="thin nowrap text-right">{translate('last_analysis')}</th>
-            <th />
-          </tr>
-        </thead>
-        <tbody>
-          {projects.map((project) => (
-            <ProjectRow
-              currentUser={currentUser}
-              key={project.key}
-              onProjectCheck={onProjectCheck}
-              project={project}
-              selected={selection.some((s) => s.key === project.key)}
-            />
-          ))}
-        </tbody>
-      </table>
-    </div>
+    <Table
+      columnCount={6}
+      header={header}
+      id="projects-management-page-projects"
+      className={classNames({ 'sw-opacity-50 sw-transition sw-duration-75 sw-ease-in': !ready })}
+    >
+      {projects.map((project) => (
+        <ProjectRow
+          currentUser={currentUser}
+          key={project.key}
+          onProjectCheck={onProjectCheck}
+          project={project}
+          selected={selection.some((s) => s.key === project.key)}
+        />
+      ))}
+    </Table>
   );
 }
index faef80ced63116ee73d419bbe13eae5d61ebd04b..b491fd281cdd7f9b48e57cee0cb4e0f0ed88e125 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 { ButtonPrimary, Modal } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { grantPermissionToUser } from '../../api/permissions';
 import { Project } from '../../api/project-management';
-import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
 import { translate } from '../../helpers/l10n';
 import { LoggedInUser } from '../../types/users';
 
@@ -37,6 +36,8 @@ interface State {
   loading: boolean;
 }
 
+const FORM_ID = 'restore-access-form';
+
 export default class RestoreAccessModal extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = { loading: false };
@@ -70,16 +71,16 @@ export default class RestoreAccessModal extends React.PureComponent<Props, State
     });
 
   render() {
+    const { loading } = this.state;
     const header = translate('global_permissions.restore_access');
 
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form onSubmit={this.handleFormSubmit}>
-          <header className="modal-head">
-            <h2>{header}</h2>
-          </header>
-
-          <div className="modal-body">
+      <Modal
+        headerTitle={header}
+        onClose={this.props.onClose}
+        loading={loading}
+        body={
+          <form id={FORM_ID} onSubmit={this.handleFormSubmit}>
             <FormattedMessage
               defaultMessage={translate('global_permissions.restore_access.message')}
               id="global_permissions.restore_access.message"
@@ -88,15 +89,15 @@ export default class RestoreAccessModal extends React.PureComponent<Props, State
                 administer: <strong>{translate('projects_role.admin')}</strong>,
               }}
             />
-          </div>
-
-          <footer className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={this.state.loading}>{translate('restore')}</SubmitButton>
-            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
+          </form>
+        }
+        primaryButton={
+          <ButtonPrimary autoFocus disabled={loading} form={FORM_ID} type="submit">
+            {translate('restore')}
+          </ButtonPrimary>
+        }
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 }
index ebdca8c53948d5f352d78ebc44288af03cdeb71d..7648facfb7897bed9a03ec818cba4d13298ec850 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,
+  Checkbox,
+  DangerButtonPrimary,
+  DatePicker,
+  HelperHintIcon,
+  InputSearch,
+  InputSelect,
+  Spinner,
+} from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { components, OptionProps, SingleValueProps } from 'react-select';
+import { OptionProps, SingleValueProps, components } from 'react-select';
 import { Project } from '../../api/project-management';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
-import { Button } from '../../components/controls/buttons';
-import Checkbox from '../../components/controls/Checkbox';
-import DateInput from '../../components/controls/DateInput';
 import HelpTooltip from '../../components/controls/HelpTooltip';
-import SearchBox from '../../components/controls/SearchBox';
-import Select, { LabelValueSelectOption } from '../../components/controls/Select';
-import QualifierIcon from '../../components/icons/QualifierIcon';
+import { LabelValueSelectOption } from '../../components/controls/Select';
 import { translate } from '../../helpers/l10n';
 import { AppState } from '../../types/appstate';
 import { Visibility } from '../../types/component';
@@ -129,6 +135,7 @@ class Search extends React.PureComponent<Props, State> {
     const checked = isAllChecked || thirdState;
     return (
       <Checkbox
+        className="it__projects-selection"
         checked={checked}
         id="projects-selection"
         onCheck={this.onCheck}
@@ -138,12 +145,7 @@ class Search extends React.PureComponent<Props, State> {
     );
   };
 
-  renderQualifierOption = (option: LabelValueSelectOption) => (
-    <div className="display-flex-center">
-      <QualifierIcon className="little-spacer-right" qualifier={option.value} />
-      {option.label}
-    </div>
-  );
+  renderQualifierOption = (option: LabelValueSelectOption) => <div>{option.label}</div>;
 
   renderQualifierFilter = () => {
     const options = this.getQualifierOptions();
@@ -151,8 +153,8 @@ class Search extends React.PureComponent<Props, State> {
       return null;
     }
     return (
-      <Select
-        className="input-medium it__project-qualifier-select"
+      <InputSelect
+        className="it__project-qualifier-select"
         isDisabled={!this.props.ready}
         name="projects-qualifier"
         onChange={this.handleQualifierChange}
@@ -175,8 +177,7 @@ class Search extends React.PureComponent<Props, State> {
       { value: Visibility.Private, label: translate('visibility.private') },
     ];
     return (
-      <Select
-        className="input-small"
+      <InputSelect
         isDisabled={!this.props.ready}
         name="projects-visibility"
         onChange={this.handleVisibilityChange}
@@ -190,64 +191,69 @@ class Search extends React.PureComponent<Props, State> {
 
   renderTypeFilter = () =>
     this.props.qualifiers === 'TRK' ? (
-      <div>
+      <div className="sw-flex sw-items-center">
         <Checkbox
           checked={this.props.provisioned}
-          className="link-checkbox-control"
           id="projects-provisioned"
           onCheck={this.props.onProvisionedChanged}
         >
-          <span className="text-middle little-spacer-left">
-            {translate('provisioning.only_provisioned')}
-          </span>
+          <span className="sw-ml-1">{translate('provisioning.only_provisioned')}</span>
           <HelpTooltip
-            className="spacer-left"
+            className="sw-ml-2"
             overlay={translate('provisioning.only_provisioned.tooltip')}
-          />
+          >
+            <HelperHintIcon />
+          </HelpTooltip>
         </Checkbox>
       </div>
     ) : null;
 
   renderDateFilter = () => {
     return (
-      <DateInput
-        inputClassName="input-medium"
+      <DatePicker
+        clearButtonLabel={translate('clear')}
         name="analyzed-before"
         onChange={this.props.onDateChanged}
         placeholder={translate('last_analysis_before')}
         value={this.props.analyzedBefore}
+        showClearButton
+        alignRight
+        size="auto"
       />
     );
   };
 
   render() {
     return (
-      <div className="big-spacer-bottom">
-        <div className="projects-management-search">
-          <div>{this.props.ready ? this.renderCheckbox() : <i className="spinner" />}</div>
+      <div className="sw-mb-4">
+        <div className="sw-flex sw-justify-start sw-items-center sw-flex-wrap sw-gap-2 sw-p-2">
+          <Spinner loading={!this.props.ready} className="sw-ml-2">
+            {this.renderCheckbox()}
+          </Spinner>
           {this.renderQualifierFilter()}
           {this.renderDateFilter()}
           {this.renderVisibilityFilter()}
           {this.renderTypeFilter()}
-          <div className="flex-grow">
-            <SearchBox
+          <div className="sw-flex-grow">
+            <InputSearch
               minLength={3}
               onChange={this.props.onSearch}
               placeholder={translate('search.search_by_name_or_key')}
               value={this.props.query}
+              size="auto"
             />
           </div>
-          <div className="bulk-actions">
-            <Button
-              className="js-bulk-apply-permission-template"
+          <div>
+            <ButtonSecondary
+              className="it__bulk-apply-permission-template"
               disabled={this.props.selection.length === 0}
               onClick={this.handleBulkApplyTemplateClick}
             >
               {translate('permission_templates.bulk_apply_permission_template')}
-            </Button>
+            </ButtonSecondary>
             {this.props.qualifiers === 'TRK' && (
-              <Button
-                className="js-delete spacer-left button-red"
+              <DangerButtonPrimary
+                className="sw-ml-2"
                 disabled={this.props.selection.length === 0}
                 onClick={this.handleDeleteClick}
                 title={
@@ -257,7 +263,7 @@ class Search extends React.PureComponent<Props, State> {
                 }
               >
                 {translate('delete')}
-              </Button>
+              </DangerButtonPrimary>
             )}
           </div>
         </div>
index 16a2141c423a8de40a5ac32da26caea2238037d0..d4cb21c7b72df2cb83e8d4561856b00554267aa9 100644 (file)
@@ -75,10 +75,10 @@ const ui = {
   }),
   projectActions: (projectName: string) =>
     byRole('button', { name: `projects_management.show_actions_for_x.${projectName}` }),
-  editPermissions: byRole('link', { name: 'edit_permissions' }),
-  showPermissions: byRole('link', { name: 'show_permissions' }),
-  applyPermissionTemplate: byRole('button', { name: 'projects_role.apply_template' }),
-  restoreAccess: byRole('button', { name: 'global_permissions.restore_access' }),
+  editPermissions: byRole('menuitem', { name: 'edit_permissions' }),
+  showPermissions: byRole('menuitem', { name: 'show_permissions' }),
+  applyPermissionTemplate: byRole('menuitem', { name: 'projects_role.apply_template' }),
+  restoreAccess: byRole('menuitem', { name: 'global_permissions.restore_access' }),
   editPermissionsPage: byText('/project_roles?id=project1'),
 
   apply: byRole('button', { name: 'apply' }),
@@ -114,7 +114,7 @@ const ui = {
   qualifierFilter: byRole('combobox', { name: 'projects_management.filter_by_component' }),
   analysisDateFilter: byPlaceholderText('last_analysis_before'),
   provisionedFilter: byRole('checkbox', {
-    name: 'provisioning.only_provisioned help',
+    name: 'provisioning.only_provisioned',
   }),
   searchFilter: byRole('searchbox', { name: 'search.search_by_name_or_key' }),
 
@@ -291,7 +291,7 @@ describe('Bulk permission templates', () => {
         .get(),
     ).toBeInTheDocument();
     await selectEvent.select(
-      ui.bulkApplyDialog.by(ui.selectTemplate('field_required')).get(),
+      ui.bulkApplyDialog.by(ui.selectTemplate('required')).get(),
       'Permission Template 2',
     );
     await user.click(ui.bulkApplyDialog.by(ui.apply).get());
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.css b/server/sonar-web/src/main/js/components/controls/DateInput.css
deleted file mode 100644 (file)
index 545e661..0000000
+++ /dev/null
@@ -1,87 +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.
- */
-.rdp {
-  --rdp-cell-size: 30px;
-  --rdp-caption-font-size: 13px;
-  /* Ensures the month/year dropdowns do not move on click, but rdp outline is not shown */
-  --rdp-outline: 2px solid transparent;
-  --rdp-outline-selected: 2px solid transparent;
-}
-
-.rdp-day_selected {
-  background-color: var(--blue);
-}
-
-.rdp-day_selected:hover {
-  background-color: var(--blue);
-}
-
-.date-input-control {
-  position: relative;
-  display: inline-block;
-  cursor: pointer;
-}
-
-.date-input-control-input {
-  width: 130px;
-  padding-left: var(--controlHeight) !important;
-  cursor: pointer;
-}
-
-.date-input-control-input.is-filled {
-  padding-right: 16px !important;
-}
-
-.date-input-control-icon {
-  position: absolute;
-  top: 4px;
-  left: 4px;
-}
-
-.date-input-control-icon path {
-  fill: var(--neutral600);
-  opacity: 0.9;
-}
-
-.date-input-control-input:focus + .date-input-control-icon path {
-  fill: var(--info500);
-}
-
-.date-input-control-reset {
-  position: absolute;
-  top: 4px;
-  right: 4px;
-  border: none;
-}
-
-.date-input-calendar {
-  position: absolute;
-  z-index: var(--dropdownMenuZIndex);
-  top: 100%;
-  left: 0;
-  border: 1px solid var(--barBorderColor);
-  background-color: #fff;
-  box-shadow: var(--defaultShadow);
-}
-
-.date-input-calendar.align-right {
-  left: initial;
-  right: 0;
-}
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx
deleted file mode 100644 (file)
index c2cf55e..0000000
+++ /dev/null
@@ -1,195 +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 classNames from 'classnames';
-import * as React from 'react';
-import { ActiveModifiers, DayPicker, Matcher } from 'react-day-picker';
-import 'react-day-picker/dist/style.css';
-import { injectIntl, WrappedComponentProps } from 'react-intl';
-import { ClearButton } from '../../components/controls/buttons';
-import OutsideClickHandler from '../../components/controls/OutsideClickHandler';
-import CalendarIcon from '../../components/icons/CalendarIcon';
-import { getShortWeekDayName, translate } from '../../helpers/l10n';
-import './DateInput.css';
-import EscKeydownHandler from './EscKeydownHandler';
-import FocusOutHandler from './FocusOutHandler';
-
-// When no minDate is given, year dropdown will show year options up to PAST_MAX_YEARS in the past
-const YEARS_TO_DISPLAY = 10;
-
-interface Props {
-  alignRight?: boolean;
-  className?: string;
-  currentMonth?: Date;
-  highlightFrom?: Date;
-  highlightTo?: Date;
-  inputClassName?: string;
-  maxDate?: Date;
-  minDate?: Date;
-  name?: string;
-  id?: string;
-  onChange: (date: Date | undefined) => void;
-  placeholder: string;
-  value?: Date;
-}
-
-interface State {
-  currentMonth: Date;
-  open: boolean;
-  lastHovered?: Date;
-}
-
-export default class DateInput extends React.PureComponent<Props, State> {
-  input?: HTMLInputElement | null;
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false };
-  }
-
-  focus = () => {
-    if (this.input) {
-      this.input.focus();
-    }
-    this.openCalendar();
-  };
-
-  handleResetClick = () => {
-    this.closeCalendar();
-    this.props.onChange(undefined);
-  };
-
-  openCalendar = () => {
-    this.setState({
-      currentMonth: this.props.value || this.props.currentMonth || new Date(),
-      lastHovered: undefined,
-      open: true,
-    });
-  };
-
-  closeCalendar = () => {
-    this.setState({ open: false });
-  };
-
-  handleDayClick = (day: Date, modifiers: ActiveModifiers) => {
-    if (!modifiers.disabled) {
-      this.closeCalendar();
-      this.props.onChange(day);
-    }
-  };
-
-  handleDayMouseEnter = (day: Date, modifiers: ActiveModifiers) => {
-    this.setState({ lastHovered: modifiers.disabled ? undefined : day });
-  };
-
-  render() {
-    const {
-      alignRight,
-      highlightFrom,
-      highlightTo,
-      minDate,
-      maxDate = new Date(),
-      value: selectedDay,
-      name,
-      className,
-      inputClassName,
-      id,
-      placeholder,
-    } = this.props;
-    const { lastHovered, currentMonth, open } = this.state;
-
-    // Infer start and end dropdown year from min/max dates, if set
-    const fromYear = minDate ? minDate.getFullYear() : new Date().getFullYear() - YEARS_TO_DISPLAY;
-    const toYear = maxDate ? maxDate.getFullYear() : new Date().getFullYear() + 1;
-
-    let highlighted: Matcher = false;
-    const lastHoveredOrValue = lastHovered || selectedDay;
-    if (highlightFrom && lastHoveredOrValue) {
-      highlighted = { from: highlightFrom, to: lastHoveredOrValue };
-    }
-    if (highlightTo && lastHoveredOrValue) {
-      highlighted = { from: lastHoveredOrValue, to: highlightTo };
-    }
-
-    return (
-      <FocusOutHandler onFocusOut={this.closeCalendar}>
-        <OutsideClickHandler onClickOutside={this.closeCalendar}>
-          <EscKeydownHandler onKeydown={this.closeCalendar}>
-            <span className={classNames('date-input-control', className)}>
-              <InputWrapper
-                className={classNames('date-input-control-input', inputClassName, {
-                  'is-filled': selectedDay !== undefined,
-                })}
-                id={id}
-                innerRef={(node: HTMLInputElement | null) => (this.input = node)}
-                name={name}
-                onFocus={this.openCalendar}
-                placeholder={placeholder}
-                readOnly
-                type="text"
-                value={selectedDay}
-              />
-              <CalendarIcon className="date-input-control-icon" fill="" />
-              {selectedDay !== undefined && (
-                <ClearButton
-                  aria-label={translate('reset_date')}
-                  className="button-tiny date-input-control-reset"
-                  iconProps={{ size: 12 }}
-                  onClick={this.handleResetClick}
-                />
-              )}
-              {open && (
-                <div className={classNames('date-input-calendar', { 'align-right': alignRight })}>
-                  <DayPicker
-                    mode="default"
-                    captionLayout="dropdown-buttons"
-                    fromYear={fromYear}
-                    toYear={toYear}
-                    disabled={{ after: maxDate, before: minDate }}
-                    weekStartsOn={1}
-                    formatters={{
-                      formatWeekdayName: (date) => getShortWeekDayName(date.getDay()),
-                    }}
-                    modifiers={{ highlighted }}
-                    modifiersClassNames={{ highlighted: 'highlighted' }}
-                    month={currentMonth}
-                    onMonthChange={(currentMonth) => this.setState({ currentMonth })}
-                    selected={selectedDay}
-                    onDayClick={this.handleDayClick}
-                    onDayMouseEnter={this.handleDayMouseEnter}
-                  />
-                </div>
-              )}
-            </span>
-          </EscKeydownHandler>
-        </OutsideClickHandler>
-      </FocusOutHandler>
-    );
-  }
-}
-
-type InputWrapperProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> &
-  WrappedComponentProps & { innerRef: React.Ref<HTMLInputElement>; value: Date | undefined };
-
-const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => {
-  const formattedValue =
-    value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' });
-  return <input {...other} ref={innerRef} value={formattedValue || ''} />;
-});
diff --git a/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx b/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx
deleted file mode 100644 (file)
index fe25a82..0000000
+++ /dev/null
@@ -1,108 +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 classNames from 'classnames';
-import { max, min } from 'date-fns';
-import * as React from 'react';
-import { translate } from '../../helpers/l10n';
-import DateInput from './DateInput';
-
-type DateRange = { from?: Date; to?: Date };
-
-interface Props {
-  className?: string;
-  maxDate?: Date;
-  minDate?: Date;
-  onChange: (date: DateRange) => void;
-  value?: DateRange;
-  alignEndDateCalandarRight?: boolean;
-}
-
-export default class DateRangeInput extends React.PureComponent<Props> {
-  toDateInput?: DateInput | null;
-
-  get from() {
-    return this.props.value && this.props.value.from;
-  }
-
-  get to() {
-    return this.props.value && this.props.value.to;
-  }
-
-  handleFromChange = (from: Date | undefined) => {
-    this.props.onChange({ from, to: this.to });
-
-    // use `setTimeout` to work around the immediate closing of the `toDateInput`
-    setTimeout(() => {
-      if (from && !this.to && this.toDateInput) {
-        this.toDateInput.focus();
-      }
-    });
-  };
-
-  handleToChange = (to: Date | undefined) => {
-    this.props.onChange({ from: this.from, to });
-  };
-
-  render() {
-    const { alignEndDateCalandarRight, minDate, maxDate } = this.props;
-
-    return (
-      <div className={classNames('display-flex-end', this.props.className)}>
-        <div className="display-flex-column">
-          <label className="text-bold little-spacer-bottom" htmlFor="date-from">
-            {translate('start_date')}
-          </label>
-          <DateInput
-            currentMonth={this.to}
-            data-test="from"
-            id="date-from"
-            highlightTo={this.to}
-            minDate={minDate}
-            maxDate={maxDate && this.to ? min([maxDate, this.to]) : maxDate || this.to}
-            onChange={this.handleFromChange}
-            placeholder={translate('start_date')}
-            value={this.from}
-          />
-        </div>
-        <span className="note little-spacer-left little-spacer-right little-spacer-bottom">
-          {translate('to_')}
-        </span>
-        <div className="display-flex-column">
-          <label className="text-bold little-spacer-bottom" htmlFor="date-to">
-            {translate('end_date')}
-          </label>
-          <DateInput
-            alignRight={alignEndDateCalandarRight}
-            currentMonth={this.from}
-            data-test="to"
-            id="date-to"
-            highlightFrom={this.from}
-            minDate={minDate && this.from ? max([minDate, this.from]) : minDate || this.from}
-            maxDate={maxDate}
-            onChange={this.handleToChange}
-            placeholder={translate('end_date')}
-            ref={(element) => (this.toDateInput = element)}
-            value={this.to}
-          />
-        </div>
-      </div>
-    );
-  }
-}