]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19472 New layout for Code page
authorJay <jeremy.davis@sonarsource.com>
Mon, 5 Jun 2023 11:22:27 +0000 (13:22 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 9 Jun 2023 20:03:10 +0000 (20:03 +0000)
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx [new file with mode: 0644]

index f0ca8055c1f2e2a7ca0b863843d2a71088be8174..4a425746158b70fdaee603df750bdb8ca1692acf 100644 (file)
@@ -43,6 +43,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/security_hotspots',
   '/component_measures',
   '/project/issues',
+  '/code',
 ];
 
 export default function GlobalContainer() {
index 1efd31d8c59107f79220356c4ef1892885060026..af5d7b5255fdc8d11e6eee6b4ec5a3436bcc29ac 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 classNames from 'classnames';
-import { debounce, intersection } from 'lodash';
+import { debounce, noop } from 'lodash';
 import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
 import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import withComponentContext from '../../../app/components/componentContext/withComponentContext';
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-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 { isPullRequest } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
 import { CodeScope, getCodeUrl, getProjectUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component';
+import { ComponentQualifier } from '../../../types/component';
 import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types';
 import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
 import '../code.css';
-import {
-  getCodeMetrics,
-  loadMoreChildren,
-  retrieveComponent,
-  retrieveComponentChildren,
-} from '../utils';
-import Breadcrumbs from './Breadcrumbs';
-import Components from './Components';
-import Search from './Search';
-import SearchResults from './SearchResults';
-import SourceViewerWrapper from './SourceViewerWrapper';
+import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
+import CodeAppRenderer from './CodeAppRenderer';
 
 interface Props {
   branchLike?: BranchLike;
@@ -244,170 +226,28 @@ class CodeApp extends React.Component<Props, State> {
   refreshBranchStatus = () => {
     const { branchLike, component } = this.props;
     if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
+      this.props.fetchBranchStatus(branchLike, component.key).catch(noop);
     }
   };
 
   render() {
-    const { branchLike, component, location } = this.props;
-    const {
-      baseComponent,
-      breadcrumbs,
-      components = [],
-      highlighted,
-      loading,
-      newCodeSelected,
-      total,
-      searchResults,
-      sourceViewer,
-    } = this.state;
-    const { canBrowseAllChildProjects, qualifier } = component;
-
-    const showSearch = searchResults !== undefined;
-
-    const hasComponents = components.length > 0 || searchResults !== undefined;
-
-    const showBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
-
-    const showComponentList = sourceViewer === undefined && components.length > 0 && !showSearch;
-
-    const componentsClassName = classNames('boxed-group', 'spacer-top', {
-      'new-loading': loading,
-      'search-results': showSearch,
-    });
-
-    const metricKeys = intersection(
-      getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }),
-      Object.keys(this.props.metrics)
-    );
-    const metrics = metricKeys.map((metric) => this.props.metrics[metric]);
-
-    let defaultTitle = translate('code.page');
-    if (isApplication(baseComponent?.qualifier)) {
-      defaultTitle = translate('projects.page');
-    } else if (isPortfolioLike(baseComponent?.qualifier)) {
-      defaultTitle = translate('portfolio_breakdown.page');
-    }
-
-    const isPortfolio = isPortfolioLike(qualifier);
-
     return (
-      <main className="page page-limited">
-        <A11ySkipTarget anchor="code_main" />
-        {!canBrowseAllChildProjects && isPortfolio && (
-          <StyledAlert variant="warning" className="it__portfolio_warning">
-            <AlertContent>
-              {translate('code_viewer.not_all_measures_are_shown')}
-              <HelpTooltip
-                className="spacer-left"
-                overlay={translate('code_viewer.not_all_measures_are_shown.help')}
-              />
-            </AlertContent>
-          </StyledAlert>
-        )}
-        <Suggestions suggestions="code" />
-        <Helmet
-          defer={false}
-          title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
-        />
-        {hasComponents && (
-          <Search
-            branchLike={branchLike}
-            component={component}
-            newCodeSelected={newCodeSelected}
-            onNewCodeToggle={this.handleSelectNewCode}
-            onSearchClear={this.handleSearchClear}
-            onSearchResults={this.handleSearchResults}
-          />
-        )}
-
-        <div className="code-components">
-          {!hasComponents && sourceViewer === undefined && (
-            <div className="display-flex-center display-flex-column no-file">
-              <span className="h1 text-muted">
-                {translate(
-                  'code_viewer.no_source_code_displayed_due_to_empty_analysis',
-                  component.qualifier
-                )}
-              </span>
-            </div>
-          )}
-          {showBreadcrumbs && (
-            <Breadcrumbs
-              branchLike={branchLike}
-              breadcrumbs={breadcrumbs}
-              rootComponent={component}
-            />
-          )}
-
-          <div className={componentsClassName}>
-            {showComponentList && (
-              <Components
-                baseComponent={baseComponent}
-                branchLike={branchLike}
-                components={components}
-                cycle
-                metrics={metrics}
-                onEndOfList={this.handleLoadMore}
-                onGoToParent={this.handleGoToParent}
-                onHighlight={this.handleHighlight}
-                onSelect={this.handleSelect}
-                rootComponent={component}
-                selected={highlighted}
-                newCodeSelected={newCodeSelected}
-                showAnalysisDate={isPortfolio}
-              />
-            )}
-
-            {showSearch && (
-              <SearchResults
-                branchLike={this.props.branchLike}
-                components={searchResults}
-                onHighlight={this.handleHighlight}
-                onSelect={this.handleSelect}
-                rootComponent={component}
-                selected={highlighted}
-              />
-            )}
-
-            <div role="status" className={showSearch ? 'text-center big-padded-bottom' : undefined}>
-              {searchResults?.length === 0 && translate('no_results')}
-            </div>
-          </div>
-
-          {showComponentList && (
-            <ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
-          )}
-
-          {sourceViewer !== undefined && !showSearch && (
-            <div className="spacer-top">
-              <SourceViewerWrapper
-                branchLike={branchLike}
-                component={sourceViewer.key}
-                componentMeasures={sourceViewer.measures}
-                isFile
-                location={location}
-                onGoToParent={this.handleGoToParent}
-                onIssueChange={this.handleIssueChange}
-              />
-            </div>
-          )}
-        </div>
-      </main>
+      <CodeAppRenderer
+        {...this.props}
+        {...this.state}
+        handleGoToParent={this.handleGoToParent}
+        handleHighlight={this.handleHighlight}
+        handleIssueChange={this.handleIssueChange}
+        handleLoadMore={this.handleLoadMore}
+        handleSearchClear={this.handleSearchClear}
+        handleSearchResults={this.handleSearchResults}
+        handleSelect={this.handleSelect}
+        handleSelectNewCode={this.handleSelectNewCode}
+      />
     );
   }
 }
 
