]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21298 Showcase Echoes' Spinner component in a few places
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Wed, 28 Feb 2024 16:30:02 +0000 (17:30 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 28 Feb 2024 20:02:44 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Spinner.tsx
server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx

index d76bbfef7755f7a7573cf39113b95d3f9bb81fea..f8215fc57393ec574c35cb19d1973a5520b3503e 100644 (file)
@@ -33,6 +33,13 @@ interface Props {
   placeholder?: boolean;
 }
 
+/** @deprecated Use Spinner from Echoes instead.
+ *
+ * Some of the props have changed or been renamed:
+ * - ~`customSpinner`~ has been removed
+ * - `loading` is now `isLoading`
+ * - `placeholder` is now `hasPlaceholder`
+ */
 export function Spinner(props: React.PropsWithChildren<Props>) {
   const intl = useIntl();
   const {
index ffc9f55bdafcfc7cf2777104838c49bfe3991668..be2ee41fbd7b61cbb3111baaf78cefc3f9d69542 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 styled from '@emotion/styled';
+import { Link, Spinner } from '@sonarsource/echoes-react';
 import { formatDistance } from 'date-fns';
-import { CheckIcon, FlagMessage, FlagWarningIcon, Link, Spinner, themeColor } from 'design-system';
+import { CheckIcon, FlagMessage, FlagWarningIcon, themeColor } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { translate, translateWithParameters } from '../../helpers/l10n';
@@ -27,19 +29,20 @@ import { AlmSyncStatus } from '../../types/provisioning';
 import { TaskStatuses } from '../../types/tasks';
 
 interface SynchronisationWarningProps {
-  short?: boolean;
   data: AlmSyncStatus;
+  short?: boolean;
 }
 
 interface LastSyncProps {
-  short?: boolean;
   info: AlmSyncStatus['lastSync'];
+  short?: boolean;
 }
 
 function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
   if (info === undefined) {
     return null;
   }
+
   const { finishedAt, errorMessage, status, summary, warningMessage } = info;
 
   const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : '';
@@ -54,13 +57,14 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
             <CheckIcon width={32} height={32} className="sw-mr-2" />
           )}
         </IconWrapper>
+
         <i>
           {warningMessage ? (
             <FormattedMessage
-              id="settings.authentication.github.synchronization_successful.with_warning"
               defaultMessage={translate(
                 'settings.authentication.github.synchronization_successful.with_warning',
               )}
+              id="settings.authentication.github.synchronization_successful.with_warning"
               values={{
                 date: formattedDate,
                 details: (
@@ -82,10 +86,10 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
       <FlagMessage variant="error">
         <div>
           <FormattedMessage
-            id="settings.authentication.github.synchronization_failed_short"
             defaultMessage={translate(
               'settings.authentication.github.synchronization_failed_short',
             )}
+            id="settings.authentication.github.synchronization_failed_short"
             values={{
               details: (
                 <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
@@ -102,9 +106,9 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
   return (
     <>
       <FlagMessage
-        variant={status === TaskStatuses.Success ? 'success' : 'error'}
-        role="alert"
         aria-live="assertive"
+        role="alert"
+        variant={status === TaskStatuses.Success ? 'success' : 'error'}
       >
         <div>
           {status === TaskStatuses.Success ? (
@@ -113,7 +117,9 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
                 'settings.authentication.github.synchronization_successful',
                 formattedDate,
               )}
+
               <br />
+
               {summary ?? ''}
             </>
           ) : (
@@ -124,12 +130,15 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
                   formattedDate,
                 )}
               </div>
+
               <br />
+
               {errorMessage ?? ''}
             </React.Fragment>
           )}
         </div>
       </FlagMessage>
+
       <FlagMessage variant="warning" role="alert" aria-live="assertive">
         {warningMessage}
       </FlagMessage>
@@ -138,8 +147,8 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
 }
 
 export default function AlmSynchronisationWarning({
-  short,
   data,
+  short,
 }: Readonly<SynchronisationWarningProps>) {
   const loadingLabel =
     data.nextSync &&
@@ -148,11 +157,13 @@ export default function AlmSynchronisationWarning({
         ? 'settings.authentication.github.synchronization_pending'
         : 'settings.authentication.github.synchronization_in_progress',
     );
+
   return (
     <>
       {!short && (
         <div className={data.nextSync ? 'sw-flex sw-gap-2 sw-mb-4' : ''}>
-          <Spinner loading={!!data.nextSync} ariaLabel={loadingLabel} />
+          <Spinner ariaLabel={loadingLabel} isLoading={!!data.nextSync} />
+
           <div>{data.nextSync && loadingLabel}</div>
         </div>
       )}
index d4a4abc4487006e5ae44765998cf0aef490bb20c..42417a21f78aba0f118017344a8502f30004c482 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 { Spinner } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { ItemButton, Spinner } from 'design-system';
+import { ItemButton } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 
@@ -36,13 +38,14 @@ export default class GlobalSearchShowMore extends React.PureComponent<Props> {
     event.preventDefault();
     event.stopPropagation();
     event.currentTarget.blur();
-    if (qualifier) {
+
+    if (qualifier !== '') {
       this.props.onMoreClick(qualifier);
     }
   };
 
   handleMouseEnter = (qualifier: string) => {
-    if (qualifier) {
+    if (qualifier !== '') {
       this.props.onSelect(`qualifier###${qualifier}`);
     }
   };
@@ -61,7 +64,7 @@ export default class GlobalSearchShowMore extends React.PureComponent<Props> {
           this.handleMouseEnter(qualifier);
         }}
       >
-        <Spinner loading={loadingMore === qualifier}>{translate('show_more')}</Spinner>
+        <Spinner isLoading={loadingMore === qualifier}>{translate('show_more')}</Spinner>
       </ItemButton>
     );
   }
index ff0e0c20fd1575c0aa9655db8975329792ae1cf5..d8cd4a408f17302db770186cf35bf2d67605d35a 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 { FlagMessage, Spinner } from 'design-system';
+
+import { Spinner } from '@sonarsource/echoes-react';
+import { FlagMessage } from 'design-system';
 import { findLastIndex, keyBy } from 'lodash';
 import * as React from 'react';
 import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
index de49740d9ad901a85012dcf68fa8463f4941e36a..51005341c296db58d33dd7486607af73010a0ceb 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 styled from '@emotion/styled';
+import { Spinner } from '@sonarsource/echoes-react';
 import {
   LAYOUT_FOOTER_HEIGHT,
   LargeCenteredLayout,
   PageContentFontWrapper,
-  Spinner,
   themeBorder,
   themeColor,
 } from 'design-system';
@@ -42,8 +43,10 @@ import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthent
 import { translate } from '../../../helpers/l10n';
 import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
 import { get, save } from '../../../helpers/storage';
+import { isDefined } from '../../../helpers/types';
 import { AppState } from '../../../types/appstate';
 import { ComponentQualifier } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
 import { RawQuery } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
 import { Query, hasFilterParams, parseUrlQuery } from '../query';
@@ -55,10 +58,10 @@ import PageSidebar from './PageSidebar';
 import ProjectsList from './ProjectsList';
 
 interface Props {
+  appState: AppState;
   currentUser: CurrentUser;
   isFavorite: boolean;
   location: Location;
-  appState: AppState;
   router: Router;
 }
 
@@ -152,19 +155,20 @@ export class AllProjects extends React.PureComponent<Props, State> {
   handlePerspectiveChange = ({ view }: { view?: string }) => {
     const query: {
       view: string | undefined;
-      sort?: string | undefined;
+      sort?: string;
     } = {
       view: view === 'overall' ? undefined : view,
     };
 
     if (this.state.query.view === 'leak' || view === 'leak') {
-      if (this.state.query.sort) {
+      if (isDefined(this.state.query.sort)) {
         const sort = parseSorting(this.state.query.sort);
 
-        if (SORTING_SWITCH[sort.sortValue]) {
+        if (isDefined(SORTING_SWITCH[sort.sortValue])) {
           query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue];
         }
       }
+
       this.props.router.push({ pathname: this.props.location.pathname, query });
     } else {
       this.updateLocationQuery(query);
@@ -214,6 +218,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
 
     return searchProjects(data).then(({ facets }) => {
       const values = facets.find((facet) => facet.property === property)?.values ?? [];
+
       return mapValues(keyBy(values, 'val'), 'count');
     });
   };
@@ -292,10 +297,10 @@ export class AllProjects extends React.PureComponent<Props, State> {
             handleFavorite={this.handleFavorite}
             isFavorite={this.props.isFavorite}
             isFiltered={hasFilterParams(this.state.query)}
+            loading={this.state.loading}
+            loadMore={this.fetchMoreProjects}
             projects={this.state.projects}
             query={this.state.query}
-            loadMore={this.fetchMoreProjects}
-            loading={this.state.loading}
             total={this.state.total}
           />
         )}
@@ -306,7 +311,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
   render() {
     return (
       <StyledWrapper id="projects-page">
-        <Suggestions suggestions="projects" />
+        <Suggestions suggestions={MetricKey.projects} />
         <Helmet defer={false} title={translate('projects.page')} />
 
         <h1 className="sw-sr-only">{translate('projects.page')}</h1>
@@ -338,27 +343,27 @@ function getStorageOptions() {
     view?: string;
   } = {};
 
-  if (get(LS_PROJECTS_SORT)) {
-    options.sort = get(LS_PROJECTS_SORT) || undefined;
+  if (get(LS_PROJECTS_SORT) !== null) {
+    options.sort = get(LS_PROJECTS_SORT) ?? undefined;
   }
 
-  if (get(LS_PROJECTS_VIEW)) {
-    options.view = get(LS_PROJECTS_VIEW) || undefined;
+  if (get(LS_PROJECTS_VIEW) !== null) {
+    options.view = get(LS_PROJECTS_VIEW) ?? undefined;
   }
 
   return options;
 }
 
-function SetSearchParamsWrapper(props: Props) {
+function SetSearchParamsWrapper(props: Readonly<Props>) {
   const [searchParams, setSearchParams] = useSearchParams();
   const savedOptions = getStorageOptions();
 
   React.useEffect(
     () => {
-      const hasViewParams = searchParams.get('sort') || searchParams.get('view');
-      const hasSavedOptions = savedOptions.sort || savedOptions.view;
+      const hasViewParams = searchParams.get('sort') ?? searchParams.get('view');
+      const hasSavedOptions = savedOptions.sort ?? savedOptions.view;
 
-      if (!hasViewParams && hasSavedOptions) {
+      if (!isDefined(hasViewParams) && isDefined(hasSavedOptions)) {
         setSearchParams(savedOptions);
       }
     },
index 9a7d6b545f89a3b91b55d984da26d1ec3b597e94..2cab4f657834f6440fc0f13698ee63a7bbba2cc7 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 { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system';
+
+import { Spinner } from '@sonarsource/echoes-react';
+import { ButtonPrimary, FlagMessage, Modal } from 'design-system';
 import { keyBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
@@ -34,16 +36,16 @@ import { SettingValue } from './hook/useConfiguration';
 import { isAllowToSignUpEnabled, isOrganizationListEmpty } from './hook/useGithubConfiguration';
 
 interface Props {
-  create: boolean;
-  loading: boolean;
-  values: Dict<SettingValue>;
-  setNewValue: (key: string, value: string | boolean) => void;
   canBeSave: boolean;
-  onClose: () => void;
-  tab: AuthenticationTabs;
+  create: boolean;
   excludedField: string[];
   hasLegacyConfiguration?: boolean;
+  loading: boolean;
+  onClose: () => void;
   provisioningStatus?: ProvisioningType;
+  setNewValue: (key: string, value: string | boolean) => void;
+  tab: AuthenticationTabs;
+  values: Dict<SettingValue>;
 }
 
 interface ErrorValue {
@@ -53,15 +55,15 @@ interface ErrorValue {
 
 export default function ConfigurationForm(props: Readonly<Props>) {
   const {
-    create,
-    loading,
-    values,
-    setNewValue,
     canBeSave,
-    tab,
+    create,
     excludedField,
     hasLegacyConfiguration,
+    loading,
     provisioningStatus,
+    setNewValue,
+    tab,
+    values,
   } = props;
   const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
   const [showConfirmModal, setShowConfirmModal] = React.useState(false);
@@ -87,12 +89,14 @@ export default function ConfigurationForm(props: Readonly<Props>) {
       const errors = Object.values(values)
         .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
         .map((v) => ({ key: v.key, message: translate('field_required') }));
+
       setErrors(keyBy(errors, 'key'));
     }
   };
 
   const onSave = async () => {
     const data = await changeConfig(Object.values(values));
+
     const errors = data
       .filter(({ success }) => !success)
       .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
@@ -110,15 +114,15 @@ export default function ConfigurationForm(props: Readonly<Props>) {
 
   const formBody = (
     <form id={FORM_ID} onSubmit={handleSubmit}>
-      <Spinner loading={loading} ariaLabel={translate('settings.authentication.form.loading')}>
+      <Spinner ariaLabel={translate('settings.authentication.form.loading')} isLoading={loading}>
         <FlagMessage
           className="sw-w-full sw-mb-8"
           variant={hasLegacyConfiguration ? 'warning' : 'info'}
         >
           <span>
             <FormattedMessage
-              id={`settings.authentication.${helpMessage}`}
               defaultMessage={translate(`settings.authentication.${helpMessage}`)}
+              id={`settings.authentication.${helpMessage}`}
               values={{
                 link: (
                   <DocumentationLink
@@ -131,21 +135,23 @@ export default function ConfigurationForm(props: Readonly<Props>) {
             />
           </span>
         </FlagMessage>
+
         {Object.values(values).map((val) => {
           if (excludedField.includes(val.key)) {
             return null;
           }
 
           const isSet = hasLegacyConfiguration ? false : !val.isNotSet;
+
           return (
             <div key={val.key} className="sw-mb-8">
               <AuthenticationFormField
-                settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
                 definition={val.definition}
+                error={errors[val.key]?.message}
+                isNotSet={!isSet}
                 mandatory={val.mandatory}
                 onFieldChange={setNewValue}
-                isNotSet={!isSet}
-                error={errors[val.key]?.message}
+                settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
               />
             </div>
           );
@@ -157,23 +163,24 @@ export default function ConfigurationForm(props: Readonly<Props>) {
   return (
     <>
       <Modal
+        body={formBody}
         headerTitle={header}
         isScrollable
         onClose={props.onClose}
-        body={formBody}
         primaryButton={
           <ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}>
             {translate('settings.almintegration.form.save')}
-            <Spinner className="sw-ml-2" loading={loading} />
+
+            <Spinner className="sw-ml-2" isLoading={loading} />
           </ButtonPrimary>
         }
       />
       {showConfirmModal && (
         <GitHubConfirmModal
-          onConfirm={onSave}
           onClose={() => setShowConfirmModal(false)}
-          values={values}
+          onConfirm={onSave}
           provisioningStatus={provisioningStatus ?? ProvisioningType.jit}
+          values={values}
         />
       )}
     </>