]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10677 Reduce amount of DOM nodes on Projects page (#456)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 29 Jun 2018 15:10:48 +0000 (17:10 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 29 Jun 2018 18:21:33 +0000 (20:21 +0200)
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap

index 05d94b9e78734d8d2028933811c17fd53d66721b..d572edb0d5b2c61fabc28844dbc75409c7d2b264 100644 (file)
@@ -23,6 +23,7 @@ import ProjectCardOverall from './ProjectCardOverall';
 import { Project } from '../types';
 
 interface Props {
+  height: number;
   organization?: { key: string };
   project: Project;
   type?: string;
index c0328c25e07dfab1b43e1047fb7d57a1ca25a165..b6a3c63c2b45f0e162076767402e7e81fec0408c 100644 (file)
@@ -31,18 +31,19 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Project } from '../types';
 
 interface Props {
+  height: number;
   organization?: { key: string };
   project: Project;
 }
 
-export default function ProjectCardLeak({ organization, project }: Props) {
+export default function ProjectCardLeak({ height, organization, project }: Props) {
   const { measures } = project;
 
   const isPrivate = project.visibility === 'private';
   const hasTags = project.tags.length > 0;
 
   return (
-    <div className="boxed-group project-card" data-key={project.key}>
+    <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
       <div className="boxed-group-header clearfix">
         <div className="project-card-header">
           {project.isFavorite != null && (
index 72d44b470f7f9c17803176d2e066691e85168914..f653d14975ddd6611051116a03411bf30ec07f9b 100644 (file)
@@ -30,18 +30,19 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Project } from '../types';
 
 interface Props {
+  height: number;
   organization?: { key: string };
   project: Project;
 }
 
-export default function ProjectCardOverall({ organization, project }: Props) {
+export default function ProjectCardOverall({ height, organization, project }: Props) {
   const { measures } = project;
 
   const isPrivate = project.visibility === 'private';
   const hasTags = project.tags.length > 0;
 
   return (
-    <div className="boxed-group project-card" data-key={project.key}>
+    <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
       <div className="boxed-group-header clearfix">
         <div className="project-card-header">
           {project.isFavorite !== undefined && (
index 491da7f539b24a19068b35073ad2f9dbfe572063..a019fca6cbf69c063c7c1fdbedb759eed3376c8e 100644 (file)
@@ -18,6 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import { List, ListRowProps } from 'react-virtualized/dist/commonjs/List';
+import { WindowScroller } from 'react-virtualized/dist/commonjs/WindowScroller';
 import ProjectCard from './ProjectCard';
 import NoFavoriteProjects from './NoFavoriteProjects';
 import EmptyInstance from './EmptyInstance';
@@ -36,6 +39,10 @@ interface Props {
 }
 
 export default class ProjectsList extends React.PureComponent<Props> {
+  getCardHeight = () => {
+    return this.props.cardType === 'leak' ? 159 : 143;
+  };
+
   renderNoProjects() {
     const { isFavorite, isFiltered, query } = this.props;
     if (isFiltered) {
@@ -44,21 +51,57 @@ export default class ProjectsList extends React.PureComponent<Props> {
     return isFavorite ? <NoFavoriteProjects /> : <EmptyInstance />;
   }
 
+  renderRow = ({ index, key, style }: ListRowProps) => {
+    const project = this.props.projects[index];
+    const height = this.getCardHeight();
+    return (
+      <div key={key} style={{ ...style, height }}>
+        <ProjectCard
+          height={height}
+          key={project.key}
+          organization={this.props.organization}
+          project={project}
+          type={this.props.cardType}
+        />
+      </div>
+    );
+  };
+
+  renderList() {
+    const cardHeight = this.getCardHeight();
+    return (
+      <WindowScroller>
+        {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => (
+          <AutoSizer disableHeight={true}>
+            {({ width }) => (
+              <div ref={registerChild as any}>
+                <List
+                  autoHeight={true}
+                  height={height}
+                  isScrolling={isScrolling}
+                  onScroll={onChildScroll}
+                  overscanRowCount={2}
+                  rowCount={this.props.projects.length}
+                  rowHeight={cardHeight + 20}
+                  rowRenderer={this.renderRow}
+                  scrollTop={scrollTop}
+                  style={{ outline: 'none' }}
+                  width={width}
+                />
+              </div>
+            )}
+          </AutoSizer>
+        )}
+      </WindowScroller>
+    );
+  }
+
   render() {
     const { projects } = this.props;
 
     return (
       <div className="projects-list">
-        {projects.length > 0
-          ? projects.map(project => (
-              <ProjectCard
-                key={project.key}
-                organization={this.props.organization}
-                project={project}
-                type={this.props.cardType}
-              />
-            ))
-          : this.renderNoProjects()}
+        {projects.length > 0 ? this.renderList() : this.renderNoProjects()}
       </div>
     );
   }
index 70bf9401111b5530f699f9a040d68909feabdb6c..3a25da57bf3b6770a09e1ecd9b78a952a567e12f 100644 (file)
@@ -40,7 +40,7 @@ const PROJECT = {
 };
 
 it('should display analysis date and leak start date', () => {
-  const card = shallow(<ProjectCardLeak project={PROJECT} />);
+  const card = shallow(<ProjectCardLeak height={100} project={PROJECT} />);
   expect(card.find('.project-card-dates').exists()).toBeTruthy();
   expect(card.find('.project-card-dates').find('DateFromNow')).toHaveLength(1);
   expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1);
@@ -48,14 +48,14 @@ it('should display analysis date and leak start date', () => {
 
 it('should not display analysis date or leak start date', () => {
   const project = { ...PROJECT, analysisDate: undefined };
-  const card = shallow(<ProjectCardLeak project={project} />);
+  const card = shallow(<ProjectCardLeak height={100} project={project} />);
   expect(card.find('.project-card-dates').exists()).toBeFalsy();
 });
 
 it('should display tags', () => {
   const project = { ...PROJECT, tags: ['foo', 'bar'] };
   expect(
-    shallow(<ProjectCardLeak project={project} />)
+    shallow(<ProjectCardLeak height={100} project={project} />)
       .find('TagsList')
       .exists()
   ).toBeTruthy();
@@ -64,12 +64,12 @@ it('should display tags', () => {
 it('should private badge', () => {
   const project = { ...PROJECT, visibility: 'private' };
   expect(
-    shallow(<ProjectCardLeak project={project} />)
+    shallow(<ProjectCardLeak height={100} project={project} />)
       .find('PrivateBadge')
       .exists()
   ).toBeTruthy();
 });
 
 it('should display the leak measures and quality gate', () => {
-  expect(shallow(<ProjectCardLeak project={PROJECT} />)).toMatchSnapshot();
+  expect(shallow(<ProjectCardLeak height={100} project={PROJECT} />)).toMatchSnapshot();
 });
index 10dc9eaa73976a33a144dd3c98466a736a4f86d0..fc3e58bbd80c2518c92ad9a7c27366fe077ff5d4 100644 (file)
@@ -40,12 +40,12 @@ const PROJECT = {
 
 it('should display analysis date (and not leak period) when defined', () => {
   expect(
-    shallow(<ProjectCardOverall project={PROJECT} />)
+    shallow(<ProjectCardOverall height={100} project={PROJECT} />)
       .find('.project-card-dates')
       .exists()
   ).toBeTruthy();
   expect(
-    shallow(<ProjectCardOverall project={{ ...PROJECT, analysisDate: undefined }} />)
+    shallow(<ProjectCardOverall height={100} project={{ ...PROJECT, analysisDate: undefined }} />)
       .find('.project-card-dates')
       .exists()
   ).toBeFalsy();
@@ -54,7 +54,7 @@ it('should display analysis date (and not leak period) when defined', () => {
 it('should not display the quality gate', () => {
   const project = { ...PROJECT, analysisDate: undefined };
   expect(
-    shallow(<ProjectCardOverall project={project} />)
+    shallow(<ProjectCardOverall height={100} project={project} />)
       .find('ProjectCardOverallQualityGate')
       .exists()
   ).toBeFalsy();
@@ -63,7 +63,7 @@ it('should not display the quality gate', () => {
 it('should display tags', () => {
   const project = { ...PROJECT, tags: ['foo', 'bar'] };
   expect(
-    shallow(<ProjectCardOverall project={project} />)
+    shallow(<ProjectCardOverall height={100} project={project} />)
       .find('TagsList')
       .exists()
   ).toBeTruthy();
@@ -72,12 +72,12 @@ it('should display tags', () => {
 it('should private badge', () => {
   const project = { ...PROJECT, visibility: 'private' };
   expect(
-    shallow(<ProjectCardOverall project={project} />)
+    shallow(<ProjectCardOverall height={100} project={project} />)
       .find('PrivateBadge')
       .exists()
   ).toBeTruthy();
 });
 
 it('should display the overall measures and quality gate', () => {
-  expect(shallow(<ProjectCardOverall project={PROJECT} />)).toMatchSnapshot();
+  expect(shallow(<ProjectCardOverall height={100} project={PROJECT} />)).toMatchSnapshot();
 });
index deca57a4c532f788da12f7ed596f6837f8fa3c8f..fa106ed2ac6daeb4f4d13898a1c8a4633442a219 100644 (file)
@@ -4,6 +4,11 @@ exports[`should display the leak measures and quality gate 1`] = `
 <div
   className="boxed-group project-card"
   data-key="foo"
+  style={
+    Object {
+      "height": 100,
+    }
+  }
 >
   <div
     className="boxed-group-header clearfix"
index a6697e18b2360044609896754426f000d80dbb6e..263d0bf5fd05eaf0f9a10f2e30be0635a247d256 100644 (file)
@@ -4,6 +4,11 @@ exports[`should display the overall measures and quality gate 1`] = `
 <div
   className="boxed-group project-card"
   data-key="foo"
+  style={
+    Object {
+      "height": 100,
+    }
+  }
 >
   <div
     className="boxed-group-header clearfix"
index b2552033befc3e8b85723f675eb99b04a92deb83..7dcb16317c27f739056681e5f7ca73c2db6344b3 100644 (file)
@@ -4,25 +4,13 @@ exports[`renders 1`] = `
 <div
   className="projects-list"
 >
-  <ProjectCard
-    key="foo"
-    project={
-      Object {
-        "key": "foo",
-        "name": "Foo",
-      }
-    }
-    type="overall"
-  />
-  <ProjectCard
-    key="bar"
-    project={
-      Object {
-        "key": "bar",
-        "name": "Bar",
-      }
-    }
-    type="overall"
+  <WindowScroller
+    onResize={[Function]}
+    onScroll={[Function]}
+    scrollElement={[Window]}
+    scrollingResetTimeInterval={150}
+    serverHeight={0}
+    serverWidth={0}
   />
 </div>
 `;