[web][config] move timezone component to preferences

- move timezone in modal
 - move state in preference
 - use patterfly component
 - update all pages

Change-Id: I60bc1665c0660b86489e375c6720ad18ed940c53
This commit is contained in:
Andy Ladjadj 2020-10-03 14:57:55 +02:00
parent da9df89591
commit 3c237009e1
12 changed files with 171 additions and 207 deletions

View File

@ -61,7 +61,6 @@ import {
import AuthContainer from './containers/auth/Auth'
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 { clearNotification } from './actions/notifications'
@ -75,8 +74,8 @@ class App extends React.Component {
notifications: PropTypes.array,
configErrors: PropTypes.array,
info: PropTypes.object,
preferences: PropTypes.object,
tenant: PropTypes.object,
timezone: PropTypes.string,
location: PropTypes.object,
history: PropTypes.object,
dispatch: PropTypes.func,
@ -256,7 +255,7 @@ class App extends React.Component {
type={notification.type}
onDismiss={() => { this.props.dispatch(clearNotification(notification.id)) }}
>
<span title={moment.utc(notification.date).tz(this.props.timezone).format()}>
<span title={moment.utc(notification.date).tz(this.props.preferences.timezone).format()}>
{notificationBody}
</span>
</TimedToastNotification>
@ -432,7 +431,6 @@ class App extends React.Component {
<BellIcon />
</NotificationBadge>
}
<SelectTz />
<ConfigModal />
{tenant.name && (<AuthContainer />)}
@ -471,8 +469,8 @@ export default withRouter(connect(
notifications: state.notifications,
configErrors: state.configErrors,
info: state.info,
preferences: state.preferences,
tenant: state.tenant,
timezone: state.timezone,
user: state.user
})
)(App))

View File

@ -1,20 +0,0 @@
// 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 TIMEZONE_SET = 'TIMEZONE_SET'
export function setTimezoneAction (name) {
return {
type: TIMEZONE_SET,
timezone: name
}
}

View File

@ -41,7 +41,7 @@ import { buildExternalLink, ExternalLink, IconProperty } from '../../Misc'
import AutoholdModal from '../autohold/autoholdModal'
function Build({ build, tenant, timezone, user }) {
function Build({ build, preferences, tenant, user }) {
const [showAutoholdModal, setShowAutoholdModal] = useState(false)
const change = build.change ? build.change : ''
const ref = build.change ? '' : build.ref
@ -173,13 +173,13 @@ function Build({ build, tenant, timezone, user }) {
<strong>Started at </strong>
{moment
.utc(build.start_time)
.tz(timezone)
.tz(preferences.timezone)
.format('YYYY-MM-DD HH:mm:ss')}
<br />
<strong>Completed at </strong>
{moment
.utc(build.end_time)
.tz(timezone)
.tz(preferences.timezone)
.format('YYYY-MM-DD HH:mm:ss')}
</span>
}
@ -284,14 +284,13 @@ function Build({ build, tenant, timezone, user }) {
Build.propTypes = {
build: PropTypes.object,
tenant: PropTypes.object,
hash: PropTypes.array,
timezone: PropTypes.string,
preferences: PropTypes.object,
teannt: PropTypes.string,
user: PropTypes.object,
}
export default connect((state) => ({
preferences: state.preferences,
tenant: state.tenant,
timezone: state.timezone,
user: state.user,
}))(Build)

View File

@ -53,8 +53,8 @@ function BuildTable({
builds,
fetching,
onClearFilters,
preferences,
tenant,
timezone,
history,
}) {
const columns = [
@ -143,7 +143,7 @@ function BuildTable({
{
title: moment
.utc(build.start_time)
.tz(timezone)
.tz(preferences.timezone)
.format('YYYY-MM-DD HH:mm:ss'),
},
{
@ -245,12 +245,12 @@ BuildTable.propTypes = {
builds: PropTypes.array.isRequired,
fetching: PropTypes.bool.isRequired,
onClearFilters: PropTypes.func.isRequired,
preferences: PropTypes.string.isRequired,
tenant: PropTypes.object.isRequired,
timezone: PropTypes.string.isRequired,
history: PropTypes.object.isRequired,
}
export default connect((state) => ({
preferences: state.preferences,
tenant: state.tenant,
timezone: state.timezone,
}))(BuildTable)

View File

@ -16,20 +16,23 @@ import { connect } from 'react-redux'
import {
Button,
ButtonVariant,
Form,
FormGroup,
Modal,
ModalVariant,
Switch
} from '@patternfly/react-core'
import Timezone from './Timezone'
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
}
@ -39,7 +42,9 @@ class ConfigModal extends React.Component {
this.state = {
isModalOpen: false,
autoReload: false,
timezone: null
}
this.handleModalToggle = () => {
this.setState(({ isModalOpen }) => ({
isModalOpen: !isModalOpen
@ -50,6 +55,7 @@ class ConfigModal extends React.Component {
this.handleSave = () => {
this.handleModalToggle()
this.props.dispatch(setPreference('autoReload', this.state.autoReload))
this.props.dispatch(setPreference('timezone', this.state.timezone))
}
this.handleAutoReload = () => {
@ -57,16 +63,21 @@ class ConfigModal extends React.Component {
autoReload: !autoReload
}))
}
this.handleTimezone = (timezone) => {
this.setState({ timezone })
}
}
resetState() {
this.setState({
autoReload: this.props.preferences.autoReload,
timezone: this.props.preferences.timezone
})
}
render() {
const { isModalOpen, autoReload } = this.state
const { isModalOpen, autoReload, timezone } = this.state
return (
<React.Fragment>
<Button
@ -91,13 +102,22 @@ 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>
<Switch
key="autoreload"
id="autoreload"
label="Auto reload status page"
isChecked={autoReload}
onChange={this.handleAutoReload}
/>
<Form>
<FormGroup>
<Switch
key="autoreload"
id="autoreload"
label="Auto reload status page"
isChecked={autoReload}
onChange={this.handleAutoReload}
/>
</FormGroup>
<FormGroup label="Select result display timezone">
<Timezone
selected={timezone}
onSelect={this.handleTimezone} />
</FormGroup>
</Form>
</div>
</Modal>
</React.Fragment>

View File

@ -0,0 +1,117 @@
// 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 moment from 'moment-timezone'
import { connect } from 'react-redux'
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'
class Timezone extends React.Component {
static propTypes = {
selected: PropTypes.string,
onSelect: PropTypes.func
}
constructor(props) {
super(props)
const defaultSelected = this.props.selected
this.state = {
options: [
{ value: "UTC", description: "This is the default timezone." },
...moment.tz.names().map(item => ({ value: item }))],
isOpen: false,
hasOnCreateOption: false,
selected: null,
}
this.onToggle = isOpen => {
this.setState({
isOpen
})
}
this.onSelect = (event, selection, isPlaceholder) => {
if (isPlaceholder) this.clearSelection()
else {
this.setState({
selected: selection,
isOpen: false
})
this.props.onSelect(selection)
}
}
this.clearSelection = () => {
this.setState({
isOpen: false,
selected: null,
})
this.props.onSelect(defaultSelected)
}
this.getSelectOptions = options => {
return options.map((option, index) => (
<SelectOption
key={index}
value={option.value}
{...(option.description && { description: option.description })}
/>
))
}
this.customFilter = (_, value) => {
let input
try {
input = new RegExp(value, 'i')
} catch (err) {
input = ''
}
return this.getSelectOptions(input !== ''
? this.state.options.filter(item => input.test(item.value))
: this.state.options)
}
}
render() {
const { isOpen, options, selected } = this.state
const titleId = 'select-timezone-typeahead'
const label = 'Timezones'
return (
<div>
<Select
variant={SelectVariant.typeahead}
typeAheadAriaLabel={label}
aria-labelledby={titleId}
isOpen={isOpen}
menuAppendTo='parent'
onClear={this.clearSelection}
onFilter={this.customFilter}
onSelect={this.onSelect}
onToggle={this.onToggle}
placeholderText={this.props.selected}
selections={selected}
maxHeight="200px"
>
{this.getSelectOptions(options)}
</Select>
</div>
)
}
}
export default connect()(Timezone)

View File

@ -1,122 +0,0 @@
// 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 Select from 'react-select'
import moment from 'moment-timezone'
import { OutlinedClockIcon } from '@patternfly/react-icons'
import { connect } from 'react-redux'
import { setTimezoneAction } from '../../actions/timezone'
class SelectTz extends React.Component {
static propTypes = {
dispatch: PropTypes.func
}
state = {
availableTz: moment.tz.names().map(item => ({value: item, label: item})),
defaultValue: {value: 'UTC', label: 'UTC'}
}
componentDidMount () {
this.loadState()
window.addEventListener('storage', this.loadState)
}
handleChange = (selectedTz) => {
const tz = selectedTz.value
localStorage.setItem('zuul_tz_string', tz)
this.updateState(tz)
}
loadState = () => {
let tz = localStorage.getItem('zuul_tz_string') || ''
if (tz) {
this.updateState(tz)
}
}
updateState = (tz) => {
this.setState({
currentValue: {value: tz, label: tz}
})
let timezoneAction = setTimezoneAction(tz)
this.props.dispatch(timezoneAction)
}
render() {
const textColor = '#d1d1d1'
const containerStyles= {
border: 'solid #2b2b2b',
borderWidth: '0 0 0 1px',
cursor: 'pointer',
display: 'initial',
padding: '6px'
}
const customStyles = {
container: () => ({
display: 'inline-block',
}),
control: () => ({
width: 'auto',
display: 'flex'
}),
singleValue: () => ({
color: textColor,
}),
input: (provided) => ({
...provided,
color: textColor
}),
dropdownIndicator:(provided) => ({
...provided,
padding: '3px'
}),
indicatorSeparator: () => {},
menu: (provided) => ({
...provided,
width: 'auto',
right: '0',
top: '22px',
})
}
return (
<div style={containerStyles}>
<OutlinedClockIcon/>
<Select
className="zuul-select-tz"
styles={customStyles}
value={this.state.currentValue}
onChange={this.handleChange}
options={this.state.availableTz}
noOptionsMessage={() => 'No api found'}
placeholder={'Select Tz'}
defaultValue={this.state.defaultValue}
theme={(theme) => ({
...theme,
borderRadius: 0,
spacing: {
...theme.spacing,
baseUnit: 2,
},
})}
/>
</div>
)
}
}
export default connect()(SelectTz)

View File

@ -29,10 +29,10 @@ import BuildTable from '../containers/build/BuildTable'
class BuildsPage extends React.Component {
static propTypes = {
tenant: PropTypes.object,
timezone: PropTypes.string,
location: PropTypes.object,
history: PropTypes.object,
location: PropTypes.object,
preferences: PropTypes.object,
tenant: PropTypes.object,
}
constructor(props) {
@ -163,7 +163,7 @@ class BuildsPage extends React.Component {
const { filters } = this.state
if (
this.props.tenant.name !== prevProps.tenant.name ||
this.props.timezone !== prevProps.timezone
this.props.preferences.timezone !== prevProps.preferences.timezone
) {
this.updateData(filters)
}
@ -215,5 +215,5 @@ class BuildsPage extends React.Component {
export default connect((state) => ({
tenant: state.tenant,
timezone: state.timezone,
preferences: state.preferences,
}))(BuildsPage)

View File

@ -36,7 +36,6 @@ class StatusPage extends React.Component {
location: PropTypes.object,
tenant: PropTypes.object,
preferences: PropTypes.object,
timezone: PropTypes.string,
remoteData: PropTypes.object,
dispatch: PropTypes.func
}
@ -182,7 +181,7 @@ class StatusPage extends React.Component {
<p>Zuul version: <span>{status.zuul_version}</span></p>
{status.last_reconfigured ? (
<p>Last reconfigured: <span>
{moment.utc(status.last_reconfigured).tz(this.props.timezone).format('llll')}
{moment.utc(status.last_reconfigured).tz(this.props.preferences.timezone).format('llll')}
</span></p>) : ''}
</React.Fragment>
)
@ -250,6 +249,5 @@ class StatusPage extends React.Component {
export default connect(state => ({
preferences: state.preferences,
tenant: state.tenant,
timezone: state.timezone,
remoteData: state.status,
}))(StatusPage)

View File

@ -34,7 +34,6 @@ import preferences from './preferences'
import status from './status'
import tenant from './tenant'
import tenants from './tenants'
import timezone from './timezone'
import user from './user'
const reducers = {
@ -57,7 +56,6 @@ const reducers = {
status,
tenant,
tenants,
timezone,
preferences,
user,
}

View File

@ -16,15 +16,13 @@ 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)
let default_prefs = {
autoReload: true,
timezone: 'UTC'
}
if (stored_prefs !== null) {
Object.assign(default_prefs, JSON.parse(stored_prefs))
}
export default (state = {

View File

@ -1,22 +0,0 @@
// 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 { TIMEZONE_SET } from '../actions/timezone'
export default (state = 'UTC', action) => {
switch (action.type) {
case TIMEZONE_SET:
return action.timezone
default:
return state
}
}