From 4f0eea2c59f33083b0619e619c32ebf6bbeb398b Mon Sep 17 00:00:00 2001 From: Matt Savoie Date: Mon, 2 Mar 2020 11:04:51 -0700 Subject: [PATCH 01/32] Remove unused code. --- app/src/js/actions/index.js | 15 --------------- app/src/js/actions/types.js | 3 --- app/src/js/reducers/granules.js | 18 ------------------ 3 files changed, 36 deletions(-) diff --git a/app/src/js/actions/index.js b/app/src/js/actions/index.js index 55e5cf36d..424e8b8aa 100644 --- a/app/src/js/actions/index.js +++ b/app/src/js/actions/index.js @@ -1,7 +1,6 @@ 'use strict'; import compareVersions from 'compare-versions'; -import moment from 'moment'; import url from 'url'; import { get as getProperty } from 'object-path'; import requestPromise from 'request-promise'; @@ -282,20 +281,6 @@ export const listGranules = (options) => { }; }; -// only query the granules from the last hour -export const getRecentGranules = () => ({ - [CALL_API]: { - type: types.RECENT_GRANULES, - method: 'GET', - path: 'granules', - qs: { - limit: 1, - fields: 'granuleId', - updatedAt__from: moment().subtract(1, 'hour').format() - } - } -}); - export const reprocessGranule = (granuleId) => ({ [CALL_API]: { type: types.GRANULE_REPROCESS, diff --git a/app/src/js/actions/types.js b/app/src/js/actions/types.js index d0d4ec822..d325d269b 100644 --- a/app/src/js/actions/types.js +++ b/app/src/js/actions/types.js @@ -64,9 +64,6 @@ export const SEARCH_GRANULES = 'SEARCH_GRANULES'; export const CLEAR_GRANULES_SEARCH = 'CLEAR_GRANULES_SEARCH'; export const FILTER_GRANULES = 'FILTER_GRANULES'; export const CLEAR_GRANULES_FILTER = 'CLEAR_GRANULES_FILTER'; -export const RECENT_GRANULES = 'RECENT_GRANULES'; -export const RECENT_GRANULES_INFLIGHT = 'RECENT_GRANULES_INFLIGHT'; -export const RECENT_GRANULES_ERROR = 'RECENT_GRANULES_ERROR'; export const OPTIONS_COLLECTIONNAME = 'OPTIONS_COLLECTIONNAME'; export const OPTIONS_COLLECTIONNAME_INFLIGHT = 'OPTIONS_COLLECTIONNAME_INFLIGHT'; export const OPTIONS_COLLECTIONNAME_ERROR = 'OPTIONS_COLLECTIONNAME_ERROR'; diff --git a/app/src/js/reducers/granules.js b/app/src/js/reducers/granules.js index da095ae5f..61fb10e7e 100644 --- a/app/src/js/reducers/granules.js +++ b/app/src/js/reducers/granules.js @@ -8,10 +8,6 @@ import { GRANULE_INFLIGHT, GRANULE_ERROR, - RECENT_GRANULES, - RECENT_GRANULES_INFLIGHT, - RECENT_GRANULES_ERROR, - GRANULES, GRANULES_INFLIGHT, GRANULES_ERROR, @@ -101,20 +97,6 @@ export default function reducer (state = initialState, action) { set(state, ['list', 'error'], action.error); break; - // basically a dummy query to get the meta object, - // which contains the number of granules updated in the last hour. - case RECENT_GRANULES: - set(state, ['recent', 'data'], data.meta); - set(state, ['recent', 'inflight'], false); - break; - case RECENT_GRANULES_INFLIGHT: - set(state, ['recent', 'inflight'], true); - break; - case RECENT_GRANULES_ERROR: - set(state, ['recent', 'inflight'], false); - set(state, ['recent', 'error'], action.error); - break; - case GRANULE_REPROCESS: set(state, ['reprocessed', id, 'status'], 'success'); set(state, ['reprocessed', id, 'error'], null); From 022e2c804de1b87aa0a9122cd3b5feb5bf67202c Mon Sep 17 00:00:00 2001 From: Lauren Frederick Date: Mon, 2 Mar 2020 15:20:13 -0500 Subject: [PATCH 02/32] Remove histogram --- app/src/css/main.scss | 1 - app/src/js/actions/index.js | 9 -- app/src/js/actions/types.js | 11 +- app/src/js/components/Chart/_chart.scss | 95 ------------ app/src/js/components/Chart/histogram.js | 182 ----------------------- app/src/js/reducers/stats.js | 32 +--- test/components/granules/overview.js | 4 +- 7 files changed, 9 insertions(+), 325 deletions(-) delete mode 100644 app/src/js/components/Chart/_chart.scss delete mode 100644 app/src/js/components/Chart/histogram.js diff --git a/app/src/css/main.scss b/app/src/css/main.scss index 4af20475c..e353379ab 100644 --- a/app/src/css/main.scss +++ b/app/src/css/main.scss @@ -28,7 +28,6 @@ @import '../js/components/DropDown/DropDown.scss'; @import 'lists'; @import '../css/modules/table'; -@import '../js/components/Chart/chart'; @import '../js/components/LoadingIndicator/loading-indicator'; @import "../js/components/Modal/modal"; @import 'pulse'; diff --git a/app/src/js/actions/index.js b/app/src/js/actions/index.js index c59479d2e..03f86c0ab 100644 --- a/app/src/js/actions/index.js +++ b/app/src/js/actions/index.js @@ -713,15 +713,6 @@ export const getSchema = (type) => ({ } }); -export const queryHistogram = (options) => ({ - [CALL_API]: { - type: types.HISTOGRAM, - method: 'GET', - url: url.resolve(root, 'stats/histogram'), - qs: options - } -}); - export const listWorkflows = (options) => ({ [CALL_API]: { type: types.WORKFLOWS, diff --git a/app/src/js/actions/types.js b/app/src/js/actions/types.js index d0d4ec822..68f0c71f4 100644 --- a/app/src/js/actions/types.js +++ b/app/src/js/actions/types.js @@ -140,6 +140,10 @@ export const SEARCH_PROVIDERS = 'SEARCH_PROVIDERS'; export const CLEAR_PROVIDERS_SEARCH = 'CLEAR_PROVIDERS_SEARCH'; export const FILTER_PROVIDERS = 'FILTER_PROVIDERS'; export const CLEAR_PROVIDERS_FILTER = 'CLEAR_PROVIDERS_FILTER'; +// Workflows +export const WORKFLOWS = 'WORKFLOWS'; +export const WORKFLOWS_INFLIGHT = 'WORKFLOWS_INFLIGHT'; +export const WORKFLOWS_ERROR = 'WORKFLOWS_ERROR'; // Logs export const LOGS = 'LOGS'; export const LOGS_INFLIGHT = 'LOGS_INFLIGHT'; @@ -148,13 +152,6 @@ export const CLEAR_LOGS = 'CLEAR_LOGS'; export const SCHEMA = 'SCHEMA'; export const SCHEMA_INFLIGHT = 'SCHEMA_INFLIGHT'; export const SCHEMA_ERROR = 'SCHEMA_ERROR'; -// Workflows -export const HISTOGRAM = 'HISTOGRAM'; -export const HISTOGRAM_INFLIGHT = 'HISTOGRAM_INFLIGHT'; -export const HISTOGRAM_ERROR = 'HISTOGRAM_ERROR'; -export const WORKFLOWS = 'WORKFLOWS'; -export const WORKFLOWS_INFLIGHT = 'WORKFLOWS_INFLIGHT'; -export const WORKFLOWS_ERROR = 'WORKFLOWS_ERROR'; // Executions export const EXECUTION_STATUS = 'EXECUTION_STATUS'; export const EXECUTION_STATUS_INFLIGHT = 'EXECUTION_STATUS_INFLIGHT'; diff --git a/app/src/js/components/Chart/_chart.scss b/app/src/js/components/Chart/_chart.scss deleted file mode 100644 index d2bbc3c24..000000000 --- a/app/src/js/components/Chart/_chart.scss +++ /dev/null @@ -1,95 +0,0 @@ -.chart__box { - display: inline-block; - width: 50%; - text-align: center; -} - -.chart__container { - height: 400px; -} - -.chart__bar { - fill: $midnight-blue; -} - -.axis__tick, -.axis__line { - stroke: $light-grey; -} - -.axis__text { - font-size: 10px; - fill: $light-grey; -} - -.tooltip { - margin-left: 1em; - margin-top: 1em; - position: fixed; - pointer-events: none; - transition: all 0.1s; - z-index: 99; -} - -.tooltip__inner { - padding: 1em; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12); - border-radius: 5px; - background: #FFF; - max-width: 240px; -} - -.clusters rect { - fill: transparent; - stroke: #555; - stroke-dasharray: 5 2; - stroke-width: 1px; -} - -text { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; - font-size: 14px; - fill: #444444; -} - -rect { - ry: 5; - rx: 5; -} - -.node rect { - stroke: #555; - fill: #fff; - stroke-width: 1px; -} - -.edgePath path { - stroke: #555; - stroke-width: 1.5px; -} - -.cluster .label { - display: none; -} - -.terminus rect { - ry: 25px; - rx: 25px; - fill: #ffda75; -} - -.Succeeded rect { - fill: #2bd62e; -} - -.InProgress rect { - fill: #53c9ed; -} - -.Cancelled rect { - fill: #ddd; -} - -.Failed rect { - fill: #de322f; -} diff --git a/app/src/js/components/Chart/histogram.js b/app/src/js/components/Chart/histogram.js deleted file mode 100644 index ddbea3035..000000000 --- a/app/src/js/components/Chart/histogram.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; -import React from 'react'; -import { get } from 'object-path'; -import { scaleLinear, scaleBand } from 'd3-scale'; -import debounce from 'lodash.debounce'; -import throttle from 'lodash.throttle'; -import PropTypes from 'prop-types'; -import { tally } from '../../utils/format'; -import LoadingIndicator from '../app/loading-indicator'; -import { window } from '../../utils/browser'; - -const noop = (x) => x; -const tooltipDelay = 100; - -const margin = { - top: 50, - right: 15, - bottom: 15, - left: 70 -}; - -class Histogram extends React.Component { - constructor () { - super(); - this.state = { - width: 0, - height: 0, - tooltip: null, - tooltipX: 0, - tooltipY: 0 - }; - this.onWindowResize = this.onWindowResize.bind(this); - this.setHoverState = this.setHoverState.bind(this); - this.mouseMove = this.mouseMove.bind(this); - this.mouseOut = this.mouseOut.bind(this); - } - - onWindowResize () { - let rect = this.refs.chartContainer.getBoundingClientRect(); - this.setState({ width: rect.width, height: rect.height }); - } - - componentDidMount () { - this.setHoverState = throttle(this.setHoverState, tooltipDelay); - this.mouseOut = debounce(this.mouseOut, tooltipDelay); - this.onWindowResize(); - this.onWindowResize = debounce(this.onWindowResize, 200); - window.addEventListener('resize', this.onWindowResize); - } - - setHoverState (tooltip, tooltipX, tooltipY) { - this.setState({ tooltip, tooltipX, tooltipY }); - } - - mouseMove (e) { - // http://stackoverflow.com/questions/38142880/react-js-throttle-mousemove-event-keep-throwing-event-persist-error - e.persist(); - this.setHoverState( - e.currentTarget.getAttribute('data-tooltip'), - e.clientX, - e.clientY - ); - } - - mouseOut () { - this.setState({ tooltip: null }); - } - - render () { - const { width, height } = this.state; - const { inflight, data } = this.props.data; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // short circuit if the area is too small; loading if the data is inflight - if (innerWidth <= 0) return
; - else if (inflight && !data) { - return ( -
- -
- ); - } - - const histogram = get(data, 'histogram', []); - - const xScale = scaleLinear() - .range([0, innerWidth]) - .domain([0, 1.25 * Math.max.apply(Math, histogram.map(d => +d.count))]); - - const scaleOrdinal = scaleBand() - .paddingInner(0.6) - .paddingOuter(0.2); - - const yScale = scaleOrdinal - .rangeRound([0, innerHeight]) - .domain(histogram.map(d => d.date)); - - const band = yScale.bandwidth(); - const tooltipFormat = this.props.tooltipFormat || noop; - - return ( -
- - - - {xScale.ticks(3).map((label, i) => { - // don't render the first tick - if (!i) return ; - return - - {tally(label)} - ; - })} - - - - - {histogram.map(d => { - return - - {d.date} - ; - })} - - - - {histogram.map(d => { - return ; - })} - - - -
-
- {tooltipFormat(this.state.tooltip)} -
-
-
- ); - } -} - -Histogram.propTypes = { - data: PropTypes.object, - tooltipFormat: PropTypes.func -}; - -export default Histogram; diff --git a/app/src/js/reducers/stats.js b/app/src/js/reducers/stats.js index 10d4524fc..83e86e95a 100644 --- a/app/src/js/reducers/stats.js +++ b/app/src/js/reducers/stats.js @@ -1,7 +1,6 @@ 'use strict'; import assignDate from './assign-date'; import { set } from 'object-path'; -import serialize from '../utils/serialize-config'; import { STATS, @@ -10,11 +9,7 @@ import { COUNT, COUNT_INFLIGHT, - COUNT_ERROR, - - HISTOGRAM, - HISTOGRAM_INFLIGHT, - HISTOGRAM_ERROR + COUNT_ERROR } from '../actions/types'; export const initialState = { @@ -35,13 +30,12 @@ export const initialState = { data: {}, inflight: false, error: null - }, - histogram: {} + } }; export default function reducer (state = initialState, action) { let nextState; - let stats, count, histogram; + let stats, count; switch (action.type) { case STATS: stats = { data: assignDate(action.data), inflight: false, error: null }; @@ -69,26 +63,6 @@ export default function reducer (state = initialState, action) { count = { data: state.count.data, inflight: false, error: action.error }; nextState = Object.assign(state, { count }); break; - - case HISTOGRAM: - histogram = Object.assign({}, state.histogram); - set(histogram, serialize(action.config.qs), { - inflight: false, - data: action.data, - error: null - }); - nextState = Object.assign(state, { histogram }); - break; - case HISTOGRAM_INFLIGHT: - histogram = Object.assign({}, state.histogram); - set(histogram, [serialize(action.config.qs), 'inflight'], true); - nextState = Object.assign(state, { histogram }); - break; - case HISTOGRAM_ERROR: - histogram = Object.assign({}, state.histogram); - set(histogram, [serialize(action.config.qs), 'error'], action.error); - nextState = Object.assign(state, { histogram }); - break; } return nextState || state; } diff --git a/test/components/granules/overview.js b/test/components/granules/overview.js index fe0a50200..a14628358 100644 --- a/test/components/granules/overview.js +++ b/test/components/granules/overview.js @@ -40,7 +40,7 @@ const data = ''; test('GranulesOverview generates bulkAction for recovery button', function (t) { const dispatch = () => {}; const workflowOptions = []; - const stats = { count: 0, histogram: {}, stats: {} }; + const stats = { count: 0, stats: {} }; const location = { pathname: 'granules' }; const config = { enableRecovery: true }; const store = { @@ -73,7 +73,7 @@ test('GranulesOverview generates bulkAction for recovery button', function (t) { test('GranulesOverview does not generate bulkAction for recovery button', function (t) { const dispatch = () => {}; const workflowOptions = []; - const stats = { count: 0, histogram: {}, stats: {} }; + const stats = { count: 0, stats: {} }; const location = { pathname: 'granules' }; const config = { enableRecovery: false }; const store = { From 2406ab2a303ddec6291b1c58c8a587559264576a Mon Sep 17 00:00:00 2001 From: dopeters Date: Tue, 3 Mar 2020 12:27:43 -0500 Subject: [PATCH 03/32] [CUMULUS-1508] Style collection granules pages (#647) * Style collection granules pages. Add breadcrumbs * Fix ava test * Pass in bulk action success and error handlers * styles for breadcrumbs and collection dropdown * Fix providers test * bulk button stylings * table styling * small table styling fix * Fix breadcrumb links Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> --- app/src/css/_base.scss | 18 +- app/src/css/_buttons.scss | 8 + app/src/css/_typography.scss | 4 + app/src/css/modules/_table.scss | 25 +- app/src/css/vendor/bootstrap/_bootstrap.scss | 1 + app/src/css/vendor/bootstrap/_components.scss | 1 + .../bootstrap/overrides/_breadcrumb.scss | 8 + .../components/AsyncCommands/AsyncCommands.js | 11 +- .../js/components/Breadcrumbs/Breadcrumbs.js | 33 ++- app/src/js/components/Button/Button.scss | 8 + app/src/js/components/Collections/granules.js | 203 +++++++++------- app/src/js/components/Collections/list.js | 15 ++ app/src/js/components/Collections/overview.js | 57 +++-- app/src/js/components/DropDown/DropDown.scss | 3 +- app/src/js/components/DropDown/dropdown.js | 6 +- .../js/components/DropDown/simple-dropdown.js | 24 +- app/src/js/components/Form/_form.scss | 14 +- .../js/components/ListActions/ListActions.js | 77 ++++++ .../js/components/ListActions/ListFilters.js | 20 ++ app/src/js/components/Search/search.js | 5 +- .../components/SortableTable/SortableTable.js | 226 ++++++++++-------- app/src/js/components/Table/Table.js | 76 ++---- app/src/js/utils/table-config/granules.js | 12 +- cypress/integration/providers_spec.js | 4 +- package-lock.json | 194 +++++++-------- test/components/table/list-view.js | 5 +- 26 files changed, 647 insertions(+), 411 deletions(-) create mode 100644 app/src/css/vendor/bootstrap/overrides/_breadcrumb.scss create mode 100644 app/src/js/components/ListActions/ListActions.js create mode 100644 app/src/js/components/ListActions/ListFilters.js diff --git a/app/src/css/_base.scss b/app/src/css/_base.scss index 718935ef9..ea1dfad4f 100644 --- a/app/src/css/_base.scss +++ b/app/src/css/_base.scss @@ -390,10 +390,9 @@ a:active { } .page__section { - margin-bottom: 3em; @extend .clearfix; - &:first-of-type { - margin-bottom: 0em; /* This is temporary until we had the datepicker component */ + &:last-of-type { + margin-bottom: 3em; } } @@ -401,6 +400,19 @@ a:active { margin-bottom: 8em; } +.collection__options--top ul{ + display: flex; + align-items: center; +} + +.collection__options--top li:nth-child(2){ + margin-left: auto; +} + +.page__section__controls{ + margin-bottom: 2em; +} + .page__section--small { margin-bottom: 2em; } diff --git a/app/src/css/_buttons.scss b/app/src/css/_buttons.scss index 075c827d7..34d4ee960 100644 --- a/app/src/css/_buttons.scss +++ b/app/src/css/_buttons.scss @@ -230,6 +230,14 @@ } } + + &--remove, &--execute, &--reingest { + &:hover { + background-color: darken($light-green, 10%); + color: #fff; + border: 0; + } + } &--download{ background-color: $light-green; diff --git a/app/src/css/_typography.scss b/app/src/css/_typography.scss index b84b5b423..ebfeaa923 100644 --- a/app/src/css/_typography.scss +++ b/app/src/css/_typography.scss @@ -79,3 +79,7 @@ h1, h2, h3, h4, h5, h6 { -webkit-font-smoothing: antialiased; transition: background-color 0.2s linear; } + +.breadcrumb, .caption { + font-size: 0.875em; +} \ No newline at end of file diff --git a/app/src/css/modules/_table.scss b/app/src/css/modules/_table.scss index b44433faf..4f217bda2 100644 --- a/app/src/css/modules/_table.scss +++ b/app/src/css/modules/_table.scss @@ -26,7 +26,7 @@ table { td { color: $white; font-weight: $base-font-semibold; - padding: 1em 2em 1.5em; + padding: 1em 2em; font-size: .86em; } } @@ -52,11 +52,28 @@ table { } } +.list-action-wrapper { + display: flex; + flex-wrap: wrap; + align-items: flex-end; +} + +.list-actions { + display: flex; + justify-content: space-around; + flex-grow: 1; + + .form--controls { + flex-grow: 1; + } +} + .list-view { position: relative; margin-top: 10px; /*border-bottom: 1px solid #E2DFDF;*/ padding-bottom: 10px; + width: 100%; } .list__wrapper{ @@ -326,7 +343,7 @@ table { background-size: 10px 13px; display: inline-block; margin-left: .5em; - margin-top: .2em; + margin-top: 0em; } } @@ -348,7 +365,7 @@ table { font-family: 'FontAwesome'; content: '\f0dd'; font-weight: 900; - top: 5px; + top: 10px; } } .table__sort--asc { @@ -358,7 +375,7 @@ table { font-family: 'FontAwesome'; content: '\f0de'; font-weight: 900; - top: 5px; + top: 15px; } } diff --git a/app/src/css/vendor/bootstrap/_bootstrap.scss b/app/src/css/vendor/bootstrap/_bootstrap.scss index d5628e906..06198e561 100644 --- a/app/src/css/vendor/bootstrap/_bootstrap.scss +++ b/app/src/css/vendor/bootstrap/_bootstrap.scss @@ -16,6 +16,7 @@ and behaviors for our Cumulus Dashboard components @import "components"; /* Bootstrap Overrides */ +@import "overrides/breadcrumb"; @import "overrides/pagination"; @import "overrides/modal"; diff --git a/app/src/css/vendor/bootstrap/_components.scss b/app/src/css/vendor/bootstrap/_components.scss index aac235655..7dc846eab 100644 --- a/app/src/css/vendor/bootstrap/_components.scss +++ b/app/src/css/vendor/bootstrap/_components.scss @@ -8,6 +8,7 @@ and behaviors for our Cumulus Dashboard components @import "~bootstrap/scss/_alert"; +@import "~bootstrap/scss/_breadcrumb"; @import "~bootstrap/scss/_card"; @import "~bootstrap/scss/_close"; @import "~bootstrap/scss/_modal"; diff --git a/app/src/css/vendor/bootstrap/overrides/_breadcrumb.scss b/app/src/css/vendor/bootstrap/overrides/_breadcrumb.scss new file mode 100644 index 000000000..ed98c3f7d --- /dev/null +++ b/app/src/css/vendor/bootstrap/overrides/_breadcrumb.scss @@ -0,0 +1,8 @@ +.breadcrumb { + background-color: initial; + padding: 0; +} + +.breadcrumb-item+.breadcrumb-item:before { + content: "|"; +} \ No newline at end of file diff --git a/app/src/js/components/AsyncCommands/AsyncCommands.js b/app/src/js/components/AsyncCommands/AsyncCommands.js index fe7171c8b..da6fc4d79 100644 --- a/app/src/js/components/AsyncCommands/AsyncCommands.js +++ b/app/src/js/components/AsyncCommands/AsyncCommands.js @@ -40,11 +40,12 @@ class AsyncCommand extends React.Component { } buttonClass (processing) { - let className = 'button button__group button--small form-group__element';// this button affects all Collections buttongroups -- the buttons need to be independent - if (processing) className += ' button--loading'; - if (this.props.disabled) className += ' button--disabled'; - if (this.props.className) className += ' ' + this.props.className; - return className; + return [ + 'button button--small form-group__element', + `${processing ? 'button--loading' : ''}`, + `${this.props.disabled ? 'button--disabled' : ''}`, + `${this.props.className ? this.props.className : 'button__group'}` + ].join(' '); } // a generic className generator for non-button elements diff --git a/app/src/js/components/Breadcrumbs/Breadcrumbs.js b/app/src/js/components/Breadcrumbs/Breadcrumbs.js index 4fc865bda..09a994990 100644 --- a/app/src/js/components/Breadcrumbs/Breadcrumbs.js +++ b/app/src/js/components/Breadcrumbs/Breadcrumbs.js @@ -1,13 +1,28 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import { Breadcrumb } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; -class Breadcrumbs extends React.Component { - render () { - return ( -
-

Future Home For Component

-
- ); - } -} +const Breadcrumbs = ({ config }) => { + return ( + + {config.map((item, index) => { + const { href, label, active } = item || {}; + return ( +
  • + {active ? {label} : {label}} +
  • + ); + })} +
    + ); +}; + +Breadcrumbs.propTypes = { + config: PropTypes.arrayOf(PropTypes.object) +}; export default Breadcrumbs; diff --git a/app/src/js/components/Button/Button.scss b/app/src/js/components/Button/Button.scss index da91bbd1e..577fb89a4 100644 --- a/app/src/js/components/Button/Button.scss +++ b/app/src/js/components/Button/Button.scss @@ -231,6 +231,14 @@ } } + + &--remove, &--execute, &--reingest { + &:hover { + background-color: darken($light-green, 10%); + color: #fff; + border: 0; + } + } &--download{ background-color: $light-green; diff --git a/app/src/js/components/Collections/granules.js b/app/src/js/components/Collections/granules.js index 8229ebc19..2106f7a7e 100644 --- a/app/src/js/components/Collections/granules.js +++ b/app/src/js/components/Collections/granules.js @@ -1,9 +1,9 @@ 'use strict'; -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Link, withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; -import { getCollectionId, lastUpdated } from '../../utils/format'; +import { getCollectionId, lastUpdated, displayCase } from '../../utils/format'; import { listGranules, filterGranules, @@ -25,133 +25,158 @@ import Search from '../Search/search'; import statusOptions from '../../utils/status'; import {strings} from '../locale'; import { workflowOptionNames } from '../../selectors'; +import ListFilters from '../ListActions/ListFilters'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; -class CollectionGranules extends React.Component { - constructor () { - super(); - this.displayName = strings.collection_granules; - this.applyWorkflow = this.applyWorkflow.bind(this); - this.generateBulkActions = this.generateBulkActions.bind(this); - this.selectWorkflow = this.selectWorkflow.bind(this); - this.getExecuteOptions = this.getExecuteOptions.bind(this); - this.generateQuery = this.generateQuery.bind(this); - this.getView = this.getView.bind(this); - this.state = {}; +const CollectionGranules = ({ + dispatch, + match, + location, + granules, + workflowOptions +}) => { + const { params } = match; + console.log(match); + const { name: collectionName, version: collectionVersion } = params; + const { pathname } = location; + const { list } = granules; + const { meta } = list; + const displayName = strings.granules; + const collectionId = getCollectionId(params); + const view = getView(); + const [workflow, setWorkflow] = useState(); + + const breadcrumbConfig = [ + { + label: 'Dashboard Home', + href: '/' + }, + { + label: 'Collections', + href: '/collections' + }, + { + label: 'Collection Overview', + href: `/collections/collection/${collectionName}/${collectionVersion}` + }, + { + label: 'Collection Granules', + href: `/collections/collection/${collectionName}/${collectionVersion}/granules`, + active: view === 'all' + } + ]; + + if (view !== 'all') { + breadcrumbConfig.push({ + label: displayCase(view), + active: true + }); } - generateQuery () { - const collectionId = getCollectionId(this.props.match.params); + function generateQuery () { const options = { collectionId }; - const view = this.getView(); if (view && view !== 'all') options.status = view; return options; } - getView () { - const { pathname } = this.props.location; + function getView () { if (pathname.includes('/granules/completed')) return 'completed'; if (pathname.includes('/granules/processing')) return 'running'; if (pathname.includes('/granules/failed')) return 'failed'; return 'all'; } - generateBulkActions () { + function generateBulkActions () { const actionConfig = { execute: { - options: this.getExecuteOptions(), - action: this.applyWorkflow + options: getExecuteOptions(), + action: applyWorkflow } }; - return bulkActions(this.props.granules, actionConfig); + return bulkActions(granules, actionConfig); } - selectWorkflow (selector, workflow) { - this.setState({ workflow }); + function selectWorkflow (selector, workflow) { + setWorkflow(workflow); } - applyWorkflow (granuleId) { - return applyWorkflowToGranule(granuleId, this.state.workflow); + function applyWorkflow (granuleId) { + return applyWorkflowToGranule(granuleId, workflow); } - getExecuteOptions () { + function getExecuteOptions () { return [ simpleDropdownOption({ - handler: this.selectWorkflow, + handler: selectWorkflow, label: 'workflow', - value: this.state.workflow, - options: this.props.workflowOptions + value: workflow, + options: workflowOptions }) ]; } - render () { - const collectionName = this.props.match.params.name; - const collectionVersion = this.props.match.params.version; - const { list } = this.props.granules; - const { meta } = list; - const view = this.getView(); + return ( +
    +
    + +
    +
    +

    + {collectionName} / {collectionVersion} +

    + + Edit + +
    +
    {lastUpdated(meta.queriedAt)}
    +
    +
    - return ( -
    -
    -

    - {collectionName} / {collectionVersion} -

    - - Edit - -
    -
    {lastUpdated(meta.queriedAt)}
    -
    -
    - -
    -
    -

    - {strings.granules}{' '} - - {meta.count ? ` ${meta.count}` : null} - -

    -
    -
    - {view === 'all' ? ( +
    +
    +

    + {`${displayCase(view)} ${displayName} `} + + {meta.count ? ` ${meta.count}` : null} + +

    +
    + + + + {view === 'all' && ( - ) : ( - )} -
    - - -
    -
    - ); - } -} + + + +
    + ); +}; CollectionGranules.propTypes = { granules: PropTypes.object, diff --git a/app/src/js/components/Collections/list.js b/app/src/js/components/Collections/list.js index c15331793..8049bd329 100644 --- a/app/src/js/components/Collections/list.js +++ b/app/src/js/components/Collections/list.js @@ -28,6 +28,18 @@ import { import Search from '../Search/search'; import List from '../Table/Table'; import { strings } from '../locale'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; + +const breadcrumbConfig = [ + { + label: 'Dashboard Home', + href: '/' + }, + { + label: 'Collections', + active: true + } +]; class CollectionList extends React.Component { constructor () { @@ -73,6 +85,9 @@ class CollectionList extends React.Component { return (
    +
    + +

    {strings.collection_overview}

    {lastUpdated(queriedAt)} diff --git a/app/src/js/components/Collections/overview.js b/app/src/js/components/Collections/overview.js index cdc6d9c01..5c0ae0b48 100644 --- a/app/src/js/components/Collections/overview.js +++ b/app/src/js/components/Collections/overview.js @@ -31,6 +31,22 @@ import Overview from '../Overview/overview'; import { tableHeader, tableRow, tableSortProps } from '../../utils/table-config/granules'; import { strings } from '../locale'; import DeleteCollection from '../DeleteCollection/DeleteCollection'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; + +const breadcrumbConfig = [ + { + label: 'Dashboard Home', + href: '/' + }, + { + label: 'Collections', + href: '/collections' + }, + { + label: 'Collection Overview', + active: true + } +]; class CollectionOverview extends React.Component { constructor (props) { @@ -168,16 +184,25 @@ class CollectionOverview extends React.Component { return (
    -
    -
    - +
    +
      +
    • + +
    • +
    • +
      + +
      +
    • +
    @@ -228,6 +253,12 @@ class CollectionOverview extends React.Component { {meta.count ? ` ${meta.count}` : 0} + + {strings.view_all_granules} +
      @@ -263,12 +294,6 @@ class CollectionOverview extends React.Component { rowId={'granuleId'} sortIdx={6} /> - - {strings.view_all_granules} -
    ); diff --git a/app/src/js/components/DropDown/DropDown.scss b/app/src/js/components/DropDown/DropDown.scss index 0a3377846..3ab302877 100644 --- a/app/src/js/components/DropDown/DropDown.scss +++ b/app/src/js/components/DropDown/DropDown.scss @@ -74,6 +74,7 @@ /*border: 1px solid $lightest-grey;*/ border-radius: .5em; position: relative; + float: right; &:after { position: absolute; font-family: 'FontAwesome'; @@ -88,6 +89,6 @@ } &__collection { - margin-bottom: 1em; + margin-bottom: 0em; } } diff --git a/app/src/js/components/DropDown/dropdown.js b/app/src/js/components/DropDown/dropdown.js index ff6cfe7f3..37d9a5766 100644 --- a/app/src/js/components/DropDown/dropdown.js +++ b/app/src/js/components/DropDown/dropdown.js @@ -90,7 +90,7 @@ class Dropdown extends React.Component { render () { // `options` are expected in the following format: // {displayValue1: optionElementValue1, displayValue2, optionElementValue2, ...} - const { options, label, paramKey } = this.props; + const { options, label, paramKey, inputProps } = this.props; const items = options ? Object.keys(options).map(label => ({label, value: options[label]})) : []; // Make sure this form ID is unique! @@ -111,6 +111,7 @@ class Dropdown extends React.Component { onChange={this.onChange} onSelect={this.onSelect} renderMenu={renderMenu} + inputProps={inputProps} />
    @@ -129,7 +130,8 @@ Dropdown.propTypes = { location: PropTypes.object, router: PropTypes.object, queryParams: PropTypes.object, - setQueryParams: PropTypes.func + setQueryParams: PropTypes.func, + inputProps: PropTypes.object }; export default withRouter(withQueryParams()(connect()(Dropdown))); diff --git a/app/src/js/components/DropDown/simple-dropdown.js b/app/src/js/components/DropDown/simple-dropdown.js index 6820a1f81..e1e58ede6 100644 --- a/app/src/js/components/DropDown/simple-dropdown.js +++ b/app/src/js/components/DropDown/simple-dropdown.js @@ -27,16 +27,22 @@ class Dropdown extends React.Component { return (
    - +
      +
    • + +
    • +
    • +
      + +
      +
    • +
    {error} -
    - -
    ); } diff --git a/app/src/js/components/Form/_form.scss b/app/src/js/components/Form/_form.scss index aaa90916a..7adaa012f 100644 --- a/app/src/js/components/Form/_form.scss +++ b/app/src/js/components/Form/_form.scss @@ -101,15 +101,17 @@ select option{ margin-left: auto; } -.filters.filters__wlabels.total_granules{ - margin-bottom: 5em; -} - .form--controls { margin-bottom: 2em; display: flex; } +.dropdown__collection .form__dropdown label { + display: inline-block; + text-align: left; + padding-right: .75em; +} + .search__wrapper { position: relative; .form__dropdown label { @@ -230,9 +232,7 @@ select option{ } .form__element__updateToggle-noHeader { - position: absolute; - right: 0; - top: -3.2rem; + margin-left: auto; } .form-group__element--right { diff --git a/app/src/js/components/ListActions/ListActions.js b/app/src/js/components/ListActions/ListActions.js new file mode 100644 index 000000000..43cf5d542 --- /dev/null +++ b/app/src/js/components/ListActions/ListActions.js @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import BatchAsyncCommand from '../BatchAsyncCommands/BatchAsyncCommands'; +import Timer from '../Timer/timer'; + +const ListActions = ({ + children, + bulkActions, + onBulkActionSuccess, + onBulkActionError, + selected, + dispatch, + action, + queryConfig, + completedBulkActions +}) => { + const hasActions = Array.isArray(bulkActions) && bulkActions.length > 0; + + function handleBulkActionSuccess () { + if (typeof onBulkActionSuccess === 'function') { + onBulkActionSuccess(); + } + } + + function handleBulkActionError () { + if (typeof onBulkActionError === 'function') { + onBulkActionError(); + } + } + + return ( +
    + {children} +
    + {hasActions && ( +
    + {bulkActions.map((item) => + )} +
    + )} + +
    +
    + ); +}; + +ListActions.propTypes = { + children: PropTypes.node, + bulkActions: PropTypes.array, + onBulkActionSuccess: PropTypes.func, + onBulkActionError: PropTypes.func, + selected: PropTypes.array, + dispatch: PropTypes.func, + action: PropTypes.func, + queryConfig: PropTypes.object, + completedBulkActions: PropTypes.number +}; + +export default ListActions; diff --git a/app/src/js/components/ListActions/ListFilters.js b/app/src/js/components/ListActions/ListFilters.js new file mode 100644 index 000000000..f8d2d376c --- /dev/null +++ b/app/src/js/components/ListActions/ListFilters.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ListFilters = ({ + children, + className = '' +}) => { + return ( +
    + {children} +
    + ); +}; + +ListFilters.propTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +export default ListFilters; diff --git a/app/src/js/components/Search/search.js b/app/src/js/components/Search/search.js index 85f5b5694..57243697f 100644 --- a/app/src/js/components/Search/search.js +++ b/app/src/js/components/Search/search.js @@ -75,7 +75,7 @@ class Search extends React.Component {
    {label ? : null}
    - +
    @@ -97,7 +97,8 @@ Search.propTypes = { router: PropTypes.object, query: PropTypes.object, queryParams: PropTypes.object, - setQueryParams: PropTypes.func + setQueryParams: PropTypes.func, + placeholder: PropTypes.string }; export default withRouter(withQueryParams()(connect(state => state)(Search))); diff --git a/app/src/js/components/SortableTable/SortableTable.js b/app/src/js/components/SortableTable/SortableTable.js index 7637b8063..32fa615df 100644 --- a/app/src/js/components/SortableTable/SortableTable.js +++ b/app/src/js/components/SortableTable/SortableTable.js @@ -1,10 +1,12 @@ 'use strict'; import Collapse from 'react-collapsible'; -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { get } from 'object-path'; import { isUndefined } from '../../utils/validate'; import { nullValue } from '../../utils/format'; +import isEmpty from 'lodash.isempty'; +import isFunction from 'lodash.isfunction'; const defaultSortOrder = 'desc'; const otherOrder = { @@ -12,32 +14,45 @@ const otherOrder = { asc: 'desc' }; -class Table extends React.Component { - constructor (props) { - super(props); - this.state = { - dumbOrder: null, - dumbSortIdx: null - }; - this.displayName = 'SortableTable'; - this.isTableDumb = this.isTableDumb.bind(this); - this.changeSort = this.changeSort.bind(this); - this.select = this.select.bind(this); - } +const Table = ({ + primaryIdx = 0, + sortIdx, + order, + props, + header, + row, + rowId, + data, + canSelect, + collapsible, + changeSortProps, + onSelect +}) => { + const [dumbState, setDumbState] = useState({ + dumbOrder: null, + dumbSortIdx: null + }); + const [selected, setSelected] = useState([]); + const isTableDumb = isUndefined(sortIdx) || !order || !Array.isArray(props); + const allChecked = !isEmpty(data) && selected.length === data.length; - isTableDumb () { - // identify whether the table is "dumb," as in it doesn't - // do its own data-updating, and cannot be sorted via its API call - return isUndefined(this.props.sortIdx) || !this.props.order || !Array.isArray(this.props.props); + if (isTableDumb) { + props = []; + sortIdx = dumbState.dumbSortIdx; + order = dumbState.dumbOrder; + const sortName = props[sortIdx]; + const primaryName = props[primaryIdx]; + data = data.sort((a, b) => + // If the sort field is the same, tie-break using the primary ID field + a[sortName] === b[sortName] ? a[primaryName] > b[primaryName] + : (order === 'asc') ? a[sortName] < b[sortName] : a[sortName] > b[sortName] + ); } - changeSort (e) { - let { sortIdx, order, props, header } = this.props; - const isTableDumb = this.isTableDumb(); - + function changeSort (e) { if (isTableDumb) { - sortIdx = this.state.dumbSortIdx; - order = this.state.dumbOrder; + sortIdx = dumbState.dumbSortIdx; + order = dumbState.dumbOrder; } const headerName = e.currentTarget.getAttribute('data-value'); @@ -45,99 +60,103 @@ class Table extends React.Component { if (!props[newSortIdx]) { return; } const newOrder = sortIdx === newSortIdx ? otherOrder[order] : defaultSortOrder; - if (typeof this.props.changeSortProps === 'function') { - this.props.changeSortProps({ sortIdx: newSortIdx, order: newOrder }); + if (typeof changeSortProps === 'function') { + changeSortProps({ sortIdx: newSortIdx, order: newOrder }); } if (isTableDumb) { - this.setState({ dumbSortIdx: newSortIdx, dumbOrder: newOrder }); + setDumbState({ dumbSortIdx: newSortIdx, dumbOrder: newOrder }); } } - select (e) { - if (typeof this.props.onSelect === 'function') { - const targetId = (e.currentTarget.getAttribute('data-value')); - this.props.onSelect(targetId); + function select (e) { + const id = (e.currentTarget.getAttribute('data-value')); + const selectedRows = selected.includes(id) + ? selected.filter(anId => anId !== id) + : [...selected, id]; + setSelected(selectedRows); + if (typeof onSelect === 'function') { + onSelect(selectedRows); } } - render () { - let { primaryIdx, sortIdx, order, props, header, row, rowId, data, selectedRows, canSelect, collapsible } = this.props; - const isTableDumb = this.isTableDumb(); - primaryIdx = primaryIdx || 0; - - if (isTableDumb) { - props = []; - sortIdx = this.state.dumbSortIdx; - order = this.state.dumbOrder; - const sortName = props[sortIdx]; - const primaryName = props[primaryIdx]; - data = data.sort((a, b) => - // If the sort field is the same, tie-break using the primary ID field - a[sortName] === b[sortName] ? a[primaryName] > b[primaryName] - : (order === 'asc') ? a[sortName] < b[sortName] : a[sortName] > b[sortName] - ); + function selectAll (e) { + if (!isEmpty(data)) { + const rowIdFn = isFunction(rowId) ? rowId : row => row[rowId]; + const allSelected = selected.length === data.length; + const selectedRows = allSelected ? [] : data.map(rowIdFn); + setSelected(selectedRows); } + } - return ( -
    -
    - - - - {canSelect && - ); - })} - - - - {data.map((d, i) => { - const dataId = typeof rowId === 'function' ? rowId(d) : d[rowId]; - const checked = canSelect && selectedRows.indexOf(dataId) !== -1; + return ( +
    + +
    } - {header.map((h, i) => { - let className = (isTableDumb || props[i]) ? 'table__sort' : ''; - if (i === sortIdx) { className += (' table__sort--' + order); } - return ( - {h} -
    + + + {canSelect && + + } + {header.map((h, i) => { + let className = (isTableDumb || props[i]) ? 'table__sort' : ''; + if (i === sortIdx) { className += (' table__sort--' + order); } return ( - - {canSelect && - - } - {row.map((accessor, k) => { - let className = k === primaryIdx ? 'table__main-asset' : ''; - let text; - - if (typeof accessor === 'function') { - text = accessor(d, k, data); - } else { - text = get(d, accessor, nullValue); - } - return ; - })} - {collapsible && - - } - + ); })} - -
    + +
    - - {text} - -
    {JSON.stringify(d.eventDetails, null, 2)}
    -
    -
    {h} +
    -
    -
    - ); - } -} + + + + {data.map((d, i) => { + const dataId = typeof rowId === 'function' ? rowId(d) : d[rowId]; + const checked = canSelect && selected.indexOf(dataId) !== -1; + return ( + + {canSelect && + + + + } + {row.map((accessor, k) => { + let className = k === primaryIdx ? 'table__main-asset' : ''; + let text; + + if (typeof accessor === 'function') { + text = accessor(d, k, data); + } else { + text = get(d, accessor, nullValue); + } + return {text}; + })} + {collapsible && + + +
    {JSON.stringify(d.eventDetails, null, 2)}
    +
    + + } + + ); + })} + + + + + ); +}; Table.propTypes = { primaryIdx: PropTypes.number, @@ -151,7 +170,6 @@ Table.propTypes = { onSelect: PropTypes.func, canSelect: PropTypes.bool, collapsible: PropTypes.bool, - selectedRows: PropTypes.array, rowId: PropTypes.any }; diff --git a/app/src/js/components/Table/Table.js b/app/src/js/components/Table/Table.js index 387d7cf67..8de52a1aa 100644 --- a/app/src/js/components/Table/Table.js +++ b/app/src/js/components/Table/Table.js @@ -3,12 +3,12 @@ import React from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -import BatchAsyncCommand from '../BatchAsyncCommands/BatchAsyncCommands'; +// import BatchAsyncCommand from '../BatchAsyncCommands/BatchAsyncCommands'; import ErrorReport from '../Errors/report'; import Loading from '../LoadingIndicator/loading-indicator'; import Pagination from '../Pagination/pagination'; import SortableTable from '../SortableTable/SortableTable'; -import Timer from '../Timer/timer'; +// import Timer from '../Timer/timer'; // import TableOptions from '../TableOptions/TableOptions' // Lodash import isEmpty from 'lodash.isempty'; @@ -16,6 +16,7 @@ import isEqual from 'lodash.isequal'; import isFunction from 'lodash.isfunction'; import isNil from 'lodash.isnil'; import omitBy from 'lodash.omitby'; +import ListActions from '../ListActions/ListActions'; class List extends React.Component { constructor (props) { @@ -29,7 +30,6 @@ class List extends React.Component { this.onBulkActionSuccess = this.onBulkActionSuccess.bind(this); this.onBulkActionError = this.onBulkActionError.bind(this); this.getQueryConfig = this.getQueryConfig.bind(this); - this.renderSelectAll = this.renderSelectAll.bind(this); const initialPage = 1; const initialSortIdx = props.sortIdx || 0; @@ -112,14 +112,8 @@ class List extends React.Component { } } - updateSelection (id) { - const { selected } = this.state; - - this.setState({ - selected: selected.includes(id) - ? selected.filter(anId => anId !== id) - : [...selected, id] - }); + updateSelection (selected) { + this.setState({selected}); } onBulkActionSuccess () { @@ -151,29 +145,11 @@ class List extends React.Component { }, isEmpty); } - renderSelectAll () { - const { list: { data } } = this.props; - const allChecked = !isEmpty(data) && this.state.selected.length === data.length; - - return ( - - ); - } - render () { const { dispatch, action, + children, tableHeader, tableRow, tableSortProps, @@ -200,33 +176,19 @@ class List extends React.Component { const hasActions = Array.isArray(bulkActions) && bulkActions.length > 0; return ( + <> + + {children} +
    - - {hasActions && ( -
    - {this.renderSelectAll()} - {bulkActions.map((item) => - )} -
    - )} - {list.inflight && } {list.error && } {bulkActionError && } @@ -261,6 +223,7 @@ class List extends React.Component { />
    + ); } } @@ -269,6 +232,7 @@ List.propTypes = { list: PropTypes.object, dispatch: PropTypes.func, action: PropTypes.func, + children: PropTypes.node, tableHeader: PropTypes.array, tableRow: PropTypes.array, tableSortProps: PropTypes.array, diff --git a/app/src/js/utils/table-config/granules.js b/app/src/js/utils/table-config/granules.js index 670b272e2..651c494cc 100644 --- a/app/src/js/utils/table-config/granules.js +++ b/app/src/js/utils/table-config/granules.js @@ -106,22 +106,26 @@ export const bulkActions = function (granules, config) { text: 'Reingest', action: reingestGranule, state: granules.reingested, - confirm: confirmReingest + confirm: confirmReingest, + className: 'button--reingest' }, { text: 'Execute', action: config.execute.action, state: granules.executed, confirm: confirmApply, - confirmOptions: config.execute.options + confirmOptions: config.execute.options, + className: 'button--execute' }, { text: strings.remove_from_cmr, action: removeGranule, state: granules.removed, - confirm: confirmRemove + confirm: confirmRemove, + className: 'button--remove' }, { text: 'Delete', action: deleteGranule, state: granules.deleted, - confirm: confirmDelete + confirm: confirmDelete, + className: 'button--delete' }]; }; diff --git a/cypress/integration/providers_spec.js b/cypress/integration/providers_spec.js index 796c5c2a7..0bfdb34a0 100644 --- a/cypress/integration/providers_spec.js +++ b/cypress/integration/providers_spec.js @@ -67,9 +67,9 @@ describe('Dashboard Providers Page', () => { .clear() .type(connectionLimit); cy.get('@providerinput') - .contains('label', 'Protocol') + .contains('.dropdown__label', 'Protocol') .siblings() - .children('select') + .find('select') .select(protocol, {force: true}) .should('have.value', protocol); cy.get('@providerinput') diff --git a/package-lock.json b/package-lock.json index b70254a1d..c38d52608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,9 +78,9 @@ } }, "@babel/compat-data": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.5.tgz", - "integrity": "sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.6.tgz", + "integrity": "sha512-CurCIKPTkS25Mb8mz267vU95vy+TyUpnctEX2lV33xWNmHAfjruztgiPBbXZRh3xZZy1CYvGx6XfxyTVS+sk7Q==", "dev": true, "requires": { "browserslist": "^4.8.5", @@ -89,18 +89,18 @@ } }, "@babel/core": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", - "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.6.tgz", + "integrity": "sha512-Sheg7yEJD51YHAvLEV/7Uvw95AeWqYPL3Vk3zGujJKIhJ+8oLw2ALaf3hbucILhKsgSoADOvtKRJuNVdcJkOrg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", + "@babel/generator": "^7.8.6", "@babel/helpers": "^7.8.4", - "@babel/parser": "^7.8.4", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.6", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", @@ -123,12 +123,12 @@ } }, "@babel/generator": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", - "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.6.tgz", + "integrity": "sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg==", "dev": true, "requires": { - "@babel/types": "^7.8.3", + "@babel/types": "^7.8.6", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -175,12 +175,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz", - "integrity": "sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.6.tgz", + "integrity": "sha512-UrJdk27hKVJSnibFcUWYLkCL0ZywTUoot8yii1lsHJcvwrypagmYKjHLMWivQPm4s6GdyygCL8fiH5EYLxhQwQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.8.4", + "@babel/compat-data": "^7.8.6", "browserslist": "^4.8.5", "invariant": "^2.2.4", "levenary": "^1.1.1", @@ -188,11 +188,12 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz", - "integrity": "sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.6.tgz", + "integrity": "sha512-bPyujWfsHhV/ztUkwGHz/RPV1T1TDEsSZDsN42JPehndA+p1KKTh3npvTadux0ZhCrytx9tvjpWNowKby3tM6A==", "dev": true, "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/helper-regex": "^7.8.3", "regexpu-core": "^4.6.0" } @@ -266,16 +267,17 @@ } }, "@babel/helper-module-transforms": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz", - "integrity": "sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.6.tgz", + "integrity": "sha512-RDnGJSR5EFBJjG3deY0NiL0K9TO8SXxS9n/MPsbPK/s9LbQymuLNtlzvDiNS7IpecuL45cMeLVkA+HfmlrnkRg==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", "@babel/helper-simple-access": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3", + "@babel/template": "^7.8.6", + "@babel/types": "^7.8.6", "lodash": "^4.17.13" } }, @@ -317,15 +319,15 @@ } }, "@babel/helper-replace-supers": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz", - "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", + "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.8.3", "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.6" } }, "@babel/helper-simple-access": { @@ -382,9 +384,9 @@ } }, "@babel/parser": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", - "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { @@ -589,9 +591,9 @@ } }, "@babel/plugin-transform-classes": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz", - "integrity": "sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.6.tgz", + "integrity": "sha512-k9r8qRay/R6v5aWZkrEclEhKO6mc1CCQr2dLsVHBmOQiMpN6I2bpjX3vgnldUWeEI1GHVNByULVxZ4BdP4Hmdg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.8.3", @@ -599,7 +601,7 @@ "@babel/helper-function-name": "^7.8.3", "@babel/helper-optimise-call-expression": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", "@babel/helper-split-export-declaration": "^7.8.3", "globals": "^11.1.0" } @@ -652,9 +654,9 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz", - "integrity": "sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.6.tgz", + "integrity": "sha512-M0pw4/1/KI5WAxPsdcUL/w2LJ7o89YHN3yLkzNjg7Yl15GlVGgzHyCU+FMeAxevHGsLVmUqbirlUIKTafPmzdw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" @@ -925,13 +927,13 @@ } }, "@babel/preset-env": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.4.tgz", - "integrity": "sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.6.tgz", + "integrity": "sha512-M5u8llV9DIVXBFB/ArIpqJuvXpO+ymxcJ6e8ZAmzeK3sQeBNOD1y+rHvHCGG4TlEmsNpIrdecsHGHT8ZCoOSJg==", "dev": true, "requires": { - "@babel/compat-data": "^7.8.4", - "@babel/helper-compilation-targets": "^7.8.4", + "@babel/compat-data": "^7.8.6", + "@babel/helper-compilation-targets": "^7.8.6", "@babel/helper-module-imports": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-proposal-async-generator-functions": "^7.8.3", @@ -954,13 +956,13 @@ "@babel/plugin-transform-async-to-generator": "^7.8.3", "@babel/plugin-transform-block-scoped-functions": "^7.8.3", "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.8.3", + "@babel/plugin-transform-classes": "^7.8.6", "@babel/plugin-transform-computed-properties": "^7.8.3", "@babel/plugin-transform-destructuring": "^7.8.3", "@babel/plugin-transform-dotall-regex": "^7.8.3", "@babel/plugin-transform-duplicate-keys": "^7.8.3", "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.8.4", + "@babel/plugin-transform-for-of": "^7.8.6", "@babel/plugin-transform-function-name": "^7.8.3", "@babel/plugin-transform-literals": "^7.8.3", "@babel/plugin-transform-member-expression-literals": "^7.8.3", @@ -981,7 +983,7 @@ "@babel/plugin-transform-template-literals": "^7.8.3", "@babel/plugin-transform-typeof-symbol": "^7.8.4", "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/types": "^7.8.3", + "@babel/types": "^7.8.6", "browserslist": "^4.8.5", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", @@ -1003,9 +1005,9 @@ } }, "@babel/register": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.8.3.tgz", - "integrity": "sha512-t7UqebaWwo9nXWClIPLPloa5pN33A2leVs8Hf0e9g9YwUP8/H9NeR7DJU+4CXo23QtjChQv5a3DjEtT83ih1rg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.8.6.tgz", + "integrity": "sha512-7IDO93fuRsbyml7bAafBQb3RcBGlCpU4hh5wADA2LJEEcYk92WkwFZ0pHyIi2fb5Auoz1714abETdZKCOxN0CQ==", "dev": true, "requires": { "find-cache-dir": "^2.0.0", @@ -1024,28 +1026,28 @@ } }, "@babel/template": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", - "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" } }, "@babel/traverse": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", - "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", + "@babel/generator": "^7.8.6", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" @@ -1063,9 +1065,9 @@ } }, "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -2223,9 +2225,9 @@ } }, "@wojtekmaj/date-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.0.tgz", - "integrity": "sha512-tHzSlWCmb1GjxjiPRTju7i2bQigYriFDmCpaf1dC4SebFRjhBiuRpIxy5oI3b3ZMg9OzqGnApdZJrlYSwEUJAg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.1.tgz", + "integrity": "sha512-l9uoI2YmXjMA0AVrIt/hngZyhDZXyL2Fo2znNQTkBdwoMnAuIwX1mkgGOQUkgFQ3mV/gXivxDScsseE1UkSKVw==" }, "@xtuc/ieee754": { "version": "1.2.0", @@ -3638,14 +3640,14 @@ } }, "browserslist": { - "version": "4.8.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.7.tgz", - "integrity": "sha512-gFOnZNYBHrEyUML0xr5NJ6edFaaKbTFX9S9kQHlYfCP0Rit/boRIz4G+Avq6/4haEKJXdGGUnoolx+5MWW2BoA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.9.0.tgz", + "integrity": "sha512-seffIXhwgB84+OCeT/aMjpZnsAsYDiMSC+CEs3UkF8iU64BZGYcu+TZYs/IBpo4nRi0vJywUJWYdbTsOhFTweg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001027", - "electron-to-chromium": "^1.3.349", - "node-releases": "^1.1.49" + "caniuse-lite": "^1.0.30001030", + "electron-to-chromium": "^1.3.361", + "node-releases": "^1.1.50" } }, "buffer": { @@ -7127,9 +7129,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.4.0.tgz", - "integrity": "sha512-bH5DOCP6WpuOqNaux2BlaDCrSgv8s5BitP90bTgtZ1ZsRn2bdIfeMDY5F2RnJVnyKDy6KRQRDbipPLZ1y77QtQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.0.tgz", + "integrity": "sha512-bzvdX47Jx847bgAYf0FPX3u1oxU+mKU8tqrpj4UX9A96SbAmj/HVEefEy6rJUog5u8QIlOPTKZcBpGn5kkKfAQ==", "dev": true }, "eslint-plugin-standard": { @@ -15210,9 +15212,9 @@ } }, "react": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", - "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz", + "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -15335,14 +15337,14 @@ "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" }, "react-dom": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", - "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==", + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz", + "integrity": "sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.18.0" + "scheduler": "^0.19.0" } }, "react-fit": { @@ -15355,9 +15357,9 @@ } }, "react-is": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", + "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -15477,15 +15479,15 @@ } }, "react-test-renderer": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.12.0.tgz", - "integrity": "sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w==", + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.0.tgz", + "integrity": "sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA==", "dev": true, "requires": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", "react-is": "^16.8.6", - "scheduler": "^0.18.0" + "scheduler": "^0.19.0" } }, "react-time-picker": { @@ -16834,9 +16836,9 @@ } }, "scheduler": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", - "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz", + "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/test/components/table/list-view.js b/test/components/table/list-view.js index bebed2f7a..d4925843f 100644 --- a/test/components/table/list-view.js +++ b/test/components/table/list-view.js @@ -3,7 +3,7 @@ import test from 'ava'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import {shallow, configure} from 'enzyme'; +import {mount, configure} from 'enzyme'; import {listGranules} from '../../../app/src/js/actions'; import { List } from '../../../app/src/js/components/Table/Table'; import Timer from '../../../app/src/js/components/Timer/timer.js'; @@ -21,7 +21,7 @@ test('table should properly initialize timer config prop', async (t) => { meta: {}, data: [] }; - const listWrapper = shallow( + const listWrapper = mount( { // Is the Timer's query configuration properly initialized via the // enclosing List's state, prior to any lifecycle method invocations? t.is(timerWrapper.props().config.q, query.q); + listWrapper.unmount(); }); From 5064bb49dfddaef19ff653fe833429303e202b72 Mon Sep 17 00:00:00 2001 From: Matt Savoie Date: Tue, 3 Mar 2020 15:02:15 -0700 Subject: [PATCH 04/32] Never modify state in a reducer. None of these are complicated state that are purposely unmutated, and if you mutate state in a reducer the component won't re-render. --- app/src/js/reducers/granule-csv.js | 6 +++--- app/src/js/reducers/schema.js | 2 +- app/src/js/reducers/stats.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/js/reducers/granule-csv.js b/app/src/js/reducers/granule-csv.js index b39b24af0..c5f4abed7 100644 --- a/app/src/js/reducers/granule-csv.js +++ b/app/src/js/reducers/granule-csv.js @@ -19,15 +19,15 @@ export default function reducer (state = initialState, action) { switch (action.type) { case GRANULE_CSV: csvData = { data: action.data, inflight: false, error: null }; - nextState = Object.assign(state, csvData); + nextState = Object.assign({}, state, csvData); break; case GRANULE_CSV_INFLIGHT: csvData = { data: state.data, inflight: true, error: state.error }; - nextState = Object.assign(state, csvData); + nextState = Object.assign({}, state, csvData); break; case GRANULE_CSV_ERROR: csvData = { data: state.data, inflight: false, error: action.error }; - nextState = Object.assign(state, csvData); + nextState = Object.assign({}, state, csvData); break; } return nextState || state; diff --git a/app/src/js/reducers/schema.js b/app/src/js/reducers/schema.js index d6b5fb7ec..5430f6fa6 100644 --- a/app/src/js/reducers/schema.js +++ b/app/src/js/reducers/schema.js @@ -10,7 +10,7 @@ export default function reducer (state = initialState, action) { const { type, config } = action; switch (type) { case SCHEMA: - return Object.assign(state, { [key(config.url)]: action.data }); + return Object.assign({}, state, { [key(config.url)]: action.data }); default: return state; } diff --git a/app/src/js/reducers/stats.js b/app/src/js/reducers/stats.js index 10d4524fc..68db7203f 100644 --- a/app/src/js/reducers/stats.js +++ b/app/src/js/reducers/stats.js @@ -45,15 +45,15 @@ export default function reducer (state = initialState, action) { switch (action.type) { case STATS: stats = { data: assignDate(action.data), inflight: false, error: null }; - nextState = Object.assign(state, { stats }); + nextState = Object.assign({}, state, { stats }); break; case STATS_INFLIGHT: stats = { data: state.stats.data, inflight: true, error: state.stats.error }; - nextState = Object.assign(state, { stats }); + nextState = Object.assign({}, state, { stats }); break; case STATS_ERROR: stats = { data: state.stats.data, inflight: false, error: action.error }; - nextState = Object.assign(state, { stats }); + nextState = Object.assign({}, state, { stats }); break; case COUNT: @@ -63,11 +63,11 @@ export default function reducer (state = initialState, action) { break; case COUNT_INFLIGHT: count = { data: state.count.data, inflight: true, error: state.count.error }; - nextState = Object.assign(state, { count }); + nextState = Object.assign({}, state, { count }); break; case COUNT_ERROR: count = { data: state.count.data, inflight: false, error: action.error }; - nextState = Object.assign(state, { count }); + nextState = Object.assign({}, state, { count }); break; case HISTOGRAM: @@ -77,17 +77,17 @@ export default function reducer (state = initialState, action) { data: action.data, error: null }); - nextState = Object.assign(state, { histogram }); + nextState = Object.assign({}, state, { histogram }); break; case HISTOGRAM_INFLIGHT: histogram = Object.assign({}, state.histogram); set(histogram, [serialize(action.config.qs), 'inflight'], true); - nextState = Object.assign(state, { histogram }); + nextState = Object.assign({}, state, { histogram }); break; case HISTOGRAM_ERROR: histogram = Object.assign({}, state.histogram); set(histogram, [serialize(action.config.qs), 'error'], action.error); - nextState = Object.assign(state, { histogram }); + nextState = Object.assign({}, state, { histogram }); break; } return nextState || state; From 42a6eff3a54d082857b1df4dd4fd1aed9ab977bd Mon Sep 17 00:00:00 2001 From: Matt Savoie Date: Tue, 3 Mar 2020 16:18:17 -0700 Subject: [PATCH 05/32] Add onChange property to Datepicker component. This is a callback that will be called when the datepicker is updated. It's called at the end of dispatchDropdownUpdate (Which is called when the dropdown is selected or the datepicker cleared.) It's also called at the end of handleDateTimeRangeChange, which will trigger when a date is selected or typed into one of the datetime boxes. --- .../js/components/Datepicker/Datepicker.js | 3 ++ app/src/js/components/home.js | 13 +++--- cypress/integration/main_page_spec.js | 45 +++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/app/src/js/components/Datepicker/Datepicker.js b/app/src/js/components/Datepicker/Datepicker.js index 401f156b8..d1bd5eb10 100644 --- a/app/src/js/components/Datepicker/Datepicker.js +++ b/app/src/js/components/Datepicker/Datepicker.js @@ -50,6 +50,7 @@ const updateDatepickerStateFromQueryParams = (props) => { class Datepicker extends React.PureComponent { constructor (props) { super(props); + this.onChange = props.onChange || (() => {}); this.handleDropdownChange = this.handleDropdownChange.bind(this); this.handleHourFormatChange = this.handleHourFormatChange.bind(this); this.handleDateTimeRangeChange = this.handleDateTimeRangeChange.bind(this); @@ -73,6 +74,7 @@ class Datepicker extends React.PureComponent { }); const datepickerState = getState().datepicker; this.updateQueryParams(datepickerState); + this.onChange(); }; } @@ -101,6 +103,7 @@ class Datepicker extends React.PureComponent { updatedProps.dateRange = allDateRanges.find(a => a.label === 'Custom'); this.props.dispatch({type: DATEPICKER_DATECHANGE, data: updatedProps}); this.updateQueryParams(updatedProps); + this.onChange(); } updateQueryParams (newProps) { diff --git a/app/src/js/components/home.js b/app/src/js/components/home.js index 46fce4834..8bed8ba00 100644 --- a/app/src/js/components/home.js +++ b/app/src/js/components/home.js @@ -30,7 +30,7 @@ import { errorTableRow, errorTableSortProps } from '../utils/table-config/granules'; -import { recent, updateInterval } from '../config'; +import { updateInterval } from '../config'; import { kibanaS3AccessErrorsLink, kibanaS3AccessSuccessesLink, @@ -54,6 +54,7 @@ class Home extends React.Component { this.displayName = 'Home'; this.query = this.query.bind(this); this.generateQuery = this.generateQuery.bind(this); + this.refreshQuery = this.refreshQuery.bind(this); } componentDidMount () { @@ -77,9 +78,7 @@ class Home extends React.Component { query () { const { dispatch } = this.props; // TODO should probably time clamp this by most recent as well? - dispatch(getStats({ - timestamp__from: recent - })); + dispatch(getStats()); dispatch(getCount({ type: 'granules', field: 'status' @@ -92,6 +91,10 @@ class Home extends React.Component { dispatch(listRules({})); } + refreshQuery () { + this.query(); + } + generateQuery () { return { q: '_exists_:error AND status:failed', @@ -193,7 +196,7 @@ class Home extends React.Component { Select date and time to refine your results. Time is UTC. - + diff --git a/cypress/integration/main_page_spec.js b/cypress/integration/main_page_spec.js index fd7ef9ef4..9eff92658 100644 --- a/cypress/integration/main_page_spec.js +++ b/cypress/integration/main_page_spec.js @@ -168,6 +168,51 @@ describe('Dashboard Home Page', () => { }); }); + it('modifies the UPDATES section when datepicker changes.', () => { + cy.server(); + cy.route('GET', '/stats?*timestamp__from=1233360000000*').as('stats'); + + cy.get('#Errors').contains('2'); + cy.get('#Collections').contains('5'); + cy.get('#Granules').contains('10'); + cy.get('#Executions').contains('6'); + // eslint-disable-next-line no-useless-escape + // cy.get('#Ingest Rules').contains('1'); + + cy.get('[data-cy=startDateTime]').within(() => { + cy.get('input[name=month]').click().type(1); + cy.get('input[name=day]').click().type(31); + cy.get('input[name=year]').click().type(2009); + cy.get('input[name=hour12]').click().type(0); + cy.get('input[name=minute]').click().type(0); + cy.get('select[name=amPm]').select('AM'); + }); + cy.get('[data-cy=endDateTime]').within(() => { + cy.get('input[name=month]').click().type(5); + cy.get('input[name=day]').click().type(1); + cy.get('input[name=year]').click().type(2010); + cy.get('input[name=hour12]').click().type(0); + cy.get('input[name=minute]').click().type(0); + cy.get('select[name=amPm]').select('AM'); + }); + + // cy.window().its('appStore').then((store) => { + // const startDateTime = new Date('2009-01-31T00:00:00-00:00'); + // const endDateTime = new Date('2010-05-01T12:00:00-00:00'); + // store.dispatch({ + // type: DATEPICKER_DATECHANGE, + // data: { startDateTime, endDateTime } + // }); + // }); + cy.wait('@stats'); + // cy.get('#Errors').contains('0'); + cy.get('#Collections').contains('5'); + cy.get('#Granules').contains('0'); + cy.get('#Executions').contains('0'); + // eslint-disable-next-line no-useless-escape + // cy.get('#Ingest\ Rules').contains('0'); + }); + it('Logging out successfully redirects to the login screen', () => { // Logging to debug intermittent timeouts cy.task('log', 'Start test'); From a90c82432144508ea1f415ca77008539a71d3acd Mon Sep 17 00:00:00 2001 From: Matt Savoie Date: Tue, 3 Mar 2020 16:43:16 -0700 Subject: [PATCH 06/32] reset interval when query is kicked off on refresh. --- app/src/js/components/home.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/js/components/home.js b/app/src/js/components/home.js index 8bed8ba00..08b84a5af 100644 --- a/app/src/js/components/home.js +++ b/app/src/js/components/home.js @@ -58,10 +58,8 @@ class Home extends React.Component { } componentDidMount () { - this.cancelInterval = interval(() => { - this.query(); - }, updateInterval, true); const { dispatch } = this.props; + this.cancelInterval = interval(this.query, updateInterval, true); dispatch(getCumulusInstanceMetadata()) .then(() => { dispatch(getDistApiGatewayMetrics(this.props.cumulusInstance)); @@ -77,7 +75,6 @@ class Home extends React.Component { query () { const { dispatch } = this.props; - // TODO should probably time clamp this by most recent as well? dispatch(getStats()); dispatch(getCount({ type: 'granules', @@ -92,7 +89,8 @@ class Home extends React.Component { } refreshQuery () { - this.query(); + if (this.cancelInterval) { this.cancelInterval(); } + this.cancelInterval = interval(this.query, updateInterval, true); } generateQuery () { From 677da88a3e52d42d30a4e9ca35bf93d0ba3d3b51 Mon Sep 17 00:00:00 2001 From: Juanisa McCoy Date: Wed, 4 Mar 2020 09:47:08 -0500 Subject: [PATCH 07/32] initial add and delete buttgroup setup --- app/src/css/_buttons.scss | 2 +- app/src/js/components/Button/Button.scss | 11 ++- app/src/js/components/Collections/index.js | 3 +- app/src/js/components/Collections/list.js | 78 +++++++++++++++++++--- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/app/src/css/_buttons.scss b/app/src/css/_buttons.scss index 34d4ee960..a813ff697 100644 --- a/app/src/css/_buttons.scss +++ b/app/src/css/_buttons.scss @@ -117,7 +117,7 @@ background-color: $light-green; position: relative; - &:before{ + &:before { color: $white; position: absolute; font-family: 'FontAwesome'; diff --git a/app/src/js/components/Button/Button.scss b/app/src/js/components/Button/Button.scss index 577fb89a4..cf185c525 100644 --- a/app/src/js/components/Button/Button.scss +++ b/app/src/js/components/Button/Button.scss @@ -117,7 +117,7 @@ background-color: $light-green; position: relative; - &:before{ + &:before { color: $white; position: absolute; font-family: 'FontAwesome'; @@ -178,10 +178,9 @@ content: '\f187'; font-weight: 900; left: 10px; - } &:hover { - background-color: darken($error-red, 5%); + background-color: darken($error-red, 10%); color: #fff; border: 0; } @@ -417,7 +416,7 @@ } } - + &__deletecollections:hover:before{ color: $white; position: absolute; @@ -441,7 +440,7 @@ } } - &__goto:hover:before{ + &__goto:hover:before { color: $white; position: absolute; font-family: 'FontAwesome'; @@ -463,4 +462,4 @@ color: $dark-grey; } } - } +} diff --git a/app/src/js/components/Collections/index.js b/app/src/js/components/Collections/index.js index 179a3c7d5..2c6475607 100644 --- a/app/src/js/components/Collections/index.js +++ b/app/src/js/components/Collections/index.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter, Link, Redirect, Route, Switch } from 'react-router-dom'; +import { withRouter, Redirect, Route, Switch } from 'react-router-dom'; import withQueryParams from 'react-router-query-params'; import Sidebar from '../Sidebar/sidebar'; import { strings } from '../locale'; @@ -29,7 +29,6 @@ class Collections extends React.Component {

    {strings.collections}

    - {existingCollection && {strings.add_a_collection}}
    diff --git a/app/src/js/components/Collections/list.js b/app/src/js/components/Collections/list.js index 8049bd329..b72bd22bd 100644 --- a/app/src/js/components/Collections/list.js +++ b/app/src/js/components/Collections/list.js @@ -3,14 +3,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; +import { withRouter, Link } from 'react-router-dom'; import moment from 'moment'; import { applyRecoveryWorkflowToCollection, clearCollectionsSearch, getCumulusInstanceMetadata, listCollections, - searchCollections + searchCollections, + deleteCollection } from '../../actions'; import { collectionSearchResult, @@ -18,6 +19,7 @@ import { tally, getCollectionId } from '../../utils/format'; +import { get } from 'object-path'; import { tableHeader, tableRow, @@ -28,6 +30,7 @@ import { import Search from '../Search/search'; import List from '../Table/Table'; import { strings } from '../locale'; +import DeleteCollection from '../DeleteCollection/DeleteCollection'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; const breadcrumbConfig = [ @@ -41,6 +44,9 @@ const breadcrumbConfig = [ } ]; +// const { pathname } = this.props.location; +// const existingCollection = pathname !== '/collections/add'; + class CollectionList extends React.Component { constructor () { super(); @@ -51,8 +57,12 @@ class CollectionList extends React.Component { '1 Month Ago': moment().subtract(1, 'months').format(), '1 Year Ago': moment().subtract(1, 'years').format() }; - this.generateQuery = this.generateQuery.bind(this); - this.generateBulkActions = this.generateBulkActions.bind(this); + [ + this.generateQuery, + this.generateBulkActions, + this.deleteMe, + this.errors + ].forEach((fn) => (this[fn.name] = fn.bind(this))); } componentDidMount () { @@ -76,6 +86,40 @@ class CollectionList extends React.Component { return actions; } + deleteMe () { + const { name, version } = this.props.match.params; + this.props.dispatch(deleteCollection(name, version)); + } + + errors () { + const { name, version } = this.props.match.params; + const collectionId = getCollectionId({name, version}); + return [ + get(this.props.collections.map, [collectionId, 'error']), + get(this.props.collections.deleted, [collectionId, 'error']) + ].filter(Boolean); + } + + renderDeleteButton () { + const { match: { params }, collections } = this.props; + const collectionId = getCollectionId(params); + const deleteStatus = get(collections.deleted, [collectionId, 'status']); + const hasGranules = get( + collections.map[collectionId], 'data.stats.total', 0) > 0; + + return ( + + ); + } + render () { const { list } = this.props.collections; // merge mmtLinks with the collection data; @@ -97,12 +141,22 @@ class CollectionList extends React.Component {

    {strings.all_collections} {count ? ` ${tally(count)}` : 0}

    -
    - +
    +
      +
    • + +
    • +
    • + {strings.add_a_collection} +
    • +
    • + {this.renderDeleteButton()} +
    • +
    Date: Thu, 5 Mar 2020 10:58:46 -0500 Subject: [PATCH 08/32] WIP:configuring buttongroup --- app/src/js/components/Collections/list.js | 47 +++++++++---------- .../DeleteCollection/DeleteCollection.js | 2 +- app/src/js/components/Form/_form.scss | 25 ++++++++++ app/src/js/components/locale.js | 4 +- app/src/js/utils/table-config/collections.js | 5 +- 5 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/src/js/components/Collections/list.js b/app/src/js/components/Collections/list.js index b72bd22bd..ea330b955 100644 --- a/app/src/js/components/Collections/list.js +++ b/app/src/js/components/Collections/list.js @@ -3,7 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; +import { Button } from 'react-bootstrap'; import moment from 'moment'; import { applyRecoveryWorkflowToCollection, @@ -57,12 +58,10 @@ class CollectionList extends React.Component { '1 Month Ago': moment().subtract(1, 'months').format(), '1 Year Ago': moment().subtract(1, 'years').format() }; - [ - this.generateQuery, - this.generateBulkActions, - this.deleteMe, - this.errors - ].forEach((fn) => (this[fn.name] = fn.bind(this))); + this.generateQuery = this.generateQuery.bind(this); + this.generateBulkActions = this.generateBulkActions.bind(this); + this.deleteMe = this.deleteMe.bind(this); + this.errors = this.errors.bind(this); } componentDidMount () { @@ -93,7 +92,7 @@ class CollectionList extends React.Component { errors () { const { name, version } = this.props.match.params; - const collectionId = getCollectionId({name, version}); + const collectionId = getCollectionId({ name, version }); return [ get(this.props.collections.map, [collectionId, 'error']), get(this.props.collections.deleted, [collectionId, 'error']) @@ -141,22 +140,22 @@ class CollectionList extends React.Component {

    {strings.all_collections} {count ? ` ${tally(count)}` : 0}

    -
    -
      -
    • - -
    • -
    • - {strings.add_a_collection} -
    • -
    • - {this.renderDeleteButton()} -
    • -
    +
    +
    + +
    +
    +
    +
      +
    • +
    • {this.renderDeleteButton()}
    • +
    +
    +
    - Delete + Delete Collection `Delete ${d} ${strings.collection}(s)?`; export const bulkActions = function (collections) { return [{ - text: 'Delete', + text: 'Delete Collection', action: (collectionId) => { const { name, version } = collectionNameVersion(collectionId); return deleteCollection(name, version); }, state: collections.deleted, - confirm: confirmDelete + confirm: confirmDelete, + className: 'button button--delete button--small form-group__element' }]; }; From f62c7f1aec4ef1798646b00c93464d36565eb7f7 Mon Sep 17 00:00:00 2001 From: dopeters Date: Thu, 5 Mar 2020 13:22:54 -0500 Subject: [PATCH 09/32] [CUMULUS-1758] Use react-table for resizing columns (#662) * Wrap getStats action creator in a thunk to get current date params. Use the state's current startDateTime and endDateTime when making calls to the stats endpoint. This will need an update to the stats endpoint to parse the dates properly * Test stats endpoint properly called with dates. * Refactor tables to use react-table * Update table documentation. Clean up styles * Fix ava tests. Update table documentation * Fix cypress tests * Update Changelog Co-authored-by: Matt Savoie --- CHANGELOG.md | 10 + TABLES.md | 35 ++- app/src/css/modules/_table.scss | 38 ++- app/src/js/components/Collections/granules.js | 16 +- app/src/js/components/Collections/list.js | 21 +- app/src/js/components/Collections/overview.js | 10 +- .../components/Executions/execution-status.js | 11 +- app/src/js/components/Executions/overview.js | 45 +-- app/src/js/components/Granules/granule.js | 29 +- app/src/js/components/Granules/list.js | 16 +- app/src/js/components/Granules/overview.js | 12 +- app/src/js/components/Operations/overview.js | 44 +-- app/src/js/components/Pdr/list.js | 14 +- app/src/js/components/Pdr/overview.js | 10 +- app/src/js/components/Pdr/pdr.js | 8 +- app/src/js/components/Providers/overview.js | 10 +- .../components/ReconciliationReports/list.js | 13 +- .../reconciliation-report.js | 64 ++-- .../ReconciliationReports/report-table.js | 86 +++--- app/src/js/components/Rules/overview.js | 74 +---- .../components/SortableTable/SortableTable.js | 285 ++++++++++-------- app/src/js/components/Table/Table.js | 66 ++-- app/src/js/components/Workflows/overview.js | 88 +++--- app/src/js/components/home.js | 12 +- app/src/js/utils/table-config/collections.js | 90 +++--- .../js/utils/table-config/execution-status.js | 41 ++- app/src/js/utils/table-config/executions.js | 40 +++ app/src/js/utils/table-config/granules.js | 122 +++++--- app/src/js/utils/table-config/instances.js | 43 ++- app/src/js/utils/table-config/operations.js | 27 ++ app/src/js/utils/table-config/pdr-progress.js | 68 +++-- app/src/js/utils/table-config/pdrs.js | 120 ++++---- app/src/js/utils/table-config/providers.js | 51 ++-- app/src/js/utils/table-config/queues.js | 25 +- .../table-config/reconciliation-reports.js | 98 +++--- app/src/js/utils/table-config/rules.js | 66 ++++ app/src/js/utils/table-config/services.js | 38 ++- app/src/js/utils/table-config/workflows.js | 15 + cypress/integration/collections_spec.js | 26 +- cypress/integration/executions_spec.js | 8 +- cypress/integration/granules_spec.js | 2 +- cypress/integration/providers_spec.js | 8 +- cypress/integration/reconciliation_spec.js | 48 +-- cypress/integration/rules_spec.js | 16 +- cypress/integration/workflows_spec.js | 8 +- package-lock.json | 117 ++++++- package.json | 1 + .../components/executions/execution-status.js | 4 +- test/components/granules/granule.js | 7 +- .../reconciliation-reports/report-table.js | 74 +++-- test/components/table/list-view.js | 11 +- test/utils/pdrs.js | 10 +- 52 files changed, 1170 insertions(+), 1031 deletions(-) create mode 100644 app/src/js/utils/table-config/executions.js create mode 100644 app/src/js/utils/table-config/operations.js create mode 100644 app/src/js/utils/table-config/rules.js create mode 100644 app/src/js/utils/table-config/workflows.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f3779cef..ddea50220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- **CUMULUS-1758** + - Adds the ability to resize table columns + +### Changed + +- **CUMULUS-1758** + - Updates table implementation to use [react-table](https://github.com/tannerlinsley/react-table) + ## [v1.7.0] - 2020-03-02 ### BREAKING CHANGES diff --git a/TABLES.md b/TABLES.md index f3ee41d92..9678ef3db 100644 --- a/TABLES.md +++ b/TABLES.md @@ -1,6 +1,8 @@ # Table documentation -A lot of logic is encapsulated in the table components. They are central abstractions and wrap functionality like sorting, pagination, selection, and search. The main components are `sortable-table` and `list-view`. +Our table components leverage [react-table](https://github.com/tannerlinsley/react-table/) to handle basic sorting, row selection, and resizable columns. The main components are `sortable-table` and `list-view`. + +react-table's main hook is `useTable()`. Options are passed into this hook for the desired functionality. ## `sortable-table` @@ -8,20 +10,24 @@ A basic table component that supports row selection and dumb sorting (see below) **Props** -- **primaryIdx**: Column number to apply bold typeface to. Default is `0` or far-left column. -- **data**: Array of data items. Items can be objects or arrays, depending on the accessor functions defined in `row`. -- **header**: Array of strings representing the header row. -- **row**: Array of items representing columns in each row. Items can be accessor functions with the arguments `data[k], k, data` (where `k` is the index of the current loop), or string values, ie `"collectionName"`. -- **props**: Array of property names to send to Elasticsearch for a re-ordering query. -- **sortIdx**: The current index of the `props` array to sort by. -- **order**: Either 'desc' or 'asc', corresponding to sort order. +- **tableColumns**: This is an array containing the column configuration for the table. It is the value for the `columns` key in the options object that is passed to `useTable()` + * Options for each column include: + - Header: *text or component that will render as the header* + - accessor: *key or function for obtaining value* + - id: *unqiure column id. required if accessor is function* + - disableSortBy: *set to true if the column should not be sortable* + - width: *default is 125. set value if column needs to be wider/smaller* + + * Additional options can be found [here](https://github.com/tannerlinsley/react-table/blob/master/docs/api/useTable.md#column-options) or in the documation for a specific plugin hook + +- **data**: Array of data items. Items can be any format. +- **sortIdx**: The id of the column to sort on. - **changeSortProps**: Callback when a new sort order is defined, passed an object with the properties `{ sortIdx, order }`. -- **onSelect**: Callback when a row is selected (or unselected), passed a string id corresponding to the `rowId` selector. +- **onSelect**: Callback when a row is selected (or unselected), passed an array containing the ids of all selected rows. - **canSelect**: Boolean value defining whether 1. rows can be selected and 2. to render check marks. -- **selectedRows**: Array of row ID's corresponding to rows that are currently selected. -- **rowId**: String accessor to define as that row's id, ie `collectionName`. +- **rowId**: String or function that defines a particular row's id. Passed to `useTable` options via `getRowId`. -Note, `props`, `sortIdx`, `order`, and `changeSortProps` only apply to components that implement smart searching, such as `list-view`. This base component does internal prop checking to determine whether it uses smart or dumb sorting, based on whether the above props are defined. +Note, `sortIdx` and `changeSortProps` only apply to components that implement smart searching, such as `list-view`. This base component does internal prop checking to determine whether it uses smart or dumb sorting, based on whether the above props are defined. ## `list-view` @@ -32,15 +38,12 @@ Wraps `sortable-table` and implements auto-update and smart sort. When a new sor - **list**: Parent data structure, ie `state.granules.list` or `state.collections.list`. Expected to contain `{ data, inflight, error, meta }` properties corresponding to all `list` state objects. - **dispatch**: Redux dispatch function. - **action**: Redux-style action to send, ie `listCollections`. -- **tableHeader**: Corresponds to `sortableTable#header`. -- **tableRow**: Corresponds to `sortableTable#row`. -- **tableSortProps**: Corresponds to `sortableTable#props`. - **sortIdx**: Corresponds to `sortableTable#sortIdx`. - **query**: Array of configuration objects passed to `batch-async-command`. - **rowId** Corresponds to `sortableTable#rowId`. ## Dumb vs smart sort -Dumb sorting uses `Array.sort()` within the component to re-order table data that has **already** been received from the API. Smart sorting initiates a new API request, passing the sort parameter to the server (elasticsearch) which returns a sorted response. +Dumb sorting uses react-table's built in sort functionality to sort table data that has **already** been received from the API. Smart sorting initiates a new API request, passing the sort parameter to the server (elasticsearch) which returns a sorted response. The `maunalSortBy` option passed to `useTable()` tells react-table whether we are doing server-side sorting (`true`) or letting react-table sort (`false`). Dumb sorting is for smaller, simple tables that do not need pagination. diff --git a/app/src/css/modules/_table.scss b/app/src/css/modules/_table.scss index 4f217bda2..37a1b0645 100644 --- a/app/src/css/modules/_table.scss +++ b/app/src/css/modules/_table.scss @@ -13,7 +13,7 @@ word-wrap: break-word; /* Internet Explorer 5.5+ */ } -table { +.table { width: 100%; line-height: 1.6em; @@ -21,9 +21,9 @@ table { max-width: 500px; } - thead { + .thead { background-color: $ocean-blue; - td { + .th { color: $white; font-weight: $base-font-semibold; padding: 1em 2em; @@ -31,13 +31,15 @@ table { } } - tbody { - tr { + .tbody { + .tr { border-bottom: 1px solid $lightest-grey; - td { + .td { font-size: .86em; padding: 1em 2em; vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; /*&:first-of-type { padding: 1em 0 1em 2em; }*/ @@ -82,15 +84,27 @@ table { border-radius: 10px; } -.list--granules { - .table__main-asset { - max-width: 200px; +.list--errors { + tbody tr td:first-of-type, + .tbody .tr .td:first-of-type { + max-width: 300px; } } -.list--errors { - tbody tr td:first-of-type { - max-width: 300px; +.resizer { + display: inline-block; + background: $lightest-grey; + width: 1px; + height: 100%; + position: absolute; + right: 0; + top: 0; + transform: translateX(50%); + z-index: 1; + touch-action:none; + + &.isResizing { + background: red; } } diff --git a/app/src/js/components/Collections/granules.js b/app/src/js/components/Collections/granules.js index 2106f7a7e..08b46d340 100644 --- a/app/src/js/components/Collections/granules.js +++ b/app/src/js/components/Collections/granules.js @@ -13,11 +13,9 @@ import { clearGranulesSearch } from '../../actions'; import { - tableHeader, - tableRow, - tableSortProps, simpleDropdownOption, - bulkActions + bulkActions, + tableColumns } from '../../utils/table-config/granules'; import List from '../Table/Table'; import Dropdown from '../DropDown/dropdown'; @@ -36,7 +34,6 @@ const CollectionGranules = ({ workflowOptions }) => { const { params } = match; - console.log(match); const { name: collectionName, version: collectionVersion } = params; const { pathname } = location; const { list } = granules; @@ -148,13 +145,12 @@ const CollectionGranules = ({ + rowId='granuleId' + sortIdx='timestamp' + tableColumns={tableColumns} + > { collection.mmtLink = mmtLinks[getCollectionId(collection)]; }); + const data = list.data.map((collection) => { + return { + ...collection, + mmtLink: mmtLinks[getCollectionId(collection)] + }; + }); const { count, queriedAt } = list.meta; return (
    @@ -107,17 +110,15 @@ class CollectionList extends React.Component { -
    ); diff --git a/app/src/js/components/Collections/overview.js b/app/src/js/components/Collections/overview.js index 5c0ae0b48..2dd0286b6 100644 --- a/app/src/js/components/Collections/overview.js +++ b/app/src/js/components/Collections/overview.js @@ -28,7 +28,7 @@ import statusOptions from '../../utils/status'; import List from '../Table/Table'; import Bulk from '../Granules/bulk'; import Overview from '../Overview/overview'; -import { tableHeader, tableRow, tableSortProps } from '../../utils/table-config/granules'; +import { tableColumns } from '../../utils/table-config/granules'; import { strings } from '../locale'; import DeleteCollection from '../DeleteCollection/DeleteCollection'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; @@ -287,12 +287,10 @@ class CollectionOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} - rowId={'granuleId'} - sortIdx={6} + rowId='granuleId' + sortIdx='timestamp' />
    diff --git a/app/src/js/components/Executions/execution-status.js b/app/src/js/components/Executions/execution-status.js index fe121c923..dffd77ac8 100644 --- a/app/src/js/components/Executions/execution-status.js +++ b/app/src/js/components/Executions/execution-status.js @@ -9,10 +9,7 @@ import { displayCase, fullDate, parseJson } from '../../utils/format'; import { withRouter, Link } from 'react-router-dom'; import { kibanaExecutionLink } from '../../utils/kibana'; -import { - tableHeader, - tableRow -} from '../../utils/table-config/execution-status'; +import { tableColumns } from '../../utils/table-config/execution-status'; import ErrorReport from '../Errors/report'; @@ -56,13 +53,11 @@ class ExecutionStatus extends React.Component { a.id > b.id ? 1 : -1)} dispatch={this.props.dispatch} - header={tableHeader} - row={tableRow} + tableColumns={tableColumns} rowId='id' - sortIdx={0} + sortIdx='id' props={[]} order='asc' - collapsible={true} /> ); } diff --git a/app/src/js/components/Executions/overview.js b/app/src/js/components/Executions/overview.js index 6e0e8a789..80a59cfad 100644 --- a/app/src/js/components/Executions/overview.js +++ b/app/src/js/components/Executions/overview.js @@ -3,7 +3,7 @@ import React from 'react'; import { get } from 'object-path'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { clearExecutionsFilter, filterExecutions, @@ -17,12 +17,9 @@ import { listWorkflows } from '../../actions'; import { - fromNow, - seconds, tally, lastUpdated, - displayCase, - truncate + displayCase } from '../../utils/format'; import { workflowOptions, @@ -34,37 +31,11 @@ import Dropdown from '../DropDown/dropdown'; import Search from '../Search/search'; import Overview from '../Overview/overview'; import _config from '../../config'; -import {strings} from '../locale'; +import { strings } from '../locale'; +import { tableColumns } from '../../utils/table-config/executions'; const { updateInterval } = _config; -const tableHeader = [ - 'Name', - 'Status', - 'Type', - 'Created', - 'Duration', - strings.collection_name -]; - -const tableRow = [ - (d) => {truncate(d.name, 24)}, - (d) => displayCase(d.status), - 'type', - (d) => fromNow(d.createdAt), - (d) => seconds(d.duration), - 'collectionId' -]; - -const tableSortProps = [ - 'name', - 'status', - 'type', - 'createdAt', - 'duration', - strings.collection_id -]; - class ExecutionOverview extends React.Component { constructor (props) { super(props); @@ -164,12 +135,10 @@ class ExecutionOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listExecutions} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={{}} - rowId={'name'} - sortIdx={3} + rowId='name' + sortIdx='createdAt' />
    diff --git a/app/src/js/components/Granules/granule.js b/app/src/js/components/Granules/granule.js index ac0521d97..8d517312b 100644 --- a/app/src/js/components/Granules/granule.js +++ b/app/src/js/components/Granules/granule.js @@ -38,22 +38,27 @@ import { simpleDropdownOption } from '../../utils/table-config/granules'; const { updateInterval } = _config; -const tableHeader = [ - 'Filename', - 'Link', - 'Bucket' -]; - const link = 'Link'; const makeLink = (bucket, key) => { return `https://${bucket}.s3.amazonaws.com/${key}`; }; -const tableRow = [ - (d) => d.fileName || '(No name)', - (d) => (d.bucket && d.key) ? ({d.fileName ? link : nullValue}) : null, - (d) => d.bucket +const tableColumns = [ + { + Header: 'Filename', + accessor: row => row.fileName || '(No name)', + id: 'fileName' + }, + { + Header: 'Link', + accessor: row => (row.bucket && row.key) ? ({row.fileName ? link : nullValue}) : null, + id: 'link' + }, + { + Header: 'Bucket', + accessor: 'bucket' + } ]; const metaAccessors = [ @@ -241,9 +246,7 @@ class GranuleOverview extends React.Component {
    diff --git a/app/src/js/components/Granules/list.js b/app/src/js/components/Granules/list.js index d1067545b..d810861d4 100644 --- a/app/src/js/components/Granules/list.js +++ b/app/src/js/components/Granules/list.js @@ -17,12 +17,8 @@ import { import { get } from 'object-path'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, - errorTableHeader, - errorTableRow, - errorTableSortProps, + tableColumns, + errorTableColumns, bulkActions, simpleDropdownOption } from '../../utils/table-config/granules'; @@ -116,7 +112,7 @@ class AllGranules extends React.Component { const logsQuery = { 'granuleId__exists': 'true' }; const view = this.getView(); const statOptions = (view === 'all') ? statusOptions : null; - const tableSortIdx = view === 'failed' ? 3 : 6; + const tableSortIdx = view === 'failed' ? 'granuleId' : 'timestamp'; return (
    @@ -153,12 +149,10 @@ class AllGranules extends React.Component {
    diff --git a/app/src/js/components/Granules/overview.js b/app/src/js/components/Granules/overview.js index 85a4ca289..8b725a5c2 100644 --- a/app/src/js/components/Granules/overview.js +++ b/app/src/js/components/Granules/overview.js @@ -20,9 +20,7 @@ import { import { get } from 'object-path'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, + tableColumns, simpleDropdownOption, bulkActions, recoverAction @@ -200,13 +198,11 @@ class GranulesOverview extends React.Component {
    diff --git a/app/src/js/components/Operations/overview.js b/app/src/js/components/Operations/overview.js index e491977c7..cd201e47b 100644 --- a/app/src/js/components/Operations/overview.js +++ b/app/src/js/components/Operations/overview.js @@ -1,6 +1,5 @@ 'use strict'; import React from 'react'; -// import { get } from 'object-path'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; @@ -16,32 +15,19 @@ import { listOperations, listWorkflows } from '../../actions'; -import { - fromNow, - tally, - displayCase -} from '../../utils/format'; +import { tally } from '../../utils/format'; import { workflowOptions, collectionOptions } from '../../selectors'; -// import statusOptions from '../../utils/status'; import List from '../Table/Table'; import Dropdown from '../DropDown/dropdown'; import Search from '../Search/search'; import _config from '../../config'; -// import {strings} from '../locale'; +import { tableColumns } from '../../utils/table-config/operations'; const { updateInterval } = _config; -const tableHeader = [ - 'Status', - 'Async ID', - 'Description', - 'Type', - 'Created' -]; - const statusOptions = { Running: 'RUNNING', Succeeded: 'SUCCEEDED', @@ -56,24 +42,6 @@ const typeOptions = { 'Kinesis Replay': 'Kinesis Replay' }; -// (d) => {truncate(d.name, 24)}, - -const tableRow = [ - (d) => displayCase(d.status), - (d) => d.id, - (d) => d.description, - (d) => d.operationType, - (d) => fromNow(d.createdAt) -]; - -const tableSortProps = [ - 'status', - 'id', - null, - 'operationType', - 'createdAt' -]; - class OperationOverview extends React.Component { constructor (props) { super(props); @@ -164,12 +132,10 @@ class OperationOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listOperations} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} - rowId={'id'} - sortIdx={4} + rowId='id' + sortIdx='createdAt' /> diff --git a/app/src/js/components/Pdr/list.js b/app/src/js/components/Pdr/list.js index d7c7c1b45..255c8ad5c 100644 --- a/app/src/js/components/Pdr/list.js +++ b/app/src/js/components/Pdr/list.js @@ -12,12 +12,8 @@ import { } from '../../actions'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, - errorTableHeader, - errorTableRow, - errorTableSortProps, + tableColumns, + errorTableColumns, bulkActions } from '../../utils/table-config/pdrs'; import Dropdown from '../DropDown/dropdown'; @@ -87,12 +83,10 @@ class ActivePdrs extends React.Component { list={list} dispatch={this.props.dispatch} action={listPdrs} - tableHeader={view === 'failed' ? errorTableHeader : tableHeader} - tableRow={view === 'failed' ? errorTableRow : tableRow} - tableSortProps={view === 'failed' ? errorTableSortProps : tableSortProps} + tableColumns={view === 'failed' ? errorTableColumns : tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'pdrName'} + rowId='pdrName' /> diff --git a/app/src/js/components/Pdr/overview.js b/app/src/js/components/Pdr/overview.js index ea4cbb737..21f90d293 100644 --- a/app/src/js/components/Pdr/overview.js +++ b/app/src/js/components/Pdr/overview.js @@ -7,7 +7,7 @@ import { get } from 'object-path'; import { interval, listPdrs, getCount } from '../../actions'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { bulkActions } from '../../utils/table-config/pdrs'; -import { tableHeader, tableRow, tableSortProps } from '../../utils/table-config/pdr-progress'; +import { tableColumns } from '../../utils/table-config/pdr-progress'; import List from '../Table/Table'; import Overview from '../Overview/overview'; import _config from '../../config'; @@ -75,13 +75,11 @@ class PdrOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listPdrs} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} - sortIdx={5} + tableColumns={tableColumns} + sortIdx='timestamp' query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'pdrName'} + rowId='pdrName' /> View Currently Active PDRs diff --git a/app/src/js/components/Pdr/pdr.js b/app/src/js/components/Pdr/pdr.js index b06282c53..1e2fece46 100644 --- a/app/src/js/components/Pdr/pdr.js +++ b/app/src/js/components/Pdr/pdr.js @@ -27,7 +27,7 @@ import { bool, deleteText } from '../../utils/format'; -import { tableHeader, tableRow, tableSortProps, bulkActions } from '../../utils/table-config/pdrs'; +import { tableColumns, bulkActions } from '../../utils/table-config/pdrs'; import { renderProgress } from '../../utils/table-config/pdr-progress'; import List from '../Table/Table'; import LogViewer from '../Logs/viewer'; @@ -184,12 +184,10 @@ class PDR extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'granuleId'} + rowId='granuleId' /> diff --git a/app/src/js/components/ReconciliationReports/list.js b/app/src/js/components/ReconciliationReports/list.js index 60d2c138d..8d4f8921b 100644 --- a/app/src/js/components/ReconciliationReports/list.js +++ b/app/src/js/components/ReconciliationReports/list.js @@ -9,12 +9,7 @@ import { listReconciliationReports } from '../../actions'; import { lastUpdated } from '../../utils/format'; -import { - tableHeader, - tableRow, - tableSortProps, - bulkActions -} from '../../utils/table-config/reconciliation-reports'; +import { tableColumns, bulkActions } from '../../utils/table-config/reconciliation-reports'; import Search from '../Search/search'; import List from '../Table/Table'; @@ -59,12 +54,10 @@ class ReconciliationReportList extends React.Component { list={list} dispatch={this.props.dispatch} action={listReconciliationReports} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'reconciliationReportName'} + rowId='reconciliationReportName' /> diff --git a/app/src/js/components/ReconciliationReports/reconciliation-report.js b/app/src/js/components/ReconciliationReports/reconciliation-report.js index 10cde4826..24efeb16e 100644 --- a/app/src/js/components/ReconciliationReports/reconciliation-report.js +++ b/app/src/js/components/ReconciliationReports/reconciliation-report.js @@ -12,18 +12,10 @@ import { } from '../../actions'; import _config from '../../config'; import { - tableHeaderS3Files, - tableRowS3File, - tablePropsS3File, - tableHeaderFiles, - tableRowFile, - tablePropsFile, - tableHeaderCollections, - tableRowCollection, - tablePropsCollection, - tableHeaderGranules, - tableRowGranule, - tablePropsGranule + tableColumnsS3Files, + tableColumnsFiles, + tableColumnsCollections, + tableColumnsGranules } from '../../utils/table-config/reconciliation-reports'; import Metadata from '../Table/Metadata'; @@ -217,66 +209,50 @@ class ReconciliationReport extends React.Component { diff --git a/app/src/js/components/ReconciliationReports/report-table.js b/app/src/js/components/ReconciliationReports/report-table.js index bc14ec241..ea4d90302 100644 --- a/app/src/js/components/ReconciliationReports/report-table.js +++ b/app/src/js/components/ReconciliationReports/report-table.js @@ -4,63 +4,55 @@ import Collapsible from 'react-collapsible'; import SortableTable from '../SortableTable/SortableTable'; -class ReportTable extends React.Component { - render () { - const { - collapsible, - collapseThreshold, - data, - title, - tableHeader, - tableRow, - tableProps - } = this.props; - - if (!data || !data.length) { - return null; - } - - let reportTable = ( - - ); - - if (collapsible && data.length > collapseThreshold) { - reportTable = ( - - { reportTable } - - ); - } +const ReportTable = ({ + collapsible, + collapseThreshold, + data, + title, + tableColumns +}) => { + if (!data || !data.length) { + return null; + } - return ( -
    -

    - {title} ({data.length}) -

    + const shouldCollapse = collapsible && data.length > collapseThreshold; + + let reportTable = ( + + ); + + if (shouldCollapse) { + reportTable = ( + { reportTable } -
    + ); } -} + + return ( +
    +

    + {title} ({data.length}) +

    + { reportTable } +
    + ); +}; ReportTable.propTypes = { collapsible: PropTypes.bool, collapseThreshold: PropTypes.number, data: PropTypes.array, title: PropTypes.string, - tableHeader: PropTypes.array, - tableRow: PropTypes.array, - tableProps: PropTypes.array + tableColumns: PropTypes.array }; ReportTable.defaultProps = { diff --git a/app/src/js/components/Rules/overview.js b/app/src/js/components/Rules/overview.js index a28947db5..9e495834f 100644 --- a/app/src/js/components/Rules/overview.js +++ b/app/src/js/components/Rules/overview.js @@ -1,70 +1,12 @@ 'use strict'; import React from 'react'; import PropTypes from 'prop-types'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; -import { - listRules, - enableRule, - disableRule, - deleteRule -} from '../../actions'; -import { - lastUpdated, - tally, - getCollectionId, - collectionLink, - providerLink, - fromNow -} from '../../utils/format'; +import { listRules } from '../../actions'; +import { lastUpdated, tally } from '../../utils/format'; import List from '../Table/Table'; -import { strings } from '../locale'; - -const tableHeader = [ - 'Name', - 'Provider', - strings.collection_id, - 'Type', - 'State', - 'Timestamp' -]; - -const tableRow = [ - (d) => {d.name}, - (d) => providerLink(d.provider), - (d) => collectionLink(getCollectionId(d.collection)), - 'rule.type', - 'state', - (d) => fromNow(d.timestamp) -]; - -const tableSortProps = [ - 'name', - 'provider', - null, - null, - 'state', - 'timestamp' -]; - -const bulkActions = (rules) => [{ - text: 'Enable', - action: (ruleName) => - enableRule(rules.list.data.find((rule) => rule.name === ruleName)), - state: rules.enabled, - confirm: (d) => `Enable ${d} Rule(s)?` -}, { - text: 'Disable', - action: (ruleName) => - disableRule(rules.list.data.find((rule) => rule.name === ruleName)), - state: rules.disabled, - confirm: (d) => `Disable ${d} Rule(s)?` -}, { - text: 'Delete', - action: deleteRule, - state: rules.deleted, - confirm: (d) => `Delete ${d} Rule(s)?` -}]; +import { tableColumns, bulkActions } from '../../utils/table-config/rules'; class RulesOverview extends React.Component { constructor () { @@ -100,13 +42,11 @@ class RulesOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listRules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={{}} - sortIdx={5} + sortIdx='timestamp' bulkActions={this.generateBulkActions()} - rowId={'name'} + rowId='name' /> diff --git a/app/src/js/components/SortableTable/SortableTable.js b/app/src/js/components/SortableTable/SortableTable.js index 32fa615df..e50fc9629 100644 --- a/app/src/js/components/SortableTable/SortableTable.js +++ b/app/src/js/components/SortableTable/SortableTable.js @@ -1,176 +1,197 @@ 'use strict'; -import Collapse from 'react-collapsible'; -import React, { useState } from 'react'; +import React, { + useMemo, + useEffect, + forwardRef, + useRef +} from 'react'; import PropTypes from 'prop-types'; -import { get } from 'object-path'; -import { isUndefined } from '../../utils/validate'; -import { nullValue } from '../../utils/format'; -import isEmpty from 'lodash.isempty'; -import isFunction from 'lodash.isfunction'; +import { useTable, useResizeColumns, useFlexLayout, useSortBy, useRowSelect } from 'react-table'; -const defaultSortOrder = 'desc'; -const otherOrder = { - desc: 'asc', - asc: 'desc' +/** + * IndeterminateCheckbox + * @description Component for rendering the header and column checkboxs when canSelect is true + * Taken from react-table examples + */ +const IndeterminateCheckbox = forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = useRef(); + const resolvedRef = ref || defaultRef; + + useEffect(() => { + resolvedRef.current.indeterminate = indeterminate; + }, [resolvedRef, indeterminate]); + + return ( + + ); + } +); + +IndeterminateCheckbox.propTypes = { + indeterminate: PropTypes.any, + onChange: PropTypes.func }; -const Table = ({ - primaryIdx = 0, +const SortableTable = ({ sortIdx, - order, - props, - header, - row, rowId, - data, + order = 'desc', canSelect, - collapsible, changeSortProps, + tableColumns = [], + data = [], onSelect }) => { - const [dumbState, setDumbState] = useState({ - dumbOrder: null, - dumbSortIdx: null - }); - const [selected, setSelected] = useState([]); - const isTableDumb = isUndefined(sortIdx) || !order || !Array.isArray(props); - const allChecked = !isEmpty(data) && selected.length === data.length; + const defaultColumn = useMemo( + () => ({ + // When using the useFlexLayout: + minWidth: 30, // minWidth is only used as a limit for resizing + width: 125, // width is used for both the flex-basis and flex-grow + maxWidth: 300, // maxWidth is only used as a limit for resizing + }), + [] + ); - if (isTableDumb) { - props = []; - sortIdx = dumbState.dumbSortIdx; - order = dumbState.dumbOrder; - const sortName = props[sortIdx]; - const primaryName = props[primaryIdx]; - data = data.sort((a, b) => - // If the sort field is the same, tie-break using the primary ID field - a[sortName] === b[sortName] ? a[primaryName] > b[primaryName] - : (order === 'asc') ? a[sortName] < b[sortName] : a[sortName] > b[sortName] - ); - } + const shouldManualSort = !!sortIdx; - function changeSort (e) { - if (isTableDumb) { - sortIdx = dumbState.dumbSortIdx; - order = dumbState.dumbOrder; + const { + getTableProps, + rows, + prepareRow, + headerGroups, + state: { + selectedRowIds, + sortBy + }, + } = useTable( + { + data, + columns: tableColumns, + defaultColumn, + getRowId: (row, relativeIndex) => typeof rowId === 'function' ? rowId(row) : row[rowId] || relativeIndex, + autoResetSelectedRows: false, + autoResetSortBy: false, + manualSortBy: shouldManualSort + }, + useFlexLayout, // this allows table to have dynamic layouts outside of standard table markup + useResizeColumns, // this allows for resizing columns + useSortBy, // this allows for sorting + useRowSelect, // this allows for checkbox in table + hooks => { + if (canSelect) { + hooks.visibleColumns.push(columns => [ + { + id: 'selection', + Header: ({ getToggleAllRowsSelectedProps }) => ( // eslint-disable-line react/prop-types + + ), + Cell: ({ row }) => ( + + ), + minWidth: 61, + width: 61, + maxWidth: 61 + }, + ...columns + ]); + } } + ); - const headerName = e.currentTarget.getAttribute('data-value'); - const newSortIdx = header.indexOf(headerName); - if (!props[newSortIdx]) { return; } - const newOrder = sortIdx === newSortIdx ? otherOrder[order] : defaultSortOrder; + useEffect(() => { + let selected = []; - if (typeof changeSortProps === 'function') { - changeSortProps({ sortIdx: newSortIdx, order: newOrder }); - } - if (isTableDumb) { - setDumbState({ dumbSortIdx: newSortIdx, dumbOrder: newOrder }); + for (let [key, value] of Object.entries(selectedRowIds)) { + if (value) { + selected.push(key); + } } - } - - function select (e) { - const id = (e.currentTarget.getAttribute('data-value')); - const selectedRows = selected.includes(id) - ? selected.filter(anId => anId !== id) - : [...selected, id]; - setSelected(selectedRows); if (typeof onSelect === 'function') { - onSelect(selectedRows); + onSelect(selected); } - } + }, [selectedRowIds, onSelect]); - function selectAll (e) { - if (!isEmpty(data)) { - const rowIdFn = isFunction(rowId) ? rowId : row => row[rowId]; - const allSelected = selected.length === data.length; - const selectedRows = allSelected ? [] : data.map(rowIdFn); - setSelected(selectedRows); + useEffect(() => { + const [sortProps = {}] = sortBy; + const { id, desc } = sortProps; + let sortOrder; + if (typeof desc !== 'undefined') { + sortOrder = desc ? 'desc' : 'asc'; } - } + const sortFieldId = id || sortIdx; + if (typeof changeSortProps === 'function') { + changeSortProps({ sortIdx: sortFieldId, order: sortOrder || order }); + } + }, [changeSortProps, sortBy, sortIdx, order]); return (
    -
    - - - {canSelect && - - } - {header.map((h, i) => { - let className = (isTableDumb || props[i]) ? 'table__sort' : ''; - if (i === sortIdx) { className += (' table__sort--' + order); } - return ( - - ); - })} - - - - {data.map((d, i) => { - const dataId = typeof rowId === 'function' ? rowId(d) : d[rowId]; - const checked = canSelect && selected.indexOf(dataId) !== -1; +
    +
    +
    + {headerGroups.map(headerGroup => ( +
    + {headerGroup.headers.map(column => { + return ( +
    +
    + {column.render('Header')} +
    +
    +
    + ); + })} +
    + ))} +
    +
    +
    + {rows.map((row, i) => { + prepareRow(row); return ( -
    - {canSelect && - - } - {row.map((accessor, k) => { - let className = k === primaryIdx ? 'table__main-asset' : ''; - let text; - - if (typeof accessor === 'function') { - text = accessor(d, k, data); - } else { - text = get(d, accessor, nullValue); - } - return ; +
    + {row.cells.map((cell, cellIndex) => { + const primaryIdx = canSelect ? 1 : 0; + return ( + +
    + {cell.render('Cell')} +
    +
    + ); })} - {collapsible && -
    - } - + ); })} - -
    - - {h} -
    - - {text} - -
    {JSON.stringify(d.eventDetails, null, 2)}
    -
    -
    + + ); }; -Table.propTypes = { +SortableTable.propTypes = { primaryIdx: PropTypes.number, data: PropTypes.array, header: PropTypes.array, - props: PropTypes.array, - row: PropTypes.array, - sortIdx: PropTypes.number, order: PropTypes.string, + row: PropTypes.array, + sortIdx: PropTypes.string, changeSortProps: PropTypes.func, onSelect: PropTypes.func, canSelect: PropTypes.bool, collapsible: PropTypes.bool, - rowId: PropTypes.any + rowId: PropTypes.any, + tableColumns: PropTypes.array }; -export default Table; +export default SortableTable; diff --git a/app/src/js/components/Table/Table.js b/app/src/js/components/Table/Table.js index 8de52a1aa..9e43416fe 100644 --- a/app/src/js/components/Table/Table.js +++ b/app/src/js/components/Table/Table.js @@ -3,13 +3,10 @@ import React from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -// import BatchAsyncCommand from '../BatchAsyncCommands/BatchAsyncCommands'; import ErrorReport from '../Errors/report'; import Loading from '../LoadingIndicator/loading-indicator'; import Pagination from '../Pagination/pagination'; import SortableTable from '../SortableTable/SortableTable'; -// import Timer from '../Timer/timer'; -// import TableOptions from '../TableOptions/TableOptions' // Lodash import isEmpty from 'lodash.isempty'; import isEqual from 'lodash.isequal'; @@ -24,7 +21,6 @@ class List extends React.Component { this.displayName = 'List'; this.queryNewPage = this.queryNewPage.bind(this); this.queryNewSort = this.queryNewSort.bind(this); - this.getSortProp = this.getSortProp.bind(this); this.selectAll = this.selectAll.bind(this); this.updateSelection = this.updateSelection.bind(this); this.onBulkActionSuccess = this.onBulkActionSuccess.bind(this); @@ -44,7 +40,7 @@ class List extends React.Component { queryConfig: { page: initialPage, order: initialOrder, - sort_by: this.getSortProp(initialSortIdx), + sort_by: initialSortIdx, ...(props.query || {}) }, params: {}, @@ -54,7 +50,7 @@ class List extends React.Component { } componentDidUpdate (prevProps) { - const { query, list, sortIdx } = this.props; + const { query, list } = this.props; if (!isEqual(query, prevProps.query)) { // eslint-disable-next-line react/no-did-update-set-state @@ -70,11 +66,6 @@ class List extends React.Component { queryConfig: this.getQueryConfig() })); } - - if (sortIdx !== this.state.sortIdx) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ sortIdx }); - } } queryNewPage (page) { @@ -90,16 +81,12 @@ class List extends React.Component { ...sortProps, queryConfig: this.getQueryConfig({ order: sortProps.order, - sort_by: this.getSortProp(sortProps.sortIdx) + sort_by: sortProps.sortIdx }), selected: [] }); } - getSortProp (idx) { - return this.props.tableSortProps[idx]; - } - selectAll (e) { const { rowId, list: { data } } = this.props; @@ -138,11 +125,11 @@ class List extends React.Component { return omitBy({ page: this.state.page, order: this.state.order, - sort_by: this.getSortProp(this.state.sortIdx), + sort_by: this.state.sortIdx, ...this.state.params, ...config, ...query - }, isEmpty); + }, isNil); } render () { @@ -150,31 +137,27 @@ class List extends React.Component { dispatch, action, children, - tableHeader, - tableRow, - tableSortProps, bulkActions, rowId, list, - list: { - meta: { - count, - limit - } - } + tableColumns, + data } = this.props; + const { meta, data: listData } = list; + const { count, limit } = meta; + const tableData = data || listData; const { page, sortIdx, order, selected, - queryConfig, completedBulkActions, bulkActionError } = this.state; - const primaryIdx = 0; const hasActions = Array.isArray(bulkActions) && bulkActions.length > 0; + const queryConfig = this.getQueryConfig(); + return ( <> */} {d.name}, - 'definition.Comment' -]; - -const tableSortProps = [ - 'name', - 'aws link' -]; - -class WorkflowOverview extends React.Component { - render () { - const { list } = this.props.workflows; - const count = list.data.length; - return ( -
    -
    -

    Workflow Overview

    -
    -
    -
    -

    All Workflows {count ? ` ${tally(count)}` : 0}

    -
    - {/* Someone needs to define the search parameters for workflows, e.g. steps, collections, granules, etc. }*/} - {/*
    - -
    */} - - { + const { list } = workflows; + const count = list.data.length; + return ( +
    +
    +

    Workflow Overview

    +
    +
    +
    +

    All Workflows {count ? ` ${tally(count)}` : 0}

    +
    + {/* Someone needs to define the search parameters for workflows, e.g. steps, collections, granules, etc. }*/} + {/*
    + -
    -
    - ); - } -} +
    */} + + + + + ); +}; WorkflowOverview.propTypes = { dispatch: PropTypes.func, diff --git a/app/src/js/components/home.js b/app/src/js/components/home.js index 46fce4834..3a0ae5a23 100644 --- a/app/src/js/components/home.js +++ b/app/src/js/components/home.js @@ -25,11 +25,7 @@ import { } from '../utils/format'; import List from './Table/Table'; import GranulesProgress from './Granules/progress'; -import { - errorTableHeader, - errorTableRow, - errorTableSortProps -} from '../utils/table-config/granules'; +import { errorTableColumns } from '../utils/table-config/granules'; import { recent, updateInterval } from '../config'; import { kibanaS3AccessErrorsLink, @@ -224,10 +220,8 @@ class Home extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={errorTableHeader} - sortIdx={4} - tableRow={errorTableRow} - tableSortProps={errorTableSortProps} + tableColumns={errorTableColumns} + sortIdx='timestamp' query={this.generateQuery()} /> diff --git a/app/src/js/utils/table-config/collections.js b/app/src/js/utils/table-config/collections.js index e97058473..9d73d03f0 100644 --- a/app/src/js/utils/table-config/collections.js +++ b/app/src/js/utils/table-config/collections.js @@ -6,40 +6,62 @@ import { fromNow, seconds, tally, collectionNameVersion } from '../format'; import { deleteCollection } from '../../actions'; import { strings } from '../../components/locale'; -export const tableHeader = [ - 'Name', - 'Version', - strings.granules, - 'Completed', - 'Running', - 'Failed', - 'MMT', - 'Duration', - 'Timestamp' -]; - -export const tableRow = [ - (d) => {d.name}, - 'version', - (d) => tally(get(d, 'stats.total')), - (d) => tally(get(d, 'stats.completed')), - (d) => tally(get(d, 'stats.running')), - (d) => tally(get(d, 'stats.failed')), - (d) => d.mmtLink ? MMT : null, - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'name', - 'version', - null, - null, - null, - null, - null, - 'duration', - 'timestamp' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name', + width: 175 + }, + { + Header: 'Version', + accessor: 'version' + }, + { + Header: strings.granules, + accessor: row => tally(get(row, 'stats.total')), + id: 'granules', + disableSortBy: true, + width: 100 + }, + { + Header: 'Completed', + accessor: row => tally(get(row, 'stats.completed')), + id: 'completed', + disableSortBy: true, + width: 100 + }, + { + Header: 'Running', + accessor: row => tally(get(row, 'stats.running')), + id: 'running', + disableSortBy: true, + width: 100 + }, + { + Header: 'Failed', + accessor: row => tally(get(row, 'stats.failed')), + id: 'failed', + disableSortBy: true, + width: 100 + }, + { + Header: 'MMT', + accessor: row => row.mmtLink ? MMT : null, + id: 'mmtLink', + disableSortBy: true, + width: 100 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: 'Timestamp', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; const confirmRecover = (d) => `Recover ${d} ${strings.collection}(s)?`; diff --git a/app/src/js/utils/table-config/execution-status.js b/app/src/js/utils/table-config/execution-status.js index 3bb931427..e5cefc772 100644 --- a/app/src/js/utils/table-config/execution-status.js +++ b/app/src/js/utils/table-config/execution-status.js @@ -1,18 +1,31 @@ 'use strict'; +import React from 'react'; +import Collapse from 'react-collapsible'; -import { - fullDate -} from '../format'; +import { fullDate } from '../format'; -export const tableHeader = [ - 'Id', - 'Type', - 'Timestamp', - 'Input Details' -]; - -export const tableRow = [ - (d) => d['id'], - 'type', - (d) => fullDate(d['timestamp']) +export const tableColumns = [ + { + Header: 'Id', + accessor: row => row['id'], + id: 'id' + }, + { + Header: 'Type', + accessor: 'type' + }, + { + Header: 'Timestamp', + accessor: row => fullDate(row['timestamp']), + id: 'timestamp' + }, + { + Header: 'Input Details', + accessor: row => ( + +
    {JSON.stringify(row.eventDetails, null, 2)}
    +
    + ), + id: 'eventDetails' + } ]; diff --git a/app/src/js/utils/table-config/executions.js b/app/src/js/utils/table-config/executions.js new file mode 100644 index 000000000..d3957aabf --- /dev/null +++ b/app/src/js/utils/table-config/executions.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + fromNow, + seconds, + displayCase, + truncate +} from '../../utils/format'; +import { strings } from '../../components/locale'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {truncate(row.name, 24)}, + id: 'name' + }, + { + Header: 'Status', + accessor: row => displayCase(row.status), + id: 'status' + }, + { + Header: 'Type', + accessor: 'type' + }, + { + Header: 'Created', + accessor: row => fromNow(row.createdAt), + id: 'createdAt' + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: strings.collection_name, + accessor: 'collectionId' + } +]; diff --git a/app/src/js/utils/table-config/granules.js b/app/src/js/utils/table-config/granules.js index 651c494cc..d43676619 100644 --- a/app/src/js/utils/table-config/granules.js +++ b/app/src/js/utils/table-config/granules.js @@ -21,58 +21,80 @@ import ErrorReport from '../../components/Errors/report'; import {strings} from '../../components/locale'; import Dropdown from '../../components/DropDown/simple-dropdown'; -export const tableHeader = [ - 'Status', - 'Name', - 'Published', - strings.collection_id, - 'Execution', - 'Duration', - 'Updated' +export const tableColumns = [ + { + Header: 'Status', + accessor: row => {displayCase(row.status)}, + id: 'status', + width: 100 + }, + { + Header: 'Name', + accessor: row => granuleLink(row.granuleId), + id: 'name', + width: 225 + }, + { + Header: 'Published', + accessor: row => row.cmrLink ? {bool(row.published)} : bool(row.published), + id: 'published' + }, + { + Header: strings.collection_id, + accessor: row => collectionLink(row.collectionId), + id: 'collectionId' + }, + { + Header: 'Execution', + accessor: row => link, + id: 'execution', + disableSortBy: true, + width: 90 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration', + width: 100 + }, + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {displayCase(d.status)}, - (d) => granuleLink(d.granuleId), - (d) => d.cmrLink ? {bool(d.published)} : bool(d.published), - (d) => collectionLink(d.collectionId), - (d) => link, - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'status', - 'granuleId', - 'published', - 'collectionId', - null, - 'duration', - 'timestamp' -]; - -export const errorTableHeader = [ - 'Error', - 'Type', - 'Granule', - 'Duration', - 'Updated' -]; - -export const errorTableRow = [ - (d) => , - (d) => get(d, 'error.Error', nullValue), - (d) => granuleLink(d.granuleId), - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const errorTableSortProps = [ - null, - null, - 'granuleId', - 'duration', - 'timestamp' +export const errorTableColumns = [ + { + Header: 'Error', + accessor: row => , + id: 'error', + disableSortBy: true, + width: 175 + }, + { + Header: 'Type', + accessor: row => get(row, 'error.Error', nullValue), + id: 'type', + disableSortBy: true, + width: 100 + }, + { + Header: 'Granule', + accessor: row => granuleLink(row.granuleId), + id: 'granuleId', + width: 200 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; export const simpleDropdownOption = function (config) { diff --git a/app/src/js/utils/table-config/instances.js b/app/src/js/utils/table-config/instances.js index f44f85327..20767ebf9 100644 --- a/app/src/js/utils/table-config/instances.js +++ b/app/src/js/utils/table-config/instances.js @@ -1,20 +1,31 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Instance ID', - 'Status', - 'Pending Tasks', - 'Running Tasks', - 'Available CPU', - 'Available Memory' -]; - -export const tableRow = [ - 'id', - 'status', - (d) => tally(d['pendingTasks']), - (d) => tally(d['runningTasks']), - 'availableCpu', - 'availableMemory' +export const tableColumns = [ + { + Header: 'Instance ID', + accessor: 'id' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Pending Tasks', + accessor: row => tally(row['pendingTasks']), + id: 'pendingTasks' + }, + { + Header: 'Running Tasks', + accessor: row => tally(row['runningTasks']), + id: 'runningTasks' + }, + { + Header: 'Available CPU', + accessor: 'availableCpu' + }, + { + Header: 'Available Memory', + accessor: 'availableMemory' + } ]; diff --git a/app/src/js/utils/table-config/operations.js b/app/src/js/utils/table-config/operations.js new file mode 100644 index 000000000..5dde49837 --- /dev/null +++ b/app/src/js/utils/table-config/operations.js @@ -0,0 +1,27 @@ +import { fromNow, displayCase } from '../../utils/format'; + +export const tableColumns = [ + { + Header: 'Status', + accessor: row => displayCase(row.status), + id: 'status' + }, + { + Header: 'Async ID', + accessor: 'id' + }, + { + Header: 'Description', + accessor: 'description', + disableSortBy: true + }, + { + Header: 'Type', + accessor: 'operationType' + }, + { + Header: 'Created', + accessor: row => fromNow(row.createdAt), + id: 'createdAt' + } +]; diff --git a/app/src/js/utils/table-config/pdr-progress.js b/app/src/js/utils/table-config/pdr-progress.js index a77f72426..3a743ba2a 100644 --- a/app/src/js/utils/table-config/pdr-progress.js +++ b/app/src/js/utils/table-config/pdr-progress.js @@ -23,8 +23,8 @@ function bar (completed, failed, text) { ); } -export const getProgress = function (d) { - const granules = d.stats; +export const getProgress = function (row) { + const granules = row.stats; // granule count in all states, total is 'null' in some pdrs const total = Object.keys(granules).filter(k => k !== 'total') .reduce((a, b) => a + get(granules, b, 0), 0); @@ -40,13 +40,13 @@ export const getProgress = function (d) { }; }; -export const renderProgress = function (d) { +export const renderProgress = function (row) { // if the status is failed, return it as such - if (d.status === 'failed') { - const error = get(d, 'error', nullValue); + if (row.status === 'failed') { + const error = get(row, 'error', nullValue); return ; - } else if (typeof d.status === 'undefined') return null; - const progress = getProgress(d); + } else if (typeof row.status === 'undefined') return null; + const progress = getProgress(row); return (
    {bar(progress.percentCompleted, progress.percentFailed, progress.granulesCompleted)} @@ -54,29 +54,35 @@ export const renderProgress = function (d) { ); }; -export const tableHeader = [ - 'Name', - 'Status', - 'Progress', - 'Errors', - 'PAN/PDRD Sent', - 'Discovered' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Progress', + accessor: renderProgress, + id: 'progress' + }, + { + Header: 'Errors', + accessor: row => tally(get(row, 'granulesStatus.failed', 0)), + id: 'errors' + }, + { + Header: 'PAN/PDRD Sent', + accessor: row => bool(row.PANSent || row.PDRDSent), + id: 'panPdrdSent' + }, + { + Header: 'Discovered', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {d.pdrName}, - 'status', - renderProgress, - (d) => tally(get(d, 'granulesStatus.failed', 0)), - (d) => bool(d.PANSent || d.PDRDSent), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'pdrName.keyword', - 'status.keyword', - 'progress', - null, - null, - 'timestamp' -]; diff --git a/app/src/js/utils/table-config/pdrs.js b/app/src/js/utils/table-config/pdrs.js index 580d4c1e5..44c10917c 100644 --- a/app/src/js/utils/table-config/pdrs.js +++ b/app/src/js/utils/table-config/pdrs.js @@ -6,62 +6,74 @@ import { seconds, fromNow, bool, nullValue } from '../format'; import { deletePdr } from '../../actions'; import { strings } from '../../components/locale'; -export const tableHeader = [ - 'Updated', - 'Name', - 'Status', - 'Duration', - strings.granules, - 'Processing', - 'Failed', - 'Completed' +export const tableColumns = [ + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + }, + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: strings.granules, + accessor: row => Object.keys(row.stats).filter(k => k !== 'total') + .reduce((a, b) => a + get(row.stats, b, 0), 0), + id: 'granules' + }, + { + Header: 'Processing', + accessor: row => get(row, ['stats', 'processing'], 0), + id: 'processing' + }, + { + Header: 'Failed', + accessor: row => get(row, ['stats', 'failed'], 0), + id: 'failed' + }, + { + Header: 'Completed', + accessor: row => get(row, ['stats', 'completed'], 0), + id: 'completed' + } ]; -export const tableRow = [ - (d) => fromNow(d.timestamp), - (d) => {d.pdrName}, - 'status', - (d) => seconds(d.duration), - (d) => Object.keys(d.stats).filter(k => k !== 'total') - .reduce((a, b) => a + get(d.stats, b, 0), 0), - (d) => get(d, ['stats', 'processing'], 0), - (d) => get(d, ['stats', 'failed'], 0), - (d) => get(d, ['stats', 'completed'], 0) -]; - -export const tableSortProps = [ - 'timestamp', - 'pdrName.keyword', - 'status.keyword', - null, - null, - null, - null, - null -]; - -export const errorTableHeader = [ - 'Updated', - 'Name', - 'Error', - 'PAN Sent', - 'PAN Message' -]; - -export const errorTableRow = [ - (d) => fromNow(d.timestamp), - (d) => {d.pdrName}, - (d) => get(d, 'error.Cause', nullValue), - (d) => bool(d.PANSent), - 'PANmessage' -]; - -export const errorTableSortProps = [ - 'timestamp', - 'pdrName', - null, - 'PANSent', - 'PANmessage' +export const errorTableColumns = [ + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'updated' + }, + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Error', + accessor: row => get(row, 'error.Cause', nullValue), + id: 'error' + }, + { + Header: 'PAN Sent', + accessor: row => bool(row.PANSent), + id: 'panSent' + }, + { + Header: 'PAN Message', + accessor: 'PANmessage' + } ]; const confirmDelete = (d) => `Delete ${d} PDR(s)?`; diff --git a/app/src/js/utils/table-config/providers.js b/app/src/js/utils/table-config/providers.js index 08294b691..8502af29b 100644 --- a/app/src/js/utils/table-config/providers.js +++ b/app/src/js/utils/table-config/providers.js @@ -4,29 +4,32 @@ import { Link } from 'react-router-dom'; import { fromNow } from '../format'; import { strings } from '../../components/locale'; -export const tableHeader = [ - 'Name', - 'Host', - strings.collections, - 'Global Connection Limit', - 'Protocol', - 'Last Updated' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.id}, + id: 'name' + }, + { + Header: 'Host', + accessor: 'host' + }, + { + Header: strings.collections, + accessor: 'collections' + }, + { + Header: 'Global Connection Limit', + accessor: 'globalConnectionLimit' + }, + { + Header: 'Protocol', + accessor: 'protocol' + }, + { + Header: 'Last Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {d.id}, - 'host', - 'collections', - 'globalConnectionLimit', - 'protocol', - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'id', - 'host', - 'collections', - 'globalConnectionLimit', - 'protocol', - 'timestamp' -]; diff --git a/app/src/js/utils/table-config/queues.js b/app/src/js/utils/table-config/queues.js index 23e76ab62..f856d39a9 100644 --- a/app/src/js/utils/table-config/queues.js +++ b/app/src/js/utils/table-config/queues.js @@ -1,14 +1,19 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Queue', - 'Messages Available', - 'Messages in Flight' -]; - -export const tableRow = [ - 'name', - (d) => tally(d['messagesAvailable']), - (d) => tally(d['messagesInFlight']) +export const tableColumns = [ + { + Header: 'Queue', + id: 'name' + }, + { + Header: 'Messages Available', + accessor: row => tally(row['messagesAvailable']), + id: 'messagesAvailable' + }, + { + Header: 'Messages in Flight', + accessor: row => tally(row['messagesInFlight']), + id: 'messagesInFlight' + } ]; diff --git a/app/src/js/utils/table-config/reconciliation-reports.js b/app/src/js/utils/table-config/reconciliation-reports.js index 97ed7c1fa..3d4b256d4 100644 --- a/app/src/js/utils/table-config/reconciliation-reports.js +++ b/app/src/js/utils/table-config/reconciliation-reports.js @@ -4,68 +4,64 @@ import { Link } from 'react-router-dom'; import { nullValue } from '../format'; -export const tableHeader = [ - 'Report filename' -]; - -export const tableRow = [ - (d) => {d.reconciliationReportName} -]; - -export const tableSortProps = [ - 'reconciliationReportName' +export const tableColumns = [ + { + Header: 'Report filename', + accessor: row => {row.reconciliationReportName} + } ]; export const bulkActions = function (reports) { return []; }; -export const tableHeaderS3Files = [ - 'Filename', - 'Bucket', - 'S3 Link' +export const tableColumnsS3Files = [ + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'path' + } ]; -export const tableRowS3File = [ - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue +export const tableColumnsFiles = [ + { + Header: 'GranuleId', + accessor: 'granuleId' + }, + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'path' + } ]; -export const tablePropsS3File = ['filename', 'bucket', 'link']; - -export const tableHeaderFiles = [ - 'GranuleId', - 'Filename', - 'Bucket', - 'S3 Link' -]; - -export const tableRowFile = [ - (d) => d.granuleId, - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue -]; - -export const tablePropsFile = ['granuleId', 'filename', 'bucket', 'link']; - -export const tableHeaderCollections = [ - 'Collection name' -]; - -export const tableRowCollection = [ - (d) => d.name -]; - -export const tablePropsCollection = ['name']; - -export const tableHeaderGranules = [ - 'Granule ID' +export const tableColumnsCollections = [ + { + Header: 'Collection name', + accessor: 'name' + } ]; -export const tableRowGranule = [ - (d) => d.granuleId +export const tableColumnsGranules = [ + { + Header: 'Granule ID', + accessor: 'granuleId' + } ]; -export const tablePropsGranule = ['granuleId']; diff --git a/app/src/js/utils/table-config/rules.js b/app/src/js/utils/table-config/rules.js new file mode 100644 index 000000000..36d756a7d --- /dev/null +++ b/app/src/js/utils/table-config/rules.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + enableRule, + disableRule, + deleteRule +} from '../../actions'; +import { + getCollectionId, + collectionLink, + providerLink, + fromNow +} from '../../utils/format'; +import { strings } from '../../components/locale'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name' + }, + { + Header: 'Provider', + accessor: row => providerLink(row.provider), + id: 'provider' + }, + { + Header: strings.collection_id, + accessor: row => collectionLink(getCollectionId(row.collection)), + id: 'collectionId', + disableSortBy: true + }, + { + Header: 'Type', + accessor: 'rule.type', + disableSortBy: true + }, + { + Header: 'State', + accessor: 'state' + }, + { + Header: 'Timestamp', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } +]; + +export const bulkActions = (rules) => [{ + text: 'Enable', + action: (ruleName) => + enableRule(rules.list.data.find((rule) => rule.name === ruleName)), + state: rules.enabled, + confirm: (d) => `Enable ${d} Rule(s)?` +}, { + text: 'Disable', + action: (ruleName) => + disableRule(rules.list.data.find((rule) => rule.name === ruleName)), + state: rules.disabled, + confirm: (d) => `Disable ${d} Rule(s)?` +}, { + text: 'Delete', + action: deleteRule, + state: rules.deleted, + confirm: (d) => `Delete ${d} Rule(s)?` +}]; diff --git a/app/src/js/utils/table-config/services.js b/app/src/js/utils/table-config/services.js index 51fa1ef4c..ac0d0da65 100644 --- a/app/src/js/utils/table-config/services.js +++ b/app/src/js/utils/table-config/services.js @@ -1,18 +1,28 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Service Name', - 'Status', - 'Desired Tasks', - 'Pending Tasks', - 'Running Tasks' -]; - -export const tableRow = [ - 'name', - 'status', - (d) => tally(d['desiredCount']), - (d) => tally(d['pendingCount']), - (d) => tally(d['runningCount']) +export const tableColumns = [ + { + Header: 'Service Name', + accessor: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Desired Tasks', + accessor: row => tally(row['desiredCount']), + id: 'desiredCount' + }, + { + Header: 'Pending Tasks', + accessor: row => tally(row['pendingCount']), + id: 'pendingCount' + }, + { + Header: 'Running Tasks', + accessor: row => tally(row['runningCount']), + id: 'runningCount' + } ]; diff --git a/app/src/js/utils/table-config/workflows.js b/app/src/js/utils/table-config/workflows.js new file mode 100644 index 000000000..408279aa8 --- /dev/null +++ b/app/src/js/utils/table-config/workflows.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name' + }, + { + Header: 'AWS Link', + accessor: 'definition.Comment', + id: 'template' + } +]; diff --git a/cypress/integration/collections_spec.js b/cypress/integration/collections_spec.js index d240269ec..7f74e82f1 100644 --- a/cypress/integration/collections_spec.js +++ b/cypress/integration/collections_spec.js @@ -44,7 +44,7 @@ describe('Dashboard Collections Page', () => { cy.url().should('include', 'collections'); cy.contains('.heading--xlarge', 'Collections'); - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); }); it('should display expected MMT Links for collections list', () => { @@ -52,16 +52,16 @@ describe('Dashboard Collections Page', () => { cy.wait('@getCollections'); let i = 0; - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); while (i < cmrFixtureIdx) cy.wait(`@cmr${i++}`, {timeout: 25000}); - cy.contains('table tbody tr', 'MOD09GQ') - .contains('td a', 'MMT') + cy.contains('.table .tbody .tr', 'MOD09GQ') + .contains('.td a', 'MMT') .should('have.attr', 'href') .and('eq', 'https://mmt.uat.earthdata.nasa.gov/collections/CMOD09GQ-CUMULUS'); - cy.contains('table tbody tr', 'L2_HR_PIXC') - .contains('td a', 'MMT') + cy.contains('.table .tbody .tr', 'L2_HR_PIXC') + .contains('.td a', 'MMT') .should('have.attr', 'href') .and('eq', 'https://mmt.uat.earthdata.nasa.gov/collections/CL2_HR_PIXC-CUMULUS'); }); @@ -113,7 +113,7 @@ describe('Dashboard Collections Page', () => { cy.contains('Back to Collections').click(); cy.wait('@getCollections'); cy.url().should('contain', '/collections/all'); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/collections/collection/${name}/${version}`); }); cy.task('resetState'); @@ -128,9 +128,9 @@ describe('Dashboard Collections Page', () => { // details page. cy.visit('/collections'); cy.wait('@getCollections'); - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/collections/collection/${name}/${version}`) .click(); cy.contains('.heading--large', `${name} / ${version}`); @@ -290,7 +290,7 @@ describe('Dashboard Collections Page', () => { `[data-value="${collection.name}___${collection.version}"] > .table__main-asset > a`, {timeout: 25000}).should(existOrNotExist); }); - cy.get('table tbody tr').its('length').should('be.eq', 4); + cy.get('.table .tbody .tr').its('length').should('be.eq', 4); cy.task('resetState'); }); @@ -318,7 +318,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Back to Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); it('should do nothing on cancel when deleting a collection with associated granules', () => { @@ -341,7 +341,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Back to Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); it('should go to granules upon request when deleting a collection with associated granules', () => { @@ -365,7 +365,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); }); }); diff --git a/cypress/integration/executions_spec.js b/cypress/integration/executions_spec.js index f6a65bf3d..10001ca13 100644 --- a/cypress/integration/executions_spec.js +++ b/cypress/integration/executions_spec.js @@ -64,7 +64,7 @@ describe('Dashboard Executions Page', () => { .should('be.eq', execution.collectionId); }); - cy.get('table tbody tr').as('list'); + cy.get('.table .tbody .tr').as('list'); cy.get('@list').its('length').should('be.eq', 6); }); @@ -88,7 +88,7 @@ describe('Dashboard Executions Page', () => { cy.url().should('include', 'executions'); cy.contains('.heading--xlarge', 'Executions'); - cy.get('table tbody tr td[class=table__main-asset]').within(() => { + cy.get('.table .tbody .tr .td.table__main-asset').within(() => { cy.get(`a[title=${executionName}]`).click({force: true}); }); @@ -104,14 +104,14 @@ describe('Dashboard Executions Page', () => { cy.contains('Ended:').next().should('have.text', fullDate('2019-12-13T15:16:52.582Z')); }); - cy.get('table tbody tr').as('events'); + cy.get('.table .tbody .tr').as('events'); cy.get('@events').its('length').should('be.eq', 7); cy.getFixture('valid-execution').as('executionStatus'); cy.get('@executionStatus').its('executionHistory').its('events').then((events) => { cy.get('@events').each(($el, index, $list) => { let timestamp = fullDate(events[index].timestamp); - cy.wrap($el).children('td').as('columns'); + cy.wrap($el).children('.td').as('columns'); cy.get('@columns').its('length').should('be.eq', 4); let idMatch = `"id": ${index + 1},`; let previousIdMatch = `"previousEventId": ${index}`; diff --git a/cypress/integration/granules_spec.js b/cypress/integration/granules_spec.js index d879e9f42..dfe6403bc 100644 --- a/cypress/integration/granules_spec.js +++ b/cypress/integration/granules_spec.js @@ -96,7 +96,7 @@ describe('Dashboard Granules Page', () => { .should('match', /.+ago$/); }); - cy.get('table tbody tr').as('list'); + cy.get('.table .tbody .tr').as('list'); cy.get('@list').its('length').should('be.eq', 10); }); diff --git a/cypress/integration/providers_spec.js b/cypress/integration/providers_spec.js index 0bfdb34a0..4590f4bcb 100644 --- a/cypress/integration/providers_spec.js +++ b/cypress/integration/providers_spec.js @@ -36,7 +36,7 @@ describe('Dashboard Providers Page', () => { cy.url().should('include', 'providers'); cy.contains('.heading--xlarge', 'Providers'); - cy.get('table tbody tr').its('length').should('be.eq', 2); + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); }); it('should add a new provider', () => { @@ -104,7 +104,7 @@ describe('Dashboard Providers Page', () => { cy.wait(1000); cy.contains('a', 'Back to Providers').click(); cy.wait('@getProviders'); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/providers/provider/${name}`); cy.task('resetState'); }); @@ -167,7 +167,7 @@ describe('Dashboard Providers Page', () => { // verify the provider is now gone cy.url().should('include', 'providers'); cy.contains('.heading--xlarge', 'Providers'); - cy.contains('table tbody tr', name).should('not.exist'); + cy.contains('.table .tbody .tr', name).should('not.exist'); cy.task('resetState'); }); @@ -189,7 +189,7 @@ describe('Dashboard Providers Page', () => { // provider should still exist in list cy.contains('a', 'Back to Providers').click(); cy.contains('.heading--xlarge', 'Providers'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); }); }); diff --git a/cypress/integration/reconciliation_spec.js b/cypress/integration/reconciliation_spec.js index 04bd6d574..53c5515ab 100644 --- a/cypress/integration/reconciliation_spec.js +++ b/cypress/integration/reconciliation_spec.js @@ -31,17 +31,17 @@ describe('Dashboard Reconciliation Reports Page', () => { it('displays a list of reconciliation reports', () => { cy.visit('/reconciliation-reports'); - cy.get('table tbody tr').its('length').should('be.eq', 2); - cy.contains('table tbody tr a', 'report-2020-01-14T20:25:29.026Z.json') + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:25:29.026Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:25:29.026Z.json'); - cy.contains('table tbody tr a', 'report-2020-01-14T20:52:38.781Z.json') + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:52:38.781Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:52:38.781Z.json'); }); it('displays a link to an individual report', () => { cy.visit('/reconciliation-reports'); - cy.contains('table tbody tr a', 'report-2020-01-14T20:52:38.781Z.json') + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:52:38.781Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:52:38.781Z.json') .click(); @@ -68,11 +68,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Files only in DynamoDB (35)') .next() - .find('table tbody') + .find('.table .tbody') .as('dynamoTable'); - cy.get('@dynamoTable').find('tr').its('length').should('be.eq', 35); + cy.get('@dynamoTable').find('.tr').its('length').should('be.eq', 35); cy.get('@dynamoTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('MOD09GQ.A9218123.TJbx_C.006.7667598863143'); @@ -84,11 +84,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Files only in S3 (216)') .next() - .find('table tbody') + .find('.table .tbody') .as('s3Table'); - cy.get('@s3Table').find('tr').its('length').should('be.eq', 216); + cy.get('@s3Table').find('.tr').its('length').should('be.eq', 216); cy.get('@s3Table') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('MOD09GQ.A3119781.haeynr.006.4074740546315.hdf.met'); @@ -102,9 +102,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Collections only in Cumulus (13)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusCollectionsTable'); - cy.get('@cumulusCollectionsTable').find('tr').its('length').should('be.eq', 13); + cy.get('@cumulusCollectionsTable').find('.tr').its('length').should('be.eq', 13); cy.get('@cumulusCollectionsTable') .within(() => { cy.contains('L2_HR_PIXC_test-mhs3-KinesisTestError-1574717133091___000'); @@ -114,9 +114,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Collections only in CMR (25)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrCollectionsTable'); - cy.get('@cmrCollectionsTable').find('tr').its('length').should('be.eq', 25); + cy.get('@cmrCollectionsTable').find('.tr').its('length').should('be.eq', 25); cy.get('@cmrCollectionsTable') .within(() => { cy.contains('A2_RainOcn_NRT___0'); @@ -128,9 +128,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granules only in Cumulus (7)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusGranulesTable'); - cy.get('@cumulusGranulesTable').find('tr').its('length').should('be.eq', 7); + cy.get('@cumulusGranulesTable').find('.tr').its('length').should('be.eq', 7); cy.get('@cumulusGranulesTable') .within(() => { cy.contains('MOD14A1.A4135026.ekHe8x.006.3355759967228'); @@ -139,9 +139,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granules only in CMR (365)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrGranulesTable'); - cy.get('@cmrGranulesTable').find('tr').its('length').should('be.eq', 365); + cy.get('@cmrGranulesTable').find('.tr').its('length').should('be.eq', 365); cy.get('@cmrGranulesTable') .within(() => { cy.contains('MOD14A1.A0031922.9lenyG.006.4681412227733'); @@ -153,11 +153,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granule files only in Cumulus (1)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusGranulesFilesTable'); - cy.get('@cumulusGranulesFilesTable').find('tr').its('length').should('be.eq', 1); + cy.get('@cumulusGranulesFilesTable').find('.tr').its('length').should('be.eq', 1); cy.get('@cumulusGranulesFilesTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('granule.001.1234'); @@ -169,11 +169,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granule files only in CMR (1)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrGranulesFilesTable'); - cy.get('@cmrGranulesFilesTable').find('tr').its('length').should('be.eq', 1); + cy.get('@cmrGranulesFilesTable').find('.tr').its('length').should('be.eq', 1); cy.get('@cmrGranulesFilesTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('granule.002.5678'); diff --git a/cypress/integration/rules_spec.js b/cypress/integration/rules_spec.js index 5fa96ea78..173991aeb 100644 --- a/cypress/integration/rules_spec.js +++ b/cypress/integration/rules_spec.js @@ -29,8 +29,8 @@ describe('Rules page', () => { cy.visit('/'); cy.get('nav').contains('Rules').click(); cy.url().should('include', '/rules'); - cy.get('table tbody tr').should('have.length', 1); - cy.contains('table tr', testRuleName) + cy.get('.table .tbody .tr').should('have.length', 1); + cy.contains('.table .tr', testRuleName) .within(() => { cy.contains(testProviderId); cy.contains(testCollectionId); @@ -41,7 +41,7 @@ describe('Rules page', () => { it('display a rule with the correct data', () => { cy.visit('/rules'); - cy.contains('table tr a', testRuleName) + cy.contains('.table .tr a', testRuleName) .click(); cy.url().should('include', `/rules/rule/${testRuleName}`); cy.get('.metadata__details') @@ -92,7 +92,7 @@ describe('Rules page', () => { cy.contains('.modal-footer button', 'Confirm Rule').click(); cy.contains('.heading--xlarge', 'Rules'); - cy.contains('table tbody tr a', ruleName) + cy.contains('.table .tbody .tr a', ruleName) .and('have.attr', 'href', `/rules/rule/${ruleName}`).click(); cy.contains('.heading--xlarge', 'Rules'); @@ -113,7 +113,7 @@ describe('Rules page', () => { it('editing a rule and returning to the rules page should show the new changes', () => { cy.visit('/rules'); - cy.contains('table tbody tr a', testRuleName) + cy.contains('.table .tbody .tr a', testRuleName) .and('have.attr', 'href', `/rules/rule/${testRuleName}`) .click(); @@ -134,7 +134,7 @@ describe('Rules page', () => { cy.contains('a', 'Back to Rules').click(); cy.contains('.heading--large', 'Rule Overview'); - cy.contains('table tr', testRuleName) + cy.contains('.table .tr', testRuleName) .within(() => { cy.contains(provider) .should('have.attr', 'href', `/providers/provider/${provider}`); @@ -143,7 +143,7 @@ describe('Rules page', () => { it('deleting a rule should remove it from the list', () => { cy.visit('/rules'); - cy.contains('table tr', testRuleName) + cy.contains('.table .tr', testRuleName) .within(() => { cy.get('input[type="checkbox"]').click(); }); @@ -154,7 +154,7 @@ describe('Rules page', () => { .get('button') .contains('Confirm') .click(); - cy.contains('table tr a', testRuleName) + cy.contains('.table .tr a', testRuleName) .should('not.exist'); cy.task('resetState'); }); diff --git a/cypress/integration/workflows_spec.js b/cypress/integration/workflows_spec.js index 6ab578632..eff8c96d8 100644 --- a/cypress/integration/workflows_spec.js +++ b/cypress/integration/workflows_spec.js @@ -27,10 +27,10 @@ describe('Dashboard Workflows Page', () => { cy.url().should('include', 'workflows'); cy.contains('.heading--xlarge', 'Workflows'); - cy.get('table tbody tr').its('length').should('be.eq', 2); - cy.contains('table tbody tr a', 'HelloWorldWorkflow') + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); + cy.contains('.table .tbody .tr a', 'HelloWorldWorkflow') .should('have.attr', 'href', '/workflows/workflow/HelloWorldWorkflow'); - cy.contains('table tbody tr a', 'SecondTestWorkflow') + cy.contains('.table .tbody .tr a', 'SecondTestWorkflow') .should('have.attr', 'href', '/workflows/workflow/SecondTestWorkflow'); }); @@ -41,7 +41,7 @@ describe('Dashboard Workflows Page', () => { cy.url().should('include', 'workflows'); cy.contains('.heading--xlarge', 'Workflows'); - cy.contains('table tbody tr a', workflowName) + cy.contains('.table .tbody .tr a', workflowName) .should('have.attr', 'href', `/workflows/workflow/${workflowName}`) .click(); diff --git a/package-lock.json b/package-lock.json index c38d52608..a8cccb9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,17 @@ "source-map": "^0.5.0" }, "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -132,6 +143,19 @@ "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-annotate-as-pure": { @@ -261,7 +285,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -279,6 +302,19 @@ "@babel/template": "^7.8.6", "@babel/types": "^7.8.6", "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-optimise-call-expression": { @@ -293,8 +329,7 @@ "@babel/helper-plugin-utils": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" }, "@babel/helper-regex": { "version": "7.8.3", @@ -328,6 +363,19 @@ "@babel/helper-optimise-call-expression": "^7.8.3", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-simple-access": { @@ -845,7 +893,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz", "integrity": "sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", @@ -989,6 +1036,19 @@ "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/preset-react": { @@ -1034,6 +1094,19 @@ "@babel/code-frame": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/traverse": { @@ -1053,6 +1126,17 @@ "lodash": "^4.17.13" }, "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1065,10 +1149,9 @@ } }, "@babel/types": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", - "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", - "dev": true, + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -7243,8 +7326,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -13438,8 +13520,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "1.8.0", @@ -15478,6 +15559,14 @@ } } }, + "react-table": { + "version": "7.0.0-rc.16", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.0.0-rc.16.tgz", + "integrity": "sha512-2wcGKO56gKlE1IwJnFatCn1yzHIAwRgbTJJQeoSiERJ/Ed6VeLdJLMJkLuSnxZCwD8CjhZD+C/iXEaFubJVW3A==", + "requires": { + "@babel/plugin-transform-runtime": "^7.8.3" + } + }, "react-test-renderer": { "version": "16.13.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.0.tgz", @@ -16106,7 +16195,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -18555,8 +18643,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", diff --git a/package.json b/package.json index 20d8cfd8a..c9c3728e0 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "react-router-dom": "^5.1.2", "react-router-query-params": "^1.0.3", "react-router-scroll-4": "^1.0.0-beta.2", + "react-table": "^7.0.0-rc.16", "recompose": "^0.30.0", "redux": "^3.6.0", "redux-devtools-extension": "^2.13.8", diff --git a/test/components/executions/execution-status.js b/test/components/executions/execution-status.js index c49419722..c06a4544c 100644 --- a/test/components/executions/execution-status.js +++ b/test/components/executions/execution-status.js @@ -33,11 +33,11 @@ test('Cumulus-690 Execution Status shows workflow task and version information', /> ); - const sortableTable = executionStatusRendered.find('Table'); + const sortableTable = executionStatusRendered.find('SortableTable'); t.is(sortableTable.length, 1); const sortableTableWrapper = sortableTable.dive(); - const moreDetails = sortableTableWrapper.find('pre'); + const moreDetails = sortableTableWrapper.find('Cell').first().find('pre'); const selectedTasks = moreDetails.findWhere((jsonDetails) => { const parsedDetailsOutput = JSON.parse(jsonDetails.text()).output; if (parsedDetailsOutput && parsedDetailsOutput.meta && parsedDetailsOutput.meta.workflow_tasks) { diff --git a/test/components/granules/granule.js b/test/components/granules/granule.js index 31d475307..8eb295e64 100644 --- a/test/components/granules/granule.js +++ b/test/components/granules/granule.js @@ -46,8 +46,11 @@ test('CUMULUS-336 Granule file links use the correct URL', function (t) { /> ); - const sortableTable = granuleOverview.find('Table'); + const sortableTable = granuleOverview.find('SortableTable'); t.is(sortableTable.length, 1); const sortableTableWrapper = sortableTable.dive(); - t.is(sortableTableWrapper.find('tbody tr td a[href="https://my-bucket.s3.amazonaws.com/my-key-path/my-name"]').length, 1); + t.is(sortableTableWrapper + .find('.tbody .tr') + .find('Cell').at(1).dive() + .find('a[href="https://my-bucket.s3.amazonaws.com/my-key-path/my-name"]').length, 1); }); diff --git a/test/components/reconciliation-reports/report-table.js b/test/components/reconciliation-reports/report-table.js index 16c85a8a2..75444aeb0 100644 --- a/test/components/reconciliation-reports/report-table.js +++ b/test/components/reconciliation-reports/report-table.js @@ -12,6 +12,26 @@ import { nullValue } from '../../../app/src/js/utils/format'; configure({ adapter: new Adapter() }); +const tableColumns = [ + { + Header: 'GranuleId', + accessor: 'granuleId' + }, + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'link' + } +] + const tableData = [{ granuleId: 'g-123', filename: 'filename.txt', @@ -19,22 +39,6 @@ const tableData = [{ path: 's3://some-bucket/filename.txt' }]; -export const tableHeader = [ - 'GranuleId', - 'Filename', - 'Bucket', - 'S3 Link' -]; - -export const tableRow = [ - (d) => d.granuleId, - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue -]; - -export const tableProps = ['granuleId', 'filename', 'bucket', 'link']; - test('render nothing when no data is provided', function (t) { const report = shallow( ); @@ -70,17 +72,17 @@ test('render basic table', function (t) { const Table = report.find(SortableTable).dive(); - const headerRow = Table.find('tr').first(); - t.is(headerRow.children('td').at(0).text(), 'GranuleId'); - t.is(headerRow.children('td').at(1).text(), 'Filename'); - t.is(headerRow.children('td').at(2).text(), 'Bucket'); - t.is(headerRow.children('td').at(3).text(), 'S3 Link'); - - const dataRow = Table.find('tr').at(1); - t.is(dataRow.children('td').at(0).text(), 'g-123'); - t.is(dataRow.children('td').at(1).text(), 'filename.txt'); - t.is(dataRow.children('td').at(2).text(), 'some-bucket'); - t.true(dataRow.children('td').at(3).contains( + const headerRow = Table.find('.thead .tr').first(); + t.is(headerRow.find('.th > div:first-child').at(0).text(), 'GranuleId'); + t.is(headerRow.find('.th > div:first-child').at(1).text(), 'Filename'); + t.is(headerRow.find('.th > div:first-child').at(2).text(), 'Bucket'); + t.is(headerRow.find('.th > div:first-child').at(3).text(), 'S3 Link'); + + const dataRow = Table.find('.tbody .tr').first(); + t.is(dataRow.find('Cell').at(0).dive().text(), 'g-123'); + t.is(dataRow.find('Cell').at(1).dive().text(), 'filename.txt'); + t.is(dataRow.find('Cell').at(2).dive().text(), 'some-bucket'); + t.true(dataRow.find('Cell').at(3).dive().contains( Link )); }); @@ -96,9 +98,7 @@ test('render buttons to show/hide table when configured', function (t) { ); @@ -122,9 +122,7 @@ test('do not render buttons to show/hide table when data length is less than thr ); @@ -146,9 +144,7 @@ test('do not render buttons to show/hide table when disabled', function (t) { diff --git a/test/components/table/list-view.js b/test/components/table/list-view.js index d4925843f..7ce4ddea5 100644 --- a/test/components/table/list-view.js +++ b/test/components/table/list-view.js @@ -7,10 +7,7 @@ import {mount, configure} from 'enzyme'; import {listGranules} from '../../../app/src/js/actions'; import { List } from '../../../app/src/js/components/Table/Table'; import Timer from '../../../app/src/js/components/Timer/timer.js'; -import { - errorTableHeader, - errorTableRow, errorTableSortProps -} from '../../../app/src/js/utils/table-config/granules'; +import { errorTableColumns } from '../../../app/src/js/utils/table-config/granules'; configure({ adapter: new Adapter() }); @@ -26,10 +23,8 @@ test('table should properly initialize timer config prop', async (t) => { list={list} dispatch={dispatch} action={listGranules} - tableHeader={errorTableHeader} - sortIdx={4} - tableRow={errorTableRow} - tableSortProps={errorTableSortProps} + tableColumns={errorTableColumns} + sortIdx='timestamp' query={query} />, { diff --git a/test/utils/pdrs.js b/test/utils/pdrs.js index 7e79f34f7..3b5003126 100644 --- a/test/utils/pdrs.js +++ b/test/utils/pdrs.js @@ -1,6 +1,6 @@ 'use strict'; import test from 'ava'; -import {tableRow} from '../../app/src/js/utils/table-config/pdrs.js'; +import {tableColumns} from '../../app/src/js/utils/table-config/pdrs.js'; import {getProgress} from '../../app/src/js/utils/table-config/pdr-progress.js'; const pdr = { @@ -31,8 +31,8 @@ test('test pdr-progress.js getProgress', function (t) { }); test('test pdrs.js tableRow', function (t) { - t.is(tableRow[4](pdr), 4); - t.is(tableRow[5](pdr), pdr.stats.processing); - t.is(tableRow[6](pdr), pdr.stats.failed); - t.is(tableRow[7](pdr), pdr.stats.completed); + t.is(tableColumns[4].accessor(pdr), 4); + t.is(tableColumns[5].accessor(pdr), pdr.stats.processing); + t.is(tableColumns[6].accessor(pdr), pdr.stats.failed); + t.is(tableColumns[7].accessor(pdr), pdr.stats.completed); }); From 3d285dc435acf84e7a3f38eb86f8254cd598ed0e Mon Sep 17 00:00:00 2001 From: dopeters Date: Thu, 5 Mar 2020 14:36:06 -0500 Subject: [PATCH 10/32] Add ability to render custom components in bulkActions (#666) * Remove histogram * Release branch 1.7 into MASTER (#658) * add initial sidebar styling, start on Bootstrap customization, refactor css variables * finish sidebar default styling * update readme docs * fix cypress test errors * correct mixin file name * adding granule option to sidebar * adding new styles for edit feature * stylings for edit window and buttons * fix cypress tests * Adding granule options to collections page * Adding apply granule workflow * Update _jsonwindow.scss Remove height restriction. * Update _jsonwindow.scss Add override to .ace_scroller for last line to show in content window. * Update _jsonwindow.scss Remove padding and margins for ace editor. * CUMULUS-1670: Fix provider update (#612) * update provider reducer to remove unnecessary properties to fix provider update * update getRule action to use individual rule GET endpoint * remove extra properties set by rule reducer and update rule disable/enable actions * update CHANGELOG * Add collection dropdown to collection page Added a simple dropdown to the collection (detail) page containing all collection IDs, sorted in ascending (case-insensitive) order, to allow the user to view a different collection without having to navigate back to the collections list page and choose it. CUMULUS-1501 * initial styling for dropdown * Add bulk granule support * default value * dropdown styling * made the bulk granule modal * default value in modal * styling modal and buttons * finish dropdown styling * fix dropdown on granule pages * fix collection cypress testissue * working bulk granules * styling fix for filters * fix import directory link * add asyncID to execution page * fix test * remove old code * operations page * add close to x button * Updated changelog * remove old comments * Adding operations page * Change enviroment variable for fakeapi. Update test ports. (#617) Before, `PORT` was used as both the env var for overriding the fake api port (normally 5001) as well as the dashboard server port (normally 3000). This changes the envvar to FAKEAPIPORT and updates the tests to select the FAKEAPIPORT rather than being hard coded to 5001. This lets us run two version of the dashboard at once during testing. * start building and styling Collection Delete Modal component * working on delete button and fixing button.scss as a component styling instead of general catch all * adding bootstrap overrides * bootstrap gulp-sass errors fixed * building modal components * finish modal component setup * Fix lint errors CUMULUS-1520 * Fix default values for config vars Changed default values from `false` to empty strings for the config vars `kibanaRoot` and `esRoot` since they are expected to be string values, not booleans. For `kibanaRoot`, using `false` as the default was causing the following error when opening the Bulk Granules modal (from the collection overview page): Warning: Received `false` for a non-boolean attribute `href`. CUMULUS-1520 * Fix scrolling warning on Bulk Granules modal This fixes the following warning that appeared when showing the Bulk Granules modal from the collection overview page: Automatically scrolling cursor into view after selection change this will be disabled in the next version set editor.$blockScrolling = Infinity to disable this message CUMULUS-1520 * Fix TypeError when closing Bulk Granules modal This fixes the following console error that appeared when clicking the close button (x) in the upper right corner of the Bulk Granules modal: Uncaught TypeError: _this.props.onHide is not a function CUMULUS-1520 * Fix ParseError Commented out import of scss file until build is updated to support it. CUMULUS-1520 * Goto granules on delete of collection with granules When clicking Delete on collection overview page, new modal sequence is shown, including prompting user when collection has associated granules. In addition, changed test data such that a rule is associated with collection MOD09GQ instead of MOD09GK. Otherwise test coverage is not possible because MOD09GK has granules associated with it. By associating a rule with one of them and granules with the other, we can properly test deleting a collection with a rule as well as test deleting a collection with granules. CUMULUS-1520 * Ensure inputs are visible before clicking An attempt to fix test that fails in CI (perhaps sporadically), but appears to pass reliably when running Cypress tests locally. CUMULUS-1520 * update styling for modal and buttons * adding options to operation page * add gotogranules styling * fix modal closeButton issue * operations filter and searches * add async search to executions page * add to changelog * initial migration gulp to webpack * fix node module compile errors * reconfigure files for webpack setup * use npm instead of yarn latest npm for using package-lock.json fix packages for security add back audit * rebuilt webpack config and fixed sass errors * fix compiling errors * setup webpack configs for dev and prod * upgrade node to 10.16.3 and packages * Update execution-status-graph.js * CUMULUS-1463: Dashboard Home - Implement Datepicker add Datepicker component rename folder fix lint initial styling and structure of datepicker ava ignoreExtensions css jpg files styling selectors finish dropdown and radio style and start cal and clock add clear button save user selection save user selection add getDateTimeRange method update changelog add clear button add calendar styling onClick on clear button fix z-index on clear button add tests fix main page test * fix config errors * eslint fixes and babel7config update * add react-router-dom to replace react-router * fix export style for file to file * fix export style * refactor execution graph code * update webpack config to fix undefined errors * change local port to 3000; * add EnvironmentPlugin to webpack to handle environment variables * add react-router-query-params package * update Oauth component to use new query params package * update properties for react-router@4 changes * change history from reactrouter to connectedrouter * build new store w react-router4 * update index and App to modern React setup * fix ref to main * fix reducers ref * add Datepicker component * Fix log search on granule detail page (#622) The UI no longer breaks (producing a blank page) when the user types in the log search box on the granule details page. CUMULUS-1679 * initial config.js export webpack fixes * fix remaining config webpack warnings * fixing some react-router errors * add withRouter to path Components * add switch for nested routes * adding react-router-query-params for query errors * fix property errors * fix component import references in App.js * fix import dependency in granulesoverview * fix function and query undefined values * update scss file node routes * rename logo.png * webpack tweaks * fix template for htmlwebpackplugin * general CSS fix * setup enviro webpack obj configs * fix logo path * fix collections modal btn stylings * styling granule redirect correct * Button style fix pr suggestion (#627) * Fix "should delete a collection" * fix 'should fail deleting a collection with an associated rule' * fixes 'should do nothing on cancel when deleting....' * fix 'should go to granules upon request when deleting....' * Removes comment. * Fix granules visitGranulesPage. * Can I save snapshots. * WIP: Refactoring with Switch * refactor routes * add normalize and reset CSS overrides * add @cypress/webpack-preprocessor@4.1.1 and configs * fix reset for table * webpack plugin changes * Fix Collection Overview * Changed async Ops response * Remove console.log calls used for debugging * Correct granule status route paths * Fix collection granule links * Correct provider links * Correct workflow links * Correct rule links * fixing filter and search * Correct reconciliation report links * Correct execution links * lint fix * updated changelog * Cumulus 1102 add local express api app (#625) * Adds docker compose files and test packages. Since we need pre-release cumulus packages, use flamingbear/release candidates. * Use pre-release cumulus/api and upgrade cypress. Need a prerelease cumuls/api package so use a personal package for now. Upgrades Cypress to 3.8.0 * Upgrade vulnerable package eslint Use legacy indentation for simplicity in upgrading. reformat json correctly * Update login helper to follow current auth API. * renames api => localApi for clarity. * Update for npm instead of yarn. * Commit lockfile too. * Checkpointing - Adds seeding of local API Moves and updates some fixtures. Adds software for seeding local dynamo database and Elasticsearch. * Adds script to seed DynamoDB & Elasticsearch This might have to be moved around, but will allow a user to seed the database and then run the api with the seeded data. * Fixes for cypress tests. * Pass providers_spec. Had to remove and change some of the "Concurrent Connection Limit" fields since the dashboard doesn't appear to support that any more. * Updates some specs and add's a lot of TODOs. Still have some basic cypress problems, but need to check other localApi issues first. * Update for new reconcilation-reports. Updated cumulus-core to load two sample reconcilation-reports that are served by the localApi. * Added additional sample workflow. * Fix capitalization replacement. * Removes fake-api (not completely, execution fixtures remain for future work) Removes fake-api * updated instructions on running integration tests. * Fix bad eslint merge * Fix bad eslint merge part 2 * First stab at testing cypress in docker-compose. docker-compose -f docker-compose-test-stack.yml -p cumulusstack up ` --abort-on-container-exit --exit-code-from e2e This will fire up everything needed to run end to end tests with cypress in containers for testing locally or on CIRCLE-CI. * Cleans up docker compose files. replaces old cumulus files with single stack that can be used to start up any single part of the stack. * Try to run compose directly in circle * Fix path for docker-compose.yml * Fix path for docker-compose.yml - redux * NOPE * move localApi DIR temporally * rename back to localAPI * and fix that too. * Separate out validation and e2e tests. Validation needs to be looked at, it's not running cypress tests, but just needs a running dashboard to run against. * I can't type. * copy pasta fix. * fixup! copy pasta fix. * Don't think this is useful. * Just testing things. Curious how this output can be used. * see if npm can replace the docker-compose command. * match your names. * D'oh * Update execution_spec for single execution test. * Show execution logs. * Migrate node to 10.16.3 * Fix limited-info execution spec. requires fix to api. * Limited execution spec uses fixture instead of localAPI. This is clearer what is being returned from the logs endpoint. * Fix last execution test. * Update compose files and fix a couple of tests. * Import api serveUtils from deep in package. Don't require the @cumulus/api to export all of the functions, grab the seed helpers directly from the files. * Clarifies README for docker development. * Adds comments for upcoming ticket Fixes one test, and commits cypres-pipe, but that may have to come out. * remove cumulus internals from seeding the database. * Use alpha version of cumulus/api * upgrade node on ci * Update package-lock for new api * Separate out the compose files and update API. alpha1 should have correct stepfunction port numbers. * Wait for s3 in localstack. * Fix validation tests with npm script. * Try to update README. adds back some removed npm scripts. * remove comment. I think it * Fix validation test call. * Removes cypress-pipe We may need this but don't yet. * fix eslint * Upgrade Cypress * Add Dashboard diagram. * Adds comments * update localAPI README * link on github this way? * Adds info to the localAPI README * Hard Coded waits. I have added 4 sec to the run time by putting in hard coded cy.waits. I think what is happening is that the DELETE is returning successful but that the containers elastic/dynamo are not updated when the GET /collections/limit=* is run, and we still see 5 collections. Although, there is still the weirdness with the stuff not being visible in the dom, but still visible to our eyes. * Need to run all of the tests. * And you need to wait for numbers not strings. * hard wait for second fix. * Take out some hard waits * get rid only. and hard waits * Hard wait() for certain elements. I think this is related to the cypress-pipe solution where the tests are acting faster than the listeners can be attached, but I wasn't able to come up with a quick solution using cypress-pipe. This should help out a lot, but is technically the wrong thing to do. * Cargocult waiting. * Bump version on @cumulus/api v1.18.0 was released with the changes that were in 1.17.1-alpha.1 * Update package-locks * Add back hard cy.waits() * Only resetState on tests that modify it. This is a way to speed up the tests, but makes them harder to understand. A test writer has to know to reset state after any tests that modifies it. * Revert "Only resetState on tests that modify it." This reverts commit e8b9cf09a9addabd0b8a718b05c31161a0b3862a. * Clarify npm commands on the diagram. * Hopefully better words on the README.md file. * PR feedback on documenation. * WIP:T/S collections path undefined issue * Changing dropdown options * Correct Collection Sidebar links * add fontawesome to main.scss * add sourcemap to style configs * fix fontawesome config and import * remove sourcemap from style-loader * WIP:T/S search.js 'search' undefined issues * WIP:T/S search.js 'search' undefined issues * fontawesome test * switch ref to FA v4 from FA v5 * update notation for Font Awesome * change from yarn to npm * Update search and dropdown query params * add fontawesome v4 vendor imports * Additional fixes for merging develop * Fix empty Granules tab in Collections * Fix lint errors * Fix routing and route params * Remove hash due to use of new react routing lib * Update D3 dependencies to remove vulnerabilities * Fix auth routing * Fix PropTypes error * Fix dropdown to populate from query param * Fix TypeError: Cannot destructure property 'ruleName' of 'params' as it is undefined. * Fix cypress tests. Change router prop to history * Fix failing collection switching cypress test * babel config fixes for ava * add babel runtime * Fix "ReferenceError: window is not defined" when running ava tests * [CUMULUS-1549] Update Logs link on header to point to Kibana * Stablize cypress collection tests (hopefully) * fix npm build issue * Fix missing DOM for ava tests * Fix several ava tests failing due to webpack/react changes * WIP: Fix last 2 ava test failures from webpack/react changes * Fix edit-record test * Fix incorrect import for CALL_API that caused test failure * [CUMULUS-1549] Remove logs page * [CUMULUS-1549] Add CALL_API type * [CUMULUS-1549] Remove require of Logs component from App.js * [CUMULUS-1549] Update logs link for Kibana to automatically show correct index * Add confirmation modal when adding a rule * Wait for ES indexing to reduce intermittent failures * Fix cypress test for adding rule * Update changelog * [CUMULUS-1549] Fix import of CALL_API type in actions/store test * fixing search on operations page * [CUMULUS-1549] Add execution log Kibana link to Execution page * [CUMULUS-1549] Update errors link on home page * [CUMULUS-1549] Update Execution page to only show one link to execution logs when Kibana is set * [CUMULUS-1549] Fix errors link when Kibana is not set * [CUMULUS-1549] Update pathObject to just path to be more accurate * update README and CHANGELOG * add delete entry for CHANGELOG * Add copy collection button * Add redux logger * Fix lint errors * Update app/src/js/utils/kibana.js set default query language * Update app/src/js/utils/kibana.js sets default query language * Add missing Modal import * Fix merge problems. * order CHANGELOG.md * Don't eslint fix without knowing what it does. * Fix cypress test * Remove commented out import * Update changelog * Update useEffect dependency list * Make getBaseRoute more concise * Remove hardcoded wait from cypress test * Remove dependency array so redirect can happen * Move status out of useEffect and add dependencies * removing nonsense code * Updates Datepicker to react to state and update the state. (#632) * Jenny's changes for datetimepicker * fix favicon and remove duplicate file. it looks like table-config/granules.js was moved, but not deleted. * Most of the Dropdown / Datepicker is working with state. Still have plenty to work on. * Adds support for hour format. and URL state * Quckie hack to see if dispatching state update works spoiler alert. It does. * Need to write tests and clean up. * Adds reducer tests. * Adds cypress integration tests for Datepicker * Clean up console.logs; remove unused exports * Handle invalid date on changes. * refactored long name. handleDateRangeDropdownChange => handleDropdownChange * Fix changelog problem * force clike the 24 hour selector. I'm probaby clicking the wrong element since this is a 0x0pix thing. * TIL fiter << find for selecting out of an array * update cypress and sinon Clarify droptown unit test. Uses sinon fake timer now so that you can know what the dropdown values should be. * cypress hasn't updated docker hub with 4.0.2 yet. * Can we enable pipelines on this project? * downgrade cypress need to match versions with the available docker image. * update package-lock * fixup! Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state * Update helper package, add PORT env option to webpack * Update labels, removes unused reducer export. * Gets QueryParams working. * fix datepicker styles * removing nonsense code * Fix removing params. * Make ava ignore styles. Also begin refactor of datepicker utils. * Cypress hasn't updated their dockerfiles yet. need exactly 4.0.1 * hide calendar and clock overlays * add cal and clock pickers back Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> * [CUMULUS-1738] Add provider form in dashboard is missing the global connection limit (#637) * [CUMULUS-1738] Update Schema to recognize 'integer' as a number field * [CUMULUS-1738] Update provider cypress tests to test for Global Connection limit * Cumulus 1729 filter api calls with correct date information (#639) * Jenny's changes for datetimepicker * fix favicon and remove duplicate file. it looks like table-config/granules.js was moved, but not deleted. * Most of the Dropdown / Datepicker is working with state. Still have plenty to work on. * Adds support for hour format. and URL state * Quckie hack to see if dispatching state update works spoiler alert. It does. * Need to write tests and clean up. * Adds reducer tests. * Adds cypress integration tests for Datepicker * Clean up console.logs; remove unused exports * Handle invalid date on changes. * refactored long name. handleDateRangeDropdownChange => handleDropdownChange * Fix changelog problem * force clike the 24 hour selector. I'm probaby clicking the wrong element since this is a 0x0pix thing. * TIL fiter << find for selecting out of an array * update cypress and sinon Clarify droptown unit test. Uses sinon fake timer now so that you can know what the dropdown values should be. * cypress hasn't updated docker hub with 4.0.2 yet. * Can we enable pipelines on this project? * downgrade cypress need to match versions with the available docker image. * update package-lock * fixup! Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state * Update helper package, add PORT env option to webpack * Update labels, removes unused reducer export. * Gets QueryParams working. * fix datepicker styles * Fix removing params. * Make ava ignore styles. Also begin refactor of datepicker utils. * Cypress hasn't updated their dockerfiles yet. need exactly 4.0.1 * hide calendar and clock overlays * add cal and clock pickers back * Bring localstack up to date with cumulus/core's localstack. * filters listGranules * cleanup commented out import * Adds code and tests for generating querystring objects These are used before dispatching API actions for certain endpoints.. * Adds querystrings to appropriate listXactions. Before dispatching action to cumulus API, first grab the datapicker state, and create a querystring object that will be used on the API request. the added functions are: + [X] asyncOperations aka listOperations + [X] listProviders ? + [X] listCollections + [X] listGranules + [X] listPdrs + [X] listRules + [X] execution logs + [X] listExecutions Excluded functions (ones that don't take a timestamp) + [X] workflows + [X] list reconciliation reports * removes queryparams from getExecutionLogs. It was wrong, and it doesn't belong on there. Commented out testing listCollections, because I'm not sure how to test that action creator. * Can't figure out listCollections test yet. * Updates tests for everything but listCollections. I'm not sure how to handle that test yet. * Turns out couldn't get the test working because the code was wrong. * Didn't need nock afterall. * Name tests better. * typo * Return the promise dispatch creates to keep these functions thenable Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> * Squashed commit of the following: (#640) commit a7b23d27920990af76025f0d1899fceaeb5a6732 Merge: 3da7a5aa a52de30f Author: Matt Savoie Date: Mon Feb 24 15:57:49 2020 -0700 Merge branch 'CUMULUS-1729-filter-api-calls-with-correct-date-information' into CUMULUS-1729-create-valid-tokens-speed-up-testing commit a52de30f21f028c18a790305912b8af3177cd036 Author: Matt Savoie Date: Mon Feb 24 15:55:02 2020 -0700 Return the promise dispatch creates to keep these functions thenable commit 3da7a5aaf840354e4947a7812d52c719bda7b33c Author: Matt Savoie Date: Mon Feb 24 11:42:11 2020 -0700 Document how to work cross-repositories with dashboard. commit 4aacd70eb4b925293e2ffe8e9667f65947952b9c Author: Matt Savoie Date: Mon Feb 24 08:54:35 2020 -0700 Move shareable URL handling into datepicker module. It didn't belong in home.js. It still needs some :heart: and feeding. commit 3a7c4a229b5decf52ff9118f379ccccb366a1337 Merge: 92f40afc 2db89bbd Author: Matt Savoie Date: Mon Feb 24 08:31:28 2020 -0700 Merge branch 'develop' into CUMULUS-1729-filter-api-calls-with-correct-date-information commit 92f40afc3a799c8aba72889fd9bd3242b65c8165 Author: Matt Savoie Date: Mon Feb 24 08:29:40 2020 -0700 typo commit 0236aaa6c978a5ddd8dbe8070894fd7d8b96e227 Author: Matt Savoie Date: Mon Feb 24 08:28:59 2020 -0700 typo commit dcc7b8a9356e45a5bcf159cd17726ca8d300bdfc Author: Matt Savoie Date: Fri Feb 21 19:14:28 2020 -0700 Fix tests and remove '#' hash objects. commit 13a6fe29a8baef7a13493ce4d8c02027689a637e Author: Matt Savoie Date: Fri Feb 21 18:02:09 2020 -0700 Fix indentation. commit 640b1259c18cf3569df2c401e63fdb9c187b44d9 Merge: 5447345f 5b7b15ec Author: Matt Savoie Date: Fri Feb 21 17:54:01 2020 -0700 Merge branch 'CUMULUS-1729-filter-api-calls-with-correct-date-information' into CUMULUS-1729-create-valid-tokens-speed-up-testing commit 5b7b15ecefc20ae5082bfe522c22c500bb85d7c5 Author: Matt Savoie Date: Fri Feb 21 17:27:44 2020 -0700 Name tests better. commit 0cef583b8b5103dcebf7a0548cb204f0ea97b496 Author: Matt Savoie Date: Fri Feb 21 17:20:20 2020 -0700 Didn't need nock afterall. commit 2f3d28798a43605e8c9bd09356346441df940c28 Author: Matt Savoie Date: Fri Feb 21 17:06:12 2020 -0700 Turns out couldn't get the test working because the code was wrong. commit 9df706e2ea004df556bac3d1d11677608d8a8d50 Author: Matt Savoie Date: Fri Feb 21 16:49:04 2020 -0700 Updates tests for everything but listCollections. I'm not sure how to handle that test yet. commit 9107722f79e131d292d03b77dd593ae1b985e4d6 Author: Matt Savoie Date: Fri Feb 21 16:30:01 2020 -0700 Can't figure out listCollections test yet. commit 623fdf700b4a2de4d5971782794c8cd7221aa32e Author: Matt Savoie Date: Fri Feb 21 15:40:52 2020 -0700 removes queryparams from getExecutionLogs. It was wrong, and it doesn't belong on there. Commented out testing listCollections, because I'm not sure how to test that action creator. commit 8aeda9016e81382c98ae777839c00a61e9a17b72 Author: Matt Savoie Date: Fri Feb 21 13:40:17 2020 -0700 Adds querystrings to appropriate listXactions. Before dispatching action to cumulus API, first grab the datapicker state, and create a querystring object that will be used on the API request. the added functions are: + [X] asyncOperations aka listOperations + [X] listProviders ? + [X] listCollections + [X] listGranules + [X] listPdrs + [X] listRules + [X] execution logs + [X] listExecutions Excluded functions (ones that don't take a timestamp) + [X] workflows + [X] list reconciliation reports commit c569f5b751f860e7094a7b3f20c6a4c152bd8b4b Author: Matt Savoie Date: Fri Feb 21 13:31:10 2020 -0700 Adds code and tests for generating querystring objects These are used before dispatching API actions for certain endpoints.. commit 19f0da0ead7cb8ac20c33b91d8c4d64960e52acf Author: Matt Savoie Date: Fri Feb 21 12:46:51 2020 -0700 cleanup commented out import commit e87367c0c9d7906b0b4ce8b0876d16dede0424e9 Merge: acb2e457 41eca4d0 Author: Matt Savoie Date: Fri Feb 21 11:35:46 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-filter-api-calls-with-correct-date-information commit 5447345fc14cbd5d245a210110fcf27815e7b463 Merge: 7f4a939f 41eca4d0 Author: Matt Savoie Date: Fri Feb 21 11:32:33 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-create-valid-tokens-speed-up-testing commit acb2e45727c0b2171ff32f8542f9833ce1d95a68 Author: Matt Savoie Date: Fri Feb 21 11:13:11 2020 -0700 filters listGranules commit 2e1aa5a0ad8158b936380e899cb8be7f2bbca649 Author: Matt Savoie Date: Fri Feb 21 09:47:14 2020 -0700 Bring localstack up to date with cumulus/core's localstack. commit 7f4a939fe4808915028606d8ce94712fafa69de7 Merge: e238e6c9 85b43163 Author: Matt Savoie Date: Fri Feb 21 09:25:37 2020 -0700 Merge branch 'CUMULUS-1729-datepicker-should-reflect-state' into CUMULUS-1729-create-valid-tokens-speed-up-testing commit 85b431630c9304f43d4a486358b7060b8a6e2585 Author: Juanisa McCoy Date: Fri Feb 21 10:37:45 2020 -0500 add cal and clock pickers back commit e238e6c99edc21d2d6a090f886d7d93fe592f3bb Author: Matt Savoie Date: Fri Feb 21 08:07:11 2020 -0700 That come from somewhere.? Get rid of bad merge artifacts. commit 2cf26e0ca736b7cec554037c9a270c4ee75a2913 Author: Juanisa McCoy Date: Fri Feb 21 08:49:05 2020 -0500 hide calendar and clock overlays commit 0285b72cde6229d5fcf52210f9172048287fbdf8 Merge: a4b231d0 28c21286 Author: Matt Savoie Date: Thu Feb 20 16:57:33 2020 -0700 Merge branch 'CUMULUS-1729-datepicker-should-reflect-state' into CUMULUS-1729-create-valid-tokens-speed-up-testing commit 28c21286f7356a5f5fb23fad46d5e775d6e88bc7 Author: Matt Savoie Date: Thu Feb 20 16:46:58 2020 -0700 Cypress hasn't updated their dockerfiles yet. need exactly 4.0.1 commit 4aaa9300683e5c7cdb42230b41dd5976c36f9329 Merge: c2a7e1c9 6ef07891 Author: Matt Savoie Date: Thu Feb 20 16:33:25 2020 -0700 Merge remote-tracking branch 'origin/Cumulus-1690-bugfix' into CUMULUS-1729-datepicker-should-reflect-state commit c2a7e1c94d40b9a66c9065583e06cae2ff3f0c9b Merge: de2c96c1 e27228f6 Author: Matt Savoie Date: Thu Feb 20 16:30:39 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state commit de2c96c163cc356b93b46b5e9053902ecb128d10 Author: Matt Savoie Date: Thu Feb 20 16:25:21 2020 -0700 Make ava ignore styles. Also begin refactor of datepicker utils. commit f52f3d941e38bc8879393ccfaa6fb4c2c42c85d6 Author: Matt Savoie Date: Thu Feb 20 14:52:36 2020 -0700 Fix removing params. commit 6f8fd0ffa38ce5d147396d6345dbce466d954420 Author: Juanisa McCoy Date: Thu Feb 20 14:40:56 2020 -0500 fix datepicker styles commit 900de86f60e18ec5033cea7af882339b0425f973 Author: Matt Savoie Date: Thu Feb 20 11:50:02 2020 -0700 Gets QueryParams working. commit 97bf600284ce04020acd897c97e326c66a9c0e5a Merge: d76a92c1 9ae0c734 Author: Matt Savoie Date: Thu Feb 20 10:20:28 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state commit d76a92c1309a1bd2bac066ea28c22ca21cd3e8ba Author: Matt Savoie Date: Thu Feb 20 10:20:11 2020 -0700 Update labels, removes unused reducer export. commit 4c4aeab9ad136f3e0711128d8a2152625d3089bc Author: Matt Savoie Date: Thu Feb 20 10:08:15 2020 -0700 Update helper package, add PORT env option to webpack commit eb940e945d84b3be46d6316070557a53059b9f84 Author: Matt Savoie Date: Thu Feb 20 08:12:53 2020 -0700 fixup! Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state commit 55f89a294f43da292650ce849ba2f3bd4d62aa57 Merge: 6e4320b2 fd168f60 Author: Matt Savoie Date: Wed Feb 19 17:50:41 2020 -0700 Merge branch 'bt/CUMULUS-1549/enhance-logs' into CUMULUS-1729-datepicker-should-reflect-state commit 6e4320b2c6e6c5e26a1af34580f0de267e2890c5 Author: Matt Savoie Date: Wed Feb 19 17:45:01 2020 -0700 update package-lock commit 5d2f9bc4552742b8588085b85e1837b7e181da99 Merge: aa8473bd 908cbd23 Author: Matt Savoie Date: Wed Feb 19 17:41:01 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state commit aa8473bde45cf16c7f513b29d542a0c69c9868d6 Merge: da8382bb 4f5d1859 Author: Matt Savoie Date: Wed Feb 19 14:15:16 2020 -0700 Merge remote-tracking branch 'origin/develop' into CUMULUS-1729-datepicker-should-reflect-state commit a4b231d0c35da857720b7d557c7623ece1a2f419 Author: Matt Savoie Date: Mon Feb 17 16:45:47 2020 -0700 TOKEN_SECRETs don't need to match, but do need to exist. commit 34a5ac066c614dc9c287b9aee8931d5954673c9b Author: Matt Savoie Date: Mon Feb 17 16:07:31 2020 -0700 Removes TOKEN_SECRET from the dashboard. commit bf98bf05cb3c0317b2e4a65a2ea1eb40f0a202d5 Author: Matt Savoie Date: Mon Feb 17 16:04:09 2020 -0700 Revert "Update Login API command." This reverts commit 39deffa5292e66265fea09437a7a9c590729f985. commit 39deffa5292e66265fea09437a7a9c590729f985 Author: Matt Savoie Date: Mon Feb 17 09:56:31 2020 -0700 Update Login API command. Actually we do need this to test authentication correctly. Specifically for generating an expired token. commit 7ed7ea34dc40b01ca8bb6c4286d44d43f608439f Author: Matt Savoie Date: Mon Feb 17 09:49:08 2020 -0700 Cleanup weird function in granules spec. commit 008fc5ea22d0ec355d573ae809784bd9af2f5331 Author: Matt Savoie Date: Mon Feb 17 09:41:08 2020 -0700 Testing doesn't support the update to the api. So it's not really that much faster to use direct token creation, over logging into the app directly. So go back to the old way for now. commit 5959e888763bcb690a38afd5f899a4e3ad47ae6f Author: Matt Savoie Date: Sun Feb 16 20:02:45 2020 -0700 Old API package. commit 4e50e71e48585b12b6b63d6bb89d2338960c255e Author: Matt Savoie Date: Sun Feb 16 19:58:58 2020 -0700 OLD login Command - Go back to original login mechanism. + It doesn't seem like this bought us much and most of the saving comes from only calling reset state when a test changes the state. commit c69623dfbfe4be810c4881e11ee3ce697b5e5126 Author: Matt Savoie Date: Sun Feb 16 17:44:36 2020 -0700 Easiest speedup. commit e95a918f52e1f71f4bbb2c13a3083c3b4a826a3f Author: Matt Savoie Date: Sun Feb 16 14:53:45 2020 -0700 Alpha version of API for TOKEN_SECRET sharing. commit 59d2c2a0cc0bd2d4a233fedb44d10b5ccaad3e5b Author: Matt Savoie Date: Sun Feb 16 12:36:21 2020 -0700 Change how authentication works on dashboard testing. Set the TOKEN_SECRET to the same value on dashboard and cumulus API and instead of hitting the API for a token, just create one locally. Additionally this removes the plugin/token.js that was a pseduo-copy of the @cumulus/api/lib/token in favor of the real thing. commit da8382bb445d012fcd4bdd2c039aafd3fdf87b25 Author: Matt Savoie Date: Sat Feb 15 13:43:00 2020 -0700 downgrade cypress need to match versions with the available docker image. commit 4153a8be124bf9e7e3a36abdc208b838c1bcc342 Author: Matt Savoie Date: Sat Feb 15 13:39:51 2020 -0700 Can we enable pipelines on this project? commit b7ea681a4f9249074f2135f3e698b1508ab0076a Author: Matt Savoie Date: Sat Feb 15 13:30:14 2020 -0700 cypress hasn't updated docker hub with 4.0.2 yet. commit 4e8ffbe1b87cd5d34148a46651eba3755000af74 Author: Matt Savoie Date: Sat Feb 15 13:09:24 2020 -0700 update cypress and sinon Clarify droptown unit test. Uses sinon fake timer now so that you can know what the dropdown values should be. commit d41b9e3f0bd30f5a59413b14745288fe2d72318f Author: Matt Savoie Date: Sat Feb 15 11:12:24 2020 -0700 TIL fiter << find for selecting out of an array commit b4740638f7718e92755e695213b4cef7f2665f06 Author: Matt Savoie Date: Sat Feb 15 11:02:29 2020 -0700 force clike the 24 hour selector. I'm probaby clicking the wrong element since this is a 0x0pix thing. commit 036b04d80339ea2840540ccea0e7b1925ed9add2 Author: Matt Savoie Date: Fri Feb 14 17:49:54 2020 -0700 Fix changelog problem commit 45a446173c76676a5a1f4b4e8d644a4fb554bdd5 Author: Matt Savoie Date: Fri Feb 14 17:41:50 2020 -0700 refactored long name. handleDateRangeDropdownChange => handleDropdownChange commit 635b1989eb6c16f728fc7b9cf0d90193807fa0e9 Author: Matt Savoie Date: Fri Feb 14 17:37:03 2020 -0700 Handle invalid date on changes. commit 2c4a0fac3d4817905094036ee93af464ee2cb963 Author: Matt Savoie Date: Fri Feb 14 17:21:19 2020 -0700 Clean up console.logs; remove unused exports commit 31931c2def67c4f004f72863611dc3b90334c72a Author: Matt Savoie Date: Fri Feb 14 17:13:20 2020 -0700 Adds cypress integration tests for Datepicker commit 63b81bff9d246b3de58b00183df82a95a2d9f70c Author: Matt Savoie Date: Fri Feb 14 14:20:37 2020 -0700 Adds reducer tests. commit 30ff778d43f77a8937c2a2ece02f1d71a5d04c1a Author: Matt Savoie Date: Thu Feb 13 13:22:48 2020 -0700 Need to write tests and clean up. commit 5d81993c73266b485abef57e7bd9d85e0c8a1aee Author: Matt Savoie Date: Thu Feb 13 12:33:09 2020 -0700 Quckie hack to see if dispatching state update works spoiler alert. It does. commit 78ad5f6a493daae85d2e3c8f2786180fd699fb4a Author: Matt Savoie Date: Wed Feb 12 16:10:46 2020 -0700 Adds support for hour format. and URL state commit a8f5e232c55e9ac009bca3ce763e5be021502fb8 Author: Matt Savoie Date: Tue Feb 11 18:46:44 2020 -0700 Most of the Dropdown / Datepicker is working with state. Still have plenty to work on. commit 58b41ce3468ad4522f8751eeaeed1dc484bf9fd9 Author: Matt Savoie Date: Fri Feb 7 15:31:48 2020 -0700 fix favicon and remove duplicate file. it looks like table-config/granules.js was moved, but not deleted. commit d92fe56efac867577b1439d43f1200327892e95c Author: Matt Savoie Date: Fri Feb 7 12:50:02 2020 -0700 Jenny's changes for datetimepicker * Update Cypress version to latest (4.0.2). (#641) * Update Cypress version to latest (4.0.2). * Fixes overzealous hash removal * [CUMULUS-1467] Fix config for Metrics ES User/Password (#642) * [CUMULUS-1565] Add confirmation modal after editing collection (#638) * Add confirmation modal after editing collection * Fix cypress test * Fix package-lock.json * Upgrade packages * Fix edit rule cypress test * Update error handling in EditRaw * Update modal error styles * Add isInflight state to modal * add CSS for close button * Remove console logs. Add button--close class * Add back cypress upgrade Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> * Give each MMT link a waitable route. (#644) Wait for those routes when testing MMT links. Removes unused CMR fixture for TESTCOLLECTION. * Save snapshots by using correct path. (#643) * Show 0 when stats are null (#645) Co-authored-by: Matt Savoie * Cumulus 1729 persist url query parameters (#648) * Save snapshots by using correct path. * Import withRouter from react-router-dom * Give each MMT link a waitable route. Wait for those routes when testing MMT links. Removes unused CMR fixture for TESTCOLLECTION. * Save videos * Save Cypress videos * Keep links first pass * Test a timeout on this. * Keep URL params when changing pages. * removes queryParams when a search or dropdown is cleared. * Can 25 seconds be enough? This is really wrong, Here's a video of it failing on 15 sec delay. https://2751-79849093-gh.circle-artifacts.com/0/~/cumulus-dashboard/cypress/videos/collections_spec.js.mp4 * Add cypress test * removes misleading test. Checking which values is on the selector isn't working. * Give location props to test fixture. * [cumulus-1729] Wrap getStats action creator in a thunk to get current date params. (#650) * Wrap getStats action creator in a thunk to get current date params. Use the state's current startDateTime and endDateTime when making calls to the stats endpoint. This will need an update to the stats endpoint to parse the dates properly * Test stats endpoint properly called with dates. * Release the dashboard. (#653) * Merge master (#655) * CUMULUS-1670: Fix provider update (#611) * update provider reducer to remove unnecessary properties to fix provider update * update getRule action to use individual rule GET endpoint * remove extra properties set by rule reducer and update rule disable/enable actions * update CHANGELOG * Add back the unreleased section. Co-authored-by: laurenfrederick Co-authored-by: Mark Boyd Co-authored-by: kkelly51 Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> Co-authored-by: Mark Boyd Co-authored-by: Chuck Daniels Co-authored-by: jennyhliu <34660846+jennyhliu@users.noreply.github.com> Co-authored-by: dopeters Co-authored-by: Brian Tennity Co-authored-by: laurenfrederick * Add ability to render custom components in bulkActions * [CUMULUS-1758] Use react-table for resizing columns (#662) * Wrap getStats action creator in a thunk to get current date params. Use the state's current startDateTime and endDateTime when making calls to the stats endpoint. This will need an update to the stats endpoint to parse the dates properly * Test stats endpoint properly called with dates. * Refactor tables to use react-table * Update table documentation. Clean up styles * Fix ava tests. Update table documentation * Fix cypress tests * Update Changelog Co-authored-by: Matt Savoie Co-authored-by: Matt Savoie Co-authored-by: kkelly51 Co-authored-by: Juanisa McCoy <40603352+jjmccoy@users.noreply.github.com> Co-authored-by: Mark Boyd Co-authored-by: Chuck Daniels Co-authored-by: jennyhliu <34660846+jennyhliu@users.noreply.github.com> Co-authored-by: Brian Tennity Co-authored-by: laurenfrederick --- CHANGELOG.md | 15 + TABLES.md | 35 ++- app/src/css/main.scss | 1 - app/src/css/modules/_table.scss | 38 ++- app/src/js/actions/index.js | 9 - app/src/js/actions/types.js | 11 +- app/src/js/components/Chart/_chart.scss | 95 ------ app/src/js/components/Chart/histogram.js | 182 ----------- app/src/js/components/Collections/granules.js | 16 +- app/src/js/components/Collections/list.js | 50 ++- app/src/js/components/Collections/overview.js | 10 +- .../components/Executions/execution-status.js | 11 +- app/src/js/components/Executions/overview.js | 45 +-- app/src/js/components/Granules/granule.js | 29 +- app/src/js/components/Granules/list.js | 16 +- app/src/js/components/Granules/overview.js | 12 +- .../js/components/ListActions/ListActions.js | 38 ++- app/src/js/components/Operations/overview.js | 44 +-- app/src/js/components/Pdr/list.js | 14 +- app/src/js/components/Pdr/overview.js | 10 +- app/src/js/components/Pdr/pdr.js | 8 +- app/src/js/components/Providers/overview.js | 10 +- .../components/ReconciliationReports/list.js | 13 +- .../reconciliation-report.js | 64 ++-- .../ReconciliationReports/report-table.js | 86 +++--- app/src/js/components/Rules/overview.js | 74 +---- .../components/SortableTable/SortableTable.js | 285 ++++++++++-------- app/src/js/components/Table/Table.js | 66 ++-- app/src/js/components/Workflows/overview.js | 88 +++--- app/src/js/components/home.js | 12 +- app/src/js/reducers/stats.js | 32 +- app/src/js/utils/table-config/collections.js | 114 ++++--- .../js/utils/table-config/execution-status.js | 41 ++- app/src/js/utils/table-config/executions.js | 40 +++ app/src/js/utils/table-config/granules.js | 122 +++++--- app/src/js/utils/table-config/instances.js | 43 ++- app/src/js/utils/table-config/operations.js | 27 ++ app/src/js/utils/table-config/pdr-progress.js | 68 +++-- app/src/js/utils/table-config/pdrs.js | 120 ++++---- app/src/js/utils/table-config/providers.js | 51 ++-- app/src/js/utils/table-config/queues.js | 25 +- .../table-config/reconciliation-reports.js | 98 +++--- app/src/js/utils/table-config/rules.js | 66 ++++ app/src/js/utils/table-config/services.js | 38 ++- app/src/js/utils/table-config/workflows.js | 15 + cypress/integration/collections_spec.js | 26 +- cypress/integration/executions_spec.js | 8 +- cypress/integration/granules_spec.js | 2 +- cypress/integration/providers_spec.js | 8 +- cypress/integration/reconciliation_spec.js | 48 +-- cypress/integration/rules_spec.js | 16 +- cypress/integration/workflows_spec.js | 8 +- package-lock.json | 117 ++++++- package.json | 1 + .../components/executions/execution-status.js | 4 +- test/components/granules/granule.js | 7 +- test/components/granules/overview.js | 4 +- .../reconciliation-reports/report-table.js | 74 +++-- test/components/table/list-view.js | 11 +- test/utils/pdrs.js | 10 +- 60 files changed, 1233 insertions(+), 1398 deletions(-) delete mode 100644 app/src/js/components/Chart/_chart.scss delete mode 100644 app/src/js/components/Chart/histogram.js create mode 100644 app/src/js/utils/table-config/executions.js create mode 100644 app/src/js/utils/table-config/operations.js create mode 100644 app/src/js/utils/table-config/rules.js create mode 100644 app/src/js/utils/table-config/workflows.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c470197..ddea50220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- **CUMULUS-1758** + - Adds the ability to resize table columns + +### Changed + +- **CUMULUS-1758** + - Updates table implementation to use [react-table](https://github.com/tannerlinsley/react-table) + ## [v1.7.0] - 2020-03-02 ### BREAKING CHANGES @@ -89,6 +99,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **CUMULUS-1690** - Removed Gulp/Browserify and their dependencies. +### Fixed + +- **CUMULUS-1670** + - Fixed bug preventing update of providers + ## [v1.6.1] - 2019-11-12 ### Fixed diff --git a/TABLES.md b/TABLES.md index f3ee41d92..9678ef3db 100644 --- a/TABLES.md +++ b/TABLES.md @@ -1,6 +1,8 @@ # Table documentation -A lot of logic is encapsulated in the table components. They are central abstractions and wrap functionality like sorting, pagination, selection, and search. The main components are `sortable-table` and `list-view`. +Our table components leverage [react-table](https://github.com/tannerlinsley/react-table/) to handle basic sorting, row selection, and resizable columns. The main components are `sortable-table` and `list-view`. + +react-table's main hook is `useTable()`. Options are passed into this hook for the desired functionality. ## `sortable-table` @@ -8,20 +10,24 @@ A basic table component that supports row selection and dumb sorting (see below) **Props** -- **primaryIdx**: Column number to apply bold typeface to. Default is `0` or far-left column. -- **data**: Array of data items. Items can be objects or arrays, depending on the accessor functions defined in `row`. -- **header**: Array of strings representing the header row. -- **row**: Array of items representing columns in each row. Items can be accessor functions with the arguments `data[k], k, data` (where `k` is the index of the current loop), or string values, ie `"collectionName"`. -- **props**: Array of property names to send to Elasticsearch for a re-ordering query. -- **sortIdx**: The current index of the `props` array to sort by. -- **order**: Either 'desc' or 'asc', corresponding to sort order. +- **tableColumns**: This is an array containing the column configuration for the table. It is the value for the `columns` key in the options object that is passed to `useTable()` + * Options for each column include: + - Header: *text or component that will render as the header* + - accessor: *key or function for obtaining value* + - id: *unqiure column id. required if accessor is function* + - disableSortBy: *set to true if the column should not be sortable* + - width: *default is 125. set value if column needs to be wider/smaller* + + * Additional options can be found [here](https://github.com/tannerlinsley/react-table/blob/master/docs/api/useTable.md#column-options) or in the documation for a specific plugin hook + +- **data**: Array of data items. Items can be any format. +- **sortIdx**: The id of the column to sort on. - **changeSortProps**: Callback when a new sort order is defined, passed an object with the properties `{ sortIdx, order }`. -- **onSelect**: Callback when a row is selected (or unselected), passed a string id corresponding to the `rowId` selector. +- **onSelect**: Callback when a row is selected (or unselected), passed an array containing the ids of all selected rows. - **canSelect**: Boolean value defining whether 1. rows can be selected and 2. to render check marks. -- **selectedRows**: Array of row ID's corresponding to rows that are currently selected. -- **rowId**: String accessor to define as that row's id, ie `collectionName`. +- **rowId**: String or function that defines a particular row's id. Passed to `useTable` options via `getRowId`. -Note, `props`, `sortIdx`, `order`, and `changeSortProps` only apply to components that implement smart searching, such as `list-view`. This base component does internal prop checking to determine whether it uses smart or dumb sorting, based on whether the above props are defined. +Note, `sortIdx` and `changeSortProps` only apply to components that implement smart searching, such as `list-view`. This base component does internal prop checking to determine whether it uses smart or dumb sorting, based on whether the above props are defined. ## `list-view` @@ -32,15 +38,12 @@ Wraps `sortable-table` and implements auto-update and smart sort. When a new sor - **list**: Parent data structure, ie `state.granules.list` or `state.collections.list`. Expected to contain `{ data, inflight, error, meta }` properties corresponding to all `list` state objects. - **dispatch**: Redux dispatch function. - **action**: Redux-style action to send, ie `listCollections`. -- **tableHeader**: Corresponds to `sortableTable#header`. -- **tableRow**: Corresponds to `sortableTable#row`. -- **tableSortProps**: Corresponds to `sortableTable#props`. - **sortIdx**: Corresponds to `sortableTable#sortIdx`. - **query**: Array of configuration objects passed to `batch-async-command`. - **rowId** Corresponds to `sortableTable#rowId`. ## Dumb vs smart sort -Dumb sorting uses `Array.sort()` within the component to re-order table data that has **already** been received from the API. Smart sorting initiates a new API request, passing the sort parameter to the server (elasticsearch) which returns a sorted response. +Dumb sorting uses react-table's built in sort functionality to sort table data that has **already** been received from the API. Smart sorting initiates a new API request, passing the sort parameter to the server (elasticsearch) which returns a sorted response. The `maunalSortBy` option passed to `useTable()` tells react-table whether we are doing server-side sorting (`true`) or letting react-table sort (`false`). Dumb sorting is for smaller, simple tables that do not need pagination. diff --git a/app/src/css/main.scss b/app/src/css/main.scss index 4af20475c..e353379ab 100644 --- a/app/src/css/main.scss +++ b/app/src/css/main.scss @@ -28,7 +28,6 @@ @import '../js/components/DropDown/DropDown.scss'; @import 'lists'; @import '../css/modules/table'; -@import '../js/components/Chart/chart'; @import '../js/components/LoadingIndicator/loading-indicator'; @import "../js/components/Modal/modal"; @import 'pulse'; diff --git a/app/src/css/modules/_table.scss b/app/src/css/modules/_table.scss index 4f217bda2..37a1b0645 100644 --- a/app/src/css/modules/_table.scss +++ b/app/src/css/modules/_table.scss @@ -13,7 +13,7 @@ word-wrap: break-word; /* Internet Explorer 5.5+ */ } -table { +.table { width: 100%; line-height: 1.6em; @@ -21,9 +21,9 @@ table { max-width: 500px; } - thead { + .thead { background-color: $ocean-blue; - td { + .th { color: $white; font-weight: $base-font-semibold; padding: 1em 2em; @@ -31,13 +31,15 @@ table { } } - tbody { - tr { + .tbody { + .tr { border-bottom: 1px solid $lightest-grey; - td { + .td { font-size: .86em; padding: 1em 2em; vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; /*&:first-of-type { padding: 1em 0 1em 2em; }*/ @@ -82,15 +84,27 @@ table { border-radius: 10px; } -.list--granules { - .table__main-asset { - max-width: 200px; +.list--errors { + tbody tr td:first-of-type, + .tbody .tr .td:first-of-type { + max-width: 300px; } } -.list--errors { - tbody tr td:first-of-type { - max-width: 300px; +.resizer { + display: inline-block; + background: $lightest-grey; + width: 1px; + height: 100%; + position: absolute; + right: 0; + top: 0; + transform: translateX(50%); + z-index: 1; + touch-action:none; + + &.isResizing { + background: red; } } diff --git a/app/src/js/actions/index.js b/app/src/js/actions/index.js index 55e5cf36d..40045a4cb 100644 --- a/app/src/js/actions/index.js +++ b/app/src/js/actions/index.js @@ -718,15 +718,6 @@ export const getSchema = (type) => ({ } }); -export const queryHistogram = (options) => ({ - [CALL_API]: { - type: types.HISTOGRAM, - method: 'GET', - url: url.resolve(root, 'stats/histogram'), - qs: options - } -}); - export const listWorkflows = (options) => ({ [CALL_API]: { type: types.WORKFLOWS, diff --git a/app/src/js/actions/types.js b/app/src/js/actions/types.js index d0d4ec822..68f0c71f4 100644 --- a/app/src/js/actions/types.js +++ b/app/src/js/actions/types.js @@ -140,6 +140,10 @@ export const SEARCH_PROVIDERS = 'SEARCH_PROVIDERS'; export const CLEAR_PROVIDERS_SEARCH = 'CLEAR_PROVIDERS_SEARCH'; export const FILTER_PROVIDERS = 'FILTER_PROVIDERS'; export const CLEAR_PROVIDERS_FILTER = 'CLEAR_PROVIDERS_FILTER'; +// Workflows +export const WORKFLOWS = 'WORKFLOWS'; +export const WORKFLOWS_INFLIGHT = 'WORKFLOWS_INFLIGHT'; +export const WORKFLOWS_ERROR = 'WORKFLOWS_ERROR'; // Logs export const LOGS = 'LOGS'; export const LOGS_INFLIGHT = 'LOGS_INFLIGHT'; @@ -148,13 +152,6 @@ export const CLEAR_LOGS = 'CLEAR_LOGS'; export const SCHEMA = 'SCHEMA'; export const SCHEMA_INFLIGHT = 'SCHEMA_INFLIGHT'; export const SCHEMA_ERROR = 'SCHEMA_ERROR'; -// Workflows -export const HISTOGRAM = 'HISTOGRAM'; -export const HISTOGRAM_INFLIGHT = 'HISTOGRAM_INFLIGHT'; -export const HISTOGRAM_ERROR = 'HISTOGRAM_ERROR'; -export const WORKFLOWS = 'WORKFLOWS'; -export const WORKFLOWS_INFLIGHT = 'WORKFLOWS_INFLIGHT'; -export const WORKFLOWS_ERROR = 'WORKFLOWS_ERROR'; // Executions export const EXECUTION_STATUS = 'EXECUTION_STATUS'; export const EXECUTION_STATUS_INFLIGHT = 'EXECUTION_STATUS_INFLIGHT'; diff --git a/app/src/js/components/Chart/_chart.scss b/app/src/js/components/Chart/_chart.scss deleted file mode 100644 index d2bbc3c24..000000000 --- a/app/src/js/components/Chart/_chart.scss +++ /dev/null @@ -1,95 +0,0 @@ -.chart__box { - display: inline-block; - width: 50%; - text-align: center; -} - -.chart__container { - height: 400px; -} - -.chart__bar { - fill: $midnight-blue; -} - -.axis__tick, -.axis__line { - stroke: $light-grey; -} - -.axis__text { - font-size: 10px; - fill: $light-grey; -} - -.tooltip { - margin-left: 1em; - margin-top: 1em; - position: fixed; - pointer-events: none; - transition: all 0.1s; - z-index: 99; -} - -.tooltip__inner { - padding: 1em; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12); - border-radius: 5px; - background: #FFF; - max-width: 240px; -} - -.clusters rect { - fill: transparent; - stroke: #555; - stroke-dasharray: 5 2; - stroke-width: 1px; -} - -text { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; - font-size: 14px; - fill: #444444; -} - -rect { - ry: 5; - rx: 5; -} - -.node rect { - stroke: #555; - fill: #fff; - stroke-width: 1px; -} - -.edgePath path { - stroke: #555; - stroke-width: 1.5px; -} - -.cluster .label { - display: none; -} - -.terminus rect { - ry: 25px; - rx: 25px; - fill: #ffda75; -} - -.Succeeded rect { - fill: #2bd62e; -} - -.InProgress rect { - fill: #53c9ed; -} - -.Cancelled rect { - fill: #ddd; -} - -.Failed rect { - fill: #de322f; -} diff --git a/app/src/js/components/Chart/histogram.js b/app/src/js/components/Chart/histogram.js deleted file mode 100644 index ddbea3035..000000000 --- a/app/src/js/components/Chart/histogram.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; -import React from 'react'; -import { get } from 'object-path'; -import { scaleLinear, scaleBand } from 'd3-scale'; -import debounce from 'lodash.debounce'; -import throttle from 'lodash.throttle'; -import PropTypes from 'prop-types'; -import { tally } from '../../utils/format'; -import LoadingIndicator from '../app/loading-indicator'; -import { window } from '../../utils/browser'; - -const noop = (x) => x; -const tooltipDelay = 100; - -const margin = { - top: 50, - right: 15, - bottom: 15, - left: 70 -}; - -class Histogram extends React.Component { - constructor () { - super(); - this.state = { - width: 0, - height: 0, - tooltip: null, - tooltipX: 0, - tooltipY: 0 - }; - this.onWindowResize = this.onWindowResize.bind(this); - this.setHoverState = this.setHoverState.bind(this); - this.mouseMove = this.mouseMove.bind(this); - this.mouseOut = this.mouseOut.bind(this); - } - - onWindowResize () { - let rect = this.refs.chartContainer.getBoundingClientRect(); - this.setState({ width: rect.width, height: rect.height }); - } - - componentDidMount () { - this.setHoverState = throttle(this.setHoverState, tooltipDelay); - this.mouseOut = debounce(this.mouseOut, tooltipDelay); - this.onWindowResize(); - this.onWindowResize = debounce(this.onWindowResize, 200); - window.addEventListener('resize', this.onWindowResize); - } - - setHoverState (tooltip, tooltipX, tooltipY) { - this.setState({ tooltip, tooltipX, tooltipY }); - } - - mouseMove (e) { - // http://stackoverflow.com/questions/38142880/react-js-throttle-mousemove-event-keep-throwing-event-persist-error - e.persist(); - this.setHoverState( - e.currentTarget.getAttribute('data-tooltip'), - e.clientX, - e.clientY - ); - } - - mouseOut () { - this.setState({ tooltip: null }); - } - - render () { - const { width, height } = this.state; - const { inflight, data } = this.props.data; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // short circuit if the area is too small; loading if the data is inflight - if (innerWidth <= 0) return
    ; - else if (inflight && !data) { - return ( -
    - -
    - ); - } - - const histogram = get(data, 'histogram', []); - - const xScale = scaleLinear() - .range([0, innerWidth]) - .domain([0, 1.25 * Math.max.apply(Math, histogram.map(d => +d.count))]); - - const scaleOrdinal = scaleBand() - .paddingInner(0.6) - .paddingOuter(0.2); - - const yScale = scaleOrdinal - .rangeRound([0, innerHeight]) - .domain(histogram.map(d => d.date)); - - const band = yScale.bandwidth(); - const tooltipFormat = this.props.tooltipFormat || noop; - - return ( -
    - - - - {xScale.ticks(3).map((label, i) => { - // don't render the first tick - if (!i) return ; - return - - {tally(label)} - ; - })} - - - - - {histogram.map(d => { - return - - {d.date} - ; - })} - - - - {histogram.map(d => { - return ; - })} - - - -
    -
    - {tooltipFormat(this.state.tooltip)} -
    -
    -
    - ); - } -} - -Histogram.propTypes = { - data: PropTypes.object, - tooltipFormat: PropTypes.func -}; - -export default Histogram; diff --git a/app/src/js/components/Collections/granules.js b/app/src/js/components/Collections/granules.js index 2106f7a7e..08b46d340 100644 --- a/app/src/js/components/Collections/granules.js +++ b/app/src/js/components/Collections/granules.js @@ -13,11 +13,9 @@ import { clearGranulesSearch } from '../../actions'; import { - tableHeader, - tableRow, - tableSortProps, simpleDropdownOption, - bulkActions + bulkActions, + tableColumns } from '../../utils/table-config/granules'; import List from '../Table/Table'; import Dropdown from '../DropDown/dropdown'; @@ -36,7 +34,6 @@ const CollectionGranules = ({ workflowOptions }) => { const { params } = match; - console.log(match); const { name: collectionName, version: collectionVersion } = params; const { pathname } = location; const { list } = granules; @@ -148,13 +145,12 @@ const CollectionGranules = ({ + rowId='granuleId' + sortIdx='timestamp' + tableColumns={tableColumns} + > { collection.mmtLink = mmtLinks[getCollectionId(collection)]; }); + const data = list.data.map((collection) => { + return { + ...collection, + mmtLink: mmtLinks[getCollectionId(collection)] + }; + }); const { count, queriedAt } = list.meta; return (
    @@ -140,37 +143,26 @@ class CollectionList extends React.Component {

    {strings.all_collections} {count ? ` ${tally(count)}` : 0}

    -
    -
    - -
    -
    -
    -
      -
    • -
    • {this.renderDeleteButton()}
    • -
    -
    -
    -
    - + sortIdx='duration' + > + + + +
    ); diff --git a/app/src/js/components/Collections/overview.js b/app/src/js/components/Collections/overview.js index 5c0ae0b48..2dd0286b6 100644 --- a/app/src/js/components/Collections/overview.js +++ b/app/src/js/components/Collections/overview.js @@ -28,7 +28,7 @@ import statusOptions from '../../utils/status'; import List from '../Table/Table'; import Bulk from '../Granules/bulk'; import Overview from '../Overview/overview'; -import { tableHeader, tableRow, tableSortProps } from '../../utils/table-config/granules'; +import { tableColumns } from '../../utils/table-config/granules'; import { strings } from '../locale'; import DeleteCollection from '../DeleteCollection/DeleteCollection'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; @@ -287,12 +287,10 @@ class CollectionOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} - rowId={'granuleId'} - sortIdx={6} + rowId='granuleId' + sortIdx='timestamp' />
    diff --git a/app/src/js/components/Executions/execution-status.js b/app/src/js/components/Executions/execution-status.js index fe121c923..dffd77ac8 100644 --- a/app/src/js/components/Executions/execution-status.js +++ b/app/src/js/components/Executions/execution-status.js @@ -9,10 +9,7 @@ import { displayCase, fullDate, parseJson } from '../../utils/format'; import { withRouter, Link } from 'react-router-dom'; import { kibanaExecutionLink } from '../../utils/kibana'; -import { - tableHeader, - tableRow -} from '../../utils/table-config/execution-status'; +import { tableColumns } from '../../utils/table-config/execution-status'; import ErrorReport from '../Errors/report'; @@ -56,13 +53,11 @@ class ExecutionStatus extends React.Component { a.id > b.id ? 1 : -1)} dispatch={this.props.dispatch} - header={tableHeader} - row={tableRow} + tableColumns={tableColumns} rowId='id' - sortIdx={0} + sortIdx='id' props={[]} order='asc' - collapsible={true} /> ); } diff --git a/app/src/js/components/Executions/overview.js b/app/src/js/components/Executions/overview.js index 6e0e8a789..80a59cfad 100644 --- a/app/src/js/components/Executions/overview.js +++ b/app/src/js/components/Executions/overview.js @@ -3,7 +3,7 @@ import React from 'react'; import { get } from 'object-path'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { clearExecutionsFilter, filterExecutions, @@ -17,12 +17,9 @@ import { listWorkflows } from '../../actions'; import { - fromNow, - seconds, tally, lastUpdated, - displayCase, - truncate + displayCase } from '../../utils/format'; import { workflowOptions, @@ -34,37 +31,11 @@ import Dropdown from '../DropDown/dropdown'; import Search from '../Search/search'; import Overview from '../Overview/overview'; import _config from '../../config'; -import {strings} from '../locale'; +import { strings } from '../locale'; +import { tableColumns } from '../../utils/table-config/executions'; const { updateInterval } = _config; -const tableHeader = [ - 'Name', - 'Status', - 'Type', - 'Created', - 'Duration', - strings.collection_name -]; - -const tableRow = [ - (d) => {truncate(d.name, 24)}, - (d) => displayCase(d.status), - 'type', - (d) => fromNow(d.createdAt), - (d) => seconds(d.duration), - 'collectionId' -]; - -const tableSortProps = [ - 'name', - 'status', - 'type', - 'createdAt', - 'duration', - strings.collection_id -]; - class ExecutionOverview extends React.Component { constructor (props) { super(props); @@ -164,12 +135,10 @@ class ExecutionOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listExecutions} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={{}} - rowId={'name'} - sortIdx={3} + rowId='name' + sortIdx='createdAt' />
    diff --git a/app/src/js/components/Granules/granule.js b/app/src/js/components/Granules/granule.js index ac0521d97..8d517312b 100644 --- a/app/src/js/components/Granules/granule.js +++ b/app/src/js/components/Granules/granule.js @@ -38,22 +38,27 @@ import { simpleDropdownOption } from '../../utils/table-config/granules'; const { updateInterval } = _config; -const tableHeader = [ - 'Filename', - 'Link', - 'Bucket' -]; - const link = 'Link'; const makeLink = (bucket, key) => { return `https://${bucket}.s3.amazonaws.com/${key}`; }; -const tableRow = [ - (d) => d.fileName || '(No name)', - (d) => (d.bucket && d.key) ? ({d.fileName ? link : nullValue}) : null, - (d) => d.bucket +const tableColumns = [ + { + Header: 'Filename', + accessor: row => row.fileName || '(No name)', + id: 'fileName' + }, + { + Header: 'Link', + accessor: row => (row.bucket && row.key) ? ({row.fileName ? link : nullValue}) : null, + id: 'link' + }, + { + Header: 'Bucket', + accessor: 'bucket' + } ]; const metaAccessors = [ @@ -241,9 +246,7 @@ class GranuleOverview extends React.Component { diff --git a/app/src/js/components/Granules/list.js b/app/src/js/components/Granules/list.js index d1067545b..d810861d4 100644 --- a/app/src/js/components/Granules/list.js +++ b/app/src/js/components/Granules/list.js @@ -17,12 +17,8 @@ import { import { get } from 'object-path'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, - errorTableHeader, - errorTableRow, - errorTableSortProps, + tableColumns, + errorTableColumns, bulkActions, simpleDropdownOption } from '../../utils/table-config/granules'; @@ -116,7 +112,7 @@ class AllGranules extends React.Component { const logsQuery = { 'granuleId__exists': 'true' }; const view = this.getView(); const statOptions = (view === 'all') ? statusOptions : null; - const tableSortIdx = view === 'failed' ? 3 : 6; + const tableSortIdx = view === 'failed' ? 'granuleId' : 'timestamp'; return (
    @@ -153,12 +149,10 @@ class AllGranules extends React.Component {
    diff --git a/app/src/js/components/Granules/overview.js b/app/src/js/components/Granules/overview.js index 85a4ca289..8b725a5c2 100644 --- a/app/src/js/components/Granules/overview.js +++ b/app/src/js/components/Granules/overview.js @@ -20,9 +20,7 @@ import { import { get } from 'object-path'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, + tableColumns, simpleDropdownOption, bulkActions, recoverAction @@ -200,13 +198,11 @@ class GranulesOverview extends React.Component {
    diff --git a/app/src/js/components/ListActions/ListActions.js b/app/src/js/components/ListActions/ListActions.js index 43cf5d542..20aff9bdb 100644 --- a/app/src/js/components/ListActions/ListActions.js +++ b/app/src/js/components/ListActions/ListActions.js @@ -34,20 +34,30 @@ const ListActions = ({
    {hasActions && (
    - {bulkActions.map((item) => - )} + {bulkActions.map((item) => { + return ( + <> + {item.Component && + item.Component + } + {!item.Component && + + } + + ); + })}
    )} {truncate(d.name, 24)}, - -const tableRow = [ - (d) => displayCase(d.status), - (d) => d.id, - (d) => d.description, - (d) => d.operationType, - (d) => fromNow(d.createdAt) -]; - -const tableSortProps = [ - 'status', - 'id', - null, - 'operationType', - 'createdAt' -]; - class OperationOverview extends React.Component { constructor (props) { super(props); @@ -164,12 +132,10 @@ class OperationOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listOperations} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} - rowId={'id'} - sortIdx={4} + rowId='id' + sortIdx='createdAt' />
    diff --git a/app/src/js/components/Pdr/list.js b/app/src/js/components/Pdr/list.js index d7c7c1b45..255c8ad5c 100644 --- a/app/src/js/components/Pdr/list.js +++ b/app/src/js/components/Pdr/list.js @@ -12,12 +12,8 @@ import { } from '../../actions'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { - tableHeader, - tableRow, - tableSortProps, - errorTableHeader, - errorTableRow, - errorTableSortProps, + tableColumns, + errorTableColumns, bulkActions } from '../../utils/table-config/pdrs'; import Dropdown from '../DropDown/dropdown'; @@ -87,12 +83,10 @@ class ActivePdrs extends React.Component { list={list} dispatch={this.props.dispatch} action={listPdrs} - tableHeader={view === 'failed' ? errorTableHeader : tableHeader} - tableRow={view === 'failed' ? errorTableRow : tableRow} - tableSortProps={view === 'failed' ? errorTableSortProps : tableSortProps} + tableColumns={view === 'failed' ? errorTableColumns : tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'pdrName'} + rowId='pdrName' /> diff --git a/app/src/js/components/Pdr/overview.js b/app/src/js/components/Pdr/overview.js index ea4cbb737..21f90d293 100644 --- a/app/src/js/components/Pdr/overview.js +++ b/app/src/js/components/Pdr/overview.js @@ -7,7 +7,7 @@ import { get } from 'object-path'; import { interval, listPdrs, getCount } from '../../actions'; import { lastUpdated, tally, displayCase } from '../../utils/format'; import { bulkActions } from '../../utils/table-config/pdrs'; -import { tableHeader, tableRow, tableSortProps } from '../../utils/table-config/pdr-progress'; +import { tableColumns } from '../../utils/table-config/pdr-progress'; import List from '../Table/Table'; import Overview from '../Overview/overview'; import _config from '../../config'; @@ -75,13 +75,11 @@ class PdrOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listPdrs} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} - sortIdx={5} + tableColumns={tableColumns} + sortIdx='timestamp' query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'pdrName'} + rowId='pdrName' /> View Currently Active PDRs diff --git a/app/src/js/components/Pdr/pdr.js b/app/src/js/components/Pdr/pdr.js index b06282c53..1e2fece46 100644 --- a/app/src/js/components/Pdr/pdr.js +++ b/app/src/js/components/Pdr/pdr.js @@ -27,7 +27,7 @@ import { bool, deleteText } from '../../utils/format'; -import { tableHeader, tableRow, tableSortProps, bulkActions } from '../../utils/table-config/pdrs'; +import { tableColumns, bulkActions } from '../../utils/table-config/pdrs'; import { renderProgress } from '../../utils/table-config/pdr-progress'; import List from '../Table/Table'; import LogViewer from '../Logs/viewer'; @@ -184,12 +184,10 @@ class PDR extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'granuleId'} + rowId='granuleId' /> diff --git a/app/src/js/components/ReconciliationReports/list.js b/app/src/js/components/ReconciliationReports/list.js index 60d2c138d..8d4f8921b 100644 --- a/app/src/js/components/ReconciliationReports/list.js +++ b/app/src/js/components/ReconciliationReports/list.js @@ -9,12 +9,7 @@ import { listReconciliationReports } from '../../actions'; import { lastUpdated } from '../../utils/format'; -import { - tableHeader, - tableRow, - tableSortProps, - bulkActions -} from '../../utils/table-config/reconciliation-reports'; +import { tableColumns, bulkActions } from '../../utils/table-config/reconciliation-reports'; import Search from '../Search/search'; import List from '../Table/Table'; @@ -59,12 +54,10 @@ class ReconciliationReportList extends React.Component { list={list} dispatch={this.props.dispatch} action={listReconciliationReports} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={this.generateQuery()} bulkActions={this.generateBulkActions()} - rowId={'reconciliationReportName'} + rowId='reconciliationReportName' /> diff --git a/app/src/js/components/ReconciliationReports/reconciliation-report.js b/app/src/js/components/ReconciliationReports/reconciliation-report.js index 10cde4826..24efeb16e 100644 --- a/app/src/js/components/ReconciliationReports/reconciliation-report.js +++ b/app/src/js/components/ReconciliationReports/reconciliation-report.js @@ -12,18 +12,10 @@ import { } from '../../actions'; import _config from '../../config'; import { - tableHeaderS3Files, - tableRowS3File, - tablePropsS3File, - tableHeaderFiles, - tableRowFile, - tablePropsFile, - tableHeaderCollections, - tableRowCollection, - tablePropsCollection, - tableHeaderGranules, - tableRowGranule, - tablePropsGranule + tableColumnsS3Files, + tableColumnsFiles, + tableColumnsCollections, + tableColumnsGranules } from '../../utils/table-config/reconciliation-reports'; import Metadata from '../Table/Metadata'; @@ -217,66 +209,50 @@ class ReconciliationReport extends React.Component { diff --git a/app/src/js/components/ReconciliationReports/report-table.js b/app/src/js/components/ReconciliationReports/report-table.js index bc14ec241..ea4d90302 100644 --- a/app/src/js/components/ReconciliationReports/report-table.js +++ b/app/src/js/components/ReconciliationReports/report-table.js @@ -4,63 +4,55 @@ import Collapsible from 'react-collapsible'; import SortableTable from '../SortableTable/SortableTable'; -class ReportTable extends React.Component { - render () { - const { - collapsible, - collapseThreshold, - data, - title, - tableHeader, - tableRow, - tableProps - } = this.props; - - if (!data || !data.length) { - return null; - } - - let reportTable = ( - - ); - - if (collapsible && data.length > collapseThreshold) { - reportTable = ( - - { reportTable } - - ); - } +const ReportTable = ({ + collapsible, + collapseThreshold, + data, + title, + tableColumns +}) => { + if (!data || !data.length) { + return null; + } - return ( -
    -

    - {title} ({data.length}) -

    + const shouldCollapse = collapsible && data.length > collapseThreshold; + + let reportTable = ( + + ); + + if (shouldCollapse) { + reportTable = ( + { reportTable } -
    + ); } -} + + return ( +
    +

    + {title} ({data.length}) +

    + { reportTable } +
    + ); +}; ReportTable.propTypes = { collapsible: PropTypes.bool, collapseThreshold: PropTypes.number, data: PropTypes.array, title: PropTypes.string, - tableHeader: PropTypes.array, - tableRow: PropTypes.array, - tableProps: PropTypes.array + tableColumns: PropTypes.array }; ReportTable.defaultProps = { diff --git a/app/src/js/components/Rules/overview.js b/app/src/js/components/Rules/overview.js index a28947db5..9e495834f 100644 --- a/app/src/js/components/Rules/overview.js +++ b/app/src/js/components/Rules/overview.js @@ -1,70 +1,12 @@ 'use strict'; import React from 'react'; import PropTypes from 'prop-types'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; -import { - listRules, - enableRule, - disableRule, - deleteRule -} from '../../actions'; -import { - lastUpdated, - tally, - getCollectionId, - collectionLink, - providerLink, - fromNow -} from '../../utils/format'; +import { listRules } from '../../actions'; +import { lastUpdated, tally } from '../../utils/format'; import List from '../Table/Table'; -import { strings } from '../locale'; - -const tableHeader = [ - 'Name', - 'Provider', - strings.collection_id, - 'Type', - 'State', - 'Timestamp' -]; - -const tableRow = [ - (d) => {d.name}, - (d) => providerLink(d.provider), - (d) => collectionLink(getCollectionId(d.collection)), - 'rule.type', - 'state', - (d) => fromNow(d.timestamp) -]; - -const tableSortProps = [ - 'name', - 'provider', - null, - null, - 'state', - 'timestamp' -]; - -const bulkActions = (rules) => [{ - text: 'Enable', - action: (ruleName) => - enableRule(rules.list.data.find((rule) => rule.name === ruleName)), - state: rules.enabled, - confirm: (d) => `Enable ${d} Rule(s)?` -}, { - text: 'Disable', - action: (ruleName) => - disableRule(rules.list.data.find((rule) => rule.name === ruleName)), - state: rules.disabled, - confirm: (d) => `Disable ${d} Rule(s)?` -}, { - text: 'Delete', - action: deleteRule, - state: rules.deleted, - confirm: (d) => `Delete ${d} Rule(s)?` -}]; +import { tableColumns, bulkActions } from '../../utils/table-config/rules'; class RulesOverview extends React.Component { constructor () { @@ -100,13 +42,11 @@ class RulesOverview extends React.Component { list={list} dispatch={this.props.dispatch} action={listRules} - tableHeader={tableHeader} - tableRow={tableRow} - tableSortProps={tableSortProps} + tableColumns={tableColumns} query={{}} - sortIdx={5} + sortIdx='timestamp' bulkActions={this.generateBulkActions()} - rowId={'name'} + rowId='name' /> diff --git a/app/src/js/components/SortableTable/SortableTable.js b/app/src/js/components/SortableTable/SortableTable.js index 32fa615df..e50fc9629 100644 --- a/app/src/js/components/SortableTable/SortableTable.js +++ b/app/src/js/components/SortableTable/SortableTable.js @@ -1,176 +1,197 @@ 'use strict'; -import Collapse from 'react-collapsible'; -import React, { useState } from 'react'; +import React, { + useMemo, + useEffect, + forwardRef, + useRef +} from 'react'; import PropTypes from 'prop-types'; -import { get } from 'object-path'; -import { isUndefined } from '../../utils/validate'; -import { nullValue } from '../../utils/format'; -import isEmpty from 'lodash.isempty'; -import isFunction from 'lodash.isfunction'; +import { useTable, useResizeColumns, useFlexLayout, useSortBy, useRowSelect } from 'react-table'; -const defaultSortOrder = 'desc'; -const otherOrder = { - desc: 'asc', - asc: 'desc' +/** + * IndeterminateCheckbox + * @description Component for rendering the header and column checkboxs when canSelect is true + * Taken from react-table examples + */ +const IndeterminateCheckbox = forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = useRef(); + const resolvedRef = ref || defaultRef; + + useEffect(() => { + resolvedRef.current.indeterminate = indeterminate; + }, [resolvedRef, indeterminate]); + + return ( + + ); + } +); + +IndeterminateCheckbox.propTypes = { + indeterminate: PropTypes.any, + onChange: PropTypes.func }; -const Table = ({ - primaryIdx = 0, +const SortableTable = ({ sortIdx, - order, - props, - header, - row, rowId, - data, + order = 'desc', canSelect, - collapsible, changeSortProps, + tableColumns = [], + data = [], onSelect }) => { - const [dumbState, setDumbState] = useState({ - dumbOrder: null, - dumbSortIdx: null - }); - const [selected, setSelected] = useState([]); - const isTableDumb = isUndefined(sortIdx) || !order || !Array.isArray(props); - const allChecked = !isEmpty(data) && selected.length === data.length; + const defaultColumn = useMemo( + () => ({ + // When using the useFlexLayout: + minWidth: 30, // minWidth is only used as a limit for resizing + width: 125, // width is used for both the flex-basis and flex-grow + maxWidth: 300, // maxWidth is only used as a limit for resizing + }), + [] + ); - if (isTableDumb) { - props = []; - sortIdx = dumbState.dumbSortIdx; - order = dumbState.dumbOrder; - const sortName = props[sortIdx]; - const primaryName = props[primaryIdx]; - data = data.sort((a, b) => - // If the sort field is the same, tie-break using the primary ID field - a[sortName] === b[sortName] ? a[primaryName] > b[primaryName] - : (order === 'asc') ? a[sortName] < b[sortName] : a[sortName] > b[sortName] - ); - } + const shouldManualSort = !!sortIdx; - function changeSort (e) { - if (isTableDumb) { - sortIdx = dumbState.dumbSortIdx; - order = dumbState.dumbOrder; + const { + getTableProps, + rows, + prepareRow, + headerGroups, + state: { + selectedRowIds, + sortBy + }, + } = useTable( + { + data, + columns: tableColumns, + defaultColumn, + getRowId: (row, relativeIndex) => typeof rowId === 'function' ? rowId(row) : row[rowId] || relativeIndex, + autoResetSelectedRows: false, + autoResetSortBy: false, + manualSortBy: shouldManualSort + }, + useFlexLayout, // this allows table to have dynamic layouts outside of standard table markup + useResizeColumns, // this allows for resizing columns + useSortBy, // this allows for sorting + useRowSelect, // this allows for checkbox in table + hooks => { + if (canSelect) { + hooks.visibleColumns.push(columns => [ + { + id: 'selection', + Header: ({ getToggleAllRowsSelectedProps }) => ( // eslint-disable-line react/prop-types + + ), + Cell: ({ row }) => ( + + ), + minWidth: 61, + width: 61, + maxWidth: 61 + }, + ...columns + ]); + } } + ); - const headerName = e.currentTarget.getAttribute('data-value'); - const newSortIdx = header.indexOf(headerName); - if (!props[newSortIdx]) { return; } - const newOrder = sortIdx === newSortIdx ? otherOrder[order] : defaultSortOrder; + useEffect(() => { + let selected = []; - if (typeof changeSortProps === 'function') { - changeSortProps({ sortIdx: newSortIdx, order: newOrder }); - } - if (isTableDumb) { - setDumbState({ dumbSortIdx: newSortIdx, dumbOrder: newOrder }); + for (let [key, value] of Object.entries(selectedRowIds)) { + if (value) { + selected.push(key); + } } - } - - function select (e) { - const id = (e.currentTarget.getAttribute('data-value')); - const selectedRows = selected.includes(id) - ? selected.filter(anId => anId !== id) - : [...selected, id]; - setSelected(selectedRows); if (typeof onSelect === 'function') { - onSelect(selectedRows); + onSelect(selected); } - } + }, [selectedRowIds, onSelect]); - function selectAll (e) { - if (!isEmpty(data)) { - const rowIdFn = isFunction(rowId) ? rowId : row => row[rowId]; - const allSelected = selected.length === data.length; - const selectedRows = allSelected ? [] : data.map(rowIdFn); - setSelected(selectedRows); + useEffect(() => { + const [sortProps = {}] = sortBy; + const { id, desc } = sortProps; + let sortOrder; + if (typeof desc !== 'undefined') { + sortOrder = desc ? 'desc' : 'asc'; } - } + const sortFieldId = id || sortIdx; + if (typeof changeSortProps === 'function') { + changeSortProps({ sortIdx: sortFieldId, order: sortOrder || order }); + } + }, [changeSortProps, sortBy, sortIdx, order]); return (
    -
    - - - {canSelect && - - } - {header.map((h, i) => { - let className = (isTableDumb || props[i]) ? 'table__sort' : ''; - if (i === sortIdx) { className += (' table__sort--' + order); } - return ( - - ); - })} - - - - {data.map((d, i) => { - const dataId = typeof rowId === 'function' ? rowId(d) : d[rowId]; - const checked = canSelect && selected.indexOf(dataId) !== -1; +
    +
    +
    + {headerGroups.map(headerGroup => ( +
    + {headerGroup.headers.map(column => { + return ( +
    +
    + {column.render('Header')} +
    +
    +
    + ); + })} +
    + ))} +
    +
    +
    + {rows.map((row, i) => { + prepareRow(row); return ( -
    - {canSelect && - - } - {row.map((accessor, k) => { - let className = k === primaryIdx ? 'table__main-asset' : ''; - let text; - - if (typeof accessor === 'function') { - text = accessor(d, k, data); - } else { - text = get(d, accessor, nullValue); - } - return ; +
    + {row.cells.map((cell, cellIndex) => { + const primaryIdx = canSelect ? 1 : 0; + return ( + +
    + {cell.render('Cell')} +
    +
    + ); })} - {collapsible && -
    - } - + ); })} - -
    - - {h} -
    - - {text} - -
    {JSON.stringify(d.eventDetails, null, 2)}
    -
    -
    + + ); }; -Table.propTypes = { +SortableTable.propTypes = { primaryIdx: PropTypes.number, data: PropTypes.array, header: PropTypes.array, - props: PropTypes.array, - row: PropTypes.array, - sortIdx: PropTypes.number, order: PropTypes.string, + row: PropTypes.array, + sortIdx: PropTypes.string, changeSortProps: PropTypes.func, onSelect: PropTypes.func, canSelect: PropTypes.bool, collapsible: PropTypes.bool, - rowId: PropTypes.any + rowId: PropTypes.any, + tableColumns: PropTypes.array }; -export default Table; +export default SortableTable; diff --git a/app/src/js/components/Table/Table.js b/app/src/js/components/Table/Table.js index 8de52a1aa..9e43416fe 100644 --- a/app/src/js/components/Table/Table.js +++ b/app/src/js/components/Table/Table.js @@ -3,13 +3,10 @@ import React from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -// import BatchAsyncCommand from '../BatchAsyncCommands/BatchAsyncCommands'; import ErrorReport from '../Errors/report'; import Loading from '../LoadingIndicator/loading-indicator'; import Pagination from '../Pagination/pagination'; import SortableTable from '../SortableTable/SortableTable'; -// import Timer from '../Timer/timer'; -// import TableOptions from '../TableOptions/TableOptions' // Lodash import isEmpty from 'lodash.isempty'; import isEqual from 'lodash.isequal'; @@ -24,7 +21,6 @@ class List extends React.Component { this.displayName = 'List'; this.queryNewPage = this.queryNewPage.bind(this); this.queryNewSort = this.queryNewSort.bind(this); - this.getSortProp = this.getSortProp.bind(this); this.selectAll = this.selectAll.bind(this); this.updateSelection = this.updateSelection.bind(this); this.onBulkActionSuccess = this.onBulkActionSuccess.bind(this); @@ -44,7 +40,7 @@ class List extends React.Component { queryConfig: { page: initialPage, order: initialOrder, - sort_by: this.getSortProp(initialSortIdx), + sort_by: initialSortIdx, ...(props.query || {}) }, params: {}, @@ -54,7 +50,7 @@ class List extends React.Component { } componentDidUpdate (prevProps) { - const { query, list, sortIdx } = this.props; + const { query, list } = this.props; if (!isEqual(query, prevProps.query)) { // eslint-disable-next-line react/no-did-update-set-state @@ -70,11 +66,6 @@ class List extends React.Component { queryConfig: this.getQueryConfig() })); } - - if (sortIdx !== this.state.sortIdx) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ sortIdx }); - } } queryNewPage (page) { @@ -90,16 +81,12 @@ class List extends React.Component { ...sortProps, queryConfig: this.getQueryConfig({ order: sortProps.order, - sort_by: this.getSortProp(sortProps.sortIdx) + sort_by: sortProps.sortIdx }), selected: [] }); } - getSortProp (idx) { - return this.props.tableSortProps[idx]; - } - selectAll (e) { const { rowId, list: { data } } = this.props; @@ -138,11 +125,11 @@ class List extends React.Component { return omitBy({ page: this.state.page, order: this.state.order, - sort_by: this.getSortProp(this.state.sortIdx), + sort_by: this.state.sortIdx, ...this.state.params, ...config, ...query - }, isEmpty); + }, isNil); } render () { @@ -150,31 +137,27 @@ class List extends React.Component { dispatch, action, children, - tableHeader, - tableRow, - tableSortProps, bulkActions, rowId, list, - list: { - meta: { - count, - limit - } - } + tableColumns, + data } = this.props; + const { meta, data: listData } = list; + const { count, limit } = meta; + const tableData = data || listData; const { page, sortIdx, order, selected, - queryConfig, completedBulkActions, bulkActionError } = this.state; - const primaryIdx = 0; const hasActions = Array.isArray(bulkActions) && bulkActions.length > 0; + const queryConfig = this.getQueryConfig(); + return ( <> */} {d.name}, - 'definition.Comment' -]; - -const tableSortProps = [ - 'name', - 'aws link' -]; - -class WorkflowOverview extends React.Component { - render () { - const { list } = this.props.workflows; - const count = list.data.length; - return ( -
    -
    -

    Workflow Overview

    -
    -
    -
    -

    All Workflows {count ? ` ${tally(count)}` : 0}

    -
    - {/* Someone needs to define the search parameters for workflows, e.g. steps, collections, granules, etc. }*/} - {/*
    - -
    */} - - { + const { list } = workflows; + const count = list.data.length; + return ( +
    +
    +

    Workflow Overview

    +
    +
    +
    +

    All Workflows {count ? ` ${tally(count)}` : 0}

    +
    + {/* Someone needs to define the search parameters for workflows, e.g. steps, collections, granules, etc. }*/} + {/*
    + -
    -
    - ); - } -} +
    */} + + + + + ); +}; WorkflowOverview.propTypes = { dispatch: PropTypes.func, diff --git a/app/src/js/components/home.js b/app/src/js/components/home.js index 46fce4834..3a0ae5a23 100644 --- a/app/src/js/components/home.js +++ b/app/src/js/components/home.js @@ -25,11 +25,7 @@ import { } from '../utils/format'; import List from './Table/Table'; import GranulesProgress from './Granules/progress'; -import { - errorTableHeader, - errorTableRow, - errorTableSortProps -} from '../utils/table-config/granules'; +import { errorTableColumns } from '../utils/table-config/granules'; import { recent, updateInterval } from '../config'; import { kibanaS3AccessErrorsLink, @@ -224,10 +220,8 @@ class Home extends React.Component { list={list} dispatch={this.props.dispatch} action={listGranules} - tableHeader={errorTableHeader} - sortIdx={4} - tableRow={errorTableRow} - tableSortProps={errorTableSortProps} + tableColumns={errorTableColumns} + sortIdx='timestamp' query={this.generateQuery()} /> diff --git a/app/src/js/reducers/stats.js b/app/src/js/reducers/stats.js index 10d4524fc..83e86e95a 100644 --- a/app/src/js/reducers/stats.js +++ b/app/src/js/reducers/stats.js @@ -1,7 +1,6 @@ 'use strict'; import assignDate from './assign-date'; import { set } from 'object-path'; -import serialize from '../utils/serialize-config'; import { STATS, @@ -10,11 +9,7 @@ import { COUNT, COUNT_INFLIGHT, - COUNT_ERROR, - - HISTOGRAM, - HISTOGRAM_INFLIGHT, - HISTOGRAM_ERROR + COUNT_ERROR } from '../actions/types'; export const initialState = { @@ -35,13 +30,12 @@ export const initialState = { data: {}, inflight: false, error: null - }, - histogram: {} + } }; export default function reducer (state = initialState, action) { let nextState; - let stats, count, histogram; + let stats, count; switch (action.type) { case STATS: stats = { data: assignDate(action.data), inflight: false, error: null }; @@ -69,26 +63,6 @@ export default function reducer (state = initialState, action) { count = { data: state.count.data, inflight: false, error: action.error }; nextState = Object.assign(state, { count }); break; - - case HISTOGRAM: - histogram = Object.assign({}, state.histogram); - set(histogram, serialize(action.config.qs), { - inflight: false, - data: action.data, - error: null - }); - nextState = Object.assign(state, { histogram }); - break; - case HISTOGRAM_INFLIGHT: - histogram = Object.assign({}, state.histogram); - set(histogram, [serialize(action.config.qs), 'inflight'], true); - nextState = Object.assign(state, { histogram }); - break; - case HISTOGRAM_ERROR: - histogram = Object.assign({}, state.histogram); - set(histogram, [serialize(action.config.qs), 'error'], action.error); - nextState = Object.assign(state, { histogram }); - break; } return nextState || state; } diff --git a/app/src/js/utils/table-config/collections.js b/app/src/js/utils/table-config/collections.js index 9781218d3..bc8e17fa4 100644 --- a/app/src/js/utils/table-config/collections.js +++ b/app/src/js/utils/table-config/collections.js @@ -5,41 +5,64 @@ import { Link } from 'react-router-dom'; import { fromNow, seconds, tally, collectionNameVersion } from '../format'; import { deleteCollection } from '../../actions'; import { strings } from '../../components/locale'; +import { Button } from 'react-bootstrap'; -export const tableHeader = [ - 'Name', - 'Version', - strings.granules, - 'Completed', - 'Running', - 'Failed', - 'MMT', - 'Duration', - 'Timestamp' -]; - -export const tableRow = [ - (d) => {d.name}, - 'version', - (d) => tally(get(d, 'stats.total')), - (d) => tally(get(d, 'stats.completed')), - (d) => tally(get(d, 'stats.running')), - (d) => tally(get(d, 'stats.failed')), - (d) => d.mmtLink ? MMT : null, - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'name', - 'version', - null, - null, - null, - null, - null, - 'duration', - 'timestamp' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name', + width: 175 + }, + { + Header: 'Version', + accessor: 'version' + }, + { + Header: strings.granules, + accessor: row => tally(get(row, 'stats.total')), + id: 'granules', + disableSortBy: true, + width: 100 + }, + { + Header: 'Completed', + accessor: row => tally(get(row, 'stats.completed')), + id: 'completed', + disableSortBy: true, + width: 100 + }, + { + Header: 'Running', + accessor: row => tally(get(row, 'stats.running')), + id: 'running', + disableSortBy: true, + width: 100 + }, + { + Header: 'Failed', + accessor: row => tally(get(row, 'stats.failed')), + id: 'failed', + disableSortBy: true, + width: 100 + }, + { + Header: 'MMT', + accessor: row => row.mmtLink ? MMT : null, + id: 'mmtLink', + disableSortBy: true, + width: 100 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: 'Timestamp', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; const confirmRecover = (d) => `Recover ${d} ${strings.collection}(s)?`; @@ -54,14 +77,19 @@ export const recoverAction = function (collections, config) { const confirmDelete = (d) => `Delete ${d} ${strings.collection}(s)?`; export const bulkActions = function (collections) { - return [{ - text: 'Delete Collection', - action: (collectionId) => { - const { name, version } = collectionNameVersion(collectionId); - return deleteCollection(name, version); + return [ + { + Component: }, - state: collections.deleted, - confirm: confirmDelete, - className: 'button button--delete button--small form-group__element' - }]; + { + text: 'Delete Collection', + action: (collectionId) => { + const { name, version } = collectionNameVersion(collectionId); + return deleteCollection(name, version); + }, + state: collections.deleted, + confirm: confirmDelete, + className: 'button button--delete button--small form-group__element' + } + ]; }; diff --git a/app/src/js/utils/table-config/execution-status.js b/app/src/js/utils/table-config/execution-status.js index 3bb931427..e5cefc772 100644 --- a/app/src/js/utils/table-config/execution-status.js +++ b/app/src/js/utils/table-config/execution-status.js @@ -1,18 +1,31 @@ 'use strict'; +import React from 'react'; +import Collapse from 'react-collapsible'; -import { - fullDate -} from '../format'; +import { fullDate } from '../format'; -export const tableHeader = [ - 'Id', - 'Type', - 'Timestamp', - 'Input Details' -]; - -export const tableRow = [ - (d) => d['id'], - 'type', - (d) => fullDate(d['timestamp']) +export const tableColumns = [ + { + Header: 'Id', + accessor: row => row['id'], + id: 'id' + }, + { + Header: 'Type', + accessor: 'type' + }, + { + Header: 'Timestamp', + accessor: row => fullDate(row['timestamp']), + id: 'timestamp' + }, + { + Header: 'Input Details', + accessor: row => ( + +
    {JSON.stringify(row.eventDetails, null, 2)}
    +
    + ), + id: 'eventDetails' + } ]; diff --git a/app/src/js/utils/table-config/executions.js b/app/src/js/utils/table-config/executions.js new file mode 100644 index 000000000..d3957aabf --- /dev/null +++ b/app/src/js/utils/table-config/executions.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + fromNow, + seconds, + displayCase, + truncate +} from '../../utils/format'; +import { strings } from '../../components/locale'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {truncate(row.name, 24)}, + id: 'name' + }, + { + Header: 'Status', + accessor: row => displayCase(row.status), + id: 'status' + }, + { + Header: 'Type', + accessor: 'type' + }, + { + Header: 'Created', + accessor: row => fromNow(row.createdAt), + id: 'createdAt' + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: strings.collection_name, + accessor: 'collectionId' + } +]; diff --git a/app/src/js/utils/table-config/granules.js b/app/src/js/utils/table-config/granules.js index 651c494cc..d43676619 100644 --- a/app/src/js/utils/table-config/granules.js +++ b/app/src/js/utils/table-config/granules.js @@ -21,58 +21,80 @@ import ErrorReport from '../../components/Errors/report'; import {strings} from '../../components/locale'; import Dropdown from '../../components/DropDown/simple-dropdown'; -export const tableHeader = [ - 'Status', - 'Name', - 'Published', - strings.collection_id, - 'Execution', - 'Duration', - 'Updated' +export const tableColumns = [ + { + Header: 'Status', + accessor: row => {displayCase(row.status)}, + id: 'status', + width: 100 + }, + { + Header: 'Name', + accessor: row => granuleLink(row.granuleId), + id: 'name', + width: 225 + }, + { + Header: 'Published', + accessor: row => row.cmrLink ? {bool(row.published)} : bool(row.published), + id: 'published' + }, + { + Header: strings.collection_id, + accessor: row => collectionLink(row.collectionId), + id: 'collectionId' + }, + { + Header: 'Execution', + accessor: row => link, + id: 'execution', + disableSortBy: true, + width: 90 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration', + width: 100 + }, + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {displayCase(d.status)}, - (d) => granuleLink(d.granuleId), - (d) => d.cmrLink ? {bool(d.published)} : bool(d.published), - (d) => collectionLink(d.collectionId), - (d) => link, - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'status', - 'granuleId', - 'published', - 'collectionId', - null, - 'duration', - 'timestamp' -]; - -export const errorTableHeader = [ - 'Error', - 'Type', - 'Granule', - 'Duration', - 'Updated' -]; - -export const errorTableRow = [ - (d) => , - (d) => get(d, 'error.Error', nullValue), - (d) => granuleLink(d.granuleId), - (d) => seconds(d.duration), - (d) => fromNow(d.timestamp) -]; - -export const errorTableSortProps = [ - null, - null, - 'granuleId', - 'duration', - 'timestamp' +export const errorTableColumns = [ + { + Header: 'Error', + accessor: row => , + id: 'error', + disableSortBy: true, + width: 175 + }, + { + Header: 'Type', + accessor: row => get(row, 'error.Error', nullValue), + id: 'type', + disableSortBy: true, + width: 100 + }, + { + Header: 'Granule', + accessor: row => granuleLink(row.granuleId), + id: 'granuleId', + width: 200 + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; export const simpleDropdownOption = function (config) { diff --git a/app/src/js/utils/table-config/instances.js b/app/src/js/utils/table-config/instances.js index f44f85327..20767ebf9 100644 --- a/app/src/js/utils/table-config/instances.js +++ b/app/src/js/utils/table-config/instances.js @@ -1,20 +1,31 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Instance ID', - 'Status', - 'Pending Tasks', - 'Running Tasks', - 'Available CPU', - 'Available Memory' -]; - -export const tableRow = [ - 'id', - 'status', - (d) => tally(d['pendingTasks']), - (d) => tally(d['runningTasks']), - 'availableCpu', - 'availableMemory' +export const tableColumns = [ + { + Header: 'Instance ID', + accessor: 'id' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Pending Tasks', + accessor: row => tally(row['pendingTasks']), + id: 'pendingTasks' + }, + { + Header: 'Running Tasks', + accessor: row => tally(row['runningTasks']), + id: 'runningTasks' + }, + { + Header: 'Available CPU', + accessor: 'availableCpu' + }, + { + Header: 'Available Memory', + accessor: 'availableMemory' + } ]; diff --git a/app/src/js/utils/table-config/operations.js b/app/src/js/utils/table-config/operations.js new file mode 100644 index 000000000..5dde49837 --- /dev/null +++ b/app/src/js/utils/table-config/operations.js @@ -0,0 +1,27 @@ +import { fromNow, displayCase } from '../../utils/format'; + +export const tableColumns = [ + { + Header: 'Status', + accessor: row => displayCase(row.status), + id: 'status' + }, + { + Header: 'Async ID', + accessor: 'id' + }, + { + Header: 'Description', + accessor: 'description', + disableSortBy: true + }, + { + Header: 'Type', + accessor: 'operationType' + }, + { + Header: 'Created', + accessor: row => fromNow(row.createdAt), + id: 'createdAt' + } +]; diff --git a/app/src/js/utils/table-config/pdr-progress.js b/app/src/js/utils/table-config/pdr-progress.js index a77f72426..3a743ba2a 100644 --- a/app/src/js/utils/table-config/pdr-progress.js +++ b/app/src/js/utils/table-config/pdr-progress.js @@ -23,8 +23,8 @@ function bar (completed, failed, text) { ); } -export const getProgress = function (d) { - const granules = d.stats; +export const getProgress = function (row) { + const granules = row.stats; // granule count in all states, total is 'null' in some pdrs const total = Object.keys(granules).filter(k => k !== 'total') .reduce((a, b) => a + get(granules, b, 0), 0); @@ -40,13 +40,13 @@ export const getProgress = function (d) { }; }; -export const renderProgress = function (d) { +export const renderProgress = function (row) { // if the status is failed, return it as such - if (d.status === 'failed') { - const error = get(d, 'error', nullValue); + if (row.status === 'failed') { + const error = get(row, 'error', nullValue); return ; - } else if (typeof d.status === 'undefined') return null; - const progress = getProgress(d); + } else if (typeof row.status === 'undefined') return null; + const progress = getProgress(row); return (
    {bar(progress.percentCompleted, progress.percentFailed, progress.granulesCompleted)} @@ -54,29 +54,35 @@ export const renderProgress = function (d) { ); }; -export const tableHeader = [ - 'Name', - 'Status', - 'Progress', - 'Errors', - 'PAN/PDRD Sent', - 'Discovered' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Progress', + accessor: renderProgress, + id: 'progress' + }, + { + Header: 'Errors', + accessor: row => tally(get(row, 'granulesStatus.failed', 0)), + id: 'errors' + }, + { + Header: 'PAN/PDRD Sent', + accessor: row => bool(row.PANSent || row.PDRDSent), + id: 'panPdrdSent' + }, + { + Header: 'Discovered', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {d.pdrName}, - 'status', - renderProgress, - (d) => tally(get(d, 'granulesStatus.failed', 0)), - (d) => bool(d.PANSent || d.PDRDSent), - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'pdrName.keyword', - 'status.keyword', - 'progress', - null, - null, - 'timestamp' -]; diff --git a/app/src/js/utils/table-config/pdrs.js b/app/src/js/utils/table-config/pdrs.js index 580d4c1e5..44c10917c 100644 --- a/app/src/js/utils/table-config/pdrs.js +++ b/app/src/js/utils/table-config/pdrs.js @@ -6,62 +6,74 @@ import { seconds, fromNow, bool, nullValue } from '../format'; import { deletePdr } from '../../actions'; import { strings } from '../../components/locale'; -export const tableHeader = [ - 'Updated', - 'Name', - 'Status', - 'Duration', - strings.granules, - 'Processing', - 'Failed', - 'Completed' +export const tableColumns = [ + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + }, + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Duration', + accessor: row => seconds(row.duration), + id: 'duration' + }, + { + Header: strings.granules, + accessor: row => Object.keys(row.stats).filter(k => k !== 'total') + .reduce((a, b) => a + get(row.stats, b, 0), 0), + id: 'granules' + }, + { + Header: 'Processing', + accessor: row => get(row, ['stats', 'processing'], 0), + id: 'processing' + }, + { + Header: 'Failed', + accessor: row => get(row, ['stats', 'failed'], 0), + id: 'failed' + }, + { + Header: 'Completed', + accessor: row => get(row, ['stats', 'completed'], 0), + id: 'completed' + } ]; -export const tableRow = [ - (d) => fromNow(d.timestamp), - (d) => {d.pdrName}, - 'status', - (d) => seconds(d.duration), - (d) => Object.keys(d.stats).filter(k => k !== 'total') - .reduce((a, b) => a + get(d.stats, b, 0), 0), - (d) => get(d, ['stats', 'processing'], 0), - (d) => get(d, ['stats', 'failed'], 0), - (d) => get(d, ['stats', 'completed'], 0) -]; - -export const tableSortProps = [ - 'timestamp', - 'pdrName.keyword', - 'status.keyword', - null, - null, - null, - null, - null -]; - -export const errorTableHeader = [ - 'Updated', - 'Name', - 'Error', - 'PAN Sent', - 'PAN Message' -]; - -export const errorTableRow = [ - (d) => fromNow(d.timestamp), - (d) => {d.pdrName}, - (d) => get(d, 'error.Cause', nullValue), - (d) => bool(d.PANSent), - 'PANmessage' -]; - -export const errorTableSortProps = [ - 'timestamp', - 'pdrName', - null, - 'PANSent', - 'PANmessage' +export const errorTableColumns = [ + { + Header: 'Updated', + accessor: row => fromNow(row.timestamp), + id: 'updated' + }, + { + Header: 'Name', + accessor: row => {row.pdrName}, + id: 'name' + }, + { + Header: 'Error', + accessor: row => get(row, 'error.Cause', nullValue), + id: 'error' + }, + { + Header: 'PAN Sent', + accessor: row => bool(row.PANSent), + id: 'panSent' + }, + { + Header: 'PAN Message', + accessor: 'PANmessage' + } ]; const confirmDelete = (d) => `Delete ${d} PDR(s)?`; diff --git a/app/src/js/utils/table-config/providers.js b/app/src/js/utils/table-config/providers.js index 08294b691..8502af29b 100644 --- a/app/src/js/utils/table-config/providers.js +++ b/app/src/js/utils/table-config/providers.js @@ -4,29 +4,32 @@ import { Link } from 'react-router-dom'; import { fromNow } from '../format'; import { strings } from '../../components/locale'; -export const tableHeader = [ - 'Name', - 'Host', - strings.collections, - 'Global Connection Limit', - 'Protocol', - 'Last Updated' +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.id}, + id: 'name' + }, + { + Header: 'Host', + accessor: 'host' + }, + { + Header: strings.collections, + accessor: 'collections' + }, + { + Header: 'Global Connection Limit', + accessor: 'globalConnectionLimit' + }, + { + Header: 'Protocol', + accessor: 'protocol' + }, + { + Header: 'Last Updated', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } ]; -export const tableRow = [ - (d) => {d.id}, - 'host', - 'collections', - 'globalConnectionLimit', - 'protocol', - (d) => fromNow(d.timestamp) -]; - -export const tableSortProps = [ - 'id', - 'host', - 'collections', - 'globalConnectionLimit', - 'protocol', - 'timestamp' -]; diff --git a/app/src/js/utils/table-config/queues.js b/app/src/js/utils/table-config/queues.js index 23e76ab62..f856d39a9 100644 --- a/app/src/js/utils/table-config/queues.js +++ b/app/src/js/utils/table-config/queues.js @@ -1,14 +1,19 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Queue', - 'Messages Available', - 'Messages in Flight' -]; - -export const tableRow = [ - 'name', - (d) => tally(d['messagesAvailable']), - (d) => tally(d['messagesInFlight']) +export const tableColumns = [ + { + Header: 'Queue', + id: 'name' + }, + { + Header: 'Messages Available', + accessor: row => tally(row['messagesAvailable']), + id: 'messagesAvailable' + }, + { + Header: 'Messages in Flight', + accessor: row => tally(row['messagesInFlight']), + id: 'messagesInFlight' + } ]; diff --git a/app/src/js/utils/table-config/reconciliation-reports.js b/app/src/js/utils/table-config/reconciliation-reports.js index 97ed7c1fa..3d4b256d4 100644 --- a/app/src/js/utils/table-config/reconciliation-reports.js +++ b/app/src/js/utils/table-config/reconciliation-reports.js @@ -4,68 +4,64 @@ import { Link } from 'react-router-dom'; import { nullValue } from '../format'; -export const tableHeader = [ - 'Report filename' -]; - -export const tableRow = [ - (d) => {d.reconciliationReportName} -]; - -export const tableSortProps = [ - 'reconciliationReportName' +export const tableColumns = [ + { + Header: 'Report filename', + accessor: row => {row.reconciliationReportName} + } ]; export const bulkActions = function (reports) { return []; }; -export const tableHeaderS3Files = [ - 'Filename', - 'Bucket', - 'S3 Link' +export const tableColumnsS3Files = [ + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'path' + } ]; -export const tableRowS3File = [ - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue +export const tableColumnsFiles = [ + { + Header: 'GranuleId', + accessor: 'granuleId' + }, + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'path' + } ]; -export const tablePropsS3File = ['filename', 'bucket', 'link']; - -export const tableHeaderFiles = [ - 'GranuleId', - 'Filename', - 'Bucket', - 'S3 Link' -]; - -export const tableRowFile = [ - (d) => d.granuleId, - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue -]; - -export const tablePropsFile = ['granuleId', 'filename', 'bucket', 'link']; - -export const tableHeaderCollections = [ - 'Collection name' -]; - -export const tableRowCollection = [ - (d) => d.name -]; - -export const tablePropsCollection = ['name']; - -export const tableHeaderGranules = [ - 'Granule ID' +export const tableColumnsCollections = [ + { + Header: 'Collection name', + accessor: 'name' + } ]; -export const tableRowGranule = [ - (d) => d.granuleId +export const tableColumnsGranules = [ + { + Header: 'Granule ID', + accessor: 'granuleId' + } ]; -export const tablePropsGranule = ['granuleId']; diff --git a/app/src/js/utils/table-config/rules.js b/app/src/js/utils/table-config/rules.js new file mode 100644 index 000000000..36d756a7d --- /dev/null +++ b/app/src/js/utils/table-config/rules.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + enableRule, + disableRule, + deleteRule +} from '../../actions'; +import { + getCollectionId, + collectionLink, + providerLink, + fromNow +} from '../../utils/format'; +import { strings } from '../../components/locale'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name' + }, + { + Header: 'Provider', + accessor: row => providerLink(row.provider), + id: 'provider' + }, + { + Header: strings.collection_id, + accessor: row => collectionLink(getCollectionId(row.collection)), + id: 'collectionId', + disableSortBy: true + }, + { + Header: 'Type', + accessor: 'rule.type', + disableSortBy: true + }, + { + Header: 'State', + accessor: 'state' + }, + { + Header: 'Timestamp', + accessor: row => fromNow(row.timestamp), + id: 'timestamp' + } +]; + +export const bulkActions = (rules) => [{ + text: 'Enable', + action: (ruleName) => + enableRule(rules.list.data.find((rule) => rule.name === ruleName)), + state: rules.enabled, + confirm: (d) => `Enable ${d} Rule(s)?` +}, { + text: 'Disable', + action: (ruleName) => + disableRule(rules.list.data.find((rule) => rule.name === ruleName)), + state: rules.disabled, + confirm: (d) => `Disable ${d} Rule(s)?` +}, { + text: 'Delete', + action: deleteRule, + state: rules.deleted, + confirm: (d) => `Delete ${d} Rule(s)?` +}]; diff --git a/app/src/js/utils/table-config/services.js b/app/src/js/utils/table-config/services.js index 51fa1ef4c..ac0d0da65 100644 --- a/app/src/js/utils/table-config/services.js +++ b/app/src/js/utils/table-config/services.js @@ -1,18 +1,28 @@ 'use strict'; import { tally } from '../format'; -export const tableHeader = [ - 'Service Name', - 'Status', - 'Desired Tasks', - 'Pending Tasks', - 'Running Tasks' -]; - -export const tableRow = [ - 'name', - 'status', - (d) => tally(d['desiredCount']), - (d) => tally(d['pendingCount']), - (d) => tally(d['runningCount']) +export const tableColumns = [ + { + Header: 'Service Name', + accessor: 'name' + }, + { + Header: 'Status', + accessor: 'status' + }, + { + Header: 'Desired Tasks', + accessor: row => tally(row['desiredCount']), + id: 'desiredCount' + }, + { + Header: 'Pending Tasks', + accessor: row => tally(row['pendingCount']), + id: 'pendingCount' + }, + { + Header: 'Running Tasks', + accessor: row => tally(row['runningCount']), + id: 'runningCount' + } ]; diff --git a/app/src/js/utils/table-config/workflows.js b/app/src/js/utils/table-config/workflows.js new file mode 100644 index 000000000..408279aa8 --- /dev/null +++ b/app/src/js/utils/table-config/workflows.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const tableColumns = [ + { + Header: 'Name', + accessor: row => {row.name}, + id: 'name' + }, + { + Header: 'AWS Link', + accessor: 'definition.Comment', + id: 'template' + } +]; diff --git a/cypress/integration/collections_spec.js b/cypress/integration/collections_spec.js index d240269ec..7f74e82f1 100644 --- a/cypress/integration/collections_spec.js +++ b/cypress/integration/collections_spec.js @@ -44,7 +44,7 @@ describe('Dashboard Collections Page', () => { cy.url().should('include', 'collections'); cy.contains('.heading--xlarge', 'Collections'); - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); }); it('should display expected MMT Links for collections list', () => { @@ -52,16 +52,16 @@ describe('Dashboard Collections Page', () => { cy.wait('@getCollections'); let i = 0; - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); while (i < cmrFixtureIdx) cy.wait(`@cmr${i++}`, {timeout: 25000}); - cy.contains('table tbody tr', 'MOD09GQ') - .contains('td a', 'MMT') + cy.contains('.table .tbody .tr', 'MOD09GQ') + .contains('.td a', 'MMT') .should('have.attr', 'href') .and('eq', 'https://mmt.uat.earthdata.nasa.gov/collections/CMOD09GQ-CUMULUS'); - cy.contains('table tbody tr', 'L2_HR_PIXC') - .contains('td a', 'MMT') + cy.contains('.table .tbody .tr', 'L2_HR_PIXC') + .contains('.td a', 'MMT') .should('have.attr', 'href') .and('eq', 'https://mmt.uat.earthdata.nasa.gov/collections/CL2_HR_PIXC-CUMULUS'); }); @@ -113,7 +113,7 @@ describe('Dashboard Collections Page', () => { cy.contains('Back to Collections').click(); cy.wait('@getCollections'); cy.url().should('contain', '/collections/all'); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/collections/collection/${name}/${version}`); }); cy.task('resetState'); @@ -128,9 +128,9 @@ describe('Dashboard Collections Page', () => { // details page. cy.visit('/collections'); cy.wait('@getCollections'); - cy.get('table tbody tr').its('length').should('be.eq', 5); + cy.get('.table .tbody .tr').its('length').should('be.eq', 5); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/collections/collection/${name}/${version}`) .click(); cy.contains('.heading--large', `${name} / ${version}`); @@ -290,7 +290,7 @@ describe('Dashboard Collections Page', () => { `[data-value="${collection.name}___${collection.version}"] > .table__main-asset > a`, {timeout: 25000}).should(existOrNotExist); }); - cy.get('table tbody tr').its('length').should('be.eq', 4); + cy.get('.table .tbody .tr').its('length').should('be.eq', 4); cy.task('resetState'); }); @@ -318,7 +318,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Back to Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); it('should do nothing on cancel when deleting a collection with associated granules', () => { @@ -341,7 +341,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Back to Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); it('should go to granules upon request when deleting a collection with associated granules', () => { @@ -365,7 +365,7 @@ describe('Dashboard Collections Page', () => { // collection should still exist in list cy.contains('a', 'Collections').click(); cy.contains('.heading--xlarge', 'Collections'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); }); }); diff --git a/cypress/integration/executions_spec.js b/cypress/integration/executions_spec.js index f6a65bf3d..10001ca13 100644 --- a/cypress/integration/executions_spec.js +++ b/cypress/integration/executions_spec.js @@ -64,7 +64,7 @@ describe('Dashboard Executions Page', () => { .should('be.eq', execution.collectionId); }); - cy.get('table tbody tr').as('list'); + cy.get('.table .tbody .tr').as('list'); cy.get('@list').its('length').should('be.eq', 6); }); @@ -88,7 +88,7 @@ describe('Dashboard Executions Page', () => { cy.url().should('include', 'executions'); cy.contains('.heading--xlarge', 'Executions'); - cy.get('table tbody tr td[class=table__main-asset]').within(() => { + cy.get('.table .tbody .tr .td.table__main-asset').within(() => { cy.get(`a[title=${executionName}]`).click({force: true}); }); @@ -104,14 +104,14 @@ describe('Dashboard Executions Page', () => { cy.contains('Ended:').next().should('have.text', fullDate('2019-12-13T15:16:52.582Z')); }); - cy.get('table tbody tr').as('events'); + cy.get('.table .tbody .tr').as('events'); cy.get('@events').its('length').should('be.eq', 7); cy.getFixture('valid-execution').as('executionStatus'); cy.get('@executionStatus').its('executionHistory').its('events').then((events) => { cy.get('@events').each(($el, index, $list) => { let timestamp = fullDate(events[index].timestamp); - cy.wrap($el).children('td').as('columns'); + cy.wrap($el).children('.td').as('columns'); cy.get('@columns').its('length').should('be.eq', 4); let idMatch = `"id": ${index + 1},`; let previousIdMatch = `"previousEventId": ${index}`; diff --git a/cypress/integration/granules_spec.js b/cypress/integration/granules_spec.js index d879e9f42..dfe6403bc 100644 --- a/cypress/integration/granules_spec.js +++ b/cypress/integration/granules_spec.js @@ -96,7 +96,7 @@ describe('Dashboard Granules Page', () => { .should('match', /.+ago$/); }); - cy.get('table tbody tr').as('list'); + cy.get('.table .tbody .tr').as('list'); cy.get('@list').its('length').should('be.eq', 10); }); diff --git a/cypress/integration/providers_spec.js b/cypress/integration/providers_spec.js index 0bfdb34a0..4590f4bcb 100644 --- a/cypress/integration/providers_spec.js +++ b/cypress/integration/providers_spec.js @@ -36,7 +36,7 @@ describe('Dashboard Providers Page', () => { cy.url().should('include', 'providers'); cy.contains('.heading--xlarge', 'Providers'); - cy.get('table tbody tr').its('length').should('be.eq', 2); + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); }); it('should add a new provider', () => { @@ -104,7 +104,7 @@ describe('Dashboard Providers Page', () => { cy.wait(1000); cy.contains('a', 'Back to Providers').click(); cy.wait('@getProviders'); - cy.contains('table tbody tr a', name) + cy.contains('.table .tbody .tr a', name) .should('have.attr', 'href', `/providers/provider/${name}`); cy.task('resetState'); }); @@ -167,7 +167,7 @@ describe('Dashboard Providers Page', () => { // verify the provider is now gone cy.url().should('include', 'providers'); cy.contains('.heading--xlarge', 'Providers'); - cy.contains('table tbody tr', name).should('not.exist'); + cy.contains('.table .tbody .tr', name).should('not.exist'); cy.task('resetState'); }); @@ -189,7 +189,7 @@ describe('Dashboard Providers Page', () => { // provider should still exist in list cy.contains('a', 'Back to Providers').click(); cy.contains('.heading--xlarge', 'Providers'); - cy.contains('table tbody tr a', name); + cy.contains('.table .tbody .tr a', name); }); }); }); diff --git a/cypress/integration/reconciliation_spec.js b/cypress/integration/reconciliation_spec.js index 04bd6d574..53c5515ab 100644 --- a/cypress/integration/reconciliation_spec.js +++ b/cypress/integration/reconciliation_spec.js @@ -31,17 +31,17 @@ describe('Dashboard Reconciliation Reports Page', () => { it('displays a list of reconciliation reports', () => { cy.visit('/reconciliation-reports'); - cy.get('table tbody tr').its('length').should('be.eq', 2); - cy.contains('table tbody tr a', 'report-2020-01-14T20:25:29.026Z.json') + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:25:29.026Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:25:29.026Z.json'); - cy.contains('table tbody tr a', 'report-2020-01-14T20:52:38.781Z.json') + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:52:38.781Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:52:38.781Z.json'); }); it('displays a link to an individual report', () => { cy.visit('/reconciliation-reports'); - cy.contains('table tbody tr a', 'report-2020-01-14T20:52:38.781Z.json') + cy.contains('.table .tbody .tr a', 'report-2020-01-14T20:52:38.781Z.json') .should('have.attr', 'href', '/reconciliation-reports/report/report-2020-01-14T20:52:38.781Z.json') .click(); @@ -68,11 +68,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Files only in DynamoDB (35)') .next() - .find('table tbody') + .find('.table .tbody') .as('dynamoTable'); - cy.get('@dynamoTable').find('tr').its('length').should('be.eq', 35); + cy.get('@dynamoTable').find('.tr').its('length').should('be.eq', 35); cy.get('@dynamoTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('MOD09GQ.A9218123.TJbx_C.006.7667598863143'); @@ -84,11 +84,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Files only in S3 (216)') .next() - .find('table tbody') + .find('.table .tbody') .as('s3Table'); - cy.get('@s3Table').find('tr').its('length').should('be.eq', 216); + cy.get('@s3Table').find('.tr').its('length').should('be.eq', 216); cy.get('@s3Table') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('MOD09GQ.A3119781.haeynr.006.4074740546315.hdf.met'); @@ -102,9 +102,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Collections only in Cumulus (13)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusCollectionsTable'); - cy.get('@cumulusCollectionsTable').find('tr').its('length').should('be.eq', 13); + cy.get('@cumulusCollectionsTable').find('.tr').its('length').should('be.eq', 13); cy.get('@cumulusCollectionsTable') .within(() => { cy.contains('L2_HR_PIXC_test-mhs3-KinesisTestError-1574717133091___000'); @@ -114,9 +114,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Collections only in CMR (25)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrCollectionsTable'); - cy.get('@cmrCollectionsTable').find('tr').its('length').should('be.eq', 25); + cy.get('@cmrCollectionsTable').find('.tr').its('length').should('be.eq', 25); cy.get('@cmrCollectionsTable') .within(() => { cy.contains('A2_RainOcn_NRT___0'); @@ -128,9 +128,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granules only in Cumulus (7)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusGranulesTable'); - cy.get('@cumulusGranulesTable').find('tr').its('length').should('be.eq', 7); + cy.get('@cumulusGranulesTable').find('.tr').its('length').should('be.eq', 7); cy.get('@cumulusGranulesTable') .within(() => { cy.contains('MOD14A1.A4135026.ekHe8x.006.3355759967228'); @@ -139,9 +139,9 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granules only in CMR (365)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrGranulesTable'); - cy.get('@cmrGranulesTable').find('tr').its('length').should('be.eq', 365); + cy.get('@cmrGranulesTable').find('.tr').its('length').should('be.eq', 365); cy.get('@cmrGranulesTable') .within(() => { cy.contains('MOD14A1.A0031922.9lenyG.006.4681412227733'); @@ -153,11 +153,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granule files only in Cumulus (1)') .next() - .find('table tbody') + .find('.table .tbody') .as('cumulusGranulesFilesTable'); - cy.get('@cumulusGranulesFilesTable').find('tr').its('length').should('be.eq', 1); + cy.get('@cumulusGranulesFilesTable').find('.tr').its('length').should('be.eq', 1); cy.get('@cumulusGranulesFilesTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('granule.001.1234'); @@ -169,11 +169,11 @@ describe('Dashboard Reconciliation Reports Page', () => { cy.contains('h3', 'Granule files only in CMR (1)') .next() - .find('table tbody') + .find('.table .tbody') .as('cmrGranulesFilesTable'); - cy.get('@cmrGranulesFilesTable').find('tr').its('length').should('be.eq', 1); + cy.get('@cmrGranulesFilesTable').find('.tr').its('length').should('be.eq', 1); cy.get('@cmrGranulesFilesTable') - .children('tr') + .children('.tr') .first() .within(() => { cy.contains('granule.002.5678'); diff --git a/cypress/integration/rules_spec.js b/cypress/integration/rules_spec.js index 5fa96ea78..173991aeb 100644 --- a/cypress/integration/rules_spec.js +++ b/cypress/integration/rules_spec.js @@ -29,8 +29,8 @@ describe('Rules page', () => { cy.visit('/'); cy.get('nav').contains('Rules').click(); cy.url().should('include', '/rules'); - cy.get('table tbody tr').should('have.length', 1); - cy.contains('table tr', testRuleName) + cy.get('.table .tbody .tr').should('have.length', 1); + cy.contains('.table .tr', testRuleName) .within(() => { cy.contains(testProviderId); cy.contains(testCollectionId); @@ -41,7 +41,7 @@ describe('Rules page', () => { it('display a rule with the correct data', () => { cy.visit('/rules'); - cy.contains('table tr a', testRuleName) + cy.contains('.table .tr a', testRuleName) .click(); cy.url().should('include', `/rules/rule/${testRuleName}`); cy.get('.metadata__details') @@ -92,7 +92,7 @@ describe('Rules page', () => { cy.contains('.modal-footer button', 'Confirm Rule').click(); cy.contains('.heading--xlarge', 'Rules'); - cy.contains('table tbody tr a', ruleName) + cy.contains('.table .tbody .tr a', ruleName) .and('have.attr', 'href', `/rules/rule/${ruleName}`).click(); cy.contains('.heading--xlarge', 'Rules'); @@ -113,7 +113,7 @@ describe('Rules page', () => { it('editing a rule and returning to the rules page should show the new changes', () => { cy.visit('/rules'); - cy.contains('table tbody tr a', testRuleName) + cy.contains('.table .tbody .tr a', testRuleName) .and('have.attr', 'href', `/rules/rule/${testRuleName}`) .click(); @@ -134,7 +134,7 @@ describe('Rules page', () => { cy.contains('a', 'Back to Rules').click(); cy.contains('.heading--large', 'Rule Overview'); - cy.contains('table tr', testRuleName) + cy.contains('.table .tr', testRuleName) .within(() => { cy.contains(provider) .should('have.attr', 'href', `/providers/provider/${provider}`); @@ -143,7 +143,7 @@ describe('Rules page', () => { it('deleting a rule should remove it from the list', () => { cy.visit('/rules'); - cy.contains('table tr', testRuleName) + cy.contains('.table .tr', testRuleName) .within(() => { cy.get('input[type="checkbox"]').click(); }); @@ -154,7 +154,7 @@ describe('Rules page', () => { .get('button') .contains('Confirm') .click(); - cy.contains('table tr a', testRuleName) + cy.contains('.table .tr a', testRuleName) .should('not.exist'); cy.task('resetState'); }); diff --git a/cypress/integration/workflows_spec.js b/cypress/integration/workflows_spec.js index 6ab578632..eff8c96d8 100644 --- a/cypress/integration/workflows_spec.js +++ b/cypress/integration/workflows_spec.js @@ -27,10 +27,10 @@ describe('Dashboard Workflows Page', () => { cy.url().should('include', 'workflows'); cy.contains('.heading--xlarge', 'Workflows'); - cy.get('table tbody tr').its('length').should('be.eq', 2); - cy.contains('table tbody tr a', 'HelloWorldWorkflow') + cy.get('.table .tbody .tr').its('length').should('be.eq', 2); + cy.contains('.table .tbody .tr a', 'HelloWorldWorkflow') .should('have.attr', 'href', '/workflows/workflow/HelloWorldWorkflow'); - cy.contains('table tbody tr a', 'SecondTestWorkflow') + cy.contains('.table .tbody .tr a', 'SecondTestWorkflow') .should('have.attr', 'href', '/workflows/workflow/SecondTestWorkflow'); }); @@ -41,7 +41,7 @@ describe('Dashboard Workflows Page', () => { cy.url().should('include', 'workflows'); cy.contains('.heading--xlarge', 'Workflows'); - cy.contains('table tbody tr a', workflowName) + cy.contains('.table .tbody .tr a', workflowName) .should('have.attr', 'href', `/workflows/workflow/${workflowName}`) .click(); diff --git a/package-lock.json b/package-lock.json index c38d52608..a8cccb9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,17 @@ "source-map": "^0.5.0" }, "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -132,6 +143,19 @@ "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-annotate-as-pure": { @@ -261,7 +285,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -279,6 +302,19 @@ "@babel/template": "^7.8.6", "@babel/types": "^7.8.6", "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-optimise-call-expression": { @@ -293,8 +329,7 @@ "@babel/helper-plugin-utils": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" }, "@babel/helper-regex": { "version": "7.8.3", @@ -328,6 +363,19 @@ "@babel/helper-optimise-call-expression": "^7.8.3", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-simple-access": { @@ -845,7 +893,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz", "integrity": "sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", @@ -989,6 +1036,19 @@ "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/preset-react": { @@ -1034,6 +1094,19 @@ "@babel/code-frame": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/traverse": { @@ -1053,6 +1126,17 @@ "lodash": "^4.17.13" }, "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1065,10 +1149,9 @@ } }, "@babel/types": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", - "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", - "dev": true, + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -7243,8 +7326,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -13438,8 +13520,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "1.8.0", @@ -15478,6 +15559,14 @@ } } }, + "react-table": { + "version": "7.0.0-rc.16", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.0.0-rc.16.tgz", + "integrity": "sha512-2wcGKO56gKlE1IwJnFatCn1yzHIAwRgbTJJQeoSiERJ/Ed6VeLdJLMJkLuSnxZCwD8CjhZD+C/iXEaFubJVW3A==", + "requires": { + "@babel/plugin-transform-runtime": "^7.8.3" + } + }, "react-test-renderer": { "version": "16.13.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.0.tgz", @@ -16106,7 +16195,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -18555,8 +18643,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", diff --git a/package.json b/package.json index 20d8cfd8a..c9c3728e0 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "react-router-dom": "^5.1.2", "react-router-query-params": "^1.0.3", "react-router-scroll-4": "^1.0.0-beta.2", + "react-table": "^7.0.0-rc.16", "recompose": "^0.30.0", "redux": "^3.6.0", "redux-devtools-extension": "^2.13.8", diff --git a/test/components/executions/execution-status.js b/test/components/executions/execution-status.js index c49419722..c06a4544c 100644 --- a/test/components/executions/execution-status.js +++ b/test/components/executions/execution-status.js @@ -33,11 +33,11 @@ test('Cumulus-690 Execution Status shows workflow task and version information', /> ); - const sortableTable = executionStatusRendered.find('Table'); + const sortableTable = executionStatusRendered.find('SortableTable'); t.is(sortableTable.length, 1); const sortableTableWrapper = sortableTable.dive(); - const moreDetails = sortableTableWrapper.find('pre'); + const moreDetails = sortableTableWrapper.find('Cell').first().find('pre'); const selectedTasks = moreDetails.findWhere((jsonDetails) => { const parsedDetailsOutput = JSON.parse(jsonDetails.text()).output; if (parsedDetailsOutput && parsedDetailsOutput.meta && parsedDetailsOutput.meta.workflow_tasks) { diff --git a/test/components/granules/granule.js b/test/components/granules/granule.js index 31d475307..8eb295e64 100644 --- a/test/components/granules/granule.js +++ b/test/components/granules/granule.js @@ -46,8 +46,11 @@ test('CUMULUS-336 Granule file links use the correct URL', function (t) { /> ); - const sortableTable = granuleOverview.find('Table'); + const sortableTable = granuleOverview.find('SortableTable'); t.is(sortableTable.length, 1); const sortableTableWrapper = sortableTable.dive(); - t.is(sortableTableWrapper.find('tbody tr td a[href="https://my-bucket.s3.amazonaws.com/my-key-path/my-name"]').length, 1); + t.is(sortableTableWrapper + .find('.tbody .tr') + .find('Cell').at(1).dive() + .find('a[href="https://my-bucket.s3.amazonaws.com/my-key-path/my-name"]').length, 1); }); diff --git a/test/components/granules/overview.js b/test/components/granules/overview.js index fe0a50200..a14628358 100644 --- a/test/components/granules/overview.js +++ b/test/components/granules/overview.js @@ -40,7 +40,7 @@ const data = ''; test('GranulesOverview generates bulkAction for recovery button', function (t) { const dispatch = () => {}; const workflowOptions = []; - const stats = { count: 0, histogram: {}, stats: {} }; + const stats = { count: 0, stats: {} }; const location = { pathname: 'granules' }; const config = { enableRecovery: true }; const store = { @@ -73,7 +73,7 @@ test('GranulesOverview generates bulkAction for recovery button', function (t) { test('GranulesOverview does not generate bulkAction for recovery button', function (t) { const dispatch = () => {}; const workflowOptions = []; - const stats = { count: 0, histogram: {}, stats: {} }; + const stats = { count: 0, stats: {} }; const location = { pathname: 'granules' }; const config = { enableRecovery: false }; const store = { diff --git a/test/components/reconciliation-reports/report-table.js b/test/components/reconciliation-reports/report-table.js index 16c85a8a2..75444aeb0 100644 --- a/test/components/reconciliation-reports/report-table.js +++ b/test/components/reconciliation-reports/report-table.js @@ -12,6 +12,26 @@ import { nullValue } from '../../../app/src/js/utils/format'; configure({ adapter: new Adapter() }); +const tableColumns = [ + { + Header: 'GranuleId', + accessor: 'granuleId' + }, + { + Header: 'Filename', + accessor: 'filename' + }, + { + Header: 'Bucket', + accessor: 'bucket' + }, + { + Header: 'S3 Link', + accessor: row => row ? Link : nullValue, + id: 'link' + } +] + const tableData = [{ granuleId: 'g-123', filename: 'filename.txt', @@ -19,22 +39,6 @@ const tableData = [{ path: 's3://some-bucket/filename.txt' }]; -export const tableHeader = [ - 'GranuleId', - 'Filename', - 'Bucket', - 'S3 Link' -]; - -export const tableRow = [ - (d) => d.granuleId, - (d) => d.filename, - (d) => d.bucket, - (d) => d ? Link : nullValue -]; - -export const tableProps = ['granuleId', 'filename', 'bucket', 'link']; - test('render nothing when no data is provided', function (t) { const report = shallow( ); @@ -70,17 +72,17 @@ test('render basic table', function (t) { const Table = report.find(SortableTable).dive(); - const headerRow = Table.find('tr').first(); - t.is(headerRow.children('td').at(0).text(), 'GranuleId'); - t.is(headerRow.children('td').at(1).text(), 'Filename'); - t.is(headerRow.children('td').at(2).text(), 'Bucket'); - t.is(headerRow.children('td').at(3).text(), 'S3 Link'); - - const dataRow = Table.find('tr').at(1); - t.is(dataRow.children('td').at(0).text(), 'g-123'); - t.is(dataRow.children('td').at(1).text(), 'filename.txt'); - t.is(dataRow.children('td').at(2).text(), 'some-bucket'); - t.true(dataRow.children('td').at(3).contains( + const headerRow = Table.find('.thead .tr').first(); + t.is(headerRow.find('.th > div:first-child').at(0).text(), 'GranuleId'); + t.is(headerRow.find('.th > div:first-child').at(1).text(), 'Filename'); + t.is(headerRow.find('.th > div:first-child').at(2).text(), 'Bucket'); + t.is(headerRow.find('.th > div:first-child').at(3).text(), 'S3 Link'); + + const dataRow = Table.find('.tbody .tr').first(); + t.is(dataRow.find('Cell').at(0).dive().text(), 'g-123'); + t.is(dataRow.find('Cell').at(1).dive().text(), 'filename.txt'); + t.is(dataRow.find('Cell').at(2).dive().text(), 'some-bucket'); + t.true(dataRow.find('Cell').at(3).dive().contains( Link )); }); @@ -96,9 +98,7 @@ test('render buttons to show/hide table when configured', function (t) { ); @@ -122,9 +122,7 @@ test('do not render buttons to show/hide table when data length is less than thr ); @@ -146,9 +144,7 @@ test('do not render buttons to show/hide table when disabled', function (t) { diff --git a/test/components/table/list-view.js b/test/components/table/list-view.js index d4925843f..7ce4ddea5 100644 --- a/test/components/table/list-view.js +++ b/test/components/table/list-view.js @@ -7,10 +7,7 @@ import {mount, configure} from 'enzyme'; import {listGranules} from '../../../app/src/js/actions'; import { List } from '../../../app/src/js/components/Table/Table'; import Timer from '../../../app/src/js/components/Timer/timer.js'; -import { - errorTableHeader, - errorTableRow, errorTableSortProps -} from '../../../app/src/js/utils/table-config/granules'; +import { errorTableColumns } from '../../../app/src/js/utils/table-config/granules'; configure({ adapter: new Adapter() }); @@ -26,10 +23,8 @@ test('table should properly initialize timer config prop', async (t) => { list={list} dispatch={dispatch} action={listGranules} - tableHeader={errorTableHeader} - sortIdx={4} - tableRow={errorTableRow} - tableSortProps={errorTableSortProps} + tableColumns={errorTableColumns} + sortIdx='timestamp' query={query} />, { diff --git a/test/utils/pdrs.js b/test/utils/pdrs.js index 7e79f34f7..3b5003126 100644 --- a/test/utils/pdrs.js +++ b/test/utils/pdrs.js @@ -1,6 +1,6 @@ 'use strict'; import test from 'ava'; -import {tableRow} from '../../app/src/js/utils/table-config/pdrs.js'; +import {tableColumns} from '../../app/src/js/utils/table-config/pdrs.js'; import {getProgress} from '../../app/src/js/utils/table-config/pdr-progress.js'; const pdr = { @@ -31,8 +31,8 @@ test('test pdr-progress.js getProgress', function (t) { }); test('test pdrs.js tableRow', function (t) { - t.is(tableRow[4](pdr), 4); - t.is(tableRow[5](pdr), pdr.stats.processing); - t.is(tableRow[6](pdr), pdr.stats.failed); - t.is(tableRow[7](pdr), pdr.stats.completed); + t.is(tableColumns[4].accessor(pdr), 4); + t.is(tableColumns[5].accessor(pdr), pdr.stats.processing); + t.is(tableColumns[6].accessor(pdr), pdr.stats.failed); + t.is(tableColumns[7].accessor(pdr), pdr.stats.completed); }); From f9de1786b5685d49c43b387c3c3ea5b138e509e7 Mon Sep 17 00:00:00 2001 From: Danielle Peters Date: Thu, 5 Mar 2020 15:13:17 -0500 Subject: [PATCH 11/32] Fix pathing for logo and favicon (#667) --- app/src/js/components/Header/header.js | 3 ++- app/src/template.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/js/components/Header/header.js b/app/src/js/components/Header/header.js index 81209fc50..171b3596a 100644 --- a/app/src/js/components/Header/header.js +++ b/app/src/js/components/Header/header.js @@ -68,10 +68,11 @@ class Header extends React.Component { render () { const { authenticated } = this.props.api; const activePaths = paths.filter(path => nav.exclude[path[0]] !== true); + const logoPath = graphicsPath.substr(-1) === '/' ? `${graphicsPath}${strings.logo}` : `${graphicsPath}/${strings.logo}`; return (
    -

    Logo

    +

    Logo