Browse Source

Committing everything before presentation

Tyler Hallada 7 years ago
commit
1072771932
9 changed files with 756 additions and 0 deletions
  1. 13 0
      .babelrc
  2. 15 0
      components/contentbox.css
  3. 16 0
      components/contentbox.js
  4. 492 0
      components/table.js
  5. 36 0
      next.config.js
  6. 32 0
      package.json
  7. 106 0
      pages/index.js
  8. 6 0
      postcss.config.js
  9. 40 0
      static/table.css

+ 13 - 0
.babelrc

@@ -0,0 +1,13 @@
1
+{
2
+  "plugins": [
3
+    [
4
+      "wrap-in-js",
5
+      {
6
+        "extensions": ["css$", "scss$"]
7
+      }
8
+    ]
9
+  ],
10
+  "presets": [
11
+    "next/babel"
12
+  ]
13
+}

+ 15 - 0
components/contentbox.css

@@ -0,0 +1,15 @@
1
+.ContentBox {
2
+  flex: 1 0 auto;
3
+  display: flex;
4
+  flex-direction: column;
5
+  background-color: #FFF;
6
+  padding: 0 1rem 1rem 1rem;
7
+  overflow: auto;
8
+  background: white;
9
+}
10
+.ContentBox:first-of-type {
11
+  padding-top: 1rem;
12
+}
13
+.AutoSizerWrapper {
14
+  flex: 1 1 auto;
15
+}

+ 16 - 0
components/contentbox.js

@@ -0,0 +1,16 @@
1
+import React from 'react';
2
+import styles from './contentbox.css';
3
+
4
+export function ContentBox ({ className, children, style }) {
5
+  return (
6
+    <div>
7
+      <style dangerouslySetInnerHTML={{ __html: styles }} />
8
+      <div
9
+        className={'ContentBox'}
10
+        style={style}
11
+      >
12
+        {children}
13
+      </div>
14
+    </div>
15
+  );
16
+}

+ 492 - 0
components/table.js

