summaryrefslogtreecommitdiff
path: root/web/src/containers
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 /web/src/containers
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
Diffstat (limited to 'web/src/containers')
-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
15 files changed, 205 insertions, 63 deletions
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}