diff options
33 files changed, 443 insertions, 113 deletions
diff --git a/releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml b/releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml new file mode 100644 index 000000000..997626af2 --- /dev/null +++ b/releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new dark theme for the Zuul web interface. + - | + Added theme selection for the Zuul web interface. The default theme is set + to Auto which means your system/browsers preference determines if the Light + or Dark theme should be used. Either can be explicitly set in the settings + for the web interface by clicking the cogs in the top right. diff --git a/web/src/Misc.jsx b/web/src/Misc.jsx index dae5a84bf..1186f42d3 100644 --- a/web/src/Misc.jsx +++ b/web/src/Misc.jsx @@ -120,4 +120,30 @@ IconProperty.propTypes = { const ConditionalWrapper = ({ condition, wrapper, children }) => condition ? wrapper(children) : children -export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, ConditionalWrapper } +function resolveDarkMode(theme) { + let darkMode = false + + if (theme === 'Auto') { + let matchMedia = window.matchMedia || function () { + return { + matches: false, + } + } + + darkMode = matchMedia('(prefers-color-scheme: dark)').matches + } else if (theme === 'Dark') { + darkMode = true + } + + return darkMode +} + +function setDarkMode(darkMode) { + if (darkMode) { + document.documentElement.classList.add('pf-theme-dark') + } else { + document.documentElement.classList.remove('pf-theme-dark') + } +} + +export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, ConditionalWrapper, resolveDarkMode, setDarkMode } diff --git a/web/src/containers/autohold/HeldBuildList.jsx b/web/src/containers/autohold/HeldBuildList.jsx index d2f45dd20..0aef3a804 100644 --- a/web/src/containers/autohold/HeldBuildList.jsx +++ b/web/src/containers/autohold/HeldBuildList.jsx @@ -62,7 +62,6 @@ class HeldBuildList extends React.Component { to={`${tenant.linkPrefix}/build/${node.build}`} style={{ textDecoration: 'none', - color: 'var(--pf-global--disabled-color--100)', }} > <DataListItemRow> diff --git a/web/src/containers/build/Artifact.jsx b/web/src/containers/build/Artifact.jsx index 3793222d9..0ada93a0c 100644 --- a/web/src/containers/build/Artifact.jsx +++ b/web/src/containers/build/Artifact.jsx @@ -18,15 +18,16 @@ import { TreeView, } from 'patternfly-react' import ReactJson from 'react-json-view' - +import { connect } from 'react-redux' class Artifact extends React.Component { static propTypes = { - artifact: PropTypes.object.isRequired + artifact: PropTypes.object.isRequired, + preferences: PropTypes.object, } render() { - const { artifact } = this.props + const { artifact, preferences } = this.props return ( <table className="table table-striped table-bordered" style={{width:'50%'}}> <tbody> @@ -41,7 +42,8 @@ class Artifact extends React.Component { collapsed={true} sortKeys={true} enableClipboard={false} - displayDataTypes={false}/> + displayDataTypes={false} + theme={preferences.darkMode ? 'tomorrow' : 'rjv-default'}/> :artifact.metadata[key].toString()} </td> </tr> @@ -54,17 +56,18 @@ class Artifact extends React.Component { class ArtifactList extends React.Component { static propTypes = { - artifacts: PropTypes.array.isRequired + artifacts: PropTypes.array.isRequired, + preferences: PropTypes.object, } render() { - const { artifacts } = this.props + const { artifacts, preferences } = this.props const nodes = artifacts.map((artifact, index) => { const node = {text: <a href={artifact.url}>{artifact.name}</a>, icon: null} if (artifact.metadata) { - node['nodes']= [{text: <Artifact key={index} artifact={artifact}/>, + node['nodes']= [{text: <Artifact key={index} artifact={artifact} preferences={preferences}/>, icon: ''}] } return node @@ -83,4 +86,10 @@ class ArtifactList extends React.Component { } } -export default ArtifactList +function mapStateToProps(state) { + return { + preferences: state.preferences, + } +} + +export default connect(mapStateToProps)(ArtifactList) diff --git a/web/src/containers/build/BuildOutput.jsx b/web/src/containers/build/BuildOutput.jsx index 1098ed2c7..58f0e13b5 100644 --- a/web/src/containers/build/BuildOutput.jsx +++ b/web/src/containers/build/BuildOutput.jsx @@ -13,6 +13,7 @@ // under the License. import * as React from 'react' +import { connect } from 'react-redux' import { Fragment } from 'react' import ReAnsi from '@softwarefactory-project/re-ansi' import PropTypes from 'prop-types' @@ -73,6 +74,7 @@ class BuildOutputLabel extends React.Component { class BuildOutput extends React.Component { static propTypes = { output: PropTypes.object, + preferences: PropTypes.object, } renderHosts (hosts) { @@ -109,8 +111,12 @@ class BuildOutput extends React.Component { renderFailedTask (host, task) { const max_lines = 42 + let zuulOutputClass = 'zuul-build-output' + if (this.props.preferences.darkMode) { + zuulOutputClass = 'zuul-build-output-dark' + } return ( - <Card key={host + task.zuul_log_id} className="zuul-task-summary-failed"> + <Card key={host + task.zuul_log_id} className="zuul-task-summary-failed" style={this.props.preferences.darkMode ? {background: 'var(--pf-global--BackgroundColor--300)'} : {}}> <CardHeader> <TimesIcon style={{ color: 'var(--pf-global--danger-color--100)' }}/> Task <strong>{task.name}</strong> @@ -119,25 +125,25 @@ class BuildOutput extends React.Component { <CardBody> {task.invocation && task.invocation.module_args && task.invocation.module_args._raw_params && ( - <pre key="cmd" title="cmd" className={`${'cmd'}`}> + <pre key="cmd" title="cmd" className={'cmd ' + zuulOutputClass}> {task.invocation.module_args._raw_params} </pre> )} {task.msg && ( - <pre key="msg" title="msg">{task.msg}</pre> + <pre key="msg" title="msg" className={zuulOutputClass}>{task.msg}</pre> )} {task.exception && ( - <pre key="exc" style={{ color: 'red' }} title="exc">{task.exception}</pre> + <pre key="exc" style={{ color: 'red' }} title="exc" className={zuulOutputClass}>{task.exception}</pre> )} {task.stdout_lines && task.stdout_lines.length > 0 && ( <Fragment> {task.stdout_lines.length > max_lines && ( <details className={`${'foldable'} ${'stdout'}`}><summary></summary> - <pre key="stdout" title="stdout"> + <pre key="stdout" title="stdout" className={zuulOutputClass}> <ReAnsi log={task.stdout_lines.slice(0, -max_lines).join('\n')} /> </pre> </details>)} - <pre key="stdout" title="stdout"> + <pre key="stdout" title="stdout" className={zuulOutputClass}> <ReAnsi log={task.stdout_lines.slice(-max_lines).join('\n')} /> </pre> </Fragment> @@ -146,12 +152,12 @@ class BuildOutput extends React.Component { <Fragment> {task.stderr_lines.length > max_lines && ( <details className={`${'foldable'} ${'stderr'}`}><summary></summary> - <pre key="stderr" title="stderr"> + <pre key="stderr" title="stderr" className={zuulOutputClass}> <ReAnsi log={task.stderr_lines.slice(0, -max_lines).join('\n')} /> </pre> </details> )} - <pre key="stderr" title="stderr"> + <pre key="stderr" title="stderr" className={zuulOutputClass}> <ReAnsi log={task.stderr_lines.slice(-max_lines).join('\n')} /> </pre> </Fragment> @@ -177,4 +183,10 @@ class BuildOutput extends React.Component { } -export default BuildOutput +function mapStateToProps(state) { + return { + preferences: state.preferences, + } +} + +export default connect(mapStateToProps)(BuildOutput) diff --git a/web/src/containers/build/BuildOutput.test.jsx b/web/src/containers/build/BuildOutput.test.jsx index c76236a2e..defa342c5 100644 --- a/web/src/containers/build/BuildOutput.test.jsx +++ b/web/src/containers/build/BuildOutput.test.jsx @@ -14,6 +14,8 @@ import React from 'react' import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import configureStore from '../../store' import BuildOutput from './BuildOutput' const fakeOutput = (width, height) => ({ @@ -31,7 +33,11 @@ it('BuildOutput renders big task', () => { const div = document.createElement('div') const output = fakeOutput(512, 1024) const begin = performance.now() - ReactDOM.render(<BuildOutput output={output} />, div, () => { + const store = configureStore() + ReactDOM.render( + <Provider store={store}> + <BuildOutput output={output} /> + </Provider>, div, () => { const end = performance.now() console.log('Render took ' + (end - begin) + ' milliseconds.') }) diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx index 2ca70549d..5492b7a13 100644 --- a/web/src/containers/build/Buildset.jsx +++ b/web/src/containers/build/Buildset.jsx @@ -47,7 +47,7 @@ import { addNotification, addApiError } from '../../actions/notifications' import { ChartModal } from '../charts/ChartModal' import BuildsetGanttChart from '../charts/GanttChart' -function Buildset({ buildset, timezone, tenant, user }) { +function Buildset({ buildset, timezone, tenant, user, preferences }) { const buildset_link = buildExternalLink(buildset) const [isGanttChartModalOpen, setIsGanttChartModalOpen] = useState(false) @@ -319,7 +319,9 @@ function Buildset({ buildset, timezone, tenant, user }) { value={ <> <strong>Message:</strong> - <pre>{buildset.message}</pre> + <div className={preferences.darkMode ? 'zuul-console-dark' : ''}> + <pre>{buildset.message}</pre> + </div> </> } /> @@ -349,10 +351,12 @@ Buildset.propTypes = { tenant: PropTypes.object, timezone: PropTypes.string, user: PropTypes.object, + preferences: PropTypes.object, } export default connect((state) => ({ tenant: state.tenant, timezone: state.timezone, user: state.user, + preferences: state.preferences, }))(Buildset) diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx index 9cd10df92..826ebe3cd 100644 --- a/web/src/containers/build/Console.jsx +++ b/web/src/containers/build/Console.jsx @@ -18,6 +18,7 @@ import * as React from 'react' import ReAnsi from '@softwarefactory-project/re-ansi' import PropTypes from 'prop-types' import ReactJson from 'react-json-view' +import { connect } from 'react-redux' import { Button, @@ -60,6 +61,7 @@ class TaskOutput extends React.Component { static propTypes = { data: PropTypes.object, include: PropTypes.array, + preferences: PropTypes.object, } renderResults(value) { @@ -130,7 +132,8 @@ class TaskOutput extends React.Component { name={null} sortKeys={true} enableClipboard={false} - displayDataTypes={false}/> + displayDataTypes={false} + theme={this.props.preferences.darkMode ? 'tomorrow' : 'rjv-default'}/> </pre> ) } else { @@ -142,7 +145,7 @@ class TaskOutput extends React.Component { } return ( - <div key={key}> + <div className={this.props.preferences.darkMode ? 'zuul-console-dark' : 'zuul-console-light'} key={key}> {ret && <h5>{key}</h5>} {ret && ret} </div> @@ -170,6 +173,7 @@ class HostTask extends React.Component { errorIds: PropTypes.object, taskPath: PropTypes.array, displayPath: PropTypes.array, + preferences: PropTypes.object, } state = { @@ -290,7 +294,7 @@ class HostTask extends React.Component { </DataListCell> ) - const content = <TaskOutput data={this.props.host} include={INTERESTING_KEYS}/> + const content = <TaskOutput data={this.props.host} include={INTERESTING_KEYS} preferences={this.props.preferences}/> let item = null if (interestingKeys) { @@ -354,7 +358,7 @@ class HostTask extends React.Component { isOpen={this.state.showModal} onClose={this.close} description={modalDescription}> - <TaskOutput data={host}/> + <TaskOutput data={host} preferences={this.props.preferences}/> </Modal> </> ) @@ -367,6 +371,7 @@ class PlayBook extends React.Component { errorIds: PropTypes.object, taskPath: PropTypes.array, displayPath: PropTypes.array, + preferences: PropTypes.object, } constructor(props) { @@ -404,8 +409,8 @@ class PlayBook extends React.Component { dataListCells.push( <DataListCell key='name' width={1}> <strong> - {playbook.phase[0].toUpperCase() + playbook.phase.slice(1)} playbook< - /strong> + {playbook.phase[0].toUpperCase() + playbook.phase.slice(1)} playbook + </strong> </DataListCell>) dataListCells.push( <DataListCell key='path' width={5}> @@ -463,7 +468,8 @@ class PlayBook extends React.Component { taskPath={taskPath.concat([ idx.toString(), idx2.toString(), hostname])} displayPath={displayPath} task={task} host={host} - errorIds={errorIds}/> + errorIds={errorIds} + preferences={this.props.preferences}/> ))))} </DataList> @@ -484,6 +490,7 @@ class Console extends React.Component { errorIds: PropTypes.object, output: PropTypes.array, displayPath: PropTypes.array, + preferences: PropTypes.object, } render () { @@ -492,7 +499,7 @@ class Console extends React.Component { return ( <React.Fragment> <br /> - <span className="zuul-console"> + <span className={`zuul-console ${this.props.preferences.darkMode ? 'zuul-console-dark' : 'zuul-console-light'}`}> <DataList isCompact={true} style={{ fontSize: 'var(--pf-global--FontSize--md)' }}> { @@ -500,6 +507,7 @@ class Console extends React.Component { <PlayBook key={idx} playbook={playbook} taskPath={[idx.toString()]} displayPath={displayPath} errorIds={errorIds} + preferences={this.props.preferences} />)) } </DataList> @@ -509,5 +517,11 @@ class Console extends React.Component { } } +function mapStateToProps(state) { + return { + preferences: state.preferences, + } +} + -export default Console +export default connect(mapStateToProps)(Console) diff --git a/web/src/containers/charts/GanttChart.jsx b/web/src/containers/charts/GanttChart.jsx index 5ac065fce..f677d83b8 100644 --- a/web/src/containers/charts/GanttChart.jsx +++ b/web/src/containers/charts/GanttChart.jsx @@ -26,7 +26,7 @@ import { buildResultLegendData, buildsBarStyle } from './Misc' function BuildsetGanttChart(props) { - const { builds, timezone } = props + const { builds, timezone, preferences } = props const sortedByStartTime = builds.sort((a, b) => { if (a.start_time > b.start_time) { return -1 @@ -64,6 +64,10 @@ function BuildsetGanttChart(props) { const chartLegend = buildResultLegendData.filter((legend) => { return uniqueResults.indexOf(legend.name) > -1 }) + let horizontalLegendTextColor = '#000' + if (preferences.darkMode) { + horizontalLegendTextColor = '#ccc' + } return ( <div style={{ height: Math.max(400, 20 * builds.length) + 'px', width: '900px' }}> @@ -81,10 +85,9 @@ function BuildsetGanttChart(props) { legendOrientation='horizontal' legendPosition='top' legendData={legendData} - legendComponent={<ChartLegend data={chartLegend} itemsPerRow={4} />} - + legendComponent={<ChartLegend data={chartLegend} itemsPerRow={4} style={{labels: {fill: horizontalLegendTextColor}}} />} > - <ChartAxis /> + <ChartAxis style={{tickLabels: {fill:horizontalLegendTextColor}}} /> <ChartAxis dependentAxis showGrid @@ -103,15 +106,16 @@ function BuildsetGanttChart(props) { return moment.duration(t, 'seconds').format(format) }} fixLabelOverlap={true} - style={{ tickLabels: { angle: -25, padding: 1, verticalAnchor: 'middle', textAnchor: 'end' } }} /> + style={{ tickLabels: { angle: -25, padding: 1, verticalAnchor: 'middle', textAnchor: 'end', fill: horizontalLegendTextColor } }} + /> <ChartBar data={data} - style={buildsBarStyle} + style={ buildsBarStyle } labelComponent={ - <ChartTooltip constrainToVisibleArea />} + <ChartTooltip constrainToVisibleArea/>} labels={({ datum }) => `${datum.result}\nStarted ${datum.started}\nEnded ${datum.ended}`} /> - </ Chart> + </Chart> </div> ) @@ -120,8 +124,10 @@ function BuildsetGanttChart(props) { BuildsetGanttChart.propTypes = { builds: PropTypes.array.isRequired, timezone: PropTypes.string, + preferences: PropTypes.object, } export default connect((state) => ({ timezone: state.timezone, -}))(BuildsetGanttChart)
\ No newline at end of file + preferences: state.preferences, +}))(BuildsetGanttChart) diff --git a/web/src/containers/config/Config.jsx b/web/src/containers/config/Config.jsx index 3d402a116..652d6702f 100644 --- a/web/src/containers/config/Config.jsx +++ b/web/src/containers/config/Config.jsx @@ -18,10 +18,14 @@ import { ButtonVariant, Modal, ModalVariant, - Switch + Switch, + Select, + SelectOption, + SelectVariant } from '@patternfly/react-core' import { CogIcon } from '@patternfly/react-icons' import { setPreference } from '../../actions/preferences' +import { resolveDarkMode, setDarkMode } from '../../Misc' class ConfigModal extends React.Component { @@ -39,6 +43,8 @@ class ConfigModal extends React.Component { this.state = { isModalOpen: false, autoReload: false, + theme: 'Auto', + isThemeOpen: false, } this.handleModalToggle = () => { this.setState(({ isModalOpen }) => ({ @@ -47,9 +53,39 @@ class ConfigModal extends React.Component { this.resetState() } + this.handleEscape = () => { + if (this.state.isThemeOpen) { + this.setState(({ isThemeOpen }) => ({ + isThemeOpen: !isThemeOpen, + })) + } else { + this.handleModalToggle() + } + } + + this.handleThemeToggle = () => { + this.setState(({ isThemeOpen }) => ({ + isThemeOpen: !isThemeOpen, + })) + } + + this.handleThemeSelect = (event, selection) => { + this.setState({ + theme: selection, + isThemeOpen: false + }) + } + + this.handleTheme = () => { + let darkMode = resolveDarkMode(this.state.theme) + setDarkMode(darkMode) + } + this.handleSave = () => { this.handleModalToggle() this.props.dispatch(setPreference('autoReload', this.state.autoReload)) + this.props.dispatch(setPreference('theme', this.state.theme)) + this.handleTheme() } this.handleAutoReload = () => { @@ -62,11 +98,12 @@ class ConfigModal extends React.Component { resetState() { this.setState({ autoReload: this.props.preferences.autoReload, + theme: this.props.preferences.theme, }) } render() { - const { isModalOpen, autoReload } = this.state + const { isModalOpen, autoReload, theme, isThemeOpen } = this.state return ( <React.Fragment> <Button @@ -80,6 +117,7 @@ class ConfigModal extends React.Component { title="Preferences" isOpen={isModalOpen} onClose={this.handleModalToggle} + onEscapePress={this.handleEscape} actions={[ <Button key="confirm" variant="primary" onClick={this.handleSave}> Confirm @@ -91,6 +129,8 @@ class ConfigModal extends React.Component { > <div> <p key="info">Application settings are saved in browser local storage only. They are applied whether authenticated or not.</p> + </div> + <div> <Switch key="autoreload" id="autoreload" @@ -99,6 +139,24 @@ class ConfigModal extends React.Component { onChange={this.handleAutoReload} /> </div> + <div style={{'paddingTop': '25px'}}> + <p key="theme-info">Select your preferred theme, auto will base it on your system preference.</p> + </div> + <div> + <Select + variant={SelectVariant.single} + label="Select Input" + onToggle={this.handleThemeToggle} + onSelect={this.handleThemeSelect} + selections={theme} + isOpen={isThemeOpen} + menuAppendTo="parent" + > + <SelectOption key="auto" value="Auto"/> + <SelectOption key="light" value="Light"/> + <SelectOption key="dark" value="Dark"/> + </Select> + </div> </Modal> </React.Fragment> ) diff --git a/web/src/containers/job/JobVariant.jsx b/web/src/containers/job/JobVariant.jsx index 9621cf333..eeb6ee52e 100644 --- a/web/src/containers/job/JobVariant.jsx +++ b/web/src/containers/job/JobVariant.jsx @@ -58,7 +58,8 @@ class JobVariant extends React.Component { static propTypes = { parent: PropTypes.object, tenant: PropTypes.object, - variant: PropTypes.object.isRequired + variant: PropTypes.object.isRequired, + preferences: PropTypes.object, } renderStatus (variant) { @@ -161,7 +162,8 @@ class JobVariant extends React.Component { collapsed={true} sortKeys={true} enableClipboard={false} - displayDataTypes={false}/> + displayDataTypes={false} + theme={this.props.preferences.darkMode ? 'tomorrow' : 'rjv-default'}/> </span> ) } @@ -200,7 +202,8 @@ class JobVariant extends React.Component { collapsed={true} sortKeys={true} enableClipboard={false} - displayDataTypes={false}/> + displayDataTypes={false} + theme={this.props.preferences.darkMode ? 'tomorrow' : 'rjv-default'}/> </span> ) nice_label = (<span><CodeIcon /> Job variables</span>) @@ -287,4 +290,7 @@ class JobVariant extends React.Component { } } -export default connect(state => ({tenant: state.tenant}))(JobVariant) +export default connect(state => ({ + tenant: state.tenant, + preferences: state.preferences, +}))(JobVariant) diff --git a/web/src/containers/jobgraph/JobGraphDisplay.jsx b/web/src/containers/jobgraph/JobGraphDisplay.jsx index e5cff9cbc..c8fb938ac 100644 --- a/web/src/containers/jobgraph/JobGraphDisplay.jsx +++ b/web/src/containers/jobgraph/JobGraphDisplay.jsx @@ -21,10 +21,15 @@ import { useHistory } from 'react-router-dom' import { makeJobGraphKey, fetchJobGraphIfNeeded } from '../../actions/jobgraph' import { graphviz } from 'd3-graphviz' -function makeDot(tenant, pipeline, project, branch, jobGraph) { +function makeDot(tenant, pipeline, project, branch, jobGraph, dark) { let ret = 'digraph job_graph {\n' + ret += ' bgcolor="transparent"\n' ret += ' rankdir=LR;\n' - ret += ' node [shape=box];\n' + if (dark) { + ret += ' node [shape=box color="white" fontcolor="white"];\n' + } else { + ret += ' node [shape=box];\n' + } jobGraph.forEach((job) => { const searchParams = new URLSearchParams('') searchParams.append('pipeline', pipeline) @@ -43,8 +48,15 @@ function makeDot(tenant, pipeline, project, branch, jobGraph) { if (job.dependencies.length) { job.dependencies.forEach((dep) => { let soft = ' [dir=back]' + if (dark) { + soft = ' [dir=back color="white" fontcolor="white"]' + } if (dep.soft) { - soft = ' [style=dashed dir=back]' + if (dark) { + soft = ' [style=dashed dir=back color="white" fontcolor="white"]' + } else { + soft = ' [style=dashed dir=back]' + } } ret += ' "' + dep.name + '" -> "' + job.name + '"' + soft + ';\n' }) @@ -99,7 +111,7 @@ GraphViz.propTypes = { function JobGraphDisplay(props) { const [dot, setDot] = useState() - const {fetchJobGraphIfNeeded, tenant, project, pipeline, branch} = props + const {fetchJobGraphIfNeeded, tenant, project, pipeline, branch, preferences } = props useEffect(() => { fetchJobGraphIfNeeded(tenant, project.name, pipeline, branch) @@ -112,9 +124,9 @@ function JobGraphDisplay(props) { const jobGraph = tenantJobGraph ? tenantJobGraph[jobGraphKey] : undefined useEffect(() => { if (jobGraph) { - setDot(makeDot(tenant, pipeline, project, branch, jobGraph)) + setDot(makeDot(tenant, pipeline, project, branch, jobGraph, preferences.darkMode)) } - }, [tenant, pipeline, project, branch, jobGraph]) + }, [tenant, pipeline, project, branch, jobGraph, preferences]) return ( <> {dot && <GraphViz dot={dot}/>} @@ -131,11 +143,13 @@ JobGraphDisplay.propTypes = { jobgraph: PropTypes.object, dispatch: PropTypes.func, state: PropTypes.object, + preferences: PropTypes.object, } function mapStateToProps(state) { return { tenant: state.tenant, jobgraph: state.jobgraph, + preferences: state.preferences, state: state, } } diff --git a/web/src/containers/jobs/Jobs.jsx b/web/src/containers/jobs/Jobs.jsx index 71395f1d1..9af8210a2 100644 --- a/web/src/containers/jobs/Jobs.jsx +++ b/web/src/containers/jobs/Jobs.jsx @@ -163,6 +163,7 @@ class JobsList extends React.Component { <FormGroup controlId='jobs'> <FormControl type='text' + className="pf-c-form-control" placeholder='job name' defaultValue={filter} inputRef={i => this.filter = i} diff --git a/web/src/containers/project/ProjectVariant.jsx b/web/src/containers/project/ProjectVariant.jsx index 36d2c09ce..7ced1e1bb 100644 --- a/web/src/containers/project/ProjectVariant.jsx +++ b/web/src/containers/project/ProjectVariant.jsx @@ -59,7 +59,7 @@ function ProjectVariant(props) { return ( <div> - <table className='table table-striped table-bordered'> + <table className={`table ${props.preferences.darkMode ? 'zuul-table-dark' : 'table-striped table-bordered'}`}> <tbody> {rows.map(item => ( <tr key={item.label}> @@ -75,12 +75,14 @@ function ProjectVariant(props) { ProjectVariant.propTypes = { tenant: PropTypes.object, - variant: PropTypes.object.isRequired + variant: PropTypes.object.isRequired, + preferences: PropTypes.object, } function mapStateToProps(state) { return { tenant: state.tenant, + preferences: state.preferences, } } diff --git a/web/src/containers/status/Change.jsx b/web/src/containers/status/Change.jsx index ac0a4e6e8..b2ab50c5b 100644 --- a/web/src/containers/status/Change.jsx +++ b/web/src/containers/status/Change.jsx @@ -48,7 +48,8 @@ class Change extends React.Component { pipeline: PropTypes.object, tenant: PropTypes.object, user: PropTypes.object, - dispatch: PropTypes.func + dispatch: PropTypes.func, + preferences: PropTypes.object } state = { @@ -268,7 +269,11 @@ class Change extends React.Component { for (i = 0; i < queue._tree_columns; i++) { let className = '' if (i < change._tree.length && change._tree[i] !== null) { - className = ' zuul-change-row-line' + if (this.props.preferences.darkMode) { + className = ' zuul-change-row-line-dark' + } else { + className = ' zuul-change-row-line' + } } row.push( <td key={i} className={'zuul-change-row' + className}> @@ -313,4 +318,5 @@ class Change extends React.Component { export default connect(state => ({ tenant: state.tenant, user: state.user, + preferences: state.preferences, }))(Change) diff --git a/web/src/containers/status/ChangePanel.jsx b/web/src/containers/status/ChangePanel.jsx index dd4fc27e5..4c8c20469 100644 --- a/web/src/containers/status/ChangePanel.jsx +++ b/web/src/containers/status/ChangePanel.jsx @@ -25,7 +25,8 @@ class ChangePanel extends React.Component { static propTypes = { globalExpanded: PropTypes.bool.isRequired, change: PropTypes.object.isRequired, - tenant: PropTypes.object + tenant: PropTypes.object, + preferences: PropTypes.object } constructor () { @@ -126,7 +127,7 @@ class ChangePanel extends React.Component { const interesting_jobs = change.jobs.filter(j => this.jobStrResult(j) !== 'skipped') let jobPercent = (100 / interesting_jobs.length).toFixed(2) return ( - <div className='progress zuul-change-total-result'> + <div className={`progress zuul-change-total-result${this.props.preferences.darkMode ? ' progress-dark' : ''}`}> {change.jobs.map((job, idx) => { let result = this.jobStrResult(job) if (['queued', 'waiting', 'skipped'].includes(result)) { @@ -204,7 +205,7 @@ class ChangePanel extends React.Component { } return ( - <div className='progress zuul-job-result' + <div className={`progress zuul-job-result${this.props.preferences.darkMode ? ' progress-dark' : ''}`} title={title}> <div className={'progress-bar ' + className} role='progressbar' @@ -321,9 +322,9 @@ class ChangePanel extends React.Component { return ( <> - <ul className='list-group zuul-patchset-body'> + <ul className={`list-group ${this.props.preferences.darkMode ? 'zuul-patchset-body-dark' : 'zuul-patchset-body'}`}> {interestingJobs.map((job, idx) => ( - <li key={idx} className='list-group-item zuul-change-job'> + <li key={idx} className={`list-group-item ${this.props.preferences.darkMode ? 'zuul-change-job-dark' : 'zuul-change-job'}`}> {this.renderJob(job, times.jobs[job.name])} </li> ))} @@ -389,8 +390,8 @@ class ChangePanel extends React.Component { } const times = this.calculateTimes(change) const header = ( - <div className='panel panel-default zuul-change'> - <div className='panel-heading zuul-patchset-header' + <div className={`panel panel-default ${this.props.preferences.darkMode ? 'zuul-change-dark' : 'zuul-change'}`}> + <div className={`panel-heading ${this.props.preferences.darkMode ? 'zuul-patchset-header-dark' : 'zuul-patchset-header'}`} onClick={this.onClick}> <div className='row'> <div className='col-xs-8'> @@ -422,4 +423,7 @@ class ChangePanel extends React.Component { } } -export default connect(state => ({tenant: state.tenant}))(ChangePanel) +export default connect(state => ({ + tenant: state.tenant, + preferences: state.preferences, +}))(ChangePanel) diff --git a/web/src/containers/timezone/SelectTz.jsx b/web/src/containers/timezone/SelectTz.jsx index 576645f6c..0740aaed6 100644 --- a/web/src/containers/timezone/SelectTz.jsx +++ b/web/src/containers/timezone/SelectTz.jsx @@ -111,6 +111,7 @@ class SelectTz extends React.Component { <OutlinedClockIcon/> <Select className="zuul-select-tz" + classNamePrefix="zuul-select-tz" styles={customStyles} components={{ DropdownIndicator }} value={this.state.currentValue} diff --git a/web/src/images/line.png b/web/src/images/line.png Binary files differdeleted file mode 100644 index ace6bab3d..000000000 --- a/web/src/images/line.png +++ /dev/null diff --git a/web/src/index.css b/web/src/index.css index 587804cfa..4cedc144f 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -40,8 +40,18 @@ a.refresh { } .zuul-select-tz { - /* That's the color PF4 uses for the dropdown items in the navbar */ - color: var(--pf-global--Color--dark-100); + /* Always use black because when using dark mode the theme will default + to another dark color which is hard to see on a white background */ + color: #000; +} + +.pf-theme-dark .zuul-select-tz .zuul-select-tz__option { + background: #222; + color: #fff; +} + +.pf-theme-dark .zuul-select-tz .zuul-select-tz__option:hover { + background: #000; } /* Config error modal */ @@ -53,6 +63,15 @@ a.refresh { margin-left: var(--pf-global--spacer--md); } +.pf-theme-dark .zuul-config-errors-title, .pf-theme-dark .zuul-config-errors-count { + color: #fff !important; +} + +.pf-theme-dark .pf-c-notification-drawer pre { + background: #000; + color: #fff; +} + /* * Build Lists and Tables */ @@ -66,6 +85,10 @@ a.refresh { font-weight: bold; } +.zuul-menu-dropdown-toggle { + background: transparent !important; +} + .zuul-menu-dropdown-toggle:before { content: none !important; } @@ -167,6 +190,11 @@ a.refresh { margin-bottom: 10px; } +.zuul-change-dark { + margin-bottom: 10px; + border-color: #222; +} + .zuul-change-id { float: right; } @@ -210,6 +238,13 @@ a.refresh { padding: 2px 8px; } +.zuul-change-job-dark { + padding: 2px 8px; + background: #000; + color: #ccc; + border: 1px solid #222; +} + /* Force_break_very_long_non_hyphenated_repo_names */ .change_project { word-break: break-all; @@ -233,6 +268,21 @@ a.refresh { padding: 8px 12px; } +.zuul-patchset-header-dark { + font-size: small; + padding: 8px 12px; + background: #000 !important; + color: #ccc !important; + border-color: #222 !important; +} + +.zuul-patchset-body { +} + +.zuul-patchset-body-dark { + border-top: 1px solid #000; +} + .zuul-log-output { color: black; } @@ -283,7 +333,7 @@ a.refresh { } .zuul-build-status { - background: white; + background: transparent; font-size: 16px; } @@ -292,14 +342,23 @@ a.refresh { } .zuul-change-row-line { - background-image: url('images/line.png'); - background-repeat: 'repeat-y'; + background: linear-gradient(#000, #000) no-repeat center/2px 100%; + background-position-y: 15px; +} + +.zuul-change-row-line-dark { + background: linear-gradient(#fff, #fff) no-repeat center/2px 100%; + background-position-y: 15px; } .progress-bar-animated { animation: progress-bar-stripes 1s linear infinite; } +.progress-dark { + background: #333 !important; +} + /* Job Tree View group gap */ div.tree-view-container ul.list-group { margin: 0px 0px; @@ -325,6 +384,10 @@ pre.version { background-color: var(--pf-global--palette--red-50) !important; } +.pf-theme-dark .zuul-console-task-failed { + background-color: var(--pf-global--palette--red-300) !important; +} + .zuul-console .pf-c-data-list__expandable-content { border: none; } @@ -344,11 +407,21 @@ pre.version { border-radius: 5px; } -.zuul-console .pf-c-data-list__item:hover +.zuul-console-light .pf-c-data-list__item:hover { background: var(--pf-global--palette--blue-50); } +.zuul-console-dark .pf-c-data-list__item:hover +{ + background: var(--pf-global--BackgroundColor--200); +} + +.zuul-console-dark pre { + background: #000; + color: #fff; +} + .zuul-console .pf-c-data-list__item:hover::before { background: var(--pf-global--active-color--400); @@ -451,3 +524,42 @@ details.foldable[open] summary::before { .zuul-task-summary-failed.pf-c-card { background: var(--pf-global--palette--red-50); } + +.pf-theme-dark .pf-c-nav__link { + color: #fff !important; +} + +.pf-theme-dark .pf-c-modal-box__title-text, .pf-theme-dark .pf-c-modal-box__body { + color: #fff !important; +} + +.pf-theme-dark .swagger-ui { + filter: invert(88%) hue-rotate(180deg); +} + +.pf-theme-dark .swagger-ui .highlight-code { + filter: invert(100%) hue-rotate(180deg); +} + +.zuul-table-dark .list-group-item { + background-color: #333 !important; +} + +.zuul-build-output { +} + +.zuul-build-output-dark { + background-color: #000 !important; + color: #fff; +} + +.pf-theme-dark .zuul-log-sev-0 { + color: #ccc !important; +} +.pf-theme-dark .zuul-log-sev-1 { + color: #ccc !important; +} + +.pf-theme-dark .pf-c-empty-state { + color: #fff !important; +} diff --git a/web/src/pages/Autohold.jsx b/web/src/pages/Autohold.jsx index 0d0198b45..cc91cbcd0 100644 --- a/web/src/pages/Autohold.jsx +++ b/web/src/pages/Autohold.jsx @@ -59,6 +59,7 @@ class AutoholdPage extends React.Component { autohold: PropTypes.object, isFetching: PropTypes.bool.isRequired, fetchAutohold: PropTypes.func.isRequired, + preferences: PropTypes.object, } updateData = () => { @@ -147,7 +148,7 @@ class AutoholdPage extends React.Component { return ( <> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Title headingLevel="h2">Autohold Request {autohold.id}</Title> <Flex className="zuul-autohold-attributes"> @@ -211,7 +212,9 @@ class AutoholdPage extends React.Component { value={ <> <strong>Reason:</strong> - <pre>{autohold.reason}</pre> + <div className={this.props.preferences.darkMode ? 'zuul-console-dark' : ''}> + <pre>{autohold.reason}</pre> + </div> </> } /> @@ -221,7 +224,7 @@ class AutoholdPage extends React.Component { </Flex> </Flex> </PageSection> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Title headingLevel="h3"> <BuildIcon style={{ @@ -243,6 +246,7 @@ function mapStateToProps(state) { autohold: state.autoholds.autohold, tenant: state.tenant, isFetching: state.autoholds.isFetching, + preferences: state.preferences, } } diff --git a/web/src/pages/Build.jsx b/web/src/pages/Build.jsx index 0386f8eef..c66af1060 100644 --- a/web/src/pages/Build.jsx +++ b/web/src/pages/Build.jsx @@ -65,6 +65,7 @@ class BuildPage extends React.Component { activeTab: PropTypes.string.isRequired, location: PropTypes.object.isRequired, history: PropTypes.object.isRequired, + preferences: PropTypes.object, } state = { @@ -250,10 +251,10 @@ class BuildPage extends React.Component { return ( <> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Build build={build} active={activeTab} hash={hash} /> </PageSection> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Tabs isFilled activeKey={activeTab} @@ -314,7 +315,7 @@ class BuildPage extends React.Component { </Tabs> </PageSection> {!this.state.topOfPageVisible && ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Button onClick={scrollToTop} variant="primary" style={{position: 'fixed', bottom: 20, right: 20, zIndex: 1}}> Go to top of page <ArrowUpIcon/> </Button> @@ -362,6 +363,7 @@ function mapStateToProps(state, ownProps) { isFetchingManifest: state.build.isFetchingManifest, isFetchingOutput: state.build.isFetchingOutput, isFetchingLogfile: state.logfile.isFetching, + preferences: state.preferences, } } diff --git a/web/src/pages/Buildset.jsx b/web/src/pages/Buildset.jsx index b19aefb81..6e1cded57 100644 --- a/web/src/pages/Buildset.jsx +++ b/web/src/pages/Buildset.jsx @@ -38,6 +38,7 @@ class BuildsetPage extends React.Component { buildset: PropTypes.object, isFetching: PropTypes.bool.isRequired, fetchBuildset: PropTypes.func.isRequired, + preferences: PropTypes.object, } updateData = () => { @@ -105,10 +106,10 @@ class BuildsetPage extends React.Component { return ( <> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Buildset buildset={buildset} /> </PageSection> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Title headingLevel="h3"> <BuildIcon style={{ @@ -134,6 +135,7 @@ function mapStateToProps(state, ownProps) { buildset, tenant: state.tenant, isFetching: state.build.isFetching, + preferences: state.preferences, } } diff --git a/web/src/pages/Buildsets.jsx b/web/src/pages/Buildsets.jsx index 938309034..3ae3b772b 100644 --- a/web/src/pages/Buildsets.jsx +++ b/web/src/pages/Buildsets.jsx @@ -32,6 +32,7 @@ class BuildsetsPage extends React.Component { tenant: PropTypes.object, location: PropTypes.object, history: PropTypes.object, + preferences: PropTypes.object, } constructor(props) { @@ -230,7 +231,7 @@ class BuildsetsPage extends React.Component { const { buildsets, fetching, filters, resultsPerPage, currentPage, itemCount } = this.state return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <FilterToolbar filterCategories={this.filterCategories} onFilterChange={this.handleFilterChange} @@ -268,4 +269,7 @@ class BuildsetsPage extends React.Component { } } -export default connect((state) => ({ tenant: state.tenant }))(BuildsetsPage) +export default connect((state) => ({ + tenant: state.tenant, + preferences: state.preferences, +}))(BuildsetsPage) diff --git a/web/src/pages/ConfigErrors.jsx b/web/src/pages/ConfigErrors.jsx index b7a85d074..b43ebf562 100644 --- a/web/src/pages/ConfigErrors.jsx +++ b/web/src/pages/ConfigErrors.jsx @@ -18,7 +18,12 @@ import { connect } from 'react-redux' import { Icon } from 'patternfly-react' -import { PageSection, PageSectionVariants } from '@patternfly/react-core' +import { + PageSection, + PageSectionVariants, + List, + ListItem, +} from '@patternfly/react-core' import { fetchConfigErrorsAction } from '../actions/configErrors' @@ -26,7 +31,8 @@ class ConfigErrorsPage extends React.Component { static propTypes = { configErrors: PropTypes.object, tenant: PropTypes.object, - dispatch: PropTypes.func + dispatch: PropTypes.func, + preferences: PropTypes.object, } updateData = () => { @@ -36,7 +42,7 @@ class ConfigErrorsPage extends React.Component { render () { const { configErrors } = this.props return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <div className="pull-right"> {/* Lint warning jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line */} @@ -45,22 +51,22 @@ class ConfigErrorsPage extends React.Component { </a> </div> <div className="pull-left"> - <ul className="list-group"> + <List isPlain isBordered> {configErrors.map((item, idx) => { let ctxPath = item.source_context.path if (item.source_context.branch !== 'master') { ctxPath += ' (' + item.source_context.branch + ')' } return ( - <li className="list-group-item" key={idx}> + <ListItem key={idx}> <h3>{item.source_context.project} - {ctxPath}</h3> <p style={{whiteSpace: 'pre-wrap'}}> {item.error} </p> - </li> + </ListItem> ) })} - </ul> + </List> </div> </PageSection> ) @@ -69,5 +75,6 @@ class ConfigErrorsPage extends React.Component { export default connect(state => ({ tenant: state.tenant, - configErrors: state.configErrors.errors + configErrors: state.configErrors.errors, + preferences: state.preferences, }))(ConfigErrorsPage) diff --git a/web/src/pages/FreezeJob.jsx b/web/src/pages/FreezeJob.jsx index a78cc7b56..491a9a1ee 100644 --- a/web/src/pages/FreezeJob.jsx +++ b/web/src/pages/FreezeJob.jsx @@ -97,7 +97,8 @@ function FreezeJobPage(props) { collapsed={false} sortKeys={true} enableClipboard={false} - displayDataTypes={false}/> + displayDataTypes={false} + theme={props.preferences.darkMode ? 'tomorrow' : 'rjv-default'}/> </span> ) } @@ -133,12 +134,14 @@ FreezeJobPage.propTypes = { fetchFreezeJobIfNeeded: PropTypes.func, tenant: PropTypes.object, freezejob: PropTypes.object, + preferences: PropTypes.object, } function mapStateToProps(state) { return { tenant: state.tenant, freezejob: state.freezejob, + preferences: state.preferences, } } diff --git a/web/src/pages/Job.jsx b/web/src/pages/Job.jsx index efb4cfddc..f29ea383d 100644 --- a/web/src/pages/Job.jsx +++ b/web/src/pages/Job.jsx @@ -26,7 +26,8 @@ class JobPage extends React.Component { match: PropTypes.object.isRequired, tenant: PropTypes.object, remoteData: PropTypes.object, - dispatch: PropTypes.func + dispatch: PropTypes.func, + preferences: PropTypes.object, } updateData = (force) => { @@ -53,7 +54,7 @@ class JobPage extends React.Component { const tenantJobs = remoteData.jobs[this.props.tenant.name] const jobName = this.props.match.params.jobName return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode? PageSectionVariants.dark : PageSectionVariants.light}> {tenantJobs && tenantJobs[jobName] && <Job job={tenantJobs[jobName]} />} </PageSection> ) @@ -63,4 +64,5 @@ class JobPage extends React.Component { export default connect(state => ({ tenant: state.tenant, remoteData: state.job, + preferences: state.preferences, }))(JobPage) diff --git a/web/src/pages/Jobs.jsx b/web/src/pages/Jobs.jsx index d0b6a1466..6484b6f48 100644 --- a/web/src/pages/Jobs.jsx +++ b/web/src/pages/Jobs.jsx @@ -26,7 +26,8 @@ class JobsPage extends React.Component { static propTypes = { tenant: PropTypes.object, remoteData: PropTypes.object, - dispatch: PropTypes.func + dispatch: PropTypes.func, + preferences: PropTypes.object, } updateData = (force) => { @@ -51,8 +52,8 @@ class JobsPage extends React.Component { const jobs = remoteData.jobs[this.props.tenant.name] return ( - <PageSection variant={PageSectionVariants.light}> - <PageSection style={{paddingRight: '5px'}}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> + <PageSection variant={PageSectionVariants.light} style={{paddingRight: '5px'}}> <Fetchable isFetching={remoteData.isFetching} fetchCallback={this.updateData} @@ -70,4 +71,5 @@ class JobsPage extends React.Component { export default connect(state => ({ tenant: state.tenant, remoteData: state.jobs, + preferences: state.preferences, }))(JobsPage) diff --git a/web/src/pages/OpenApi.jsx b/web/src/pages/OpenApi.jsx index 7ccf5f34c..ec8ef8c56 100644 --- a/web/src/pages/OpenApi.jsx +++ b/web/src/pages/OpenApi.jsx @@ -26,7 +26,8 @@ class OpenApiPage extends React.Component { static propTypes = { tenant: PropTypes.object, remoteData: PropTypes.object, - dispatch: PropTypes.func + dispatch: PropTypes.func, + preferences: PropTypes.object, } updateData = (force) => { @@ -51,7 +52,7 @@ class OpenApiPage extends React.Component { render() { return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <div id="swaggerContainer" /> </PageSection> ) @@ -61,4 +62,5 @@ class OpenApiPage extends React.Component { export default connect(state => ({ tenant: state.tenant, remoteData: state.openapi, + preferences: state.preferences, }))(OpenApiPage) diff --git a/web/src/pages/Project.jsx b/web/src/pages/Project.jsx index 06e8612c7..0c808ec09 100644 --- a/web/src/pages/Project.jsx +++ b/web/src/pages/Project.jsx @@ -46,7 +46,7 @@ function ProjectPage(props) { return ( <> - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <TextContent> <Text component="h2">Project {projectName}</Text> <Fetchable @@ -70,12 +70,14 @@ ProjectPage.propTypes = { tenant: PropTypes.object, remoteData: PropTypes.object, fetchProjectIfNeeded: PropTypes.func, + preferences: PropTypes.object, } function mapStateToProps(state) { return { tenant: state.tenant, remoteData: state.project, + preferences: state.preferences, } } diff --git a/web/src/pages/Semaphore.jsx b/web/src/pages/Semaphore.jsx index a0ae8ddca..2cfdfb73f 100644 --- a/web/src/pages/Semaphore.jsx +++ b/web/src/pages/Semaphore.jsx @@ -25,7 +25,7 @@ import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { fetchSemaphoresIfNeeded } from '../actions/semaphores' import Semaphore from '../containers/semaphore/Semaphore' -function SemaphorePage({ match, semaphores, tenant, fetchSemaphoresIfNeeded, isFetching }) { +function SemaphorePage({ match, semaphores, tenant, fetchSemaphoresIfNeeded, isFetching, preferences }) { const semaphoreName = match.params.semaphoreName @@ -38,7 +38,7 @@ function SemaphorePage({ match, semaphores, tenant, fetchSemaphoresIfNeeded, isF e => e.name === semaphoreName) : undefined return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Title headingLevel="h2"> Details for Semaphore <span style={{color: 'var(--pf-global--primary-color--100)'}}>{semaphoreName}</span> </Title> @@ -55,6 +55,7 @@ SemaphorePage.propTypes = { tenant: PropTypes.object.isRequired, isFetching: PropTypes.bool.isRequired, fetchSemaphoresIfNeeded: PropTypes.func.isRequired, + preferences: PropTypes.object, } const mapDispatchToProps = { fetchSemaphoresIfNeeded } @@ -63,6 +64,7 @@ function mapStateToProps(state) { tenant: state.tenant, semaphores: state.semaphores.semaphores, isFetching: state.semaphores.isFetching, + preferences: state.preferences, } } diff --git a/web/src/pages/Status.jsx b/web/src/pages/Status.jsx index ac3dc7840..e61ceb3e2 100644 --- a/web/src/pages/Status.jsx +++ b/web/src/pages/Status.jsx @@ -197,6 +197,7 @@ class StatusPage extends React.Component { <FormGroup controlId='status'> <FormControl type='text' + className="pf-c-form-control" placeholder='change or project name' defaultValue={filter} inputRef={i => this.filter = i} @@ -222,7 +223,7 @@ class StatusPage extends React.Component { </Form> ) return ( - <PageSection variant={PageSectionVariants.light}> + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <div style={{display: 'flex', float: 'right'}}> <Fetchable isFetching={remoteData.isFetching} diff --git a/web/src/pages/Stream.jsx b/web/src/pages/Stream.jsx index df2a2ad96..f058d29a2 100644 --- a/web/src/pages/Stream.jsx +++ b/web/src/pages/Stream.jsx @@ -31,7 +31,8 @@ class StreamPage extends React.Component { static propTypes = { match: PropTypes.object.isRequired, location: PropTypes.object.isRequired, - tenant: PropTypes.object + tenant: PropTypes.object, + preferences: PropTypes.object, } state = { @@ -167,10 +168,11 @@ class StreamPage extends React.Component { render () { return ( - <PageSection variant={PageSectionVariants.light} > + <PageSection variant={this.props.preferences.darkMode ? PageSectionVariants.dark : PageSectionVariants.light}> <Form inline> <FormGroup controlId='stream'> <FormControl + className="pf-c-form-control" type='text' placeholder='search' onKeyPress={this.handleKeyPress} @@ -201,4 +203,7 @@ class StreamPage extends React.Component { } -export default connect(state => ({tenant: state.tenant}))(StreamPage) +export default connect(state => ({ + tenant: state.tenant, + preferences: state.preferences, +}))(StreamPage) diff --git a/web/src/reducers/preferences.js b/web/src/reducers/preferences.js index 1fba8eacf..0a2755257 100644 --- a/web/src/reducers/preferences.js +++ b/web/src/reducers/preferences.js @@ -15,13 +15,14 @@ import { PREFERENCE_SET, } from '../actions/preferences' - +import { resolveDarkMode, setDarkMode } from '../Misc' const stored_prefs = localStorage.getItem('preferences') let default_prefs if (stored_prefs === null) { default_prefs = { - autoReload: true + autoReload: true, + theme: 'Auto' } } else { default_prefs = JSON.parse(stored_prefs) @@ -30,13 +31,15 @@ if (stored_prefs === null) { export default (state = { ...default_prefs }, action) => { - let newstate - switch (action.type) { - case PREFERENCE_SET: - newstate = { ...state, [action.key]: action.value } - localStorage.setItem('preferences', JSON.stringify(newstate)) - return newstate - default: - return state + if (action.type === PREFERENCE_SET) { + let newstate = { ...state, [action.key]: action.value } + delete newstate.darkMode + localStorage.setItem('preferences', JSON.stringify(newstate)) + let darkMode = resolveDarkMode(newstate.theme) + setDarkMode(darkMode) + return { ...newstate, darkMode: darkMode } } + let darkMode = resolveDarkMode(state.theme) + setDarkMode(darkMode) + return { ...state, darkMode: darkMode } } |