@@ -0,0 +1,492 @@
1
+import Immutable from 'immutable'
2
+import PropTypes from 'prop-types'
3
+import React, { PureComponent } from 'react';
4
+import ReactDOM from 'react-dom';
5
+import { AutoSizer, Column, Table, SortDirection, SortIndicator } from 'react-virtualized';
6
+import styles from 'react-virtualized/styles.css'; // only needs to be imported once
7
+import tableStyles from '../static/table.css';
8
+import { ContentBox } from './contentbox.js';
9
+import moment from 'moment';
10
+
11
+export default class CoursesTable extends PureComponent {
12
+  // static contextTypes = {
13
+    // list: PropTypes.instanceOf(Immutable.List).isRequired
14
+  // };
15
+
16
+  constructor (props, context) {
17
+    super(props, context);
18
+
19
+    this.state = {
20
+      headerHeight: 30,
21
+      height: 500,
22
+      overscanRowCount: 10,
23
+      rowHeight: 50,
24
+      // sort
25
+      sortBy: 'catalog_course_title',
26
+      sortDirection: SortDirection.ASC,
27
+      // filters, shouldComponentUpdate only does a shallowEqual, so I can't nest these
28
+      availability: Immutable.Map({
29
+        Archived: false,
30
+        Current: false,
31
+        Upcoming: false,
32
+        Unknown: false
33
+      }),
34
+      pacing_type: Immutable.Map({
35
+        instructor_paced: false,
36
+        self_paced: false
37
+      }),
38
+      search: ''
39
+    };
40
+
41
+    this._getRowHeight = this._getRowHeight.bind(this);
42
+    this._noRowsRenderer = this._noRowsRenderer.bind(this);
43
+    this._rowClassName = this._rowClassName.bind(this);
44
+    this._sort = this._sort.bind(this);
45
+    this._onFilterCheck = this._onFilterCheck.bind(this);
46
+    this._onSearchChange = this._onSearchChange.bind(this);
47
+    this._removeFilter = this._removeFilter.bind(this);
48
+  }
49
+
50
+  render () {
51
+    const {
52
+      headerHeight,
53
+      height,
54
+      overscanRowCount,
55
+      rowHeight,
56
+      sortBy,
57
+      sortDirection,
58
+      availability,
59
+      pacing_type,
60
+      search
61
+    } = this.state;
62
+
63
+    const list = Immutable.List(this.props.list);
64
+    let activatedAvailability = [],
65
+        activatedPacing = [];
66
+
67
+    // filter on availability
68
+    availability.mapEntries(entry => {
69
+      if (entry[1]) {
70
+        activatedAvailability.push(entry[0]);
71
+      }
72
+    });
73
+    let filteredList = Immutable.List(list);
74
+    if (activatedAvailability.length > 0) {
75
+      filteredList = list.filter(elem => {
76
+        if (activatedAvailability.indexOf(elem.availability) === -1) {
77
+          return false;
78
+        }
79
+        return true;
80
+      });
81
+    }
82
+
83
+    // filter on pacing_type
84
+    activatedPacing = [];
85
+    pacing_type.mapEntries(entry => {
86
+      if (entry[1]) {
87
+        activatedPacing.push(entry[0]);
88
+      }
89
+    });
90
+    if (activatedPacing.length > 0) {
91
+      filteredList = filteredList.filter(elem => {
92
+        if (activatedPacing.indexOf(elem.pacing_type) === -1) {
93
+          return false;
94
+        }
95
+        return true;
96
+      });
97
+    }
98
+
99
+    // filter on search
100
+    if (search !== '') {
101
+      filteredList = filteredList.filter(elem => {
102
+        if ((elem.catalog_course !== null &&
103
+             elem.catalog_course.toLowerCase().includes(search.toLowerCase())) ||
104
+            (elem.catalog_course_title !== null &&
105
+             elem.catalog_course_title.toLowerCase().includes(search.toLowerCase()))) {
106
+          return true;
107
+        }
108
+        return false;
109
+      });
110
+    }
111
+
112
+    // sort
113
+    const sortedList = filteredList
114
+      .sortBy(item => item[sortBy])
115
+      .update(filteredList =>
116
+        sortDirection === SortDirection.DESC
117
+          ? filteredList.reverse()
118
+          : filteredList
119
+      );
120
+    
121
+    // calculate summaries
122
+    const totalEmtSum = sortedList.reduce((sum, elem) => {
123
+      return sum + elem.cumulative_count;
124
+    }, 0).toLocaleString();
125
+    const currEmtSum = sortedList.reduce((sum, elem) => {
126
+      return sum + elem.count;
127
+    }, 0).toLocaleString();
128
+    const changeEmtSum = sortedList.reduce((sum, elem) => {
129
+      return sum + elem.count_change_7_days;
130
+    }, 0).toLocaleString();
131
+    const verifiedEmtSum = sortedList.reduce((sum, elem) => {
132
+      return sum + elem.enrollment_modes.verified.count;
133
+    }, 0).toLocaleString();
134
+
135
+    const rowCount = sortedList.size;
136
+    const rowGetter = ({ index }) => this._getDatum(sortedList, index);
137
+    const source = (typeof window !== 'undefined' && window.document && window.document.createElement)
138
+                   ? 'client' : 'server';
139
+    console.log('RENDER ' + source + ' ' + rowCount);
140
+
141
+    return (
142
+      <div>
143
+        <h2>Across all your courses</h2>
144
+        <hr style={{
145
+          marginBottom: '10px'
146
+        }} />
147
+        <div style={{
148
+          display: 'flex',
149
+          flexDirection: 'row',
150
+          height: '100px',
151
+          justifyContent: 'space-around',
152
+          marginBottom: '110px',
153
+        }}>
154
+          <style jsx>{`
155
+            .card {
156
+              flex-grow: 0.75;
157
+              min-width: 100px;
158
+              height: 80px;
159
+              margin: 30px;
160
+              background-color: white;
161
+              box-shadow: 1px 1px 7px #888888;
162
+              text-align: center;
163
+              padding-left: 10px;
164
+              padding-right: 10px;
165
+              padding-top: 30px;
166
+              padding-bottom: 30px;
167
+            }
168
+            .card .number {
169
+              display: block;
170
+              font-size: 30px;
171
+              font-weight: 500;
172
+              line-height: 33px;
173
+            }
174
+            .card .number-desc {
175
+              display: block;
176
+              font-size: 18px;
177
+              font-weight: 500;
178
+              line-height: 19.8px;
179
+            }
180
+          `}</style>
181
+          <div className="card card1">
182
+            <span className="number">{totalEmtSum}</span><br />
183
+            <span className="number-desc">Total Enrollment</span>
184
+          </div>
185
+          <div className="card card2">
186
+            <span className="number">{currEmtSum}</span><br />
187
+            <span className="number-desc">Current Enrollment</span>
188
+          </div>
189
+          <div className="card card3">
190
+            <span className="number">{changeEmtSum}</span><br />
191
+            <span className="number-desc">Change Last 7 Days</span>
192
+          </div>
193
+          <div className="card card4">
194
+            <span className="number">{verifiedEmtSum}</span><br />
195
+            <span className="number-desc">Verified Enrollment</span>
196
+          </div>
197
+        </div>
198
+        <div>
199
+          <h2>Course List</h2>
200
+          <hr style={{
201
+            marginBottom: '20px'
202
+          }}/>
203
+          <style jsx>{`
204
+            form.controls {
205
+              float: left;
206
+              min-width: 125px;
207
+              max-width: 180px;
208
+            }
209
+            div.ContentBox {
210
+              min-width: 300px;
211
+            }
212
+            .active-filters {
213
+              display: block;
214
+              height: 25px;
215
+              margin-left: 175px;
216
+              padding-bottom: 10px;
217
+            }
218
+            .active-filter {
219
+              background-color: white;
220
+              padding: 5px;
221
+              margin-right: 10px;
222
+              box-shadow: 1px 1px 3px #888888;
223
+            }
224
+            button {
225
+              border: none;
226
+            }
227
+          `}</style>
228
+          <div className="active-filters">
229
+            {search !== '' || activatedPacing.length > 0 || activatedAvailability.length > 0 ?
230
+              <div>
231
+                <span>Active Filters: </span>
232
+                {search !== '' ?
233
+                  <button className="active-filter" name="search" onClick={this._removeFilter}>
234
+                    Search: "{search}" ✕
235
+                  </button> : ''}
236
+                {activatedAvailability.length > 0 ?
237
+                  <button className="active-filter" name="availability" onClick={this._removeFilter}>
238
+                    Availability: "{activatedAvailability.join(', ')}" ✕
239
+                  </button> : ''}
240
+                {activatedPacing.length > 0 ?
241
+                  <button className="active-filter" name="pacing_type" onClick={this._removeFilter}>
242
+                    Pacing: "{activatedPacing.join(', ')}" ✕
243
+                  </button> : ''}
244
+              </div>
245
+            : ''}
246
+          </div>
247
+          <form className="controls">
248
+            <h3 style={{
249
+              marginTop: '1rem',
250
+              marginBottom: '0.5rem'
251
+            }}>Search:</h3>
252
+            <label>
253
+              <input id="search" type="text"
254
+                onInput={this._onSearchChange}
255
+                style={{
256
+                  marginRight: '20px',
257
+                  width: '150px'
258
+                }}
259
+                value={search}
260
+              />
261
+            </label><br />
262
+            <h3 style={{
263
+              marginTop: '1rem',
264
+              marginBottom: '0.5rem'
265
+            }}>Availability</h3>
266
+            <label>
267
+              <input className="availability" id="Archived" value="Archived" type="checkbox"
268
+                checked={availability.get('Archived')}
269
+                onChange={this._onFilterCheck}
270
+              />
271
+              Archived
272
+            </label><br />
273
+            <label>
274
+              <input className="availability" id="Current" value="Current" type="checkbox"
275
+                checked={availability.get('Current')}
276
+                onChange={this._onFilterCheck}
277
+              />
278
+              Current
279
+            </label><br />
280
+            <label>
281
+              <input className="availability" id="unknown" value="unknown" type="checkbox"
282
+                checked={availability.get('unknown')}
283
+                onChange={this._onFilterCheck}
284
+              />
285
+              Unknown
286
+            </label><br />
287
+            <label>
288
+              <input className="availability" id="Upcoming" value="Upcoming" type="checkbox"
289
+                checked={availability.get('Upcoming')}
290
+                onChange={this._onFilterCheck}
291
+              />
292
+              Upcoming
293
+            </label><br />
294
+            <h3 style={{
295
+              marginTop: '1rem',
296
+              marginBottom: '0.5rem'
297
+            }}>Pacing Type</h3>
298
+            <label>
299
+              <input className="pacing_type" id="instructor_paced" value="instructor_paced" type="checkbox"
300
+                checked={pacing_type.get('instructor_paced')}
301
+                onChange={this._onFilterCheck}
302
+              />
303
+              Instructor-Paced
304
+            </label><br />
305
+            <label>
306
+              <input className="pacing_type" id="self_paced" value="self_paced" type="checkbox"
307
+                checked={pacing_type.get('self_paced')}
308
+                onChange={this._onFilterCheck}
309
+              />
310
+              Self-Paced
311
+            </label><br />
312
+          </form>
313
+          <ContentBox
314
+            style={{
315
+              marginLeft: '20px',
316
+              marginRight: '20px',
317
+              boxShadow: '1px 1px 7px #888888'
318
+            }}
319
+          >
320
+            <style dangerouslySetInnerHTML={{ __html: styles }} />
321
+            <style dangerouslySetInnerHTML={{ __html: tableStyles }} />
322
+            <AutoSizer disableHeight>
323
+              {({ width }) => (
324
+                <Table
325
+                  ref='Table'
326
+                  headerClassName='headerColumn'
327
+                  headerHeight={headerHeight}
328
+                  height={height}
329
+                  noRowsRenderer={this._noRowsRenderer}
330
+                  overscanRowCount={overscanRowCount}
331
+                  rowClassName={this._rowClassName}
332
+                  rowHeight={rowHeight}
333
+                  rowGetter={rowGetter}
334
+                  rowCount={rowCount}
335
+                  sort={this._sort}
336
+                  sortBy={sortBy}
337
+                  sortDirection={sortDirection}
338
+                  width={width}
339
+                >
340
+                  <Column
341
+                    label='Course Name'
342
+                    dataKey='catalog_course_title'
343
+                    cellRenderer={this._courseNameRenderer}
344
+                    width={200}
345
+                    flexGrow={1}
346
+                  />
347
+                  <Column
348
+                    label='Start Date'
349
+                    dataKey='start_date'
350
+                    cellRenderer={this._dateRenderer}
351
+                    width={100}
352
+                  />
353
+                  <Column
354
+                    label='End Date'
355
+                    dataKey='end_date'
356
+                    cellRenderer={this._dateRenderer}
357
+                    width={100}
358
+                  />
359
+                  <Column
360
+                    label='Total'
361
+                    dataKey='cumulative_count'
362
+                    cellRenderer={this._numberRenderer}
363
+                    width={100}
364
+                  />
365
+                  <Column
366
+                    label='Current'
367
+                    dataKey='count'
368
+                    cellRenderer={this._numberRenderer}
369
+                    width={100}
370
+                  />
371
+                  <Column
372
+                    label='Last 7 Days'
373
+                    dataKey='count_change_7_days'
374
+                    cellRenderer={this._numberRenderer}
375
+                    width={100}
376
+                  />
377
+                </Table>
378
+              )}
379
+            </AutoSizer>
380
+          </ContentBox>
381
+        </div>
382
+      </div>
383
+    );
384
+  }
385
+
386
+  _getDatum (list, index) {
387
+    return list.get(index);
388
+  }
389
+
390
+  _getRowHeight ({ index }) {
391
+    const list = Immutable.List(this.props.list);
392
+
393
+    return this._getDatum(list, index).size;
394
+  }
395
+
396
+  _noRowsRenderer () {
397
+    return (
398
+      <div className='noRows'>
399
+        No rows
400
+      </div>
401
+    );
402
+  }
403
+
404
+  _rowClassName ({ index }) {
405
+    if (index < 0) {
406
+      return 'headerRow';
407
+    } else {
408
+      return index % 2 === 0 ? 'evenRow' : 'oddRow';
409
+    }
410
+  }
411
+
412
+  _sort ({ sortBy, sortDirection }) {
413
+    this.setState({ sortBy, sortDirection });
414
+  }
415
+
416
+  _courseNameRenderer ({cellData, columnData, dataKey, isScrolling, rowData, rowIndex}) {
417
+    if (rowData['catalog_course'] === null) {
418
+      return String('--');
419
+    } else {
420
+      return (
421
+        <a href="#">
422
+          <style jsx>{`
423
+            a {
424
+              text-decoration: none;
425
+            }
426
+
427
+            .course-title {
428
+              color: #0075b4;
429
+            }
430
+
431
+            .course-id {
432
+              color: #414141;
433
+            }
434
+          `}</style>
435
+          <span className="course-title">{rowData['catalog_course_title']}</span><br />
436
+          <span className="course-id">{rowData['catalog_course']}</span>
437
+        </a>
438
+      );
439
+    }
440
+  }
441
+
442
+  _dateRenderer ({cellData, columnData, dataKey, isScrolling, rowData, rowIndex}) {
443
+    moment.locale('en');
444
+    if (cellData === null) {
445
+      return String('--');
446
+    } else {
447
+      return moment.utc(cellData.split('T')[0]).format('L');
448
+    }
449
+  }
450
+
451
+  _numberRenderer ({cellData, columnData, dataKey, isScrolling, rowData, rowIndex}) {
452
+    if (cellData === null) {
453
+      return String('--');
454
+    } else {
455
+      return parseInt(cellData, 10).toLocaleString();
456
+    }
457
+  }
458
+
459
+  _onFilterCheck (event) {
460
+    console.log('FILTER');
461
+    let filterState = this.state[event.target.className],
462
+      newState = {};
463
+    filterState = filterState.set(event.target.value, event.target.checked);
464
+    newState[event.target.className] = filterState;
465
+    this.setState(newState);
466
+  }
467
+
468
+  _onSearchChange (event) {
469
+    console.log('SEARCH');
470
+    this.setState({search: event.target.value});
471
+  }
472
+
473
+  _removeFilter (event) {
474
+    console.log('REMOVE FILTER');
475
+    const filterKey = event.target.name;
476
+    if (filterKey === 'search') {
477
+      this.setState({search: ''});
478
+    } else if (filterKey === 'availability') {
479
+      this.setState({availability: Immutable.Map({
480
+        Archived: false,
481
+        Current: false,
482
+        Upcoming: false,
483
+        Unknown: false
484
+      })});
485
+    } else if (filterKey === 'pacing_type') {
486
+      this.setState({pacing_type: Immutable.Map({
487
+        instructor_paced: false,
488
+        self_paced: false
489
+      })});
490
+    }
491
+  }
492
+}

