Add compile status indicator to new React-based errors UI.

GitOrigin-RevId: 545953e156d589a56ffd038bb7a40bba97770d06
This commit is contained in:
Paulo Reis 2020-09-29 12:08:49 +01:00 committed by Copybot
parent 9c7e9cf125
commit 876c292d22
12 changed files with 334 additions and 52 deletions

View file

@ -5,16 +5,16 @@ div.full-size.pdf(ng-controller="PdfController")
isAutoCompileOn: autocompile_enabled,
isCompiling: pdf.compiling,
isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error
isSyntaxCheckOn: stop_on_validation_error,
logEntries: pdf.logEntries ? pdf.logEntries : {}
}`
log-entries="pdf.logEntries ? pdf.logEntries.all : []"
on-recompile="recompile"
on-run-syntax-check-now="runSyntaxCheckNow"
on-set-auto-compile="setAutoCompile"
on-set-draft-mode="setDraftMode"
on-set-syntax-check="setSyntaxCheck"
on-toggle-logs="toggleLogs"
should-show-logs="shouldShowLogs"
show-logs="shouldShowLogs"
)
else
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")

View file

@ -21,5 +21,9 @@
"loading",
"no_messages",
"send_first_message",
"your_message"
"your_message",
"your_project_has_errors",
"view_warnings",
"view_logs",
"view_pdf"
]

View file

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import Icon from '../../../shared/components/icon'
function PreviewLogEntry({ file, line, message, content, raw, level }) {
const logEntryClasses = classNames('alert', {
@ -11,7 +12,7 @@ function PreviewLogEntry({ file, line, message, content, raw, level }) {
return (
<div className={logEntryClasses}>
<span className="line-no">
<i className="fa fa-link" aria-hidden="true" />
<Icon type="link" />
{file ? <span>{file}</span> : null}
{line ? <span>, {line}</span> : null}
</span>

View file

@ -0,0 +1,110 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
function PreviewLogsToggleButton({
onToggle,
showLogs,
logsState: { nErrors, nWarnings }
}) {
const toggleButtonClasses = classNames('btn', 'btn-xs', 'btn-toggle-logs', {
'btn-danger': !showLogs && nErrors,
'btn-warning': !showLogs && !nErrors && nWarnings,
'btn-default': showLogs || (!nErrors && !nWarnings)
})
function handleOnClick(e) {
e.currentTarget.blur()
onToggle()
}
return (
<button
type="button"
className={toggleButtonClasses}
onClick={handleOnClick}
>
{showLogs ? (
<ViewPdfButton />
) : (
<CompilationResultIndicator nErrors={nErrors} nWarnings={nWarnings} />
)}
</button>
)
}
function CompilationResultIndicator({ nErrors, nWarnings }) {
if (nErrors) {
return <ErrorsCompilationResultIndicator nErrors={nErrors} />
} else if (nWarnings) {
return <WarningsCompilationResultIndicator nWarnings={nWarnings} />
} else {
return <ViewLogsButton />
}
}
function ErrorsCompilationResultIndicator({ nErrors }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">
{`${t('your_project_has_errors')} (${nErrors > 9 ? '9+' : nErrors})`}
</span>
</>
)
}
function WarningsCompilationResultIndicator({ nWarnings }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">
{`${t('view_warnings')} (${nWarnings > 9 ? '9+' : nWarnings})`}
</span>
</>
)
}
function ViewLogsButton() {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">{t('view_logs')}</span>
</>
)
}
function ViewPdfButton() {
const { t } = useTranslation()
return (
<>
<Icon type="file-pdf-o" />
<span className="btn-toggle-logs-label">{t('view_pdf')}</span>
</>
)
}
PreviewLogsToggleButton.propTypes = {
onToggle: PropTypes.func.isRequired,
logsState: PropTypes.shape({
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired
}
ErrorsCompilationResultIndicator.propTypes = {
nErrors: PropTypes.number.isRequired
}
WarningsCompilationResultIndicator.propTypes = {
nWarnings: PropTypes.number.isRequired
}
export default PreviewLogsToggleButton

View file

@ -5,19 +5,33 @@ import PreviewLogsPane from './preview-logs-pane'
function PreviewPane({
compilerState,
logEntries,
onRecompile,
onRunSyntaxCheckNow,
onSetAutoCompile,
onSetDraftMode,
onSetSyntaxCheck,
onToggleLogs,
shouldShowLogs
showLogs
}) {
const nErrors =
compilerState.logEntries && compilerState.logEntries.errors
? compilerState.logEntries.errors.length
: 0
const nWarnings =
compilerState.logEntries && compilerState.logEntries.warnings
? compilerState.logEntries.warnings.length
: 0
const nLogEntries =
compilerState.logEntries && compilerState.logEntries.all
? compilerState.logEntries.all.length
: 0
return (
<>
<PreviewToolbar
compilerState={compilerState}
logsState={{ nErrors, nWarnings, nLogEntries }}
showLogs={showLogs}
onRecompile={onRecompile}
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetAutoCompile={onSetAutoCompile}
@ -25,7 +39,9 @@ function PreviewPane({
onSetSyntaxCheck={onSetSyntaxCheck}
onToggleLogs={onToggleLogs}
/>
{shouldShowLogs ? <PreviewLogsPane logEntries={logEntries} /> : null}
{showLogs ? (
<PreviewLogsPane logEntries={compilerState.logEntries.all} />
) : null}
</>
)
}
@ -35,16 +51,16 @@ PreviewPane.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired
isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}),
logEntries: PropTypes.array,
onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired,
onSetDraftMode: PropTypes.func.isRequired,
onSetSyntaxCheck: PropTypes.func.isRequired,
onToggleLogs: PropTypes.func.isRequired,
shouldShowLogs: PropTypes.bool.isRequired
showLogs: PropTypes.bool.isRequired
}
export default PreviewPane

View file

@ -1,8 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { Dropdown, MenuItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
function PreviewRecompileButton({
compilerState: {
@ -19,20 +19,6 @@ function PreviewRecompileButton({
}) {
const { t } = useTranslation()
const iconClasses = {
recompile: classNames('fa', 'fa-refresh', {
'fa-spin': isCompiling
}),
autoCompileOn: classNames('fa', 'fa-fw', { 'fa-check': isAutoCompileOn }),
autoCompileOff: classNames('fa', 'fa-fw', { 'fa-check': !isAutoCompileOn }),
compileModeNormal: classNames('fa', 'fa-fw', {
'fa-check': !isDraftModeOn
}),
compileModeDraft: classNames('fa', 'fa-fw', { 'fa-check': isDraftModeOn }),
syntaxCheckOn: classNames('fa', 'fa-fw', { 'fa-check': isSyntaxCheckOn }),
syntaxCheckOff: classNames('fa', 'fa-fw', { 'fa-check': !isSyntaxCheckOn })
}
function handleSelectAutoCompileOn() {
onSetAutoCompile(true)
}
@ -60,7 +46,7 @@ function PreviewRecompileButton({
return (
<Dropdown id="pdf-recompile-dropdown" className="btn-recompile-group">
<button className="btn btn-recompile" onClick={onRecompile}>
<i className={iconClasses.recompile} aria-hidden="true" />
<Icon type="refresh" spin={isCompiling} />
{isCompiling ? (
<span className="btn-recompile-label">
{t('compiling')}
@ -74,33 +60,33 @@ function PreviewRecompileButton({
<Dropdown.Menu>
<MenuItem header>{t('auto_compile')}</MenuItem>
<MenuItem onSelect={handleSelectAutoCompileOn}>
<i className={iconClasses.autoCompileOn} aria-hidden="true" />
<Icon type={isAutoCompileOn ? 'check' : ''} modifier="fw" />
{t('on')}
</MenuItem>
<MenuItem onSelect={handleSelectAutoCompileOff}>
<i className={iconClasses.autoCompileOff} aria-hidden="true" />
<Icon type={!isAutoCompileOn ? 'check' : ''} modifier="fw" />
{t('off')}
</MenuItem>
<MenuItem header>{t('compile_mode')}</MenuItem>
<MenuItem onSelect={handleSelectDraftModeOff}>
<i className={iconClasses.compileModeNormal} aria-hidden="true" />
<Icon type={!isDraftModeOn ? 'check' : ''} modifier="fw" />
{t('normal')}
</MenuItem>
<MenuItem onSelect={handleSelectDraftModeOn}>
<i className={iconClasses.compileModeDraft} aria-hidden="true" />
<Icon type={isDraftModeOn ? 'check' : ''} modifier="fw" />
{t('fast')} <span className="subdued">[draft]</span>
</MenuItem>
<MenuItem header>Syntax Checks</MenuItem>
<MenuItem onSelect={handleSelectSyntaxCheckOn}>
<i className={iconClasses.syntaxCheckOn} aria-hidden="true" />
<Icon type={isSyntaxCheckOn ? 'check' : ''} modifier="fw" />
{t('stop_on_validation_error')}
</MenuItem>
<MenuItem onSelect={handleSelectSyntaxCheckOff}>
<i className={iconClasses.syntaxCheckOff} aria-hidden="true" />
<Icon type={!isSyntaxCheckOn ? 'check' : ''} modifier="fw" />
{t('ignore_validation_errors')}
</MenuItem>
<MenuItem onSelect={onRunSyntaxCheckNow}>
<i className="fa fa-fw" aria-hidden="true" />
<Icon type="" modifier="fw" />
{t('run_syntax_check_now')}
</MenuItem>
</Dropdown.Menu>
@ -113,7 +99,8 @@ PreviewRecompileButton.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired
isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}),
onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired,

View file

@ -1,29 +1,38 @@
import React from 'react'
import PropTypes from 'prop-types'
import PreviewRecompileButton from './preview-recompile-button'
import PreviewLogsToggleButton from './preview-logs-toggle-button'
function PreviewToolbar({
compilerState,
logsState,
onRecompile,
onRunSyntaxCheckNow,
onSetAutoCompile,
onSetDraftMode,
onSetSyntaxCheck,
onToggleLogs
onToggleLogs,
showLogs
}) {
return (
<div className="toolbar toolbar-pdf">
<PreviewRecompileButton
compilerState={compilerState}
onRecompile={onRecompile}
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetAutoCompile={onSetAutoCompile}
onSetDraftMode={onSetDraftMode}
onSetSyntaxCheck={onSetSyntaxCheck}
/>
<button className="btn btn-sm btn-secondary" onClick={onToggleLogs}>
Toggle logs
</button>
<div className="toolbar-pdf-left">
<PreviewRecompileButton
compilerState={compilerState}
onRecompile={onRecompile}
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetAutoCompile={onSetAutoCompile}
onSetDraftMode={onSetDraftMode}
onSetSyntaxCheck={onSetSyntaxCheck}
/>
</div>
<div className="toolbar-pdf-right">
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
</div>
</div>
)
}
@ -33,8 +42,15 @@ PreviewToolbar.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired
isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}),
logsState: PropTypes.shape({
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired,
onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired,

View file

@ -25,7 +25,7 @@ export default (PdfManager = class PdfManager {
failure: false, // PDF failed to compile
compiling: false,
uncompiled: true,
logEntries: [],
logEntries: {},
logEntryAnnotations: {},
rawLog: '',
view: null, // 'pdf' 'logs'

View file

@ -271,7 +271,7 @@ App.controller('PdfController', function(
}
// if the previous run was a check, clear the error logs
if ($scope.check) {
$scope.pdf.logEntries = []
$scope.pdf.logEntries = {}
}
// keep track of whether this is a compile or check
$scope.check = !!options.check
@ -622,7 +622,7 @@ App.controller('PdfController', function(
// output the results
function handleError() {
$scope.pdf.logEntries = []
$scope.pdf.logEntries = {}
$scope.pdf.rawLog = ''
}

View file

@ -33,6 +33,28 @@
border-bottom: 0;
}
.toolbar-pdf-left,
.toolbar-pdf-right {
display: flex;
align-items: center;
align-self: stretch;
flex: 1 1 100%;
}
.toolbar-pdf-right {
flex: 1 0 0px;
}
.btn-toggle-logs {
&:focus,
&:active:focus {
outline: none;
}
}
.btn-toggle-logs-label {
padding-left: @line-height-computed / 4;
}
.pdf {
background-color: @pdf-bg;
}

View file

@ -207,7 +207,7 @@
@btn-info-border: transparent;
@btn-warning-color: #fff;
@btn-warning-bg: @ol-red;
@btn-warning-bg: @orange;
@btn-warning-border: transparent;
@btn-danger-color: #fff;

View file

@ -0,0 +1,126 @@
import React from 'react'
import { screen, render } from '@testing-library/react'
import PreviewLogsToggleButton from '../../../../../frontend/js/features/preview/components/preview-logs-toggle-button'
describe('<PreviewLogsToggleButton />', function() {
describe('basic toggle functionality', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
const onToggleLogs = () => {}
it('should render a view logs button when previewing the PDF', function() {
const showLogs = false
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View logs' })
})
it('should render a view PDF button when viewing logs', function() {
const showLogs = true
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View PDF' })
})
})
describe('compile status indicator', function() {
const showLogs = false
const onToggleLogs = () => {}
it('should render a view logs button by default', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View logs' })
})
it('should render an error status message when there are errors', function() {
const logsState = {
nErrors: 1,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `Your project has errors (${logsState.nErrors})`
})
})
it('should render an error status message when there are both errors and warnings', function() {
const logsState = {
nErrors: 1,
nWarnings: 1,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `Your project has errors (${logsState.nErrors})`
})
})
it('should render a warning status message when there are warnings but no errors', function() {
const logsState = {
nErrors: 0,
nWarnings: 1,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `View warnings (${logsState.nWarnings})`
})
})
it('should render 9+ errors when there are more than nine errors', function() {
const logsState = {
nErrors: 10,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: `Your project has errors (9+)` })
})
})
})