summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Urdin <tobias.urdin@binero.se>2023-03-15 23:36:45 +0000
committerTobias Urdin <tobias.urdin@binero.se>2023-04-21 11:23:56 +0000
commit59cd5de78baa31150958e6d0d6733407c0e95805 (patch)
treeed72bd938cc5d40a04de612e30547a78ea9c53cf
parentde9dfa2bc416d9b1bb6159ed39a014fbba267db5 (diff)
downloadzuul-59cd5de78baa31150958e6d0d6733407c0e95805.tar.gz
web: add dark mode and theme selection
This adds a theme selection in the preferences in the config modal and adds a new dark theme. Removes the line.png image and instead uses CSS linear-gradient that is available in all browsers since around 2018, also fixes the 15 pixels spacing issue that is there today. You can select between three different themes. Auto will use your system preference to choose either the light or dark theme, changes dynamically based on your system preference. Light is the current theme. Dark is the theme added by this patch series. The UX this changes is that if somebody has their system preferences set to dark, for example in Mac OS X that is in System Settings -> Appearance -> Dark the user will get the Zuul web UI in dark by default and same for the opposite. This uses a poor man's dark mode for swagger-ui as per the comment in [1]. [1] https://github.com/swagger-api/swagger-ui/issues/5327#issuecomment-742375520 Change-Id: I01cf32f3decdb885307a76eb79d644667bbbf9a3
-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 }
}