diff options
author | Sorin Sbarnea <ssbarnea@redhat.com> | 2020-07-06 12:21:00 +0100 |
---|---|---|
committer | Sorin Sbarnea <ssbarnea@redhat.com> | 2020-07-30 13:51:09 +0100 |
commit | 1ecbe5847455c9cd5123f1e2c7109c56bc850be1 (patch) | |
tree | 272ef13707c76809495fb781c17bcf4aab92b5e3 | |
parent | 9c623851c422f29f641a71fe90cf91c1cf1a1274 (diff) | |
download | zuul-1ecbe5847455c9cd5123f1e2c7109c56bc850be1.tar.gz |
Add user preferences dialog
In order to avoid cluttering Zuul UI with user preferences in various
places, we introduce a simple config dialog.
This moves "auto reload" option to the preferences panel.
Change-Id: I7201d9e021c99f8e5a936dc98efe3c9d563fbcbc
Co-Authored-By: James E. Blair <jeblair@redhat.com>
-rw-r--r-- | web/src/App.jsx | 2 | ||||
-rw-r--r-- | web/src/actions/preferences.js | 23 | ||||
-rw-r--r-- | web/src/containers/config/Config.jsx | 110 | ||||
-rw-r--r-- | web/src/pages/Status.jsx | 23 | ||||
-rw-r--r-- | web/src/reducers/index.js | 2 | ||||
-rw-r--r-- | web/src/reducers/preferences.js | 44 |
6 files changed, 192 insertions, 12 deletions
diff --git a/web/src/App.jsx b/web/src/App.jsx index 5ed512a07..6bfc755f4 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -66,6 +66,7 @@ import { import ErrorBoundary from './containers/ErrorBoundary' import { Fetching } from './containers/Fetching' import SelectTz from './containers/timezone/SelectTz' +import ConfigModal from './containers/config/Config' import logo from './images/logo.svg' import { clearError } from './actions/errors' import { fetchConfigErrorsAction } from './actions/configErrors' @@ -385,6 +386,7 @@ class App extends React.Component { </NotificationBadge> } <SelectTz/> + <ConfigModal/> </PageHeaderTools> ) diff --git a/web/src/actions/preferences.js b/web/src/actions/preferences.js new file mode 100644 index 000000000..03e599323 --- /dev/null +++ b/web/src/actions/preferences.js @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +export const PREFERENCE_SET = 'PREFERENCE_SET' + +export function setPreference (key, value) { + return { + type: PREFERENCE_SET, + key, + value, + } +} diff --git a/web/src/containers/config/Config.jsx b/web/src/containers/config/Config.jsx new file mode 100644 index 000000000..36de8fd5e --- /dev/null +++ b/web/src/containers/config/Config.jsx @@ -0,0 +1,110 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import { + Button, + ButtonVariant, + Modal, + ModalVariant, + Switch +} from '@patternfly/react-core' +import { CogIcon } from '@patternfly/react-icons' +import { setPreference } from '../../actions/preferences' + +class ConfigModal extends React.Component { + + static propTypes = { + location: PropTypes.object, + tenant: PropTypes.object, + preferences: PropTypes.object, + timezone: PropTypes.string, + remoteData: PropTypes.object, + dispatch: PropTypes.func + } + + constructor(props) { + super(props) + this.state = { + isModalOpen: false, + autoReload: false, + } + this.handleModalToggle = () => { + this.setState(({ isModalOpen }) => ({ + isModalOpen: !isModalOpen + })) + this.resetState() + } + + this.handleSave = () => { + this.handleModalToggle() + this.props.dispatch(setPreference('autoReload', this.state.autoReload)) + } + + this.handleAutoReload = () => { + this.setState(({ autoReload }) => ({ + autoReload: !autoReload + })) + } + } + + resetState() { + this.setState({ + autoReload: this.props.preferences.autoReload, + }) + } + + render() { + const { isModalOpen, autoReload } = this.state + return ( + <React.Fragment> + <Button + variant={ButtonVariant.plain} + key="cog" + onClick={this.handleModalToggle}> + <CogIcon /> + </Button> + <Modal + variant={ModalVariant.small} + title="Preferences" + isOpen={isModalOpen} + onClose={this.handleModalToggle} + actions={[ + <Button key="confirm" variant="primary" onClick={this.handleSave}> + Confirm + </Button>, + <Button key="cancel" variant="link" onClick={this.handleModalToggle}> + Cancel + </Button> + ]} + > + <div> + <p key="info">User configurable settings are saved in browser local storage only.</p> + <Switch + key="autoreload" + id="autoreload" + label="Auto reload status page" + isChecked={autoReload} + onChange={this.handleAutoReload} + /> + </div> + </Modal> + </React.Fragment> + ) + } +} + +export default connect(state => ({ + preferences: state.preferences, + }))(ConfigModal) diff --git a/web/src/pages/Status.jsx b/web/src/pages/Status.jsx index 7588ea162..7e33928b9 100644 --- a/web/src/pages/Status.jsx +++ b/web/src/pages/Status.jsx @@ -34,6 +34,7 @@ class StatusPage extends React.Component { static propTypes = { location: PropTypes.object, tenant: PropTypes.object, + preferences: PropTypes.object, timezone: PropTypes.string, remoteData: PropTypes.object, dispatch: PropTypes.func @@ -42,7 +43,6 @@ class StatusPage extends React.Component { state = { filter: null, expanded: false, - autoReload: true } visibilityListener = () => { @@ -80,9 +80,9 @@ class StatusPage extends React.Component { } updateData = (force) => { - if (force || (this.visible && this.state.autoReload)) { + if (force || (this.visible && this.props.preferences.autoReload)) { this.props.dispatch(fetchStatusIfNeeded(this.props.tenant)) - .then(() => {if (this.state.autoReload && this.visible) { + .then(() => {if (this.props.preferences.autoReload && this.visible) { this.timer = setTimeout(this.updateData, 5000) }}) } @@ -97,14 +97,18 @@ class StatusPage extends React.Component { document.title = 'Zuul Status' this.loadState() if (this.props.tenant.name) { - this.updateData() + this.updateData(true) } window.addEventListener('storage', this.loadState) } componentDidUpdate (prevProps) { if (this.props.tenant.name !== prevProps.tenant.name) { - this.updateData() + this.updateData(true) + } + // If the user just enabled auto-reload + if (this.props.preferences.autoReload && !this.timer) { + this.updateData(true) } } @@ -185,7 +189,7 @@ class StatusPage extends React.Component { render () { const { remoteData } = this.props - const { autoReload, filter, expanded } = this.state + const { filter, expanded } = this.state const status = remoteData.status if (this.filter && !this.filterLoaded && filter) { this.filterLoaded = true @@ -227,12 +231,6 @@ class StatusPage extends React.Component { isFetching={remoteData.isFetching} fetchCallback={this.updateData} /> - <Checkbox - defaultChecked={autoReload} - onChange={(e) => {this.setState({autoReload: e.target.checked})}} - style={{marginTop: '0px', marginLeft: '10px'}}> - auto reload - </Checkbox> </div> {status && this.renderStatusHeader(status)} @@ -253,6 +251,7 @@ class StatusPage extends React.Component { } export default connect(state => ({ + preferences: state.preferences, tenant: state.tenant, timezone: state.timezone, remoteData: state.status, diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js index 84fe8e6fd..faee01bcb 100644 --- a/web/src/reducers/index.js +++ b/web/src/reducers/index.js @@ -27,6 +27,7 @@ import nodes from './nodes' import openapi from './openapi' import project from './project' import projects from './projects' +import preferences from './preferences' import status from './status' import tenant from './tenant' import tenants from './tenants' @@ -50,6 +51,7 @@ const reducers = { tenant, tenants, timezone, + preferences, } export default combineReducers(reducers) diff --git a/web/src/reducers/preferences.js b/web/src/reducers/preferences.js new file mode 100644 index 000000000..860e91a78 --- /dev/null +++ b/web/src/reducers/preferences.js @@ -0,0 +1,44 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import update from 'immutability-helper' + +import { + PREFERENCE_SET, +} from '../actions/preferences' + + +const stored_prefs = localStorage.getItem('preferences') +let default_prefs +if (stored_prefs === null) { + default_prefs = { + autoReload: true + } +} else { + default_prefs = JSON.parse(stored_prefs) +} + +export default (state = { + ...default_prefs +}, action) => { + let newstate + switch (action.type) { + case PREFERENCE_SET: + newstate = update(state, {$merge: {[action.key]: action.value}}) + localStorage.setItem('preferences', JSON.stringify(newstate)) + return newstate + default: + return state + } +} |