diff --git a/.nvmrc b/.nvmrc index b06cd07c4..9ddeebac8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.18.0 +14.19.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9822a69cc..2fff832bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,55 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [v11.0.0] - 2022-04-19 + +## Breaking Changes + +This version of the dashboard requires Cumulus API v11.1.0 + +### Added + +- **CUMULUS-2704** + - Added option to create ORCA Reconciliation Report to dashboard + +- **CUMULUS-2748** + - Added ORCA Reconciliation Report display to dashboard + +### Changed + +- **CUMULUS-2903** + - Bumped Node version from 12.18.0 to 14.19.1 to match Core + ## [v10.0.0] - 2022-02-25 ## Breaking Changes -This version of the dashboard requires Cumulus API v10.1.0 +This version of the dashboard requires Cumulus API v10.1.1 + +- **CUMULUS-2728** + - Removes kibana links and Metrics integration. To get this functionality, + use the Metric's ELK stack and custom Kibana displays. + `KIBANAROOT` is still used to send the operator to the kibana instance where bulk operation queries and custom visualizations can be found. + + The following variables have been removed and no longer serve any purpose in the application. + + + `ESROOT` + + `ES_CLOUDWATCH_TARGET_PATTERN` + + `ES_DISTRIBUTION_TARGET_PATTERN` + + `ES_PASSWORD` + + `ES_USER` + + `SHOW_DISTRIBUTION_API_METRICS` + + `SHOW_TEA_METRICS` ### Changed +- **CUMULUS-NONE** + - Updates Cumulus development dependencies to v10.1.1 and upgrades localstack to 0.11.5 to work with latest Cumulus. - **CUMULUS-2843** - Create provider and create rule modals now dislpay the provider [rule] schema title directly as read from the Cumulus API. + ## [v9.0.0] - 2022-02-01 ## Breaking Changes @@ -1238,7 +1275,8 @@ Fix for serving the dashboard through the Cumulus API. ### Added - Versioning and changelog [CUMULUS-197] by @kkelly51 - [Unreleased]: https://github.com/nasa/cumulus-dashboard/compare/v10.0.0...HEAD + [Unreleased]: https://github.com/nasa/cumulus-dashboard/compare/v11.0.0...HEAD + [v11.0.0]: https://github.com/nasa/cumulus-dashboard/compare/v10.0.0...v11.0.0 [v10.0.0]: https://github.com/nasa/cumulus-dashboard/compare/v9.0.0...v10.0.0 [v9.0.0]: https://github.com/nasa/cumulus-dashboard/compare/v8.0.0...v9.0.0 [v8.0.0]: https://github.com/nasa/cumulus-dashboard/compare/v7.1.0...v8.0.0 diff --git a/README.md b/README.md index 28b1a0b7f..556aefdb5 100644 --- a/README.md +++ b/README.md @@ -34,19 +34,7 @@ Setting the following environment variables can override the default values. | HIDE\_PDR | Whether to hide (or show) the PDR menu. | *true* | | LABELS | Choose `gitc` or `daac` localization. | *daac* | | STAGE | Identifier displayed at top of dashboard page: e.g. PROD, UAT | *development* | - - Environment options to configure metrics displays. **All** of the below are optional configurations to display metrics on the Cumulus Dashboard. - -| Env Name | Description | Default | -| -----|----|---- | -| ESROOT | Should point to an Elasticsearch endpoint. Must be set for distribution metrics to be displayed. | | -| ES\_PASSWORD | Elasticsearch password, needed when protected by basic authorization. | | -| ES\_USER | Elasticsearch username, needed when protected by basic authorization. | | -| ES\_CLOUDWATCH\_TARGET\_PATTERN | The Elasticsearch target pattern to find cloudwatch events. e.g. `-cloudwatch-cumulus--*` | | -| ES\_DISTRIBUTION\_TARGET\_PATTERN | The Elasticsearch target pattern to find s3 access log distribution events. e.g. `-distribution--*` | | | KIBANAROOT | \ Points to a Kibana endpoint. | | -| SHOW\_DISTRIBUTION\_API\_METRICS | \ Display metrics from the Cumulus Distribution API. | *false* | -| SHOW\_TEA\_METRICS | \ Display metrics from the Thin Egress Application (TEA). | *true* | ## Quick start @@ -81,7 +69,7 @@ Set the environment and build the dashboard with these commands: $ source production.env && ./bin/build_dashboard_via_docker.sh ``` -This script uses Docker Compose to build and copy the a compiled dashboard into the `./dist` directory. You can now deploy this directory to AWS behind [CloudFront](https://aws.amazon.com/cloudfront/). If you are in NGAP, follow the instructions for "Request Public or Protected Access to the APIs and Dashboard" on the earthdata wiki page [Using Cumulus with Private APIs](https://wiki.earthdata.nasa.gov/display/CUMULUS/Cumulus+Deployments+in+NGAP). +This script uses Docker Compose to build and copy the compiled dashboard into the `./dist` directory. You can now deploy this directory to AWS behind [CloudFront](https://aws.amazon.com/cloudfront/). If you are in NGAP, follow the instructions for "Request Public or Protected Access to the APIs and Dashboard" on the earthdata wiki page [Using Cumulus with Private APIs](https://wiki.earthdata.nasa.gov/display/CUMULUS/Cumulus+Deployments+in+NGAP). ### Run the dashboard locally via Docker Image @@ -108,7 +96,7 @@ In this example, the dashboard would be available at `http://localhost:3000/` in ### Build the dashboard -The dashboard uses node v12.18.0. To build/run the dashboard on your local machine, install [nvm](https://github.com/creationix/nvm) and run `nvm install v12.18.0`. +The dashboard uses node v14.19.1. To build/run the dashboard on your local machine, install [nvm](https://github.com/creationix/nvm) and run `nvm install v14.19.1`. #### install requirements We use npm for local package management, to install the requirements: @@ -211,7 +199,7 @@ Serve the cumulus API (separate terminal) Serve the dashboard web application (another terminal) ```bash - $ [HIDE_PDR=false SHOW_DISTRIBUTION_API_METRICS=true ENABLE_RECOVERY=true ESROOT=http://example.com APIROOT=http://localhost:5001] npm run serve + $ [HIDE_PDR=false ENABLE_RECOVERY=true APIROOT=http://localhost:5001] npm run serve ``` If you're just testing dashboard code, you can generally run all of the above commands as a single docker-compose stack. @@ -242,7 +230,7 @@ These are started and stopped with the commands: $ npm run stop-localstack ``` -After these containers are running, you can start a cumulus API locally in a terminal window `npm run serve-api`, the dashboard in another window. `[HIDE_PDR=false SHOW_DISTRIBUTION_API_METRICS=true ENABLE_RECOVERY=true ESROOT=http://example.com ES_CLOUDWATCH_TARGET_PATTERN=cwpattern ES_DISTRIBUTION_TARGET_PATTERN=distpattern APIROOT=http://localhost:5001] npm run serve` and finally cypress in a third window. `npm run cypress`. +After these containers are running, you can start a cumulus API locally in a terminal window `npm run serve-api`, the dashboard in another window. `[HIDE_PDR=false ENABLE_RECOVERY=true APIROOT=http://localhost:5001] npm run serve` and finally cypress in a third window. `npm run cypress`. Once the Docker app is running, If you would like to see sample data you can seed the database. This will load the same sample data into the application that is used during cypress testing. ```bash @@ -279,7 +267,7 @@ localstack_1 | Ready. you should be able to verify access to the local Cumulus API at http://localhost:5001/token -Then you can run the dashboard locally (without Docker) `[HIDE_PDR=false SHOW_DISTRIBUTION_API_METRICS=true ESROOT=http://example.com APIROOT=http://localhost:5001] npm run serve` and open cypress tests `npm run cypress`. +Then you can run the dashboard locally (without Docker) `[HIDE_PDR=false APIROOT=http://localhost:5001] npm run serve` and open cypress tests `npm run cypress`. The Docker compose stack also includes a command to let a developer start all development containers with a single command. diff --git a/app/src/css/_base.scss b/app/src/css/_base.scss index 5e20414d1..50c336eec 100644 --- a/app/src/css/_base.scss +++ b/app/src/css/_base.scss @@ -745,6 +745,7 @@ a:active { margin-right: 2em; } + &--found, &--success { background-color: $light-green; } @@ -754,6 +755,7 @@ a:active { background-color: $orange; } + &--notfound, &--failed { background-color: $error-red; } diff --git a/app/src/css/_buttons.scss b/app/src/css/_buttons.scss index d3dd19569..fdd7b6bc1 100644 --- a/app/src/css/_buttons.scss +++ b/app/src/css/_buttons.scss @@ -798,6 +798,19 @@ $delete-btn-hover-bg-color: darken($error-red, 10%); font-size: 1.1em; line-height: 1.1em; box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12); + + &--check { + background-color: transparent; + box-shadow: none; + pointer-events: none; + &:before { + content: '\f00c'; + font-family: 'FontAwesome'; + background-color: transparent; + color: $black; + font-weight: 900; + } + } &--download { &:before { content: '\f0ab'; diff --git a/app/src/js/actions/actions-metrics/apiGatewaySearch.js b/app/src/js/actions/actions-metrics/apiGatewaySearch.js deleted file mode 100644 index 4e10b694e..000000000 --- a/app/src/js/actions/actions-metrics/apiGatewaySearch.js +++ /dev/null @@ -1,82 +0,0 @@ -export const apiGatewaySearchTemplate = (prefix, startTimeEpochMilli, endTimeEpochMilli) => `{ - "aggs": { - "2": { - "filters": { - "filters": { - "ApiExecutionErrors": { - "query_string": { - "query": "+\\"Method completed with status:\\" +(4?? 5??)", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "ApiExecutionSuccesses": { - "query_string": { - "query": "+\\"Method completed with status:\\" +(2?? 3??)", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "ApiAccessErrors": { - "query_string": { - "query": "statusCode:[400 TO 599]", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "ApiAccessSuccesses": { - "query_string": { - "query": "statusCode:[200 TO 399]", - "analyze_wildcard": true, - "default_field": "*" - } - } - } - } - } - }, - "size": 0, - "_source": { - "excludes": [] - }, - "stored_fields": [ - "*" - ], - "script_fields": {}, - "docvalue_fields": [ - { - "field": "@timestamp", - "format": "date_time" - } - ], - "query": { - "bool": { - "must": [ - { - "match_all": {} - }, - { - "range": { - "@timestamp": { - "gte": ${startTimeEpochMilli}, - "lte": ${endTimeEpochMilli}, - "format": "epoch_millis" - } - } - }, - { - "match_phrase": { - "logGroup": { - "query": "\\"API\\\\-Gateway\\\\-Execution*\\"" - } - } - } - ], - "filter": [], - "should": [], - "must_not": [] - } - } -}`; - -export default apiGatewaySearchTemplate; diff --git a/app/src/js/actions/actions-metrics/apiLambdaSearch.js b/app/src/js/actions/actions-metrics/apiLambdaSearch.js deleted file mode 100644 index c790a3d57..000000000 --- a/app/src/js/actions/actions-metrics/apiLambdaSearch.js +++ /dev/null @@ -1,68 +0,0 @@ -export const apiLambdaSearchTemplate = (prefix, startTimeEpochMilli, endTimeEpochMilli) => `{ - "aggs": { - "2": { - "filters": { - "filters": { - "LambdaAPIErrors": { - "query_string": { - "query": "message:(+GET +HTTP +(4?? 5??) -(200 307))", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "LambdaAPISuccesses": { - "query_string": { - "query": "message:(+GET +HTTP +(2?? 3??))", - "analyze_wildcard": true, - "default_field": "*" - } - } - } - } - } - }, - "size": 0, - "_source": { - "excludes": [] - }, - "stored_fields": [ - "*" - ], - "script_fields": {}, - "docvalue_fields": [ - { - "field": "@timestamp", - "format": "date_time" - } - ], - "query": { - "bool": { - "must": [ - { - "match_all": {} - }, - { - "range": { - "@timestamp": { - "gte": ${startTimeEpochMilli}, - "lte": ${endTimeEpochMilli}, - "format": "epoch_millis" - } - } - }, - { - "match_phrase": { - "logGroup": { - "query": "/aws/lambda/${prefix}-ApiDistribution" - } - } - } - ], - "filter": [], - "should": [], - "must_not": [] - } - } -}`; - -export default apiLambdaSearchTemplate; diff --git a/app/src/js/actions/actions-metrics/s3AccessSearch.js b/app/src/js/actions/actions-metrics/s3AccessSearch.js deleted file mode 100644 index 6131b6549..000000000 --- a/app/src/js/actions/actions-metrics/s3AccessSearch.js +++ /dev/null @@ -1,68 +0,0 @@ -export const s3AccessSearchTemplate = (prefix, startTimeEpochMilli, endTimeEpochMilli) => `{ - "aggs": { - "2": { - "filters": { - "filters": { - "s3AccessSuccesses": { - "query_string": { - "query": "response:200", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "s3AccessFailures": { - "query_string": { - "query": "NOT response:200", - "analyze_wildcard": true, - "default_field": "*" - } - } - } - } - } - }, - "size": 0, - "_source": { - "excludes": [] - }, - "stored_fields": [ - "*" - ], - "script_fields": {}, - "docvalue_fields": [ - { - "field": "@timestamp", - "format": "date_time" - } - ], - "query": { - "bool": { - "must": [ - { - "match_all": {} - }, - { - "range": { - "@timestamp": { - "gte": ${startTimeEpochMilli}, - "lte": ${endTimeEpochMilli}, - "format": "epoch_millis" - } - } - }, - { - "match_phrase": { - "operation": { - "query": "REST.GET.OBJECT" - } - } - } - ], - "filter": [], - "should": [], - "must_not": [] - } - } -}`; - -export default s3AccessSearchTemplate; diff --git a/app/src/js/actions/actions-metrics/searchTarget.js b/app/src/js/actions/actions-metrics/searchTarget.js deleted file mode 100644 index e6a81d9e6..000000000 --- a/app/src/js/actions/actions-metrics/searchTarget.js +++ /dev/null @@ -1,10 +0,0 @@ -const path = require('path'); - -/** - * - * @param {string} targetPattern - Metrics ES target pattern to prefix your search URI - * @returns {string} correct index search string for searching metrics. - */ -const searchTarget = (targetPattern) => path.join(targetPattern, '_search/'); - -export default searchTarget; diff --git a/app/src/js/actions/actions-metrics/teaLambdaSearch.js b/app/src/js/actions/actions-metrics/teaLambdaSearch.js deleted file mode 100644 index 4dcc71aa5..000000000 --- a/app/src/js/actions/actions-metrics/teaLambdaSearch.js +++ /dev/null @@ -1,68 +0,0 @@ -export const teaLambdaSearchTemplate = (prefix, startTimeEpochMilli, endTimeEpochMilli) => `{ - "aggs": { - "2": { - "filters": { - "filters": { - "TEALambdaErrors": { - "query_string": { - "query": "response_status:failure", - "analyze_wildcard": true, - "default_field": "*" - } - }, - "TEALambdaSuccesses": { - "query_string": { - "query": "response_status:success", - "analyze_wildcard": true, - "default_field": "*" - } - } - } - } - } - }, - "size": 0, - "_source": { - "excludes": [] - }, - "stored_fields": [ - "*" - ], - "script_fields": {}, - "docvalue_fields": [ - { - "field": "@timestamp", - "format": "date_time" - } - ], - "query": { - "bool": { - "must": [ - { - "match_all": {} - }, - { - "range": { - "@timestamp": { - "gte": ${startTimeEpochMilli}, - "lte": ${endTimeEpochMilli}, - "format": "epoch_millis" - } - } - }, - { - "match_phrase": { - "logGroup": { - "query": "/aws/lambda/${prefix}-thin-egress-app-EgressLambda" - } - } - } - ], - "filter": [], - "should": [], - "must_not": [] - } - } -}`; - -export default teaLambdaSearchTemplate; diff --git a/app/src/js/actions/index.js b/app/src/js/actions/index.js index 06bebb483..bfe58230f 100644 --- a/app/src/js/actions/index.js +++ b/app/src/js/actions/index.js @@ -11,22 +11,11 @@ import _config from '../config'; import { getCollectionId, collectionNameVersion } from '../utils/format'; import { fetchCurrentTimeFilters } from '../utils/datepicker'; import log from '../utils/log'; -import { authHeader } from '../utils/basic-auth'; -import apiGatewaySearchTemplate from './actions-metrics/apiGatewaySearch'; -import apiLambdaSearchTemplate from './actions-metrics/apiLambdaSearch'; -import teaLambdaSearchTemplate from './actions-metrics/teaLambdaSearch'; -import s3AccessSearchTemplate from './actions-metrics/s3AccessSearch'; -import searchTarget from './actions-metrics/searchTarget'; import * as types from './types'; import { historyPushWithQueryParams } from '../utils/url-helper'; const { CALL_API } = types; const { - esRoot, - esCloudwatchTargetPattern, - esDistributionTargetPattern, - showDistributionAPIMetrics, - showTeaMetrics, apiRoot: root, defaultPageLimit, minCompatibleApiVersion @@ -455,95 +444,6 @@ export const getCMRInfo = () => ({ } }); -export const metricsConfigured = () => { - if (esRoot !== '' && - esCloudwatchTargetPattern !== '' && - esDistributionTargetPattern !== '') return true; - return false; -}; - -export const getDistApiGatewayMetrics = (cumulusInstanceMeta) => { - if (!metricsConfigured()) return { type: types.NOOP }; - return (dispatch, getState) => { - const { stackName } = cumulusInstanceMeta; - const timeFilters = fetchCurrentTimeFilters(getState().datepicker); - const endTime = timeFilters.timestamp__to || Date.now(); - const startTime = timeFilters.timestamp__from || 0; - return dispatch({ - [CALL_API]: { - type: types.DIST_APIGATEWAY, - skipAuth: true, - method: 'POST', - url: `${esRoot}/${searchTarget(esCloudwatchTargetPattern)}`, - headers: authHeader(), - data: JSON.parse(apiGatewaySearchTemplate(stackName, startTime, endTime)) - } - }); - }; -}; - -export const getDistApiLambdaMetrics = (cumulusInstanceMeta) => { - if (!metricsConfigured()) return { type: types.NOOP }; - if (!showDistributionAPIMetrics) return { type: types.NOOP }; - return (dispatch, getState) => { - const { stackName } = cumulusInstanceMeta; - const timeFilters = fetchCurrentTimeFilters(getState().datepicker); - const endTime = timeFilters.timestamp__to || Date.now(); - const startTime = timeFilters.timestamp__from || 0; - return dispatch({ - [CALL_API]: { - type: types.DIST_API_LAMBDA, - skipAuth: true, - method: 'POST', - url: `${esRoot}/${searchTarget(esCloudwatchTargetPattern)}`, - headers: authHeader(), - data: JSON.parse(apiLambdaSearchTemplate(stackName, startTime, endTime)) - } - }); - }; -}; - -export const getTEALambdaMetrics = (cumulusInstanceMeta) => { - if (!metricsConfigured()) return { type: types.NOOP }; - if (!showTeaMetrics) return { type: types.NOOP }; - return (dispatch, getState) => { - const { stackName } = cumulusInstanceMeta; - const timeFilters = fetchCurrentTimeFilters(getState().datepicker); - const endTime = timeFilters.timestamp__to || Date.now(); - const startTime = timeFilters.timestamp__from || 0; - return dispatch({ - [CALL_API]: { - type: types.DIST_TEA_LAMBDA, - skipAuth: true, - method: 'POST', - url: `${esRoot}/${searchTarget(esCloudwatchTargetPattern)}`, - headers: authHeader(), - data: JSON.parse(teaLambdaSearchTemplate(stackName, startTime, endTime)) - } - }); - }; -}; - -export const getDistS3AccessMetrics = (cumulusInstanceMeta) => { - if (!metricsConfigured()) return { type: types.NOOP }; - return (dispatch, getState) => { - const { stackName } = cumulusInstanceMeta; - const timeFilters = fetchCurrentTimeFilters(getState().datepicker); - const endTime = timeFilters.timestamp__to || Date.now(); - const startTime = timeFilters.timestamp__from || 0; - return dispatch({ - [CALL_API]: { - type: types.DIST_S3ACCESS, - skipAuth: true, - method: 'POST', - url: `${esRoot}/${searchTarget(esDistributionTargetPattern)}`, - headers: authHeader(), - data: JSON.parse(s3AccessSearchTemplate(stackName, startTime, endTime)) - } - }); - }; -}; - // count queries *must* include type and field properties. export const getCount = (options = {}) => { const { sidebarCount, type, field, ...restOptions } = options; diff --git a/app/src/js/actions/types.js b/app/src/js/actions/types.js index 1f5cc86d5..59644f732 100644 --- a/app/src/js/actions/types.js +++ b/app/src/js/actions/types.js @@ -81,19 +81,6 @@ export const OPTIONS_COLLECTIONNAME_ERROR = 'OPTIONS_COLLECTIONNAME_ERROR'; export const STATS = 'STATS'; export const STATS_INFLIGHT = 'STATS_INFLIGHT'; export const STATS_ERROR = 'STATS_ERROR'; -// AWS -export const DIST_APIGATEWAY = 'DIST_APIGATEWAY'; -export const DIST_APIGATEWAY_INFLIGHT = 'DIST_APIGATEWAY_INFLIGHT'; -export const DIST_APIGATEWAY_ERROR = 'DIST_APIGATEWAY_ERROR'; -export const DIST_API_LAMBDA = 'DIST_API_LAMBDA'; -export const DIST_API_LAMBDA_INFLIGHT = 'DIST_API_LAMBDA_INFLIGHT'; -export const DIST_API_LAMBDA_ERROR = 'DIST_API_LAMBDA_ERROR'; -export const DIST_TEA_LAMBDA = 'DIST_TEA_LAMBDA'; -export const DIST_TEA_LAMBDA_INFLIGHT = 'DIST_TEA_LAMBDA_INFLIGHT'; -export const DIST_TEA_LAMBDA_ERROR = 'DIST_TEA_LAMBDA_ERROR'; -export const DIST_S3ACCESS = 'DIST_S3ACCESS'; -export const DIST_S3ACCESS_INFLIGHT = 'DIST_S3ACCESS_INFLIGHT'; -export const DIST_S3ACCESS_ERROR = 'DIST_S3ACCESS_ERROR'; // Count export const COUNT = 'COUNT'; export const COUNT_SIDEBAR = 'COUNT_SIDEBAR'; diff --git a/app/src/js/components/Form/_form.scss b/app/src/js/components/Form/_form.scss index d921c1da4..5a804d770 100644 --- a/app/src/js/components/Form/_form.scss +++ b/app/src/js/components/Form/_form.scss @@ -425,6 +425,11 @@ select option{ width: 100%; } } + .react-datetime-picker__inputGroup { + input { + padding: 0; + } + } .subform__item--expanded { width: 87%; } @@ -697,3 +702,34 @@ select option{ border-radius: 0 0 10px 10px; } } + +.reconciliation-granule__content { + .metadata__details .meta__row { + dt, dd { + vertical-align: top; + } + dt { + width: 20%; + } + dd { + width: 40%; + } + .granule__location div{ + display: inline-block; + width: 30%; + } + } +} + +.reconciliation-reports__header { + &--tooltip { + display: flex; + align-items: center; + + svg { + color: $ocean-blue; + margin-left: .5em; + padding: .5em; + } + } +} diff --git a/app/src/js/components/Header/header.js b/app/src/js/components/Header/header.js index a79725c2b..9a235636e 100644 --- a/app/src/js/components/Header/header.js +++ b/app/src/js/components/Header/header.js @@ -13,7 +13,7 @@ import { import { graphicsPath, nav } from '../../config'; import { window } from '../../utils/browser'; import { strings } from '../locale'; -import { kibanaAllLogsLink } from '../../utils/kibana'; +import linkToKibana from '../../utils/kibana'; import { getPersistentQueryParams } from '../../utils/url-helper'; const paths = [ @@ -67,7 +67,7 @@ class Header extends React.Component { linkTo(path, search) { if (path[0] === 'Logs') { - const kibanaLink = kibanaAllLogsLink(); + const kibanaLink = linkToKibana(); return ( {path[0]} diff --git a/app/src/js/components/ReconciliationReports/backup-report-granule-details.js b/app/src/js/components/ReconciliationReports/backup-report-granule-details.js new file mode 100644 index 000000000..6ab3597c1 --- /dev/null +++ b/app/src/js/components/ReconciliationReports/backup-report-granule-details.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import List from '../Table/Table'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; +import Tooltip from '../Tooltip/tooltip'; +import { + collectionLink, + granuleLink, + providerLink, + fullDate, +} from '../../utils/format'; +import Metadata from '../Table/Metadata'; +import { tableColumnsGranuleConflictDetails } from '../../utils/table-config/reconciliation-reports'; + +const renderDataLocation = (reportType, granule) => { + const { cumulusFilesCount = 0, orcaFilesCount = 0 } = granule; + if (reportType === 'ORCA Backup') { + return
+
    +
    + Cumulus + } + tip={
    {cumulusFilesCount ? 'Found' : 'Not Found'}
    } + /> +
    +
    + Number of Files: {cumulusFilesCount} +
    +
