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
This commit is contained in:
parent
de9dfa2bc4
commit
59cd5de78b
9
releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml
Normal file
9
releasenotes/notes/dark-mode-e9b1cca960d4b906.yaml
Normal file
@ -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.
|
@ -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 }
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -13,6 +13,7 @@
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Fragment } from 'react'
|
||||
import ReAnsi from '@softwarefactory-project/re-ansi'
|
||||
import PropTypes from 'prop-types'
|
||||
@ -73,6 +74,7 @@ class BuildOutputLabel extends React.Component {
|
||||
class BuildOutput extends React.Component {
|
||||
static propTypes = {
|
||||
output: PropTypes.object,
|
||||
preferences: PropTypes.object,
|
||||
}
|
||||
|
||||
renderHosts (hosts) {
|
||||
@ -109,8 +111,12 @@ class BuildOutput extends React.Component {
|
||||
|
||||
renderFailedTask (host, task) {
|
||||
const max_lines = 42
|
||||
let zuulOutputClass = 'zuul-build-output'
|
||||
if (this.props.preferences.darkMode) {
|
||||
zuulOutputClass = 'zuul-build-output-dark'
|
||||
}
|
||||
return (
|
||||
<Card key={host + task.zuul_log_id} className="zuul-task-summary-failed">
|
||||
<Card key={host + task.zuul_log_id} className="zuul-task-summary-failed" style={this.props.preferences.darkMode ? {background: 'var(--pf-global--BackgroundColor--300)'} : {}}>
|
||||
<CardHeader>
|
||||
<TimesIcon style={{ color: 'var(--pf-global--danger-color--100)' }}/>
|
||||
Task <strong>{task.name}</strong>
|
||||
@ -119,25 +125,25 @@ class BuildOutput extends React.Component {
|
||||
<CardBody>
|
||||
{task.invocation && task.invocation.module_args &&
|
||||
task.invocation.module_args._raw_params && (
|
||||
<pre key="cmd" title="cmd" className={`${'cmd'}`}>
|
||||
<pre key="cmd" title="cmd" className={'cmd ' + zuulOutputClass}>
|
||||
{task.invocation.module_args._raw_params}
|
||||
</pre>
|
||||
)}
|
||||
{task.msg && (
|
||||
<pre key="msg" title="msg">{task.msg}</pre>
|
||||
<pre key="msg" title="msg" className={zuulOutputClass}>{task.msg}</pre>
|
||||
)}
|
||||
{task.exception && (
|
||||
<pre key="exc" style={{ color: 'red' }} title="exc">{task.exception}</pre>
|
||||
<pre key="exc" style={{ color: 'red' }} title="exc" className={zuulOutputClass}>{task.exception}</pre>
|
||||
)}
|
||||
{task.stdout_lines && task.stdout_lines.length > 0 && (
|
||||
<Fragment>
|
||||
{task.stdout_lines.length > max_lines && (
|
||||
<details className={`${'foldable'} ${'stdout'}`}><summary></summary>
|
||||
<pre key="stdout" title="stdout">
|
||||
<pre key="stdout" title="stdout" className={zuulOutputClass}>
|
||||
<ReAnsi log={task.stdout_lines.slice(0, -max_lines).join('\n')} />
|
||||
</pre>
|
||||
</details>)}
|
||||
<pre key="stdout" title="stdout">
|
||||
<pre key="stdout" title="stdout" className={zuulOutputClass}>
|
||||
<ReAnsi log={task.stdout_lines.slice(-max_lines).join('\n')} />
|
||||
</pre>
|
||||
</Fragment>
|
||||
@ -146,12 +152,12 @@ class BuildOutput extends React.Component {
|
||||
<Fragment>
|
||||
{task.stderr_lines.length > max_lines && (
|
||||
<details className={`${'foldable'} ${'stderr'}`}><summary></summary>
|
||||
<pre key="stderr" title="stderr">
|
||||
<pre key="stderr" title="stderr" className={zuulOutputClass}>
|
||||
<ReAnsi log={task.stderr_lines.slice(0, -max_lines).join('\n')} />
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
<pre key="stderr" title="stderr">
|
||||
<pre key="stderr" title="stderr" className={zuulOutputClass}>
|
||||
<ReAnsi log={task.stderr_lines.slice(-max_lines).join('\n')} />
|
||||
</pre>
|
||||
</Fragment>
|
||||
@ -177,4 +183,10 @@ class BuildOutput extends React.Component {
|
||||
}
|
||||
|
||||
|
||||
export default BuildOutput
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
preferences: state.preferences,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(BuildOutput)
|
||||
|
@ -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.')
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
preferences: state.preferences,
|
||||
}))(BuildsetGanttChart)
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 183 B |
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user