+ 36 - 0
next.config.js

@@ -0,0 +1,36 @@
1
+const path = require('path')
2
+const glob = require('glob')
3
+
4
+module.exports = {
5
+  webpack: (config, { dev }) => {
6
+    config.module.rules.push(
7
+      {
8
+        test: /\.(css|scss)/,
9
+        loader: 'emit-file-loader',
10
+        options: {
11
+          name: 'dist/[path][name].[ext]'
12
+        }
13
+      }
14
+    ,
15
+      {
16
+        test: /\.css$/,
17
+        use: ['babel-loader', 'raw-loader', 'postcss-loader']
18
+      }
19
+    ,
20
+      {
21
+        test: /\.s(a|c)ss$/,
22
+        use: ['babel-loader', 'raw-loader', 'postcss-loader',
23
+          { loader: 'sass-loader',
24
+            options: {
25
+              includePaths: ['styles', 'node_modules']
26
+                .map((d) => path.join(__dirname, d))
27
+                .map((g) => glob.sync(g))
28
+                .reduce((a, c) => a.concat(c), [])
29
+            }
30
+          }
31
+        ]
32
+      }
33
+    )
34
+    return config
35
+  }
36
+}

+ 32 - 0
package.json

@@ -0,0 +1,32 @@
1
+{
2
+  "name": "insights-react",
3
+  "version": "1.0.0",
4
+  "description": "Hackathon 2017-04-13 Project: Insights Courses Page in React",
5
+  "main": "index.js",
6
+  "dependencies": {
7
+    "autoprefixer": "^6.7.7",
8
+    "babel-plugin-module-resolver": "^2.7.0",
9
+    "babel-plugin-wrap-in-js": "^1.1.1",
10
+    "glob": "^7.1.1",
11
+    "immutable": "^3.8.1",
12
+    "isomorphic-fetch": "^2.2.1",
13
+    "moment": "^2.18.1",
14
+    "next": "^2.1.1",
15
+    "postcss-easy-import": "^2.0.0",
16
+    "postcss-loader": "^1.3.3",
17
+    "prop-types": "^15.5.8",
18
+    "raw-loader": "^0.5.1",
19
+    "react": "^15.5.4",
20
+    "react-dom": "^15.5.4",
21
+    "react-virtualized": "^9.7.3"
22
+  },
23
+  "devDependencies": {},
24
+  "scripts": {
25
+    "dev": "next",
26
+    "build": "next build",
27
+    "start": "next start",
28
+    "test": "echo \"Error: no test specified\" && exit 1"
29
+  },
30
+  "author": "Tyler Hallada <thallada@edx.org>",
31
+  "license": "MIT"
32
+}

