diff --git a/src/gmp/commands/users.js b/src/gmp/commands/users.js index d4549a997d..7aab548cd3 100644 --- a/src/gmp/commands/users.js +++ b/src/gmp/commands/users.js @@ -70,6 +70,11 @@ export const DEFAULT_FILTER_SETTINGS = { vulnerability: '17c9d269-95e7-4bfa-b1b2-bc106a2175c7', }; +const PARAM_KEYS = { + DATE: 'date_format', + TIME: 'time_format', +}; + const saveDefaultFilterSettingId = entityType => `settings_filter:${DEFAULT_FILTER_SETTINGS[entityType]}`; @@ -251,9 +256,12 @@ export class UserCommand extends EntityCommand { saveSettings(data) { log.debug('Saving settings', data); + return this.httpPost({ cmd: 'save_my_settings', text: data.timezone, + [PARAM_KEYS.DATE]: data.dateFormat, + [PARAM_KEYS.TIME]: data.timeFormat, old_password: data.oldPassword, password: data.newPassword, lang: data.userInterfaceLanguage, diff --git a/src/gmp/locale/date.js b/src/gmp/locale/date.js index 253a60b3a3..6d03596357 100644 --- a/src/gmp/locale/date.js +++ b/src/gmp/locale/date.js @@ -34,7 +34,7 @@ export const ensureDate = date => { return date; }; -export const dateFormat = (date, format, tz) => { +export const getFormattedDate = (date, format, tz) => { date = ensureDate(date); if (!isDefined(date)) { return undefined; @@ -43,14 +43,49 @@ export const dateFormat = (date, format, tz) => { if (isDefined(tz)) { date.tz(tz); } + return date.format(format); }; -export const shortDate = (date, tz) => dateFormat(date, 'L', tz); +export const dateTimeFormatOptions = { + time: {12: 'h:mm A', 24: 'H:mm'}, + date: {wmdy: 'ddd, MMM D, YYYY', wdmy: 'ddd, D MMM YYYY'}, +}; + +export const shortDate = (date, tz, userInterfaceDateFormat) => { + const formatString = dateTimeFormatOptions.date[userInterfaceDateFormat] + ? 'DD/MM/YYYY' + : 'L'; + + return getFormattedDate(date, formatString, tz); +}; + +export const longDate = ( + date, + tz, + userInterfaceTimeFormat, + userInterfaceDateFormat, +) => { + const formatString = + dateTimeFormatOptions.date[userInterfaceDateFormat] && + dateTimeFormatOptions.time[userInterfaceTimeFormat] + ? `${dateTimeFormatOptions.date[userInterfaceDateFormat]} ${dateTimeFormatOptions.time[userInterfaceTimeFormat]} z` + : 'llll'; -export const longDate = (date, tz) => dateFormat(date, 'llll', tz); + return getFormattedDate(date, formatString, tz); +}; -export const dateTimeWithTimeZone = (date, tz) => - dateFormat(date, 'llll z', tz); +export const dateTimeWithTimeZone = ( + date, + tz, + userInterfaceTimeFormat, + userInterfaceDateFormat, +) => { + const formatString = + dateTimeFormatOptions.date[userInterfaceDateFormat] && + dateTimeFormatOptions.time[userInterfaceTimeFormat] + ? `${dateTimeFormatOptions.date[userInterfaceDateFormat]} ${dateTimeFormatOptions.time[userInterfaceTimeFormat]} z` + : 'llll z'; -// vim: set ts=2 sw=2 tw=80: + return getFormattedDate(date, formatString, tz); +}; diff --git a/src/web/components/date/__tests__/datetime.jsx b/src/web/components/date/__tests__/datetime.jsx index edcf0bfaeb..55933e789b 100644 --- a/src/web/components/date/__tests__/datetime.jsx +++ b/src/web/components/date/__tests__/datetime.jsx @@ -4,7 +4,14 @@ */ /* eslint-disable no-console */ -import {describe, test, expect, testing} from '@gsa/testing'; +import { + describe, + test, + expect, + testing, + beforeAll, + afterAll, +} from '@gsa/testing'; import Date from 'gmp/models/date'; @@ -13,10 +20,35 @@ import {rendererWith} from 'web/utils/testing'; import {setTimezone} from 'web/store/usersettings/actions'; import DateTime from '../datetime'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +const getSetting = testing.fn().mockResolvedValue({}); + +const gmp = { + user: { + getSetting, + }, +}; describe('DateTime render tests', () => { + let originalSetItem; + + beforeAll(() => { + originalSetItem = localStorage.setItem; + localStorage.setItem = testing.fn(); + }); + + afterAll(() => { + localStorage.setItem = originalSetItem; + }); test('should render nothing if date is undefined', () => { - const {render} = rendererWith({store: true}); + const {render, store} = rendererWith({gmp, store: true}); + store.dispatch( + loadingActions.success({ + userinterfacetimeformat: {value: 12}, + userinterfacedateformat: {value: 'wdmy'}, + }), + ); const {element} = render(); @@ -25,10 +57,16 @@ describe('DateTime render tests', () => { test('should render nothing for invalid date', () => { // deactivate console.warn for test - const consolewarn = console.warn; + const consoleWarn = console.warn; console.warn = () => {}; - const {render} = rendererWith({store: true}); + const {render, store} = rendererWith({gmp, store: true}); + store.dispatch( + loadingActions.success({ + userinterfacetimeformat: {value: 12}, + userinterfacedateformat: {value: 'wdmy'}, + }), + ); const date = Date('foo'); @@ -38,12 +76,13 @@ describe('DateTime render tests', () => { expect(element).toBeNull(); - console.warn = consolewarn; + console.warn = consoleWarn; }); test('should call formatter', () => { const formatter = testing.fn().mockReturnValue('foo'); - const {render, store} = rendererWith({store: true}); + + const {render, store} = rendererWith({gmp, store: true}); const date = Date('2019-01-01T12:00:00Z'); @@ -51,6 +90,9 @@ describe('DateTime render tests', () => { store.dispatch(setTimezone('CET')); + localStorage.setItem('userInterfaceTimeFormat', 12); + localStorage.setItem('userInterfaceDateFormat', 'wdmy'); + const {baseElement} = render( , ); @@ -59,17 +101,49 @@ describe('DateTime render tests', () => { expect(baseElement).toHaveTextContent('foo'); }); - test('should render with default formatter', () => { - const {render, store} = rendererWith({store: true}); + test.each([ + [ + 'should render with default formatter', + { + userinterfacetimeformat: {value: undefined}, + userinterfacedateformat: {value: undefined}, + }, + 'Tue, Jan 1, 2019 1:00 PM CET', + ], + [ + 'should render with 24 h and WeekDay, Month, Day, Year formatter', + { + userinterfacetimeformat: {value: 24}, + userinterfacedateformat: {value: 'wmdy'}, + }, + 'Tue, Jan 1, 2019 13:00 CET', + ], + [ + 'should render with 12 h and WeekDay, Day, Month, Year formatter', + { + userinterfacetimeformat: {value: 12}, + userinterfacedateformat: {value: 'wdmy'}, + }, + 'Tue, 1 Jan 2019 1:00 PM CET', + ], + ])('%s', (_, settings, expectedText) => { + const {render, store} = rendererWith({gmp, store: true}); + + localStorage.setItem( + 'userInterfaceTimeFormat', + settings.userinterfacetimeformat.value, + ); + localStorage.setItem( + 'userInterfaceDateFormat', + settings.userinterfacedateformat.value, + ); const date = Date('2019-01-01T12:00:00Z'); - expect(date.isValid()).toEqual(true); store.dispatch(setTimezone('CET')); const {baseElement} = render(); - - expect(baseElement).toHaveTextContent('Tue, Jan 1, 2019 1:00 PM CET'); + expect(baseElement).toHaveTextContent(expectedText); }); }); diff --git a/src/web/hooks/__tests__/useUserSessionTimeout.jsx b/src/web/hooks/__tests__/useUserSessionTimeout.jsx index 3cd07b265c..eea92334d0 100644 --- a/src/web/hooks/__tests__/useUserSessionTimeout.jsx +++ b/src/web/hooks/__tests__/useUserSessionTimeout.jsx @@ -5,7 +5,7 @@ import {describe, test, expect} from '@gsa/testing'; -import {dateFormat} from 'gmp/locale/date'; +import {getFormattedDate} from 'gmp/locale/date'; import date from 'gmp/models/date'; import {setSessionTimeout as setSessionTimeoutAction} from 'web/store/usersettings/actions'; @@ -21,7 +21,7 @@ const TestUserSessionTimeout = () => { onClick={() => setSessionTimeout(date('2020-03-10'))} onKeyDown={() => {}} > - {dateFormat(sessionTimeout, 'DD-MM-YY')} + {getFormattedDate(sessionTimeout, 'DD-MM-YY')} ); }; diff --git a/src/web/pages/login/loginpage.jsx b/src/web/pages/login/loginpage.jsx index 74d867c631..c261029269 100644 --- a/src/web/pages/login/loginpage.jsx +++ b/src/web/pages/login/loginpage.jsx @@ -94,38 +94,46 @@ class LoginPage extends React.Component { this.login(gmp.settings.guestUsername, gmp.settings.guestPassword); } - login(username, password) { + async login(username, password) { const {gmp} = this.props; - gmp.login(username, password).then( - data => { - const {locale, timezone, sessionTimeout} = data; - - const {location, navigate} = this.props; - - this.props.setTimezone(timezone); - this.props.setLocale(locale); - this.props.setSessionTimeout(sessionTimeout); - this.props.setUsername(username); - // must be set before changing the location - this.props.setIsLoggedIn(true); - - if ( - location && - location.state && - location.state.next && - location.state.next !== location.pathname - ) { - navigate(location.state.next, {replace: true}); - } else { - navigate('/dashboards', {replace: true}); - } - }, - rej => { - log.error(rej); - this.setState({error: rej}); - }, - ); + try { + const data = await gmp.login(username, password); + + const {location, navigate} = this.props; + const {locale, timezone, sessionTimeout} = data; + + this.props.setTimezone(timezone); + this.props.setLocale(locale); + this.props.setSessionTimeout(sessionTimeout); + this.props.setUsername(username); + // must be set before changing the location + this.props.setIsLoggedIn(true); + + if (location?.state?.next && location.state.next !== location.pathname) { + navigate(location.state.next, {replace: true}); + } else { + navigate('/dashboards', {replace: true}); + } + } catch (error) { + log.error(error); + this.setState({error}); + } + + try { + const userSettings = await gmp.user.currentSettings(); + + localStorage.setItem( + 'userInterfaceTimeFormat', + userSettings.data.userinterfacetimeformat.value, + ); + localStorage.setItem( + 'userInterfaceDateFormat', + userSettings.data.userinterfacedateformat.value, + ); + } catch (error) { + log.error(error); + } } componentDidMount() { diff --git a/src/web/pages/usersettings/dialog.jsx b/src/web/pages/usersettings/dialog.jsx index 1673f37f13..a2172e1cd3 100644 --- a/src/web/pages/usersettings/dialog.jsx +++ b/src/web/pages/usersettings/dialog.jsx @@ -39,7 +39,7 @@ const FormGroupSizer = styled(Column)` const fieldsToValidate = ['rowsPerPage']; -let UserSettingsDialog = ({ +const UserSettingsDialogComponent = ({ alerts, credentials, filters, @@ -49,6 +49,8 @@ let UserSettingsDialog = ({ schedules, targets, timezone, + userInterfaceTimeFormat, + userInterfaceDateFormat, userInterfaceLanguage, rowsPerPage, maxRowsPerPage, @@ -102,6 +104,8 @@ let UserSettingsDialog = ({ }) => { const settings = { timezone, + userInterfaceTimeFormat, + userInterfaceDateFormat, oldPassword: '', newPassword: '', confPassword: '', @@ -194,6 +198,8 @@ let UserSettingsDialog = ({ { +const UserSettingsDialog = connect(rootState => { const entities = isDefined(rootState.entities) ? rootState.entities : []; return { entities, }; -})(UserSettingsDialog); +})(UserSettingsDialogComponent); export default UserSettingsDialog; diff --git a/src/web/pages/usersettings/generalpart.jsx b/src/web/pages/usersettings/generalpart.jsx index c8470ae5af..236ceb1f3f 100644 --- a/src/web/pages/usersettings/generalpart.jsx +++ b/src/web/pages/usersettings/generalpart.jsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - import React from 'react'; import styled from 'styled-components'; @@ -68,6 +67,8 @@ Notification.propTypes = { const GeneralPart = ({ timezone, + userInterfaceDateFormat, + userInterfaceTimeFormat, oldPassword, newPassword, confPassword, @@ -82,11 +83,35 @@ const GeneralPart = ({ onChange, }) => { const [_] = useTranslation(); + return ( <> + + + { - this.closeDialog(); - this.props.setLocale( - userInterfaceLanguage === BROWSER_LANGUAGE - ? undefined - : userInterfaceLanguage, + this.handleInteraction(); + await gmp.user.saveSetting( + 'd9857b7c-1159-4193-9bc0-18fae5473a69', + data.userInterfaceDateFormat, + ); + await gmp.user.saveSetting( + '11deb7ff-550b-4950-aacf-06faeb7c61b9', + data.userInterfaceTimeFormat, ); - this.props.setTimezone(timezone); - this.loadSettings(); - }); + await gmp.user.saveSettings(data).then(() => { + this.closeDialog(); + this.props.setLocale( + userInterfaceLanguage === BROWSER_LANGUAGE + ? undefined + : userInterfaceLanguage, + ); + this.props.setTimezone(timezone); + + localStorage.setItem( + 'userInterfaceTimeFormat', + data.userInterfaceTimeFormat, + ); + localStorage.setItem( + 'userInterfaceDateFormat', + data.userInterfaceDateFormat, + ); + + this.loadSettings(); + }); + } catch (error) { + console.error(error); + } } handleValueChange(value, name) { @@ -274,6 +295,8 @@ class UserSettings extends React.Component { targets, isLoading = true, timezone, + userInterfaceDateFormat = {}, + userInterfaceTimeFormat = {}, userInterfaceLanguage = {}, rowsPerPage = {}, maxRowsPerPage = {}, @@ -364,11 +387,10 @@ class UserSettings extends React.Component { nvtFilter = hasValue(nvtFilter) ? nvtFilter : {}; certBundFilter = hasValue(certBundFilter) ? certBundFilter : {}; dfnCertFilter = hasValue(dfnCertFilter) ? dfnCertFilter : {}; - const openVasScanners = scanners.filter(openVasScannersFilter); return ( - + <> ) : ( - + <> {_('Timezone')} {timezone} + + {_('Time Format')} + + {userInterfaceTimeFormat.value}h + + + + {_('Date Format')} + {userInterfaceDateFormat.value} + + {_('Password')} ******** @@ -723,7 +756,7 @@ class UserSettings extends React.Component { )} - + )} {dialogVisible && !isLoading && ( )} - + ); } } @@ -811,6 +846,7 @@ UserSettings.propTypes = { credentials: PropTypes.array, credentialsFilter: PropTypes.object, cveFilter: PropTypes.object, + userInterfaceDateFormat: PropTypes.oneOf(['wdmy', 'wmdy']), defaultAlert: PropTypes.object, defaultEsxiCredential: PropTypes.object, defaultOpenvasScanConfig: PropTypes.object, @@ -870,6 +906,7 @@ UserSettings.propTypes = { tasksFilter: PropTypes.object, ticketsFilter: PropTypes.object, timezone: PropTypes.string, + userInterfaceTimeFormat: PropTypes.oneOf([12, 24]), tlsCertificatesFilter: PropTypes.object, userInterfaceLanguage: PropTypes.object, usersFilter: PropTypes.object, @@ -879,11 +916,21 @@ UserSettings.propTypes = { const mapStateToProps = rootState => { const userDefaultsSelector = getUserSettingsDefaults(rootState); + const userDefaultFilterSelector = getUserSettingsDefaultFilter(rootState); const userInterfaceLanguage = userDefaultsSelector.getByName( 'userinterfacelanguage', ); + + const userInterfaceTimeFormat = userDefaultsSelector.getByName( + 'userinterfacetimeformat', + ); + + const userInterfaceDateFormat = userDefaultsSelector.getByName( + 'userinterfacedateformat', + ); + const rowsPerPage = userDefaultsSelector.getByName('rowsperpage'); const detailsExportFileName = userDefaultsSelector.getByName( 'detailsexportfilename', @@ -1006,6 +1053,8 @@ const mapStateToProps = rootState => { schedules: schedulesSel.getEntities(ALL_FILTER), targets: targetsSel.getEntities(ALL_FILTER), timezone: getTimezone(rootState), + userInterfaceTimeFormat, + userInterfaceDateFormat, userInterfaceLanguage, rowsPerPage, detailsExportFileName, diff --git a/src/web/store/usersettings/reducers.js b/src/web/store/usersettings/reducers.js index 311422ea19..9110a1eebd 100644 --- a/src/web/store/usersettings/reducers.js +++ b/src/web/store/usersettings/reducers.js @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - import {combineReducers} from 'web/store/utils'; import defaults from './defaults/reducers'; diff --git a/src/web/store/usersettings/selectors.js b/src/web/store/usersettings/selectors.js index 28e99689f1..215d38ef8c 100644 --- a/src/web/store/usersettings/selectors.js +++ b/src/web/store/usersettings/selectors.js @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - export const getReportComposerDefaults = rootState => { const {userSettings = {}} = rootState; const {reportComposerDefaults} = userSettings; diff --git a/src/web/utils/render.jsx b/src/web/utils/render.jsx index f5a5097c92..6abb0ec7fa 100644 --- a/src/web/utils/render.jsx +++ b/src/web/utils/render.jsx @@ -3,13 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - import {format} from 'd3-format'; import React from 'react'; import {_} from 'gmp/locale/lang'; -import {dateFormat} from 'gmp/locale/date'; +import {getFormattedDate} from 'gmp/locale/date'; import {isDefined, isFunction, isObject} from 'gmp/utils/identity'; import {isEmpty, shorten, split} from 'gmp/utils/string'; @@ -521,12 +520,12 @@ export const generateFilename = ({ mTime = currentTime; } - const percentC = dateFormat(cTime, 'YYYYMMDD'); - const percentc = dateFormat(cTime, 'HHMMSS'); - const percentD = dateFormat(currentTime, 'YYYYMMDD'); - const percentt = dateFormat(currentTime, 'HHMMSS'); - const percentM = dateFormat(mTime, 'YYYYMMDD'); - const percentm = dateFormat(mTime, 'HHMMSS'); + const percentC = getFormattedDate(cTime, 'YYYYMMDD'); + const percentc = getFormattedDate(cTime, 'HHMMSS'); + const percentD = getFormattedDate(currentTime, 'YYYYMMDD'); + const percentt = getFormattedDate(currentTime, 'HHMMSS'); + const percentM = getFormattedDate(mTime, 'YYYYMMDD'); + const percentm = getFormattedDate(mTime, 'HHMMSS'); const percentN = isDefined(resourceName) ? resourceName : resourceType; const fileNameMap = { diff --git a/src/web/utils/userSettingTimeDateFormatters.js b/src/web/utils/userSettingTimeDateFormatters.js new file mode 100644 index 0000000000..cfd2d4d457 --- /dev/null +++ b/src/web/utils/userSettingTimeDateFormatters.js @@ -0,0 +1,40 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {longDate, shortDate, dateTimeWithTimeZone} from 'gmp/locale/date'; + +export const formattedUserSettingShortDate = (date, tz) => { + const userInterfaceDateFormat = localStorage.getItem( + 'userInterfaceTimeFormat', + ); + + return shortDate(date, tz, userInterfaceDateFormat); +}; + +export const formattedUserSettingLongDate = (date, tz) => { + const userInterfaceDateFormat = localStorage.getItem( + 'userInterfaceDateFormat', + ); + + const userInterfaceTimeFormat = localStorage.getItem( + 'userInterfaceTimeFormat', + ); + return longDate(date, tz, userInterfaceTimeFormat, userInterfaceDateFormat); +}; + +export const formattedUserSettingDateTimeWithTimeZone = (date, tz) => { + const userInterfaceDateFormat = localStorage.getItem( + 'userInterfaceDateFormat', + ); + const userInterfaceTimeFormat = localStorage.getItem( + 'userInterfaceTimeFormat', + ); + return dateTimeWithTimeZone( + date, + tz, + userInterfaceTimeFormat, + userInterfaceDateFormat, + ); +};