]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15177 Improve code viewer when no component is analysis
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 10 Aug 2021 10:09:03 +0000 (12:09 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 Aug 2021 20:08:08 +0000 (20:08 +0000)
server/sonar-web/src/main/js/apps/code/code.css
server/sonar-web/src/main/js/apps/code/components/App.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/components/AppCode.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/routes.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 734b3128f5146ff419e2aa8904c0bd6bbc9997b5..39d4ff9255b087f87bbbe4178a437caf466eac2c 100644 (file)
@@ -99,3 +99,8 @@ table > thead > tr.code-components-header > th {
   height: 8px;
   width: 4px;
 }
+
+.code-components .no-file .h1 {
+  position: fixed;
+  top: 50%;
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx
deleted file mode 100644 (file)
index 6215ebc..0000000
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as classNames from 'classnames';
-import { Location } from 'history';
-import { debounce } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
-import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
-import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
-import { fetchBranchStatus, fetchMetrics } from '../../../store/rootActions';
-import { getMetrics } from '../../../store/rootReducer';
-import { BranchLike } from '../../../types/branch-like';
-import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
-import '../code.css';
-import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
-import Breadcrumbs from './Breadcrumbs';
-import Components from './Components';
-import Search from './Search';
-import SourceViewerWrapper from './SourceViewerWrapper';
-
-interface StateToProps {
-  metrics: T.Dict<T.Metric>;
-}
-
-interface DispatchToProps {
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
-  fetchMetrics: () => void;
-}
-
-interface OwnProps {
-  branchLike?: BranchLike;
-  component: T.Component;
-  location: Pick<Location, 'query'>;
-  router: Pick<InjectedRouter, 'push'>;
-}
-
-type Props = StateToProps & DispatchToProps & OwnProps;
-
-interface State {
-  baseComponent?: T.ComponentMeasure;
-  breadcrumbs: T.Breadcrumb[];
-  components?: T.ComponentMeasure[];
-  highlighted?: T.ComponentMeasure;
-  loading: boolean;
-  page: number;
-  searchResults?: T.ComponentMeasure[];
-  sourceViewer?: T.ComponentMeasure;
-  total: number;
-}
-
-export class App extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      breadcrumbs: [],
-      loading: true,
-      page: 0,
-      total: 0
-    };
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.props.fetchMetrics();
-    this.handleComponentChange();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (
-      prevProps.component !== this.props.component ||
-      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
-    ) {
-      this.handleComponentChange();
-    } else if (prevProps.location !== this.props.location) {
-      this.handleUpdate();
-    }
-  }
-
-  componentWillUnmount() {
-    clearBucket();
-    this.mounted = false;
-  }
-
-  loadComponent = (componentKey: string) => {
-    this.setState({ loading: true });
-    retrieveComponent(
-      componentKey,
-      this.props.component.qualifier,
-      this,
-      this.props.branchLike
-    ).then(r => {
-      if (this.mounted) {
-        if (['FIL', 'UTS'].includes(r.component.qualifier)) {
-          this.setState({
-            breadcrumbs: r.breadcrumbs,
-            components: r.components,
-            loading: false,
-            page: 0,
-            searchResults: undefined,
-            sourceViewer: r.component,
-            total: 0
-          });
-        } else {
-          this.setState({
-            baseComponent: r.component,
-            breadcrumbs: r.breadcrumbs,
-            components: r.components,
-            loading: false,
-            page: r.page,
-            searchResults: undefined,
-            sourceViewer: undefined,
-            total: r.total
-          });
-        }
-      }
-    }, this.stopLoading);
-  };
-
-  stopLoading = () => {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  };
-
-  handleComponentChange = () => {
-    const { branchLike, component } = this.props;
-
-    // we already know component's breadcrumbs,
-    addComponentBreadcrumbs(component.key, component.breadcrumbs);
-
-    this.setState({ loading: true });
-    retrieveComponentChildren(component.key, component.qualifier, this, branchLike).then(() => {
-      addComponent(component);
-      if (this.mounted) {
-        this.handleUpdate();
-      }
-    }, this.stopLoading);
-  };
-
-  handleLoadMore = () => {
-    const { baseComponent, components, page } = this.state;
-    if (!baseComponent || !components) {
-      return;
-    }
-    loadMoreChildren(
-      baseComponent.key,
-      page + 1,
-      this.props.component.qualifier,
-      this,
-      this.props.branchLike
-    ).then(r => {
-      if (this.mounted && r.components.length) {
-        this.setState({
-          components: [...components, ...r.components],
-          page: r.page,
-          total: r.total
-        });
-      }
-    }, this.stopLoading);
-  };
-
-  handleGoToParent = () => {
-    const { branchLike, component } = this.props;
-    const { breadcrumbs = [] } = this.state;
-
-    if (breadcrumbs.length > 1) {
-      const parentComponent = breadcrumbs[breadcrumbs.length - 2];
-      this.props.router.push(getCodeUrl(component.key, branchLike, parentComponent.key));
-      this.setState({ highlighted: breadcrumbs[breadcrumbs.length - 1] });
-    }
-  };
-
-  handleHighlight = (highlighted: T.ComponentMeasure) => {
-    this.setState({ highlighted });
-  };
-
-  handleIssueChange = (_: T.Issue) => {
-    this.refreshBranchStatus();
-  };
-
-  handleSearchClear = () => {
-    this.setState({ searchResults: undefined });
-  };
-
-  handleSearchResults = (searchResults: T.ComponentMeasure[] = []) => {
-    this.setState({ searchResults });
-  };
-
-  handleSelect = (component: T.ComponentMeasure) => {
-    const { branchLike, component: rootComponent } = this.props;
-
-    if (component.refKey) {
-      this.props.router.push(getProjectUrl(component.refKey));
-    } else {
-      this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key));
-    }
-
-    this.setState({ highlighted: undefined });
-  };
-
-  handleUpdate = () => {
-    const { component, location } = this.props;
-    const { selected } = location.query;
-    const finalKey = selected || component.key;
-
-    this.loadComponent(finalKey);
-  };
-
-  refreshBranchStatus = () => {
-    const { branchLike, component } = this.props;
-    if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
-    }
-  };
-
-  render() {
-    const { branchLike, component, location } = this.props;
-    const {
-      baseComponent,
-      breadcrumbs,
-      components = [],
-      highlighted,
-      loading,
-      total,
-      searchResults,
-      sourceViewer
-    } = this.state;
-
-    const showSearch = searchResults !== undefined;
-
-    const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
-    const shouldShowComponentList =
-      sourceViewer === undefined && components.length > 0 && !showSearch;
-
-    const componentsClassName = classNames('boxed-group', 'spacer-top', {
-      'new-loading': loading,
-      'search-results': showSearch
-    });
-
-    const defaultTitle =
-      baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier)
-        ? translate('projects.page')
-        : translate('code.page');
-
-    return (
-      <div className="page page-limited">
-        <Suggestions suggestions="code" />
-        <Helmet
-          defer={false}
-          title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
-        />
-        <A11ySkipTarget anchor="code_main" />
-
-        <Search
-          branchLike={branchLike}
-          component={component}
-          onSearchClear={this.handleSearchClear}
-          onSearchResults={this.handleSearchResults}
-        />
-
-        <div className="code-components">
-          {shouldShowBreadcrumbs && (
-            <Breadcrumbs
-              branchLike={branchLike}
-              breadcrumbs={breadcrumbs}
-              rootComponent={component}
-            />
-          )}
-
-          {shouldShowComponentList && (
-            <>
-              <div className={componentsClassName}>
-                <Components
-                  baseComponent={baseComponent}
-                  branchLike={branchLike}
-                  components={components}
-                  cycle={true}
-                  metrics={this.props.metrics}
-                  onEndOfList={this.handleLoadMore}
-                  onGoToParent={this.handleGoToParent}
-                  onHighlight={this.handleHighlight}
-                  onSelect={this.handleSelect}
-                  rootComponent={component}
-                  selected={highlighted}
-                />
-              </div>
-              <ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
-            </>
-          )}
-
-          {showSearch && searchResults && (
-            <div className={componentsClassName}>
-              <Components
-                branchLike={this.props.branchLike}
-                components={searchResults}
-                metrics={{}}
-                onHighlight={this.handleHighlight}
-                onSelect={this.handleSelect}
-                rootComponent={component}
-                selected={highlighted}
-              />
-            </div>
-          )}
-
-          {sourceViewer !== undefined && !showSearch && (
-            <div className="spacer-top">
-              <SourceViewerWrapper
-                branchLike={branchLike}
-                component={sourceViewer.key}
-                componentMeasures={sourceViewer.measures}
-                isFile={true}
-                location={location}
-                onGoToParent={this.handleGoToParent}
-                onIssueChange={this.handleIssueChange}
-              />
-            </div>
-          )}
-        </div>
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = (state: any): StateToProps => ({
-  metrics: getMetrics(state)
-});
-
-const mapDispatchToProps: DispatchToProps = {
-  fetchBranchStatus: fetchBranchStatus as any,
-  fetchMetrics
-};
-
-export default connect<StateToProps, DispatchToProps, Props>(
-  mapStateToProps,
-  mapDispatchToProps
-)(App);
diff --git a/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx b/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx
new file mode 100644 (file)
index 0000000..33d3f56
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { Location } from 'history';
+import { debounce } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { connect } from 'react-redux';
+import { InjectedRouter } from 'react-router';
+import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
+import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
+import { fetchBranchStatus, fetchMetrics } from '../../../store/rootActions';
+import { getMetrics } from '../../../store/rootReducer';
+import { BranchLike } from '../../../types/branch-like';
+import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
+import '../code.css';
+import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
+import Breadcrumbs from './Breadcrumbs';
+import Components from './Components';
+import Search from './Search';
+import SourceViewerWrapper from './SourceViewerWrapper';
+
+interface StateToProps {
+  metrics: T.Dict<T.Metric>;
+}
+
+interface DispatchToProps {
+  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
+  fetchMetrics: () => void;
+}
+
+interface OwnProps {
+  branchLike?: BranchLike;
+  component: T.Component;
+  location: Pick<Location, 'query'>;
+  router: Pick<InjectedRouter, 'push'>;
+}
+
+type Props = StateToProps & DispatchToProps & OwnProps;
+
+interface State {
+  baseComponent?: T.ComponentMeasure;
+  breadcrumbs: T.Breadcrumb[];
+  components?: T.ComponentMeasure[];
+  highlighted?: T.ComponentMeasure;
+  loading: boolean;
+  page: number;
+  searchResults?: T.ComponentMeasure[];
+  sourceViewer?: T.ComponentMeasure;
+  total: number;
+}
+
+export class AppCode extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      breadcrumbs: [],
+      loading: true,
+      page: 0,
+      total: 0
+    };
+    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.props.fetchMetrics();
+    this.handleComponentChange();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      prevProps.component !== this.props.component ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+    ) {
+      this.handleComponentChange();
+    } else if (prevProps.location !== this.props.location) {
+      this.handleUpdate();
+    }
+  }
+
+  componentWillUnmount() {
+    clearBucket();
+    this.mounted = false;
+  }
+
+  loadComponent = (componentKey: string) => {
+    this.setState({ loading: true });
+    retrieveComponent(
+      componentKey,
+      this.props.component.qualifier,
+      this,
+      this.props.branchLike
+    ).then(r => {
+      if (this.mounted) {
+        if (['FIL', 'UTS'].includes(r.component.qualifier)) {
+          this.setState({
+            breadcrumbs: r.breadcrumbs,
+            components: r.components,
+            loading: false,
+            page: 0,
+            searchResults: undefined,
+            sourceViewer: r.component,
+            total: 0
+          });
+        } else {
+          this.setState({
+            baseComponent: r.component,
+            breadcrumbs: r.breadcrumbs,
+            components: r.components,
+            loading: false,
+            page: r.page,
+            searchResults: undefined,
+            sourceViewer: undefined,
+            total: r.total
+          });
+        }
+      }
+    }, this.stopLoading);
+  };
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  handleComponentChange = () => {
+    const { branchLike, component } = this.props;
+
+    // we already know component's breadcrumbs,
+    addComponentBreadcrumbs(component.key, component.breadcrumbs);
+
+    this.setState({ loading: true });
+    retrieveComponentChildren(component.key, component.qualifier, this, branchLike).then(() => {
+      addComponent(component);
+      if (this.mounted) {
+        this.handleUpdate();
+      }
+    }, this.stopLoading);
+  };
+
+  handleLoadMore = () => {
+    const { baseComponent, components, page } = this.state;
+    if (!baseComponent || !components) {
+      return;
+    }
+    loadMoreChildren(
+      baseComponent.key,
+      page + 1,
+      this.props.component.qualifier,
+      this,
+      this.props.branchLike
+    ).then(r => {
+      if (this.mounted && r.components.length) {
+        this.setState({
+          components: [...components, ...r.components],
+          page: r.page,
+          total: r.total
+        });
+      }
+    }, this.stopLoading);
+  };
+
+  handleGoToParent = () => {
+    const { branchLike, component } = this.props;
+    const { breadcrumbs = [] } = this.state;
+
+    if (breadcrumbs.length > 1) {
+      const parentComponent = breadcrumbs[breadcrumbs.length - 2];
+      this.props.router.push(getCodeUrl(component.key, branchLike, parentComponent.key));
+      this.setState({ highlighted: breadcrumbs[breadcrumbs.length - 1] });
+    }
+  };
+
+  handleHighlight = (highlighted: T.ComponentMeasure) => {
+    this.setState({ highlighted });
+  };
+
+  handleIssueChange = (_: T.Issue) => {
+    this.refreshBranchStatus();
+  };
+
+  handleSearchClear = () => {
+    this.setState({ searchResults: undefined });
+  };
+
+  handleSearchResults = (searchResults: T.ComponentMeasure[] = []) => {
+    this.setState({ searchResults });
+  };
+
+  handleSelect = (component: T.ComponentMeasure) => {
+    const { branchLike, component: rootComponent } = this.props;
+
+    if (component.refKey) {
+      this.props.router.push(getProjectUrl(component.refKey));
+    } else {
+      this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key));
+    }
+
+    this.setState({ highlighted: undefined });
+  };
+
+  handleUpdate = () => {
+    const { component, location } = this.props;
+    const { selected } = location.query;
+    const finalKey = selected || component.key;
+
+    this.loadComponent(finalKey);
+  };
+
+  refreshBranchStatus = () => {
+    const { branchLike, component } = this.props;
+    if (branchLike && component && isPullRequest(branchLike)) {
+      this.props.fetchBranchStatus(branchLike, component.key);
+    }
+  };
+
+  render() {
+    const { branchLike, component, location } = this.props;
+    const {
+      baseComponent,
+      breadcrumbs,
+      components = [],
+      highlighted,
+      loading,
+      total,
+      searchResults,
+      sourceViewer
+    } = this.state;
+
+    const showSearch = searchResults !== undefined;
+
+    const hasNoFile = components.length === 0 && searchResults === undefined;
+
+    const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
+    const shouldShowComponentList =
+      sourceViewer === undefined && components.length > 0 && !showSearch;
+
+    const componentsClassName = classNames('boxed-group', 'spacer-top', {
+      'new-loading': loading,
+      'search-results': showSearch
+    });
+
+    const defaultTitle =
+      baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier)
+        ? translate('projects.page')
+        : translate('code.page');
+
+    return (
+      <div className="page page-limited">
+        <Suggestions suggestions="code" />
+        <Helmet
+          defer={false}
+          title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
+        />
+        <A11ySkipTarget anchor="code_main" />
+
+        {!hasNoFile && (
+          <Search
+            branchLike={branchLike}
+            component={component}
+            onSearchClear={this.handleSearchClear}
+            onSearchResults={this.handleSearchResults}
+          />
+        )}
+
+        <div className="code-components">
+          {hasNoFile && (
+            <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>
+          )}
+          {shouldShowBreadcrumbs && (
+            <Breadcrumbs
+              branchLike={branchLike}
+              breadcrumbs={breadcrumbs}
+              rootComponent={component}
+            />
+          )}
+
+          {shouldShowComponentList && (
+            <>
+              <div className={componentsClassName}>
+                <Components
+                  baseComponent={baseComponent}
+                  branchLike={branchLike}
+                  components={components}
+                  cycle={true}
+                  metrics={this.props.metrics}
+                  onEndOfList={this.handleLoadMore}
+                  onGoToParent={this.handleGoToParent}
+                  onHighlight={this.handleHighlight}
+                  onSelect={this.handleSelect}
+                  rootComponent={component}
+                  selected={highlighted}
+                />
+              </div>
+              <ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
+            </>
+          )}
+
+          {showSearch && searchResults && (
+            <div className={componentsClassName}>
+              <Components
+                branchLike={this.props.branchLike}
+                components={searchResults}
+                metrics={{}}
+                onHighlight={this.handleHighlight}
+                onSelect={this.handleSelect}
+                rootComponent={component}
+                selected={highlighted}
+              />
+            </div>
+          )}
+
+          {sourceViewer !== undefined && !showSearch && (
+            <div className="spacer-top">
+              <SourceViewerWrapper
+                branchLike={branchLike}
+                component={sourceViewer.key}
+                componentMeasures={sourceViewer.measures}
+                isFile={true}
+                location={location}
+                onGoToParent={this.handleGoToParent}
+                onIssueChange={this.handleIssueChange}
+              />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: any): StateToProps => ({
+  metrics: getMetrics(state)
+});
+
+const mapDispatchToProps: DispatchToProps = {
+  fetchBranchStatus: fetchBranchStatus as any,
+  fetchMetrics
+};
+
+export default connect<StateToProps, DispatchToProps, Props>(
+  mapStateToProps,
+  mapDispatchToProps
+)(AppCode);
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx
deleted file mode 100644 (file)
index bdf98a7..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
-import { mockIssue, mockRouter } from '../../../../helpers/testMocks';
-import { retrieveComponent } from '../../utils';
-import { App } from '../App';
-
-jest.mock('../../utils', () => ({
-  retrieveComponent: jest.fn().mockResolvedValue({
-    breadcrumbs: [],
-    component: { qualifier: 'APP' },
-    components: [],
-    page: 0,
-    total: 1
-  }),
-  retrieveComponentChildren: () => Promise.resolve()
-}));
-
-const METRICS = {
-  coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
-  new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
-};
-
-beforeEach(() => {
-  (retrieveComponent as jest.Mock<any>).mockClear();
-});
-
-it('should have correct title for APP based component', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('Helmet')).toMatchSnapshot();
-});
-
-it('should have correct title for portfolio base component', async () => {
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component: { qualifier: 'VW' },
-    components: [],
-    page: 0,
-    total: 1
-  });
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('Helmet')).toMatchSnapshot();
-});
-
-it('should have correct title for project component', async () => {
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component: { qualifier: 'TRK' },
-    components: [],
-    page: 0,
-    total: 1
-  });
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('Helmet')).toMatchSnapshot();
-});
-
-it('should refresh branch status if issues are updated', async () => {
-  const fetchBranchStatus = jest.fn();
-  const branchLike = mockPullRequest();
-  const wrapper = shallowRender({ branchLike, fetchBranchStatus });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  instance.handleIssueChange(mockIssue());
-  expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo');
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(
-    <App
-      component={{
-        breadcrumbs: [],
-        name: 'foo',
-        key: 'foo',
-        qualifier: 'FOO'
-      }}
-      fetchBranchStatus={jest.fn()}
-      fetchMetrics={jest.fn()}
-      location={{ query: { branch: 'b', id: 'foo', line: '7' } }}
-      metrics={METRICS}
-      router={mockRouter()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx
new file mode 100644 (file)
index 0000000..8db2522
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
+import {
+  mockComponent,
+  mockComponentMeasure,
+  mockIssue,
+  mockRouter
+} from '../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../types/component';
+import { loadMoreChildren, retrieveComponent } from '../../utils';
+import { AppCode } from '../AppCode';
+
+jest.mock('../../utils', () => ({
+  loadMoreChildren: jest.fn().mockResolvedValue({}),
+  retrieveComponent: jest.fn().mockResolvedValue({
+    breadcrumbs: [],
+    component: { qualifier: 'APP' },
+    components: [],
+    page: 0,
+    total: 1
+  }),
+  retrieveComponentChildren: () => Promise.resolve()
+}));
+
+const METRICS = {
+  coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
+  new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
+};
+
+beforeEach(() => {
+  (retrieveComponent as jest.Mock<any>).mockClear();
+});
+
+it.each([
+  [ComponentQualifier.Application],
+  [ComponentQualifier.Project],
+  [ComponentQualifier.Portfolio],
+  [ComponentQualifier.SubPortfolio]
+])('should render correclty when no sub component for %s', async qualifier => {
+  const component = { breadcrumbs: [], name: 'foo', key: 'foo', qualifier };
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component,
+    components: [],
+    page: 0,
+    total: 1
+  });
+  const wrapper = shallowRender({ component });
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.instance().handleSearchResults([]);
+  expect(wrapper).toMatchSnapshot('no search');
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component,
+    components: [mockComponent({ qualifier: ComponentQualifier.File })],
+    page: 0,
+    total: 1
+  });
+  wrapper.instance().loadComponent(component.key);
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot('with sub component');
+});
+
+it('should refresh branch status if issues are updated', async () => {
+  const fetchBranchStatus = jest.fn();
+  const branchLike = mockPullRequest();
+  const wrapper = shallowRender({ branchLike, fetchBranchStatus });
+  const instance = wrapper.instance();
+  await waitAndUpdate(wrapper);
+
+  instance.handleIssueChange(mockIssue());
+  expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo');
+});
+
+it('should load more behave correctly', async () => {
+  const component1 = mockComponent();
+  const component2 = mockComponent();
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component: mockComponent(),
+    components: [component1],
+    page: 0,
+    total: 1
+  });
+  let wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  (loadMoreChildren as jest.Mock<any>).mockResolvedValueOnce({
+    components: [component2],
+    page: 0,
+    total: 1
+  });
+
+  wrapper.instance().handleLoadMore();
+  expect(wrapper.state().components).toContainEqual(component1);
+  expect(wrapper.state().components).toContainEqual(component2);
+
+  (retrieveComponent as jest.Mock<any>).mockRejectedValueOnce({});
+  wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleLoadMore();
+  expect(wrapper.state().components).toBeUndefined();
+});
+
+it('should handle go to parent correctly', async () => {
+  const router = mockRouter();
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component: mockComponent(),
+    components: [],
+    page: 0,
+    total: 1
+  });
+  let wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleGoToParent();
+  expect(wrapper.state().highlighted).toBeUndefined();
+
+  const breadcrumb = { key: 'key2', name: 'name2', qualifier: ComponentQualifier.Directory };
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [
+      { key: 'key1', name: 'name1', qualifier: ComponentQualifier.Directory },
+      breadcrumb
+    ],
+    component: mockComponent(),
+    components: [],
+    page: 0,
+    total: 1
+  });
+  wrapper = shallowRender({ router });
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleGoToParent();
+  expect(wrapper.state().highlighted).toBe(breadcrumb);
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/code',
+    query: { id: 'foo', line: undefined, selected: 'key1' }
+  });
+});
+
+it('should handle select correctly', () => {
+  const router = mockRouter();
+  const wrapper = shallowRender({ router });
+  wrapper.setState({ highlighted: mockComponentMeasure() });
+
+  wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' }));
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/dashboard',
+    query: { branch: undefined, id: 'test' }
+  });
+  expect(wrapper.state().highlighted).toBeUndefined();
+
+  wrapper.instance().handleSelect(mockComponentMeasure());
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/dashboard',
+    query: { branch: undefined, id: 'test' }
+  });
+});
+
+function shallowRender(props: Partial<AppCode['props']> = {}) {
+  return shallow<AppCode>(
+    <AppCode
+      component={{
+        breadcrumbs: [],
+        name: 'foo',
+        key: 'foo',
+        qualifier: 'FOO'
+      }}
+      fetchBranchStatus={jest.fn()}
+      fetchMetrics={jest.fn()}
+      location={{ query: { branch: 'b', id: 'foo', line: '7' } }}
+      metrics={METRICS}
+      router={mockRouter()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap
deleted file mode 100644 (file)
index 7d2227b..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should have correct title for APP based component 1`] = `
-<Helmet
-  defer={false}
-  encodeSpecialCharacters={true}
-  title="projects.page"
-/>
-`;
-
-exports[`should have correct title for portfolio base component 1`] = `
-<Helmet
-  defer={false}
-  encodeSpecialCharacters={true}
-  title="projects.page"
-/>
-`;
-
-exports[`should have correct title for project component 1`] = `
-<Helmet
-  defer={false}
-  encodeSpecialCharacters={true}
-  title="code.page"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap
new file mode 100644 (file)
index 0000000..233b60a
--- /dev/null
@@ -0,0 +1,765 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correclty when no sub component for APP 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.APP
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for APP: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "APP",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for APP: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "APP",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "SVW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "SVW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "TRK",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "TRK",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.VW
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "VW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "VW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
index 854cd1ef1209fa1e3463be72d5a7c2965f018e75..14681d9db0c0ccdcc4a89e18d8af6151a107e30b 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'
 
 const routes = [
   {
-    indexRoute: { component: lazyLoadComponent(() => import('./components/App')) }
+    indexRoute: { component: lazyLoadComponent(() => import('./components/AppCode')) }
   }
 ];
 
index f9ea058a2a3c1a50ee73e76bf0b2a4e30b293f1b..ae92f530541cf9d7367b2100dce9bf69171fdf13 100644 (file)
@@ -1401,6 +1401,10 @@ duplications.dups_found_on_deleted_resource=This file contains duplicated blocks
 # GENERIC CODE VIEWER
 #
 #------------------------------------------------------------------------------
+code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK=No code files were found for analysis.
+code_viewer.no_source_code_displayed_due_to_empty_analysis.APP=No projects in this application.
+code_viewer.no_source_code_displayed_due_to_empty_analysis.VW=No projects, applications, or portfolios in this portfolio.
+code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW=No projects, applications, or portfolios in this portfolio.
 code_viewer.no_source_code_displayed_due_to_security=Due to security settings, no source code can be displayed.
 code_viewer.no_source_code_displayed_due_to_source_removed=The file was removed, no source code can be displayed.