+ 106 - 0
pages/index.js

@@ -0,0 +1,106 @@
1
+import 'isomorphic-fetch';
2
+import Immutable from 'immutable';
3
+import PropTypes from 'prop-types';
4
+import React, { PureComponent } from 'react';
5
+import Head from 'next/head';
6
+import Table from '../components/table.js';
7
+
8
+export default class Application extends PureComponent {
9
+
10
+  constructor (props, context) {
11
+    super(props, context);
12
+
13
+    this.state = {
14
+      courses: this.props.courses,
15
+      refreshing: false
16
+    }
17
+
18
+    this._onRefreshClick = this._onRefreshClick.bind(this);
19
+  }
20
+
21
+  static async getInitialProps ({ req }) {
22
+    console.log('INITIAL PROPS');
23
+    const courses = await Application.fetchCourses();
24
+    return {'courses': courses};
25
+  }
26
+
27
+  static async fetchCourses () {
28
+    let res;
29
+    try {
30
+      res = await fetch('http://localhost:9001/api/v0/course_summaries/', {
31
+        headers: {
32
+          'Authorization': 'Token edx'
33
+        }
34
+      });
35
+    } catch (exception) {
36
+      console.error(exception);
37
+    }
38
+    if (res !== undefined) {
39
+      const json = await res.json();
40
+      const list = Immutable.List(json);
41
+      return list;
42
+    } else {
43
+      return Immutable.List([]);
44
+    }
45
+  }
46
+
47
+  _onRefreshClick (event) {
48
+    this.refresh();
49
+    return true;
50
+  }
51
+
52
+  async refresh () {
53
+    this.setState({refreshing: true});
54
+    const courses = await Application.fetchCourses();
55
+    this.setState({'courses': courses});
56
+    this.setState({refreshing: false});
57
+  }
58
+
59
+  render () {
60
+    const courses = this.state.courses ? this.state.courses : this.props.courses;
61
+    return (
62
+      <div>
63
+        <Head>
64
+          <title>Insights React Demo</title>
65
+          <meta name="viewport" content="initial-scale=1.0, width=device-width" /> 
66
+          <style>{`
67
+            html, body {
68
+              height: 98%;
69
+              margin: 0;
70
+						}
71
+						html {
72
+							font-size: 14px;
73
+						}
74
+						body {
75
+							font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
76
+							font-size: 1rem;
77
+							font-weight: normal;
78
+							line-height: 20px;
79
+							-webkit-font-smoothing: antialiased;
80
+							background-color: whitesmoke;
81
+            }
82
+            .contents {
83
+              margin: 10px;
84
+            }
85
+          `}</style>
86
+        </Head>
87
+        <div className="contents">
88
+          <h1 style={{
89
+            marginBottom: '40px',
90
+            fontWeight: '300'
91
+          }}>edX Insights</h1>
92
+          <button
93
+            style={{
94
+              float: 'right'
95
+            }}
96
+            onClick={this._onRefreshClick}
97
+            disabled={this.state.refreshing ? 'disabled' : ''}
98
+          >
99
+            {this.state.refreshing ? '↻' : ''}Refresh
100
+          </button>
101
+          <Table list={this.props.courses} />
102
+        </div>
103
+      </div>
104
+    );
105
+  }
106
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
1
+module.exports = {
2
+  plugins: [
3
+    require('postcss-easy-import')({prefix: '_'}), // keep this first
4
+    require('autoprefixer')({ /* ...options */ }) // so imports are auto-prefixed too
5
+  ]
6
+}

+ 40 - 0
static/table.css

@@ -0,0 +1,40 @@
1
+.Table {
2
+  width: 100%;
3
+  margin-top: 15px;
4
+}
5
+.headerRow,
6
+.evenRow,
7
+.oddRow {
8
+  border-bottom: 1px solid #e0e0e0;
9
+}
10
+.oddRow {
11
+  background-color: #fafafa;
12
+}
13
+.headerColumn {
14
+  text-transform: none;
15
+}
16
+.exampleColumn {
17
+  white-space: nowrap;
18
+  overflow: hidden;
19
+  text-overflow: ellipsis;
20
+}
21
+
22
+.checkboxLabel {
23
+  margin-left: .5rem;
24
+}
25
+.checkboxLabel:first-of-type {
26
+  margin-left: 0;
27
+}
28
+
29
+.noRows {
30
+  position: absolute;
31
+  top: 0;
32
+  bottom: 0;
33
+  left: 0;
34
+  right: 0;
35
+  display: flex;
36
+  align-items: center;
37
+  justify-content: center;
38
+  font-size: 1em;
39
+  color: #bdbdbd;
40
+}