diff --git a/cypress/fixtures/2021-FRONTENDTESTBANK9999-no-edits.txt b/cypress/fixtures/2021-FRONTENDTESTBANK9999-no-edits.txt
new file mode 100644
index 000000000..198d64669
--- /dev/null
+++ b/cypress/fixtures/2021-FRONTENDTESTBANK9999-no-edits.txt
@@ -0,0 +1,2 @@
+1|FRONTENDTESTBANK9999|2021|4|Mr. Smug Pockets|555-555-5555|pockets@ficus.com|1234 Hocus Potato Way|Tatertown|UT|84096|9|1|53-1111111|FRONTENDTESTBANK9999
+2|FRONTENDTESTBANK9999|FRONTENDTESTBANK9999JAJZMZSDXF8A57HP1HJZQOZ66|20200613|3|2|2|2|3|218910|1|20210213|1234 Hocus Potato Way|Tatertown|NM|14755|35003|35003976400|1||||||4||||||2|3|1||||||||8||||||||1|4|2|4|2|3|75|8888|100|0|NA|3|2|8888|8888|9||9||10|||||NA|NA|NA|NA|NA|NA|32|NA|NA|256|29|2|2|2|2|NA|2|2|4|NA|2|2|53535|3||||||1||||||2|2|2
\ No newline at end of file
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 292ef878b..04f46bcb5 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -23,7 +23,7 @@ http {
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
# CSP
- add_header Content-Security-Policy "default-src 'self' blob:; script-src 'self' 'unsafe-inline' blob: data: https://tagmanager.google.com https://www.googletagmanager.com https://www.google-analytics.com https://*.cfpb.gov https://www.consumerfinance.gov; img-src 'self' blob: data: https://www.google-analytics.com https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; frame-src 'self' https://www.youtube.com/ https://ffiec.cfpb.gov/; connect-src 'self' https://*.cfpb.gov https://www.consumerfinance.gov https://raw.githubusercontent.com https://ffiec-api.cfpb.gov https://ffiec.cfpb.gov https://*.mapbox.com https://www.google-analytics.com https://s3.amazonaws.com;";
+ add_header Content-Security-Policy "default-src 'self' blob:; script-src 'self' 'unsafe-inline' blob: data: https://tagmanager.google.com https://www.googletagmanager.com https://www.google-analytics.com https://*.cfpb.gov https://www.consumerfinance.gov; img-src 'self' blob: data: https://www.google-analytics.com https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; frame-src 'self' https://www.youtube.com/ https://ffiec.cfpb.gov/; connect-src 'self' ws://*.cfpb.gov wss://*.cfpb.gov https://*.cfpb.gov https://www.consumerfinance.gov https://raw.githubusercontent.com https://ffiec-api.cfpb.gov https://ffiec.cfpb.gov https://*.mapbox.com https://www.google-analytics.com https://s3.amazonaws.com;";
# Restrict referrer
add_header Referrer-Policy "strict-origin";
@@ -82,6 +82,16 @@ http {
try_files $uri =404;
}
+ # Pass Websocket Upgrade requests to the backend
+ location ~* /v2/filing/(.*)/progress$ {
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
+
# Whitelisted extensions
location ~* \.(html|css|js|json|png|jpg|svg|eot|ttf|woff|woff2|map|ico)$ {
limit_except GET {
diff --git a/src/App.jsx b/src/App.jsx
index 091da7e15..30f8bde23 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -67,6 +67,8 @@ const App = () => {
{showFooter && }
+ {console.log(window.location)
+ }
)
}
diff --git a/src/filing/actions/fetchUpload.js b/src/filing/actions/fetchUpload.js
index 4196f5793..60ea6b96f 100644
--- a/src/filing/actions/fetchUpload.js
+++ b/src/filing/actions/fetchUpload.js
@@ -6,10 +6,12 @@ import receiveUpload from './receiveUpload.js'
import hasHttpError from './hasHttpError.js'
import receiveUploadError from './receiveUploadError.js'
import { error } from '../utils/log.js'
+import requestProcessingProgress from './requestProcessingProgress'
export default function fetchUpload(file) {
return dispatch => {
dispatch(requestUpload())
+ dispatch(requestProcessingProgress())
const data = new FormData()
data.append('file', file)
diff --git a/src/filing/actions/listenForProgress.js b/src/filing/actions/listenForProgress.js
new file mode 100644
index 000000000..7388765f1
--- /dev/null
+++ b/src/filing/actions/listenForProgress.js
@@ -0,0 +1,180 @@
+import fetchEdits from './fetchEdits.js'
+import receiveSubmission from './receiveSubmission.js'
+import receiveError from './receiveError.js'
+import hasHttpError from './hasHttpError.js'
+import { getLatestSubmission } from '../api/api.js'
+import requestProcessingProgress from './requestProcessingProgress'
+import receiveProcessingProgress from './receiveProcessingProgress'
+import { error } from '../utils/log.js'
+import {
+ SYNTACTICAL_VALIDITY_EDITS,
+ NO_MACRO_EDITS,
+ MACRO_EDITS,
+ UPLOADED
+} from '../constants/statusCodes.js'
+import * as AccessToken from '../../common/api/AccessToken.js'
+
+let keepSocketAlive
+
+// Extract completion percentage
+export const parseProgress = string => {
+ if (!string.match(/^InProgress/)) return string
+ return string.match(/\d{1,}/)[0]
+}
+
+const shouldSkipKey = key => ['done', 'fetched'].indexOf(key) > -1
+
+/* Websocket Listener */
+export default function listenForProgress() {
+ return (dispatch) => {
+ if (!window.location.pathname.match('/upload'))
+ return Promise.resolve(null)
+
+ return getLatestSubmission()
+ .then((json) => {
+ return hasHttpError(json).then((hasError) => {
+ if (hasError) {
+ dispatch(receiveError(json))
+ throw new Error(json && `${json.status}: ${json.statusText}`)
+ }
+ console.log('- Getting latest submission JSON')
+ return dispatch(receiveSubmission(json))
+ })
+ })
+ .then((json) => {
+ if (!json) {
+ console.warn('-- No submission JSON found, skipping WS connection')
+
+ return
+ }
+
+ const { status, id } = json
+ const { lei, period, sequenceNumber } = id
+ const { year, quarter } = period
+ const { code } = status
+
+ if (code >= UPLOADED) {
+ console.log('- Opening websocket to listen for progress...')
+
+ // Open a websocket and listen for updates
+ const wsBaseUrl = process.env.REACT_APP_ENVIRONMENT === 'CI'
+ ? `${window.location.hostname}:8080` // `IP-ADDRESS:8080`
+ : `${window.location.host}/v2/filing`
+
+ const socketType = window.location.protocol == 'https:' ? 'wss' : 'ws'
+
+ const wsProgressUrl = quarter
+ ? `/institutions/${lei}/filings/${year}/quarter/${quarter}/submissions/${sequenceNumber}/progress`
+ : `/institutions/${lei}/filings/${year}/submissions/${sequenceNumber}/progress`
+
+ let socket
+
+ try {
+ console.log(`-- Attempting connection to ${socketType}://${wsBaseUrl}${wsProgressUrl}`)
+ document.cookie = 'X-Authorization-Token=' + AccessToken.get() + '; path=/';
+ socket = new WebSocket(`${socketType}://${wsBaseUrl}${wsProgressUrl}`)
+ } catch (e) {
+ console.log(`--- Connection to ${socketType}://${wsBaseUrl}${wsProgressUrl} failed!`)
+ error(e)
+ console.log('---')
+ }
+
+ socket.onopen = () => {
+ console.log('-- Socket open! Sending Bearer token and then listening for Progress...')
+ dispatch(requestProcessingProgress())
+
+ // Keep connection alive by pinging server every 60s
+ keepSocketAlive = setInterval(() => {
+ const timestamp = new Date().toLocaleString('en-US', {
+ timeZone: 'America/New_York',
+ })
+ socket.send(JSON.stringify({ keepAlive: `${timestamp} ET` }))
+ }, 60000)
+ }
+
+ // Listen for messages
+ socket.onmessage = (event) => {
+ const data = event.data && JSON.parse(event.data)[1]
+
+ if (!data) return
+
+ const uploadStatus = {
+ syntactical: parseProgress(data.syntactical),
+ quality: parseProgress(data.quality),
+ macro: parseProgress(data.macro),
+ }
+
+ // No Syntactical errors and all others Completed
+ uploadStatus.done =
+ !!uploadStatus.syntactical.match(/Error/) ||
+ Object.keys(uploadStatus).every((key) => {
+ if (shouldSkipKey(key)) return true
+ return uploadStatus[key].match(/^Completed/)
+ })
+
+ console.log('> Progress: ', uploadStatus)
+
+ // Update Submission for status messaging
+ getLatestSubmission().then((json) => {
+ return hasHttpError(json).then((hasError) => {
+ if (hasError) {
+ dispatch(receiveError(json))
+ throw new Error(json && `${json.status}: ${json.statusText}`)
+ }
+ return dispatch(receiveSubmission(json))
+ })
+ })
+
+ if (uploadStatus.done) {
+ console.log('<<< Closing Socket!')
+ socket.close(1000, 'Done Processing')
+
+ // Save status updates
+ dispatch(receiveProcessingProgress({ status: uploadStatus }))
+
+ const hasEdits = Object.keys(uploadStatus).some((key) => {
+ if (shouldSkipKey(key)) return false
+ return uploadStatus[key].match(/Error/)
+ })
+
+ if (hasEdits) return dispatch(fetchEdits())
+ }
+ else {
+ dispatch(receiveProcessingProgress({ status: uploadStatus }))
+ }
+ }
+
+ // TODO: Anything special on close?
+ socket.onclose = (event) => {
+ if (event.wasClean) {
+ console.log(
+ `[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`
+ )
+ }
+ else {
+ // e.g. server process killed or network down
+ // event.code is usually 1006 in this case
+ console.log('[socket onclose] Connection died', event)
+ }
+
+ clearInterval(keepSocketAlive)
+ }
+
+ // TODO: What to do on websocket error?
+ socket.onerror = function (error) {
+ console.log(`[socket onerror] ${error.message}`, error)
+ }
+ }
+ else if (
+ // only get edits when we've reached a terminal edit state
+ code === SYNTACTICAL_VALIDITY_EDITS ||
+ code === NO_MACRO_EDITS ||
+ code === MACRO_EDITS) {
+ return dispatch(fetchEdits())
+ }
+ })
+ .catch((err) => {
+ error(err)
+ })
+ }
+}
diff --git a/src/filing/actions/pollForProgress.js b/src/filing/actions/pollForProgress.js
index 183712644..6b9c6d84e 100644
--- a/src/filing/actions/pollForProgress.js
+++ b/src/filing/actions/pollForProgress.js
@@ -1,5 +1,6 @@
import fetchEdits from './fetchEdits.js'
import receiveSubmission from './receiveSubmission.js'
+import listenForProgress from './listenForProgress'
import receiveError from './receiveError.js'
import hasHttpError from './hasHttpError.js'
import { getLatestSubmission } from '../api/api.js'
@@ -9,7 +10,9 @@ import {
SYNTACTICAL_VALIDITY_EDITS,
NO_MACRO_EDITS,
MACRO_EDITS,
- VALIDATED
+ UPLOADED,
+ VALIDATED,
+ VALIDATING,
} from '../constants/statusCodes.js'
export const makeDurationGetter = () => {
@@ -51,6 +54,11 @@ export default function pollForProgress() {
.then(json => {
if (!json) return
const { code } = json.status
+
+ // Switch to Websocket if file is uploaded and still processing
+ if(code >= UPLOADED && code <= VALIDATING)
+ return dispatch(listenForProgress())
+
if (
// continue polling until we reach a status that isn't processing
code !== PARSED_WITH_ERRORS &&
diff --git a/src/filing/actions/receiveProcessingProgress.js b/src/filing/actions/receiveProcessingProgress.js
new file mode 100644
index 000000000..3cf2ff00d
--- /dev/null
+++ b/src/filing/actions/receiveProcessingProgress.js
@@ -0,0 +1,10 @@
+import * as types from '../constants'
+
+export default function receiveProcessingProgress({ status }) {
+ return (dispatch) => {
+ return dispatch({
+ type: types.RECEIVE_PROCESSING_PROGRESS,
+ status
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/filing/actions/requestProcessingProgress.js b/src/filing/actions/requestProcessingProgress.js
new file mode 100644
index 000000000..c705b6a2e
--- /dev/null
+++ b/src/filing/actions/requestProcessingProgress.js
@@ -0,0 +1,7 @@
+import * as types from '../constants'
+
+export default function requestProcessingProgress() {
+ return {
+ type: types.REQUEST_PROCESSING_PROGRESS
+ }
+}
\ No newline at end of file
diff --git a/src/filing/constants/index.js b/src/filing/constants/index.js
index 39de4c9af..0c178fee6 100644
--- a/src/filing/constants/index.js
+++ b/src/filing/constants/index.js
@@ -82,4 +82,7 @@ export const RECEIVE_LATEST_SUBMISSION = 'RECEIVE_LATEST_SUBMISSION'
export const RECEIVE_INSTITUTION_NOT_FOUND = 'RECEIVE_INSTITUTION_NOT_FOUND'
-export const RECEIVE_FILING_PAGE = 'RECEIVE_FILING_PAGE'
\ No newline at end of file
+export const RECEIVE_FILING_PAGE = 'RECEIVE_FILING_PAGE'
+
+export const REQUEST_PROCESSING_PROGRESS = 'REQUEST_PROCESSING_PROGRESS'
+export const RECEIVE_PROCESSING_PROGRESS = 'RECEIVE_PROCESSING_PROGRESS'
\ No newline at end of file
diff --git a/src/filing/index.js b/src/filing/index.js
index c64e82553..8a85b8580 100644
--- a/src/filing/index.js
+++ b/src/filing/index.js
@@ -24,11 +24,13 @@ if (process.env.NODE_ENV !== 'production') {
// use redux dev tools, extension required
// see https://github.com/zalmoxisus/redux-devtools-extension#installation
const { composeWithDevTools } = require('redux-devtools-extension')
+ const composeEnhancers = composeWithDevTools({trace: true, traceLimit: 25 })
+
store = createStore(
combineReducers({
app: appReducer
}),
- composeWithDevTools(applyMiddleware(...middleware))
+ composeEnhancers(applyMiddleware(...middleware))
)
} else {
store = createStore(
diff --git a/src/filing/institutions/Progress.jsx b/src/filing/institutions/Progress.jsx
index 995f35bbd..8bd0303e2 100644
--- a/src/filing/institutions/Progress.jsx
+++ b/src/filing/institutions/Progress.jsx
@@ -20,31 +20,31 @@ const navMap = {
submission.status.code === PARSED_WITH_ERRORS || submission.isStalled,
isCompleted: submission => submission.status.code > UPLOADED,
errorText: 'upload error',
- completedText: 'uploaded'
+ completedText: () => 'uploaded'
},
'syntactical & validity edits': {
isErrored: submission => submission.status.code === SYNTACTICAL_VALIDITY_EDITS,
isCompleted: submission => submission.status.code >= NO_SYNTACTICAL_VALIDITY_EDITS,
errorText: 'syntactical & validity edits',
- completedText: 'no syntactical & validity edits'
+ completedText: () => 'no syntactical & validity edits'
},
'quality edits': {
isErrored: submission => submission.qualityExists && !submission.qualityVerified,
isCompleted: submission => submission.status.code >= NO_QUALITY_EDITS && (!submission.qualityExists || submission.qualityVerified),
errorText: 'quality edits' ,
- completedText: 'quality edits verified'
+ completedText: (submission) => submission.qualityExists ? 'quality edits verified' : 'no quality edits'
},
'macro quality edits': {
isErrored: submission => submission.macroExists && !submission.macroVerified,
isCompleted: submission => (submission.status.code > MACRO_EDITS || submission.status.code === NO_MACRO_EDITS) && (!submission.macroExists || submission.macroVerified),
errorText: 'macro quality edits',
- completedText: 'macro quality edits verified'
+ completedText: (submission) => submission.macroExists ? 'macro quality edits verified' : 'no macro edits'
},
submission: {
isReachable: submission => submission.status.code >= VALIDATED || submission.status.code === NO_MACRO_EDITS ,
isErrored: () => false,
isCompleted: submission => submission.status.code === SIGNED,
- completedText: 'submitted'
+ completedText: () => 'submitted'
}
}
@@ -58,7 +58,7 @@ const renderNavItem = (submission, name, i) => {
renderedName = navItem.errorText
navClass = 'error'
} else if (completed) {
- renderedName = navItem.completedText
+ renderedName = navItem.completedText(submission)
navClass = 'complete'
}
diff --git a/src/filing/institutions/ViewButton.jsx b/src/filing/institutions/ViewButton.jsx
index 2a29ed81c..cd02a26c0 100644
--- a/src/filing/institutions/ViewButton.jsx
+++ b/src/filing/institutions/ViewButton.jsx
@@ -30,7 +30,7 @@ const InstitutionViewButton = ({ submission, institution, filingPeriod, isClosed
text = 'View upload progress'
} else if (code === PARSED_WITH_ERRORS) {
text = 'Review formatting errors'
- } else if (code < NO_SYNTACTICAL_VALIDITY_EDITS) {
+ } else if (code < NO_MACRO_EDITS) {
text = 'View progress'
} else if (code > VALIDATING && code < VALIDATED && code !== NO_MACRO_EDITS) {
text = 'Review edits'
diff --git a/src/filing/reducers/index.js b/src/filing/reducers/index.js
index 74deaec54..dde4e30c5 100644
--- a/src/filing/reducers/index.js
+++ b/src/filing/reducers/index.js
@@ -20,6 +20,7 @@ import redirecting from './redirecting.js'
import latestSubmissions from './latestSubmissions'
import refiling from './refiling'
import filingPeriodOptions from './filingPeriodOptions'
+import processProgress from './processProgress'
export default combineReducers({
lei,
@@ -41,5 +42,6 @@ export default combineReducers({
redirecting,
latestSubmissions,
refiling,
- filingPeriodOptions
+ filingPeriodOptions,
+ processProgress
})
diff --git a/src/filing/reducers/processProgress.js b/src/filing/reducers/processProgress.js
new file mode 100644
index 000000000..7124697e7
--- /dev/null
+++ b/src/filing/reducers/processProgress.js
@@ -0,0 +1,19 @@
+import * as types from '../constants'
+
+const defaultState = {
+ syntactical: 'Waiting',
+ quality: 'Waiting',
+ macro: 'Waiting',
+ done: false
+}
+
+export default (state = defaultState, action) => {
+ switch (action.type) {
+ case types.REQUEST_PROCESSING_PROGRESS:
+ return { ...defaultState, fetched: true }
+ case types.RECEIVE_PROCESSING_PROGRESS:
+ return { ...state, ...action.status }
+ default:
+ return state
+ }
+}
\ No newline at end of file
diff --git a/src/filing/submission/Nav.jsx b/src/filing/submission/Nav.jsx
index 0efebf9e3..44505a31b 100644
--- a/src/filing/submission/Nav.jsx
+++ b/src/filing/submission/Nav.jsx
@@ -31,7 +31,7 @@ export default class EditsNav extends Component {
isCompleted: () => this.props.code > VALIDATING,
errorClass: 'error',
errorText: 'uploaded with formatting errors',
- completedText: 'uploaded',
+ completedText: () => 'uploaded',
link: 'upload'
},
'syntactical & validity edits': {
@@ -45,7 +45,7 @@ export default class EditsNav extends Component {
this.props.code === NO_SYNTACTICAL_VALIDITY_EDITS,
errorClass: 'warning-exclamation',
errorText: 'syntactical & validity edits found',
- completedText: 'no syntactical & validity edits',
+ completedText: () => 'no syntactical & validity edits',
link: 'syntacticalvalidity'
},
'quality edits': {
@@ -58,7 +58,9 @@ export default class EditsNav extends Component {
(this.props.qualityVerified || !this.props.qualityExists),
errorClass: 'warning-question',
errorText: 'quality edits found',
- completedText: 'quality edits verified',
+ completedText: () => !this.props.qualityExists
+ ? 'no quality edits'
+ : 'quality edits verified',
link: 'quality'
},
'macro quality edits': {
@@ -70,14 +72,16 @@ export default class EditsNav extends Component {
(!this.props.macroExists || this.props.macroVerified),
errorClass: 'warning-question',
errorText: 'macro quality edits found',
- completedText: 'macro quality edits verified',
+ completedText: () => !this.props.macroExists
+ ? 'no macro quality edits'
+ : 'macro quality edits verified',
link: 'macro'
},
submission: {
isReachable: () => this.props.code >= VALIDATED || this.props.code === NO_MACRO_EDITS,
isErrored: () => false,
isCompleted: () => this.props.code === SIGNED,
- completedText: 'submitted',
+ completedText: () => 'submitted',
link: 'submission'
}
}
@@ -129,7 +133,7 @@ export default class EditsNav extends Component {
const renderedName = errored
? navItem.errorText
: completed
- ? navItem.completedText
+ ? navItem.completedText()
: name
let navClass = errored
diff --git a/src/filing/submission/NavButton.jsx b/src/filing/submission/NavButton.jsx
index 7edbb5312..aa814689d 100644
--- a/src/filing/submission/NavButton.jsx
+++ b/src/filing/submission/NavButton.jsx
@@ -4,33 +4,54 @@ import { Link } from 'react-router-dom'
import Loading from '../../common/LoadingIcon.jsx'
import {
VALIDATING,
- SYNTACTICAL_VALIDITY_EDITS
+ SYNTACTICAL_VALIDITY_EDITS,
+ VALIDATED,
} from '../constants/statusCodes.js'
import './NavButton.css'
+const nextActionableStep = ({
+ qualityExists,
+ qualityVerified,
+ macroExists,
+ macroVerified,
+ code,
+}) => {
+ if (code === VALIDATED) return 'submission'
+ if (qualityExists && !qualityVerified) return 'quality'
+ if (macroExists && !macroVerified) return 'macro'
+ return 'syntacticalvalidity'
+}
+
const NavButton = ({ page, base, code, editsFetched, validationComplete, qualityExists, qualityVerified, macroExists, macroVerified }) => {
let className
let suffix
let spinOn = false
const editFetchInProgress = code < 14 && !editsFetched
const preError = code <= VALIDATING || !validationComplete
+ const actionableArgs = {
+ code,
+ qualityExists,
+ qualityVerified,
+ macroExists,
+ macroVerified,
+ }
switch (page) {
case 'upload':
- suffix = 'syntacticalvalidity'
+ suffix = nextActionableStep(actionableArgs)
if (preError || editFetchInProgress) className = 'hidden'
if (editFetchInProgress && code > VALIDATING && code !== 8 && code !== 11) spinOn = true
break
case 'syntacticalvalidity':
- suffix = 'quality'
+ suffix = nextActionableStep(actionableArgs)
if (preError || code === SYNTACTICAL_VALIDITY_EDITS || editFetchInProgress) className = 'hidden'
break
case 'quality':
- suffix = 'macro'
+ suffix = nextActionableStep(actionableArgs)
if (preError || (qualityExists && !qualityVerified) || editFetchInProgress) className = 'hidden'
break
case 'macro':
- suffix = 'submission'
+ suffix = nextActionableStep(actionableArgs)
if (preError || (macroExists && !macroVerified) || editFetchInProgress) className = 'hidden'
break
default:
diff --git a/src/filing/submission/upload/FileProcessingProgress.css b/src/filing/submission/upload/FileProcessingProgress.css
new file mode 100644
index 000000000..a1b586567
--- /dev/null
+++ b/src/filing/submission/upload/FileProcessingProgress.css
@@ -0,0 +1,32 @@
+#fileProcessProgress {
+ margin: 2rem 0 0 0;
+}
+
+.barLabel {
+ margin: .5rem 0;
+ font-weight: bold;
+}
+
+.progressBar {
+ height: 25px;
+ width: 100%;
+ background-color: #e0e0de;
+ border-radius: 10px;
+ margin: 1rem 0;
+}
+
+.progressBar .fill {
+ line-height: 25px;
+ height: 100%;
+ width: 1%;
+ background-color: grey;
+ border-radius: inherit;
+ text-align: right;
+ transition: width 1s ease-in-out;
+}
+
+.progressBar .label {
+ padding: 5px;
+ color: white;
+ font-weight: bold;
+}
diff --git a/src/filing/submission/upload/FileProcessingProgress.jsx b/src/filing/submission/upload/FileProcessingProgress.jsx
new file mode 100644
index 000000000..744a018a2
--- /dev/null
+++ b/src/filing/submission/upload/FileProcessingProgress.jsx
@@ -0,0 +1,68 @@
+import React, { useEffect } from 'react';
+import { UPLOADING } from '../../constants/statusCodes'
+import { UploadBar } from './UploadBar'
+import ProgressBar from './ProgressBar/'
+import StackedProgressBars from './ProgressBar/StackedProgressBars'
+import { STATUS } from './ProgressBar/constants'
+import './FileProcessingProgress.css'
+
+const parsePercent = str => {
+ const digits = str.match(/^\d/) && str
+ if(digits) return digits
+ return '100'
+}
+
+const getStatus = (str, prevErrors, isSV) => {
+ if (prevErrors) return STATUS.SKIP
+ if (str === 'Waiting' || prevErrors) return STATUS.PENDING
+ if (str === 'Completed') return STATUS.DONE
+ if (str === 'CompletedWithErrors' && !isSV) return STATUS.EDITS
+ if (hasError(str)) return STATUS.ERROR
+
+ return STATUS.PROGRESS
+}
+
+const hasError = str => str.match(/Err/)
+
+const FileProcessingProgress = ({ progress, uploading, code, watchProgress, filingPeriod, lei }) => {
+ const { syntactical, macro, quality, fetched } = progress
+
+ useEffect(() => {
+ if (!fetched) watchProgress()
+ }, [fetched, watchProgress])
+
+ if (code < UPLOADING && !uploading) return null
+
+ const hasSynEdits = hasError(syntactical)
+
+ return (
+
+ )
+}
+
+export default FileProcessingProgress
diff --git a/src/filing/submission/upload/ProgressBar.jsx b/src/filing/submission/upload/ProgressBar.jsx
new file mode 100644
index 000000000..00acab765
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar.jsx
@@ -0,0 +1,43 @@
+import React from 'react'
+
+const YELLOW = '#dc731c'
+const BLUE = '#0071bc'
+const RED = '#e31c3d'
+
+const calcPercent = pct => {
+ if(typeof pct === 'number') return pct
+ if(pct.match(/^\d/)) return parseInt(pct)
+ if(pct.match(/^Done|^Errors/)) return 100
+ return 0
+}
+
+export const ProgressBar = ({ percent, label, error, hasPrevError }) => {
+ if([null, undefined].indexOf(percent) > -1) return null
+ let pct = calcPercent(percent)
+
+ const fillStyles = {
+ width: `${pct}%`,
+ backgroundColor: pct === 100 ? BLUE : YELLOW
+ }
+ if (error) fillStyles.backgroundColor = RED
+
+ const progressBarStyles = {}
+ if(hasPrevError) progressBarStyles.backgroundColor = YELLOW
+
+ return (
+ <>
+
{label}
+
+ >
+ )
+}
+
+ProgressBar.defaultProps = {
+ percent: '0',
+ error: false,
+ waiting: false
+}
diff --git a/src/filing/submission/upload/ProgressBar/ProgressBar.css b/src/filing/submission/upload/ProgressBar/ProgressBar.css
new file mode 100644
index 000000000..65e50339a
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar/ProgressBar.css
@@ -0,0 +1,117 @@
+/* Container */
+.meter {
+ height: 26px;
+ position: relative;
+ background: #555;
+ -moz-border-radius: 25px;
+ -webkit-border-radius: 25px;
+ border-radius: 25px;
+ box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3);
+ border: 1px solid black;
+}
+
+/* Fill */
+.meter > .fill {
+ display: block;
+ height: 100%;
+ border-top-right-radius: 20px;
+ border-bottom-right-radius: 20px;
+ border-top-left-radius: 20px;
+ border-bottom-left-radius: 20px;
+ background-color: rgb(43, 194, 83);
+ background-image: linear-gradient(
+ center bottom,
+ rgb(43, 194, 83) 37%,
+ rgb(84, 240, 84) 69%
+ );
+ box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3),
+ inset 0 -2px 6px rgba(0, 0, 0, 0.4);
+ position: relative;
+ overflow: visible;
+ transition: width 0.5s, background-image .5s;
+}
+
+/* Fill Statuses */
+.meter.pending, .meter.skipped { background: #80807e; }
+.meter.pending .fill, .meter.skipped .fill { background: #555; }
+
+.meter.done,
+.meter.done .fill { background: #0071bc; }
+
+.meter.error { background: #f9c0c0; }
+.meter.error .fill { background: #e31c3d; }
+
+.meter.warn,
+.meter.warn .fill { background: #dc731c; }
+
+.meter.progress { background: #75afe9; }
+.meter.progress .fill { background: #308ae4; }
+
+/* Striped Overlay */
+.meter.striped > .fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background-image: linear-gradient(
+ -45deg,
+ rgba(255, 255, 255, 0.2) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.2) 50%,
+ rgba(255, 255, 255, 0.2) 75%,
+ transparent 75%,
+ transparent
+ );
+ z-index: 1;
+ background-size: 50px 50px;
+ border-top-right-radius: 25px;
+ border-bottom-right-radius: 25px;
+ border-top-left-radius: 25px;
+ border-bottom-left-radius: 25px;
+ overflow: visible;
+}
+
+/* Step label (over bar) */
+.meter .step-label {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: -3rem;
+ bottom: 0;
+ z-index: 99;
+ font-weight: bold;
+ overflow: visible;
+ padding: 3px 0;
+ background: transparent;
+ text-align: center;
+}
+
+/* Progress label (within bar) */
+.meter .fill .status-label {
+ text-align: center;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ padding-top: 2px;
+ color: white;
+}
+
+/* Animate Stripes */
+.meter.animate > .fill::after,
+.meter.animate > .fill > span {
+ animation: barberPole 2s linear infinite;
+ text-align: center;
+}
+
+/* Keyframes */
+@keyframes barberPole {
+ 0% {
+ background-position: 0 0;
+ }
+ 100% {
+ background-position: 50px 50px;
+ }
+}
diff --git a/src/filing/submission/upload/ProgressBar/StackedProgressBars.css b/src/filing/submission/upload/ProgressBar/StackedProgressBars.css
new file mode 100644
index 000000000..a8d5a5f53
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar/StackedProgressBars.css
@@ -0,0 +1,35 @@
+/* Stacked */
+.stacked-progress-bars {
+ display: flex;
+ flex-flow: row nowrap;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+}
+
+.stacked-progress-bars .meter {
+ width: 100%;
+ margin: 0;
+ margin-top: 2rem;
+}
+
+.stacked-progress-bars .meter,
+.stacked-progress-bars .fill,
+.stacked-progress-bars .meter > .fill::after {
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.stacked-progress-bars .meter:first-child,
+.stacked-progress-bars .meter:first-child .fill,
+.stacked-progress-bars .meter:first-child > .fill::after {
+ border-top-left-radius: 25px;
+ border-bottom-left-radius: 25px;
+}
+
+.stacked-progress-bars .meter:last-child,
+.stacked-progress-bars .meter:last-child .fill,
+.stacked-progress-bars .meter:last-child > .fill::after {
+ border-top-right-radius: 25px;
+ border-bottom-right-radius: 25px;
+}
\ No newline at end of file
diff --git a/src/filing/submission/upload/ProgressBar/StackedProgressBars.jsx b/src/filing/submission/upload/ProgressBar/StackedProgressBars.jsx
new file mode 100644
index 000000000..3aa5895de
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar/StackedProgressBars.jsx
@@ -0,0 +1,8 @@
+import React from 'react'
+import './StackedProgressBars.css'
+
+const StackedProgressBars = (props) => (
+ {props.children}
+)
+
+export default StackedProgressBars
diff --git a/src/filing/submission/upload/ProgressBar/constants.js b/src/filing/submission/upload/ProgressBar/constants.js
new file mode 100644
index 000000000..d803ce997
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar/constants.js
@@ -0,0 +1,18 @@
+export const STATUS = {
+ PENDING: 0,
+ PROGRESS: 1,
+ DONE: 2,
+ EDITS: 3,
+ ERROR: 4,
+ SKIP: 5,
+}
+
+export const CHECK = {
+ isPending: (status) => status === STATUS.PENDING,
+ isProgress: (status) => status === STATUS.PROGRESS,
+ isDone: (status) => status === STATUS.DONE,
+ isEdits: (status) => status === STATUS.EDITS,
+ isError: (status) => status === STATUS.ERROR,
+ isSkip: (status) => status === STATUS.SKIP,
+}
+
diff --git a/src/filing/submission/upload/ProgressBar/index.jsx b/src/filing/submission/upload/ProgressBar/index.jsx
new file mode 100644
index 000000000..7f5a83683
--- /dev/null
+++ b/src/filing/submission/upload/ProgressBar/index.jsx
@@ -0,0 +1,59 @@
+import React from 'react'
+import { STATUS, CHECK } from './constants'
+import './ProgressBar.css'
+
+const ProgressBar = (props) => {
+ const { label } = props
+
+ return (
+
+ {label}
+
+ {fillLabel(props)}
+
+
+ )
+}
+
+const containerClass = ({ status }) => {
+ let cname = 'meter'
+ if (CHECK.isPending(status)) return cname + ' pending'
+ if (CHECK.isProgress(status)) return cname + ' progress animate striped'
+ if (CHECK.isDone(status)) return cname + ' done'
+ if (CHECK.isError(status)) return cname + ' error'
+ if (CHECK.isEdits(status)) return cname + ' warn'
+ if (CHECK.isSkip(status)) return cname + ' skipped'
+ return cname
+}
+
+const fillStyles = ({ pct, minWidth, maxWidth, status, label }) => {
+ let status100 = [STATUS.PENDING, STATUS.DONE, STATUS.EDITS, STATUS.ERROR]
+ let pctAdjusted
+
+ if (status100.indexOf(status) > -1) pctAdjusted = 100
+ else pctAdjusted = pct > maxWidth ? maxWidth : pct < minWidth ? minWidth : pct
+
+ return { width: `${pctAdjusted}%` }
+}
+
+
+const fillLabel = ({ status, pct, isSV }) => {
+ if (CHECK.isProgress(status)) return `${pct}%`
+ if (CHECK.isError(status) && isSV) return 'Corrections Required'
+ if (CHECK.isError(status)) return 'Error'
+ if (CHECK.isEdits(status)) return 'Edits Found'
+ if (CHECK.isPending(status)) return 'Pending'
+ if (CHECK.isSkip(status)) return ''
+ return 'Complete'
+}
+
+ProgressBar.defaultProps = {
+ animate: false,
+ isSV: false,
+ maxWidth: 100,
+ minWidth: 0,
+ pct: 45,
+ status: null,
+}
+
+export default ProgressBar
diff --git a/src/filing/submission/upload/UploadBar.jsx b/src/filing/submission/upload/UploadBar.jsx
new file mode 100644
index 000000000..7af770264
--- /dev/null
+++ b/src/filing/submission/upload/UploadBar.jsx
@@ -0,0 +1,94 @@
+import React, { Component } from 'react'
+import ProgressBar from "./ProgressBar/"
+import { STATUS } from './ProgressBar/constants'
+
+const MIN_PCT = 10
+export class UploadBar extends Component {
+ constructor(props) {
+ super(props)
+ this.SCALING_FACTOR = 1
+ if (props.file) {
+ this.SCALING_FACTOR = props.file.size / 1e6
+ if (this.SCALING_FACTOR < 1) this.SCALING_FACTOR = 1
+ if (this.SCALING_FACTOR > 5) this.SCALING_FACTOR = 5
+ }
+ const fillWidth = this.getSavedWidth(props.filingPeriod, props.lei) || MIN_PCT
+ this.state = { fillWidth, firstRender: true }
+ this.setState = this.setState.bind(this)
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.timeout)
+ this.timeout = null
+ }
+
+ componentDidMount() {
+ if (this.state.firstRender) {
+ const fillWidth = this.getSavedWidth(this.props.filingPeriod, this.props.lei)
+ this.setState({ firstRender: false, fillWidth })
+ }
+ }
+
+ saveWidth(filingPeriod, lei, width) {
+ localStorage.setItem(`HMDA_UPLOAD_PROGRESS/${filingPeriod}/${lei}`, JSON.stringify(width))
+ }
+
+ getSavedWidth(filingPeriod, lei) {
+ let storedValue = 0
+
+ if (lei) {
+ const itemKey = `HMDA_UPLOAD_PROGRESS/${filingPeriod}/${lei}`
+ storedValue = JSON.parse(localStorage.getItem(itemKey))
+ }
+
+ return storedValue || MIN_PCT
+ }
+
+ getNextWidth() {
+ const fillWidth = this.state.fillWidth
+ this.timeout = setTimeout(
+ this.setNextWidth(fillWidth),
+ this.SCALING_FACTOR * 75 * Math.pow(2, 50 / (100 - fillWidth))
+ )
+ }
+
+ setNextWidth(currWidth) {
+ return () => {
+ this.timeout = null
+ let nextWidth = parseInt(currWidth) + 1
+ if (nextWidth > 100) nextWidth = '100'
+ if (nextWidth < MIN_PCT) nextWidth = `${MIN_PCT}`
+ this.saveWidth(this.props.filingPeriod, this.props.lei, nextWidth)
+ this.setState({ fillWidth: nextWidth })
+ }
+ }
+
+ getFillWidth() {
+ if (this.state.firstRender)
+ return this.getSavedWidth(this.props.filingPeriod, this.props.lei)
+
+ if (parseInt(this.state.fillWidth) > 100 || !this.props.uploading) {
+ this.saveWidth(this.props.filingPeriod, this.props.lei, 0)
+ return '100'
+ }
+
+ if (!this.timeout) this.getNextWidth()
+
+ return this.state.fillWidth
+ }
+
+ status() {
+ if(this.props.uploading) return STATUS.PROGRESS
+ return STATUS.DONE
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/filing/submission/upload/ValidationProgress.css b/src/filing/submission/upload/ValidationProgress.css
index d585fdbea..045f2c480 100644
--- a/src/filing/submission/upload/ValidationProgress.css
+++ b/src/filing/submission/upload/ValidationProgress.css
@@ -14,7 +14,7 @@
}
.ValidationProgress {
- padding-top: 1em;
+ padding-top: 0rem;
position: relative;
}
.ValidationProgress .usa-button {
diff --git a/src/filing/submission/upload/ValidationProgress.jsx b/src/filing/submission/upload/ValidationProgress.jsx
index 2b2ca6bfd..74829d0cf 100644
--- a/src/filing/submission/upload/ValidationProgress.jsx
+++ b/src/filing/submission/upload/ValidationProgress.jsx
@@ -8,10 +8,6 @@ import {
NO_MACRO_EDITS,
UPLOADING
} from '../../constants/statusCodes.js'
-/* TODO
-we may need to update this
-we'll have to see what a clean file upload does
-*/
import './ValidationProgress.css'
@@ -101,13 +97,6 @@ export default class ValidationProgress extends PureComponent {
if (code < UPLOADING && !uploading) return null
return (
- {/* the background bar */}
-
- {/* the progress bar */}
-
+