summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-05-03 00:30:33 +0000
committerGerrit Code Review <review@openstack.org>2023-05-03 00:30:33 +0000
commit51194abf561de04972996199d825613a94cd3b2f (patch)
treead759bc813af0bb716135251da1e54135748c276
parentbbdbe81790f4926e5e00085309589a2c52e5230b (diff)
parent59cd5de78baa31150958e6d0d6733407c0e95805 (diff)
downloadzuul-51194abf561de04972996199d825613a94cd3b2f.tar.gz
Merge "web: add dark mode and theme selection"
-rw-r--r--releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml9
-rw-r--r--web/src/Misc.jsx28
-rw-r--r--web/src/containers/autohold/HeldBuildList.jsx1
-rw-r--r--web/src/containers/build/Artifact.jsx25
-rw-r--r--web/src/containers/build/BuildOutput.jsx30
-rw-r--r--web/src/containers/build/BuildOutput.test.jsx8
-rw-r--r--web/src/containers/build/Buildset.jsx8
-rw-r--r--web/src/containers/build/Console.jsx32
-rw-r--r--web/src/containers/charts/GanttChart.jsx24
-rw-r--r--web/src/containers/config/Config.jsx62
-rw-r--r--web/src/containers/job/JobVariant.jsx14
-rw-r--r--web/src/containers/jobgraph/JobGraphDisplay.jsx26
-rw-r--r--web/src/containers/jobs/Jobs.jsx1
-rw-r--r--web/src/containers/project/ProjectVariant.jsx6
-rw-r--r--web/src/containers/status/Change.jsx10
-rw-r--r--web/src/containers/status/ChangePanel.jsx20
-rw-r--r--web/src/containers/timezone/SelectTz.jsx1
-rw-r--r--web/src/images/line.pngbin183 -> 0 bytes
-rw-r--r--web/src/index.css124
-rw-r--r--web/src/pages/Autohold.jsx10
-rw-r--r--web/src/pages/Build.jsx8
-rw-r--r--web/src/pages/Buildset.jsx6
-rw-r--r--web/src/pages/Buildsets.jsx8
-rw-r--r--web/src/pages/ConfigErrors.jsx23
-rw-r--r--web/src/pages/FreezeJob.jsx5
-rw-r--r--web/src/pages/Job.jsx6
-rw-r--r--web/src/pages/Jobs.jsx8
-rw-r--r--web/src/pages/OpenApi.jsx6
-rw-r--r--web/src/pages/Project.jsx4
-rw-r--r--web/src/pages/Semaphore.jsx6
-rw-r--r--web/src/pages/Status.jsx3
-rw-r--r--web/src/pages/Stream.jsx11
-rw-r--r--web/src/reducers/preferences.js23
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)' }}/>
&nbsp;Task&nbsp;<strong>{task.name}</strong>&nbsp;
@@ -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
deleted file mode 100644
index ace6bab3d..000000000
--- a/web/src/images/line.png
+++ /dev/null
Binary files differ
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 }
}