-const StyledAlert = styled(Alert)`
-  display: inline-flex;
-  margin-bottom: 15px;
-`;
-
-const AlertContent = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
 export default withRouter(
   withComponentContext(withBranchStatusActions(withMetricsContext(CodeApp)))
 );
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
new file mode 100644 (file)
index 0000000..919958d
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
+import classNames from 'classnames';
+import { LargeCenteredLayout } from 'design-system';
+import { intersection } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import ListFooter from '../../../components/controls/ListFooter';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { Location } from '../../../components/hoc/withRouter';
+import { Alert } from '../../../components/ui/Alert';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { isApplication, isPortfolioLike } from '../../../types/component';
+import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types';
+import '../code.css';
+import { getCodeMetrics } from '../utils';
+import Breadcrumbs from './Breadcrumbs';
+import Components from './Components';
+import Search from './Search';
+import SearchResults from './SearchResults';
+import SourceViewerWrapper from './SourceViewerWrapper';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+  location: Location;
+  metrics: Dict<Metric>;
+  baseComponent?: ComponentMeasure;
+  breadcrumbs: Breadcrumb[];
+  components?: ComponentMeasure[];
+  highlighted?: ComponentMeasure;
+  loading: boolean;
+  searchResults?: ComponentMeasure[];
+  sourceViewer?: ComponentMeasure;
+  total: number;
+  newCodeSelected: boolean;
+
+  handleGoToParent: () => void;
+  handleHighlight: (highlighted: ComponentMeasure) => void;
+  handleIssueChange: (issue: Issue) => void;
+  handleLoadMore: () => void;
+  handleSearchClear: () => void;
+  handleSearchResults: (searchResults: ComponentMeasure[]) => void;
+  handleSelect: (component: ComponentMeasure) => void;
+  handleSelectNewCode: (newCodeSelected: boolean) => void;
+}
+
+export default function CodeAppRenderer(props: Props) {
+  const {
+    branchLike,
+    component,
+    location,
+    baseComponent,
+    breadcrumbs,
+    components = [],
+    highlighted,
+    loading,
+    metrics,
+    newCodeSelected,
+    total,
+    searchResults,
+    sourceViewer,
+  } = props;
+  const { canBrowseAllChildProjects, qualifier } = component;
+
+  const showSearch = searchResults !== undefined;
+
+  const hasComponents = components.length > 0 || searchResults !== undefined;
+
+  const showBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
+
+  const showComponentList = sourceViewer === undefined && components.length > 0 && !showSearch;
+
+  const componentsClassName = classNames('boxed-group', 'spacer-top', {
+    'new-loading': loading,
+    'search-results': showSearch,
+  });
+
+  const metricKeys = intersection(
+    getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }),
+    Object.keys(metrics)
+  );
+  const filteredMetrics = metricKeys.map((metric) => metrics[metric]);
+
+  let defaultTitle = translate('code.page');
+  if (isApplication(baseComponent?.qualifier)) {
+    defaultTitle = translate('projects.page');
+  } else if (isPortfolioLike(baseComponent?.qualifier)) {
+    defaultTitle = translate('portfolio_breakdown.page');
+  }
+
+  const isPortfolio = isPortfolioLike(qualifier);
+
+  return (
+    <LargeCenteredLayout className="sw-py-8 sw-body-md">
+      <A11ySkipTarget anchor="code_main" />
+
+      {!canBrowseAllChildProjects && isPortfolio && (
+        <StyledAlert variant="warning" className="it__portfolio_warning">
+          <AlertContent>
+            {translate('code_viewer.not_all_measures_are_shown')}
+            <HelpTooltip
+              className="spacer-left"
+              overlay={translate('code_viewer.not_all_measures_are_shown.help')}
+            />
+          </AlertContent>
+        </StyledAlert>
+      )}
+
+      <Suggestions suggestions="code" />
+
+      <Helmet defer={false} title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} />
+
+      {hasComponents && (
+        <Search
+          branchLike={branchLike}
+          component={component}
+          newCodeSelected={newCodeSelected}
+          onNewCodeToggle={props.handleSelectNewCode}
+          onSearchClear={props.handleSearchClear}
+          onSearchResults={props.handleSearchResults}
+        />
+      )}
+
+      <div className="code-components">
+        {!hasComponents && sourceViewer === undefined && (
+          <div className="display-flex-center display-flex-column no-file">
+            <span className="h1 text-muted">
+              {translate(
+                'code_viewer.no_source_code_displayed_due_to_empty_analysis',
+                component.qualifier
+              )}
+            </span>
+          </div>
+        )}
+
+        {showBreadcrumbs && (
+          <Breadcrumbs
+            branchLike={branchLike}
+            breadcrumbs={breadcrumbs}
+            rootComponent={component}
+          />
+        )}
+
+        <div className={componentsClassName}>
+          {showComponentList && (
+            <Components
+              baseComponent={baseComponent}
+              branchLike={branchLike}
+              components={components}
+              cycle
+              metrics={filteredMetrics}
+              onEndOfList={props.handleLoadMore}
+              onGoToParent={props.handleGoToParent}
+              onHighlight={props.handleHighlight}
+              onSelect={props.handleSelect}
+              rootComponent={component}
+              selected={highlighted}
+              newCodeSelected={newCodeSelected}
+              showAnalysisDate={isPortfolio}
+            />
+          )}
+
+          {showSearch && (
+            <SearchResults
+              branchLike={branchLike}
+              components={searchResults}
+              onHighlight={props.handleHighlight}
+              onSelect={props.handleSelect}
+              rootComponent={component}
+              selected={highlighted}
+            />
+          )}
+
+          <div role="status" className={showSearch ? 'text-center big-padded-bottom' : undefined}>
+            {searchResults?.length === 0 && translate('no_results')}
+          </div>
+        </div>
+
+        {showComponentList && (
+          <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
+        )}
+
+        {sourceViewer !== undefined && !showSearch && (
+          <div className="spacer-top">
+            <SourceViewerWrapper
+              branchLike={branchLike}
+              component={sourceViewer.key}
+              componentMeasures={sourceViewer.measures}
+              isFile
+              location={location}
+              onGoToParent={props.handleGoToParent}
+              onIssueChange={props.handleIssueChange}
+            />
+          </div>
+        )}
+      </div>
+    </LargeCenteredLayout>
+  );
+}
+
+const StyledAlert = styled(Alert)`
+  display: inline-flex;
+  margin-bottom: 15px;
+`;
+
+const AlertContent = styled.div`
+  display: flex;
+  align-items: center;
+`;