+
    +
    + Orca + } + tip={
    {orcaFilesCount ? 'Found' : 'Not Found'}
    } + /> +
    +
    + Number of Files: {orcaFilesCount} +
    +
+
; + } + return <>
    Cumulus Found Number of Files: ${cumulusFilesCount}
; +}; + +const metaAccessors = (reportType, granule) => ([ + { + label: 'Collection', + property: 'collectionId', + accessor: collectionLink, + }, + { + label: 'Provider', + property: 'provider', + accessor: providerLink, + }, + { + label: 'Ingest', + property: 'createdAt', + accessor: fullDate, + }, + { + label: 'Location', + property: 'createdAt', + accessor: () => renderDataLocation(reportType, granule), + }, +]); + +const BackupReportGranuleDetails = ({ location = {} }) => { + const { state: locationState } = location; + const { reportName, reportType, granule } = locationState || {}; + + const breadcrumbConfig = [ + { + label: 'Dashboard Home', + href: '/', + }, + { + label: 'Reports', + href: '/reconciliation-reports', + }, + { + label: reportName, + href: `/reconciliation-reports/report/${reportName}`, + }, + { + label: 'Granule Conflict Details', + active: true, + }, + ]; + + return ( +
+ + Granule Conflict Details + +
+
+ +
+

+ {reportType} Report: {reportName} +

+

+ Granule: { + ['onlyInCumulus', 'withConflicts'].includes(granule.conflictType) + ? granuleLink(granule.granuleId) + : granule.granuleId + } +

+

+ Below is a deep dive into where the backup issues are for this granule. +

+
+
+

+ Conflict Details +

+
+
+ +
+
+
+ +
+
+
+ ); +}; + +BackupReportGranuleDetails.propTypes = { + location: PropTypes.object, +}; + +export default BackupReportGranuleDetails; diff --git a/app/src/js/components/ReconciliationReports/backup-report.js b/app/src/js/components/ReconciliationReports/backup-report.js new file mode 100644 index 000000000..88a8e3ca1 --- /dev/null +++ b/app/src/js/components/ReconciliationReports/backup-report.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { + searchReconciliationReport, + clearReconciliationSearch, +} from '../../actions'; +import List from '../Table/Table'; +import Search from '../Search/search'; +import ReportHeading from './report-heading'; +import { handleDownloadUrlClick } from '../../utils/download-file'; +import { tableColumnsBackup } from '../../utils/table-config/reconciliation-reports'; + +const BackupReport = ({ + filterString, + legend, + onSelect, + recordData, + reportName, + reportType, + reportUrl +}) => { + const { + reportStartTime = null, + reportEndTime = null, + error = null, + granules: { + withConflicts = [], + onlyInCumulus = [], + onlyInOrca = [] + } + } = recordData || {}; + + let records = withConflicts.map((g) => ({ ...g, conflictType: 'withConflicts' })).concat( + onlyInCumulus.map((g) => ({ ...g, conflictType: 'onlyInCumulus' })), + onlyInOrca.map((g) => ({ ...g, conflictType: 'onlyInOrca' })) + ); + + if (filterString) { + records = records.filter((file) => file.granuleId.toLowerCase() + .includes(filterString.toLowerCase())); + } + + function handleDownloadClick(e) { + handleDownloadUrlClick(e, { url: reportUrl }); + } + + return ( +
+ +
+
+ +
+ +
+
+ ); +}; + +BackupReport.propTypes = { + filterString: PropTypes.string, + legend: PropTypes.node, + onSelect: PropTypes.func, + recordData: PropTypes.object, + reportName: PropTypes.string, + reportType: PropTypes.string, + reportUrl: PropTypes.string +}; + +export default BackupReport; diff --git a/app/src/js/components/ReconciliationReports/create.js b/app/src/js/components/ReconciliationReports/create.js index e0600fee5..988372f64 100644 --- a/app/src/js/components/ReconciliationReports/create.js +++ b/app/src/js/components/ReconciliationReports/create.js @@ -141,9 +141,9 @@ const CreateReconciliationReport = ({ function renderForm({ handleSubmit, submitting, values }) { const { collectionId, granuleId, provider, reportType } = values || {}; - const collectionIdDisabled = !!granuleId || !!provider; - const granuleIdDisabled = !!collectionId || !!provider; - const providerDisabled = !!collectionId || !!granuleId; + const collectionIdDisabled = reportType !== 'ORCA Backup' && (!!granuleId || !!provider); + const granuleIdDisabled = reportType !== 'ORCA Backup' && (!!collectionId || !!provider); + const providerDisabled = reportType !== 'ORCA Backup' && (!!collectionId || !!granuleId); return (
@@ -208,18 +208,20 @@ const CreateReconciliationReport = ({
Additional Filters - - } - tip="Only one of Provider, Collection ID, or Granule ID may be applied for each report" - /> + {reportType !== 'ORCA Backup' && ( + + } + tip="Only one of Provider, Collection ID, or Granule ID may be applied for each report" + /> + )}
diff --git a/app/src/js/components/ReconciliationReports/index.js b/app/src/js/components/ReconciliationReports/index.js index 5f5b38951..e1d0619af 100644 --- a/app/src/js/components/ReconciliationReports/index.js +++ b/app/src/js/components/ReconciliationReports/index.js @@ -10,6 +10,7 @@ import { getCount, listReconciliationReports } from '../../actions'; import CreateReconciliationReport from './create'; import ReconciliationReportList from './list'; import ReconciliationReport from './reconciliation-report'; +import BackupReportGranuleDetails from './backup-report-granule-details'; import DatePickerHeader from '../DatePickerHeader/DatePickerHeader'; import { filterQueryParams } from '../../utils/url-helper'; @@ -50,6 +51,7 @@ const ReconciliationReports = ({ } /> +
diff --git a/app/src/js/components/ReconciliationReports/reconciliation-report.js b/app/src/js/components/ReconciliationReports/reconciliation-report.js index 16c3b8dac..f31de5ebd 100644 --- a/app/src/js/components/ReconciliationReports/reconciliation-report.js +++ b/app/src/js/components/ReconciliationReports/reconciliation-report.js @@ -11,6 +11,7 @@ import Loading from '../LoadingIndicator/loading-indicator'; import InventoryReport from './inventory-report'; import GnfReport from './gnf-report'; import Legend from './legend'; +import BackupReport from './backup-report'; const ReconciliationReport = ({ dispatch = {}, @@ -67,6 +68,14 @@ const ReconciliationReport = ({ recordData={recordData} reportName={reconciliationReportName} reportUrl={reportUrl} + />, + 'ORCA Backup': } + recordData={recordData} + reportName={reconciliationReportName} + reportType={reportType} + reportUrl={reportUrl} /> }[reportType] } diff --git a/app/src/js/components/ReconciliationReports/report-heading.js b/app/src/js/components/ReconciliationReports/report-heading.js index 1b4e0f84b..ca18a6372 100644 --- a/app/src/js/components/ReconciliationReports/report-heading.js +++ b/app/src/js/components/ReconciliationReports/report-heading.js @@ -1,9 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Dropdown as DropdownBootstrap } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import moment from 'moment'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; import ErrorReport from '../Errors/report'; +import Tooltip from '../Tooltip/tooltip'; + +const backupOptionsInfo = We have two options for operators to backup + and recovery their data and investigate in the dashboard. They are + the following: ORCA (Operational Recovery Cloud Archive): Data + backup and recovery of granules in S3. Sometimes referred to + S3 Glacier. LZARDS (Level Zero and Repositories Data Store): + An offline backup solution.; /** * ReportHeading @@ -42,6 +52,7 @@ const ReportHeading = ({ const formattedEndTime = endTime ? moment(endTime).utc().format('YYYY-MM-DD H:mm:ss') : 'missing'; + return ( <>
@@ -101,13 +112,30 @@ const ReportHeading = ({ -
+
{ { Inventory: 'The reports below compare datasets and display the conflicts in each data location.', 'Granule Not Found': 'The report below shows a comparison across each data bucket/repository for granule issues.', + 'ORCA Backup': + <> + The report below compares datasets and displays the conflicts with ORCA and Cumulus. + + } + tip={backupOptionsInfo} + /> + + }[type] }
diff --git a/app/src/js/components/home.js b/app/src/js/components/home.js index 5927d2584..171b82fd3 100644 --- a/app/src/js/components/home.js +++ b/app/src/js/components/home.js @@ -8,26 +8,20 @@ import { get } from 'object-path'; import { getCount, getCumulusInstanceMetadata, - getDistApiGatewayMetrics, - getDistApiLambdaMetrics, - getTEALambdaMetrics, - getDistS3AccessMetrics, getStats, listExecutions, listGranules, listRules, - metricsConfigured, } from '../actions'; import { nullValue, tally, seconds } from '../utils/format'; -import { pageSection, section, sectionHeader } from './Section/section'; -import ErrorReport from './Errors/report'; +import { pageSection, sectionHeader } from './Section/section'; import List from './Table/Table'; import { errorTableColumns } from '../utils/table-config/granules'; -import { kibanaGranuleErrorsLink, kibanaAllLogsLink } from '../utils/kibana'; +import linkToKibana from '../utils/kibana'; import DatepickerRange from './Datepicker/DatepickerRange'; import { strings } from './locale'; import { getPersistentQueryParams } from '../utils/url-helper'; @@ -48,10 +42,6 @@ class Home extends React.Component { const { dispatch } = this.props; dispatch(getStats()); dispatch(getCount({ type: 'granules', field: 'status' })); - dispatch(getDistApiGatewayMetrics(this.props.cumulusInstance)); - dispatch(getTEALambdaMetrics(this.props.cumulusInstance)); - dispatch(getDistApiLambdaMetrics(this.props.cumulusInstance)); - dispatch(getDistS3AccessMetrics(this.props.cumulusInstance)); dispatch(listExecutions({})); dispatch(listGranules(this.generateQuery())); dispatch(listRules({})); @@ -110,23 +100,6 @@ class Home extends React.Component { ); } - /** - * - * @param {Object} dist - distribution state object. - * @returns - Error Report when any distribution metric contains an error. - */ - distributionConnectionErrors (dist) { - const errors = Object.keys(dist).filter((key) => dist[key].error).map((key) => dist[key].error); - if (errors.length === 0) return undefined; - const uniqueErrors = [...new Set(errors)]; - return section({ - children: -
- -
- }); - } - getCountByKey (counts, key) { const granuleCount = counts.find((c) => c.key === key); @@ -138,10 +111,10 @@ class Home extends React.Component { render () { const { list } = this.props.granules; const { stats, count } = this.props.stats; - const { dist, location } = this.props; + const { location } = this.props; const searchString = getPersistentQueryParams(location); const overview = [ - [tally(get(stats.data, 'errors.value')), 'Errors', kibanaGranuleErrorsLink()], + [tally(get(stats.data, 'errors.value')), 'Errors', linkToKibana()], [tally(get(stats.data, 'collections.value')), strings.collections, '/collections'], [tally(get(stats.data, 'granules.value')), strings.granules, '/granules'], [tally(get(this.props.executions, 'list.meta.count')), 'Executions', '/executions'], @@ -149,22 +122,6 @@ class Home extends React.Component { [seconds(get(stats.data, 'processingTime.value', nullValue)), 'Average processing Time', '/'] ]; - const distSuccessStats = [ - [tally(get(dist, 's3Access.successes')), 'S3 Access Successes', kibanaAllLogsLink()], - [tally(get(dist, 'teaLambda.successes')), 'TEA Lambda Successes', kibanaAllLogsLink()], - [tally(get(dist, 'apiLambda.successes')), 'Distribution API Lambda Successes', kibanaAllLogsLink()], - [tally(get(dist, 'apiGateway.execution.successes')), 'Gateway Execution Successes', kibanaAllLogsLink()], - [tally(get(dist, 'apiGateway.access.successes')), 'Gateway Access Successes', kibanaAllLogsLink()] - ]; - - const distErrorStats = [ - [tally(get(dist, 's3Access.errors')), 'S3 Access Errors', kibanaAllLogsLink()], - [tally(get(dist, 'teaLambda.errors')), 'TEA Lambda Errors', kibanaAllLogsLink()], - [tally(get(dist, 'apiLambda.errors')), 'Distribution API Lambda Errors', kibanaAllLogsLink()], - [tally(get(dist, 'apiGateway.execution.errors')), 'Gateway Execution Errors', kibanaAllLogsLink()], - [tally(get(dist, 'apiGateway.access.errors')), 'Gateway Access Errors', kibanaAllLogsLink()] - ]; - const granuleCount = get(count.data, 'granules.meta.count'); const numGranules = !Number.isNaN(+granuleCount) ? `${tally(granuleCount)}` : 0; const granuleStatus = get(count.data, 'granules.count', []); @@ -198,15 +155,6 @@ class Home extends React.Component { {sectionHeader('Metrics Overview', 'metricsOverview')} {this.buttonListSection(overview, 'Updates')} - {metricsConfigured() && - <> - {sectionHeader('Distribution Overview', 'distributionOverview')} - {this.distributionConnectionErrors(dist)} - {this.buttonListSection(distErrorStats, 'Distribution Errors', 'distributionErrors')} - {this.buttonListSection(distSuccessStats, 'Distribution Successes', 'distributionSuccesses')} - - } - {sectionHeader( 'Granules Updates', 'updateGranules', diff --git a/app/src/js/config/config.js b/app/src/js/config/config.js index 564b734bc..2da5c1bd7 100644 --- a/app/src/js/config/config.js +++ b/app/src/js/config/config.js @@ -25,15 +25,8 @@ const config = { awsRegion: process.env.AWS_REGION || 'us-west-2', oauthMethod: process.env.AUTH_METHOD || 'earthdata', kibanaRoot: process.env.KIBANAROOT || '', - esRoot: process.env.ESROOT || '', - esCloudwatchTargetPattern: process.env.ES_CLOUDWATCH_TARGET_PATTERN || '', - esDistributionTargetPattern: process.env.ES_DISTRIBUTION_TARGET_PATTERN || '', - showTeaMetrics: computeBool(process.env.SHOW_TEA_METRICS, true), - showDistributionAPIMetrics: computeBool(process.env.SHOW_DISTRIBUTION_API_METRICS, false), graphicsPath: process.env.BUCKET || '', enableRecovery: computeBool(process.env.ENABLE_RECOVERY, false), - esUser: process.env.ES_USER || '', - esPassword: process.env.ES_PASSWORD || '', servedByCumulusAPI: computeBool(process.env.SERVED_BY_CUMULUS_API, '') }; diff --git a/app/src/js/config/index.js b/app/src/js/config/index.js index dff5bf16b..e109ed356 100644 --- a/app/src/js/config/index.js +++ b/app/src/js/config/index.js @@ -8,7 +8,7 @@ const deploymentConfig = require('./config'); const baseConfig = { environment: 'development', requireEarthdataLogin: false, - minCompatibleApiVersion: 'v10.1.0', + minCompatibleApiVersion: 'v11.1.0', oauthMethod: 'earthdata', graphicsPath: '/src/assets/images/', diff --git a/app/src/js/reducers/dist.js b/app/src/js/reducers/dist.js deleted file mode 100644 index 473f7d835..000000000 --- a/app/src/js/reducers/dist.js +++ /dev/null @@ -1,90 +0,0 @@ -import get from 'lodash/get'; -import { createReducer } from '@reduxjs/toolkit'; -import { - DIST_APIGATEWAY, - DIST_APIGATEWAY_INFLIGHT, - DIST_APIGATEWAY_ERROR, - DIST_API_LAMBDA, - DIST_API_LAMBDA_INFLIGHT, - DIST_API_LAMBDA_ERROR, - DIST_TEA_LAMBDA, - DIST_TEA_LAMBDA_INFLIGHT, - DIST_TEA_LAMBDA_ERROR, - DIST_S3ACCESS, - DIST_S3ACCESS_INFLIGHT, - DIST_S3ACCESS_ERROR, -} from '../actions/types'; - -export const initialState = { - apiGateway: { - execution: { errors: null, successes: null }, - access: { errors: null, successes: null }, - }, - apiLambda: { errors: null, successes: null }, - teaLambda: { errors: null, successes: null }, - s3Access: { errors: null, successes: null }, -}; - -const count = (data, name) => get(data, `aggregations.2.buckets.${name}.doc_count`, 0); - -export default createReducer(initialState, { - [DIST_APIGATEWAY]: (state, action) => { - const { data } = action; - state.apiGateway.error = null; - state.apiGateway.inflight = false; - state.apiGateway.queriedAt = Date.now(); - state.apiGateway.access.errors = count(data, 'ApiAccessErrors'); - state.apiGateway.access.successes = count(data, 'ApiAccessSuccesses'); - state.apiGateway.execution.errors = count(data, 'ApiExecutionErrors'); - state.apiGateway.execution.successes = count(data, 'ApiExecutionSuccesses'); - }, - [DIST_APIGATEWAY_INFLIGHT]: (state) => { - state.apiGateway.inflight = true; - }, - [DIST_APIGATEWAY_ERROR]: (state, action) => { - state.apiGateway.inflight = false; - state.apiGateway.error = action.error; - }, - [DIST_API_LAMBDA]: (state, action) => { - state.apiLambda.error = null; - state.apiLambda.inflight = false; - state.apiLambda.queriedAt = Date.now(); - state.apiLambda.errors = count(action.data, 'LambdaAPIErrors'); - state.apiLambda.successes = count(action.data, 'LambdaAPISuccesses'); - }, - [DIST_API_LAMBDA_INFLIGHT]: (state) => { - state.apiLambda.inflight = true; - }, - [DIST_API_LAMBDA_ERROR]: (state, action) => { - state.apiLambda.inflight = false; - state.apiLambda.error = action.error; - }, - [DIST_TEA_LAMBDA]: (state, action) => { - state.teaLambda.error = null; - state.teaLambda.inflight = false; - state.teaLambda.queriedAt = Date.now(); - state.teaLambda.errors = count(action.data, 'TEALambdaErrors'); - state.teaLambda.successes = count(action.data, 'TEALambdaSuccesses'); - }, - [DIST_TEA_LAMBDA_INFLIGHT]: (state) => { - state.teaLambda.inflight = true; - }, - [DIST_TEA_LAMBDA_ERROR]: (state, action) => { - state.teaLambda.inflight = false; - state.teaLambda.error = action.error; - }, - [DIST_S3ACCESS]: (state, action) => { - state.s3Access.error = null; - state.s3Access.inflight = false; - state.s3Access.queriedAt = Date.now(); - state.s3Access.errors = count(action.data, 's3AccessFailures'); - state.s3Access.successes = count(action.data, 's3AccessSuccesses'); - }, - [DIST_S3ACCESS_INFLIGHT]: (state) => { - state.s3Access.inflight = true; - }, - [DIST_S3ACCESS_ERROR]: (state, action) => { - state.s3Access.inflight = false; - state.s3Access.error = action.error; - }, -}); diff --git a/app/src/js/reducers/index.js b/app/src/js/reducers/index.js index 640130d97..464c799d5 100644 --- a/app/src/js/reducers/index.js +++ b/app/src/js/reducers/index.js @@ -6,7 +6,6 @@ import apiVersion from './api-version'; import cmrInfo from './cmr-info'; import collections from './collections'; import config from './utils/config'; -import dist from './dist'; import datepicker from './datepicker'; import granules from './granules'; import granulesExecutions from './granules-executions'; @@ -37,7 +36,6 @@ export const reducers = { cmrInfo, collections, config, - dist, datepicker, cumulusInstance, granules, diff --git a/app/src/js/utils/basic-auth.js b/app/src/js/utils/basic-auth.js deleted file mode 100644 index 1aa988d22..000000000 --- a/app/src/js/utils/basic-auth.js +++ /dev/null @@ -1,15 +0,0 @@ -import { esUser, esPassword } from '../config'; - -// Build basic auth string -export const partsEncode = (user, password) => Buffer.from(`${user}:${password}`).toString('base64'); - -export const basicAuth = (user, password) => `Basic ${partsEncode(user, password)}`; - -export const buildAuthHeader = (user, password) => { - if (!user || !password) return {}; - return { - Authorization: basicAuth(user, password) - }; -}; - -export const authHeader = () => buildAuthHeader(esUser, esPassword); diff --git a/app/src/js/utils/kibana.js b/app/src/js/utils/kibana.js index 67b326462..b283bb7a3 100644 --- a/app/src/js/utils/kibana.js +++ b/app/src/js/utils/kibana.js @@ -2,11 +2,9 @@ import _config from '../config'; const kibanaConfigured = () => !!_config.kibanaRoot; -const genericKibanaLink = () => { +const linkToKibana = () => { if (!kibanaConfigured()) return ''; return `${_config.kibanaRoot}/app/discover#/`; }; -export const kibanaAllLogsLink = genericKibanaLink; -export const kibanaGranuleErrorsLink = genericKibanaLink; -export const kibanaExecutionLink = (cumulusInstanceMeta, executionNameOrArn) => genericKibanaLink(); +export default linkToKibana; diff --git a/app/src/js/utils/table-config/execution-status.js b/app/src/js/utils/table-config/execution-status.js index 825f1ec94..a7237b311 100644 --- a/app/src/js/utils/table-config/execution-status.js +++ b/app/src/js/utils/table-config/execution-status.js @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import get from 'lodash/get'; import { displayCase, fullDate, parseJson } from '../format'; import { getPersistentQueryParams } from '../url-helper'; -import { kibanaExecutionLink } from '../kibana'; +import linkToKibana from '../kibana'; import { window } from '../browser'; import DefaultModal from '../../components/Modal/modal'; @@ -232,7 +232,7 @@ export const metaAccessors = ({ label: 'Logs', property: 'executionArn', accessor: (d) => { - const kibanaLink = kibanaExecutionLink(cumulusInstance, d); + const kibanaLink = linkToKibana(); const className = 'button button--small button__goto button__arrow button__animation button__arrow--white'; if (kibanaLink && kibanaLink.length) { diff --git a/app/src/js/utils/table-config/reconciliation-reports.js b/app/src/js/utils/table-config/reconciliation-reports.js index 795f2583e..e7ed561cf 100644 --- a/app/src/js/utils/table-config/reconciliation-reports.js +++ b/app/src/js/utils/table-config/reconciliation-reports.js @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import get from 'lodash/get'; -import { nullValue, dateOnly, IndicatorWithTooltip, collectionHrefFromId } from '../format'; +import { nullValue, dateOnly, IndicatorWithTooltip, collectionHrefFromId, providerLink } from '../format'; import { getReconciliationReport, deleteReconciliationReport, listReconciliationReports } from '../../actions'; import { getPersistentQueryParams } from '../url-helper'; @@ -252,3 +252,99 @@ export const tableColumnsGnf = [ width: 50, }, ]; + +export const tableColumnsBackup = ({ reportType, reportName }) => ([ + { + Header: 'Granule ID', + accessor: 'granuleId', + width: 200, + }, + { + Header: 'Conflict Type', + accessor: 'conflictType', + }, + { + Header: 'Conflict Details', + id: 'conflictDetails', + Cell: ({ row: { original: granule, values: { granuleId } } }) => ( // eslint-disable-line react/prop-types + View Details + ), + disableSortBy: true + }, + { + Header: 'Collection ID', + accessor: 'collectionId', + // eslint-disable-next-line react/prop-types + Cell: ({ cell: { value } }) => ({ + pathname: collectionHrefFromId(value), search: getPersistentQueryParams(location) + })}>{value}, + width: 125, + }, + { + Header: 'Provider', + accessor: 'provider', + Cell: ({ cell: { value } }) => providerLink(value) + } +]); + +const fileLink = (bucket, key) => `https://${bucket}.s3.amazonaws.com/${key}`; +export const tableColumnsGranuleConflictDetails = ({ reportType }) => { + const checkButton =