diff --git a/frontend/.gitignore b/frontend/.gitignore index 75d2b429f7..4f302a196a 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -29,6 +29,7 @@ build/Release # Dependency directories node_modules jspm_packages +package-lock.json # Optional npm cache directory .npm @@ -40,3 +41,8 @@ typings/ # Built dist + +# IDE +.idea/ + +.DS_Store diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 7d4cdbc99f..25ed7a8e48 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). The latest version of this document is always available in [releases][releases-url]. +## [3.0.0-alpha.8] +- feat(timestamps): Make formatting of timestamps configurable. +- fix(package): Update react-mdl to version 1.11.0 +- fix(package): update normalize.css to version 8.0.0 + ## [3.0.0-alpha.7] - Move metrics poller to seperate class - Bugfix: CreatedAt set when creating new toggle diff --git a/frontend/package.json b/frontend/package.json index a05706b20d..f363955d66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "unleash-frontend", "description": "unleash your features", - "version": "3.0.0-alpha.7", + "version": "3.0.0-alpha.8", "keywords": [ "unleash", "feature toggle", @@ -32,6 +32,7 @@ "start": "NODE_ENV=development webpack-dev-server --progress --colors", "start:heroku": "UNLEASH_API=http://unleash.herokuapp.com npm run start", "lint": "eslint . --ext js,jsx", + "lint:fix": "eslint . --ext js,jsx --fix", "test": "jest", "test:ci": "npm run lint && npm run build && npm run test", "prepublish": "npm run build" @@ -40,13 +41,13 @@ "dependencies": { "debug": "^3.1.0", "immutable": "^3.8.1", - "normalize.css": "^7.0.0", + "normalize.css": "^8.0.0", "prop-types": "^15.5.10", "react": "^15.6.1", "react-dnd": "^2.1.4", "react-dnd-html5-backend": "^2.1.2", "react-dom": "^15.6.1", - "react-mdl": "^1.9.0", + "react-mdl": "^1.11.0", "react-modal": "^1.6.4", "react-redux": "^4.4.5", "react-router": "^3.0.0", diff --git a/frontend/public/en-GB.png b/frontend/public/en-GB.png new file mode 100644 index 0000000000..1276ee49e2 Binary files /dev/null and b/frontend/public/en-GB.png differ diff --git a/frontend/public/nb-NO.png b/frontend/public/nb-NO.png new file mode 100644 index 0000000000..f8c8d2ea3a Binary files /dev/null and b/frontend/public/nb-NO.png differ diff --git a/frontend/public/unknown-locale.png b/frontend/public/unknown-locale.png new file mode 100644 index 0000000000..00b618ad5a Binary files /dev/null and b/frontend/public/unknown-locale.png differ diff --git a/frontend/public/unknown-user.png b/frontend/public/unknown-user.png new file mode 100644 index 0000000000..47214107be Binary files /dev/null and b/frontend/public/unknown-user.png differ diff --git a/frontend/public/us-US.png b/frontend/public/us-US.png new file mode 100644 index 0000000000..143ebb0f30 Binary files /dev/null and b/frontend/public/us-US.png differ diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js index 6806a27e39..2f8e0cfaa1 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -21,7 +21,7 @@ import { Switch, } from 'react-mdl'; import { IconLink, shorten, styles as commonStyles } from '../common'; -import { formatFullDateTime } from '../common/util'; +import { formatFullDateTimeWithLocale } from '../common/util'; class StatefulTextfield extends Component { static propTypes = { @@ -59,6 +59,7 @@ class ClientApplications extends PureComponent { fetchApplication: PropTypes.func.isRequired, appName: PropTypes.string, application: PropTypes.object, + location: PropTypes.object, storeApplicationMetaData: PropTypes.func.isRequired, }; @@ -70,7 +71,9 @@ class ClientApplications extends PureComponent { componentDidMount() { this.props.fetchApplication(this.props.appName); } - + formatFullDateTime(v) { + return formatFullDateTimeWithLocale(v, this.props.location.locale); + } render() { if (!this.props.application) { return ; @@ -142,7 +145,8 @@ class ClientApplications extends PureComponent { icon="timeline" subtitle={ - {clientIp} last seen at {formatFullDateTime(lastSeen)} + {clientIp} last seen at{' '} + {this.formatFullDateTime(lastSeen)} } > diff --git a/frontend/src/component/application/application-edit-container.js b/frontend/src/component/application/application-edit-container.js index b3b2d66989..82d9ebde37 100644 --- a/frontend/src/component/application/application-edit-container.js +++ b/frontend/src/component/application/application-edit-container.js @@ -4,11 +4,13 @@ import { fetchApplication, storeApplicationMetaData } from '../../store/applicat const mapStateToProps = (state, props) => { let application = state.applications.getIn(['apps', props.appName]); + const location = state.settings.toJS().location || {}; if (application) { application = application.toJS(); } return { application, + location, }; }; diff --git a/frontend/src/component/common/__tests__/util-test.jsx b/frontend/src/component/common/__tests__/util-test.jsx index 83ad202f4f..cb86e4fdf8 100644 --- a/frontend/src/component/common/__tests__/util-test.jsx +++ b/frontend/src/component/common/__tests__/util-test.jsx @@ -1,7 +1,17 @@ -import { formatFullDateTime } from '../util'; +import { formatFullDateTimeWithLocale } from '../util'; test('formats dates correctly', () => { - expect(formatFullDateTime(1487861809466)).toEqual('2017-02-23 14:56:49'); - expect(formatFullDateTime(1487232809466)).toEqual('2017-02-16 08:13:29'); - expect(formatFullDateTime(1477232809466)).toEqual('2016-10-23 14:26:49'); + expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'UTC')).toEqual('2017-02-23 14:56:49'); + expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Paris')).toEqual('2017-02-23 15:56:49'); + expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Oslo')).toEqual('2017-02-23 15:56:49'); + expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/London')).toEqual('2017-02-23 14:56:49'); + expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Paris')).toEqual('02/23/2017, 3:56:49 PM'); + expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Oslo')).toEqual('02/23/2017, 3:56:49 PM'); + expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/London')).toEqual('02/23/2017, 2:56:49 PM'); + + expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO')).toEqual( + expect.stringMatching(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) + ); + expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB')).toEqual(expect.stringContaining('02/23/2017')); + expect(formatFullDateTimeWithLocale(1487861809466, 'en-US')).toEqual(expect.stringContaining('02/23/2017')); }); diff --git a/frontend/src/component/common/util.js b/frontend/src/component/common/util.js index dd404fff2f..0439f99dfb 100644 --- a/frontend/src/component/common/util.js +++ b/frontend/src/component/common/util.js @@ -6,5 +6,9 @@ const dateTimeOptions = { minute: '2-digit', second: '2-digit', }; - -export const formatFullDateTime = v => new Date(v).toLocaleString('nb-NO', dateTimeOptions); +export const formatFullDateTimeWithLocale = (v, locale, tz) => { + if (tz) { + dateTimeOptions.timeZone = tz; + } + return new Date(v).toLocaleString(locale, dateTimeOptions); +}; diff --git a/frontend/src/component/feature/metric-component.jsx b/frontend/src/component/feature/metric-component.jsx index 5bfae8ff4b..33bd4fa947 100644 --- a/frontend/src/component/feature/metric-component.jsx +++ b/frontend/src/component/feature/metric-component.jsx @@ -4,7 +4,7 @@ import { Grid, Cell, Icon, Chip, ChipContact } from 'react-mdl'; import Progress from './progress'; import { Link } from 'react-router'; import { AppsLinkList, calc } from '../common'; -import { formatFullDateTime } from '../common/util'; +import { formatFullDateTimeWithLocale } from '../common/util'; import styles from './metrics.scss'; const StrategyChipItem = ({ strategy }) => ( @@ -38,13 +38,16 @@ export default class MetricComponent extends React.Component { featureToggle: PropTypes.object.isRequired, fetchSeenApps: PropTypes.func.isRequired, fetchFeatureMetrics: PropTypes.func.isRequired, + location: PropTypes.object, }; componentWillMount() { this.props.fetchSeenApps(); this.props.fetchFeatureMetrics(); } - + formatFullDateTime(v) { + return formatFullDateTimeWithLocale(v, this.props.location.locale); + } render() { const { metrics = {}, featureToggle } = this.props; const { @@ -107,7 +110,7 @@ export default class MetricComponent extends React.Component { )} - Created {formatFullDateTime(featureToggle.createdAt)} + Created {this.formatFullDateTime(featureToggle.createdAt)}
diff --git a/frontend/src/component/feature/metric-container.jsx b/frontend/src/component/feature/metric-container.jsx index 6058ae41c3..544b038893 100644 --- a/frontend/src/component/feature/metric-container.jsx +++ b/frontend/src/component/feature/metric-container.jsx @@ -23,6 +23,7 @@ function getMetricsForToggle(state, toggleName) { export default connect( (state, props) => ({ metrics: getMetricsForToggle(state, props.featureToggle.name), + location: state.settings.toJS().location || {}, }), { fetchFeatureMetrics, diff --git a/frontend/src/component/history/history-list-component.jsx b/frontend/src/component/history/history-list-component.jsx index 1b01155fdd..20d4539cfd 100644 --- a/frontend/src/component/history/history-list-component.jsx +++ b/frontend/src/component/history/history-list-component.jsx @@ -4,7 +4,7 @@ import HistoryItemDiff from './history-item-diff'; import HistoryItemJson from './history-item-json'; import { Table, TableHeader } from 'react-mdl'; import { DataTableHeader, SwitchWithLabel, styles as commonStyles } from '../common'; -import { formatFullDateTime } from '../common/util'; +import { formatFullDateTimeWithLocale } from '../common/util'; import styles from './history.scss'; @@ -13,13 +13,16 @@ class HistoryList extends Component { title: PropTypes.string, history: PropTypes.array, settings: PropTypes.object, + location: PropTypes.object, updateSetting: PropTypes.func.isRequired, }; toggleShowDiff() { this.props.updateSetting('showData', !this.props.settings.showData); } - + formatFulldateTime(v) { + return formatFullDateTimeWithLocale(v, this.props.location.locale); + } render() { const showData = this.props.settings.showData; const { history } = this.props; @@ -62,7 +65,12 @@ class HistoryList extends Component { User Diff - + Time diff --git a/frontend/src/component/history/history-list-container.jsx b/frontend/src/component/history/history-list-container.jsx index dafc7057a2..5cbfb3d388 100644 --- a/frontend/src/component/history/history-list-container.jsx +++ b/frontend/src/component/history/history-list-container.jsx @@ -4,9 +4,10 @@ import { updateSettingForGroup } from '../../store/settings/actions'; const mapStateToProps = state => { const settings = state.settings.toJS().history || {}; - + const location = state.settings.toJS().location || {}; return { settings, + location, }; }; diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx index 815df9282d..4375f6bb5c 100644 --- a/frontend/src/component/user/show-user-component.jsx +++ b/frontend/src/component/user/show-user-component.jsx @@ -5,19 +5,51 @@ import styles from './user.scss'; export default class ShowUserComponent extends React.Component { static propTypes = { profile: PropTypes.object, + location: PropTypes.object, fetchUser: PropTypes.func.isRequired, + updateSettingLocation: PropTypes.func.isRequired, }; + possibleLocales = [ + { value: 'nb-NO', image: 'nb-NO' }, + { value: 'us-US', image: 'us-US' }, + { value: 'en-GB', image: 'en-GB' }, + ]; componentDidMount() { this.props.fetchUser(); + // find default locale and add it in choices if not present + let locale = navigator.language; + let found = this.possibleLocales.find(l => l.value === locale); + if (!found) { + this.possibleLocales.push({ value: locale, image: 'unknown-locale' }); + } + } + + updateLocale() { + const locale = this.props.location + ? this.props.location.locale + : this.possibleLocales[this.possibleLocales.length - 1]; + let index = this.possibleLocales.findIndex(v => v.value === locale); + index = (index + 1) % this.possibleLocales.length; + this.props.updateSettingLocation('locale', this.possibleLocales[index].value); } render() { const email = this.props.profile ? this.props.profile.email : ''; - const imageUrl = email ? this.props.profile.imageUrl : ''; + const locale = this.props.location + ? this.props.location.locale + : this.possibleLocales[this.possibleLocales.length - 1].value; + let foundLocale = this.possibleLocales.find(l => l.value === locale); + const imageUrl = email ? this.props.profile.imageUrl : 'public/unknown-user.png'; + const imageLocale = foundLocale ? `public/${foundLocale.image}.png` : `public/unknown-locale.png`; return ( -
- {email} +
+
+ {locale} +
  +
+ {email} +
); } diff --git a/frontend/src/component/user/show-user-container.jsx b/frontend/src/component/user/show-user-container.jsx index db4b061518..acb836e5ec 100644 --- a/frontend/src/component/user/show-user-container.jsx +++ b/frontend/src/component/user/show-user-container.jsx @@ -1,13 +1,16 @@ import { connect } from 'react-redux'; import ShowUserComponent from './show-user-component'; import { fetchUser } from '../../store/user/actions'; +import { updateSettingForGroup } from '../../store/settings/actions'; const mapDispatchToProps = { fetchUser, + updateSettingLocation: updateSettingForGroup('location'), }; const mapStateToProps = state => ({ profile: state.user.get('profile'), + location: state.settings ? state.settings.toJS().location : {}, }); export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent); diff --git a/frontend/src/component/user/user.scss b/frontend/src/component/user/user.scss index d8a430fc46..522ea55d8d 100644 --- a/frontend/src/component/user/user.scss +++ b/frontend/src/component/user/user.scss @@ -2,4 +2,15 @@ border-radius: 25px; height: 32px; border: 2px solid #ffffff; -} \ No newline at end of file +} + +.showLocale img { + border-radius: 2px; + height: 30px; + margin: 0 10px; +} + +.showUserSettings { + display: flex; + align-items: center; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index dca98e040e..0fa73a2fa8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4271,9 +4271,9 @@ normalize-url@^1.4.0: query-string "^4.1.0" sort-keys "^1.0.0" -normalize.css@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-7.0.0.tgz#abfb1dd82470674e0322b53ceb1aaf412938e4bf" +normalize.css@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.0.tgz#14ac5e461612538a4ce9be90a7da23f86e718493" npm-run-path@^2.0.0: version "2.0.2" @@ -5035,9 +5035,9 @@ react-dom@^15.6.1: object-assign "^4.1.0" prop-types "^15.5.10" -react-mdl@^1.9.0: - version "1.10.3" - resolved "https://registry.yarnpkg.com/react-mdl/-/react-mdl-1.10.3.tgz#f783e26a5eea4154a32129ab2562c09d5eeacf0d" +react-mdl@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-mdl/-/react-mdl-1.11.0.tgz#7e07ee1009dd9b358b616dc400ff2ae1845a2e67" dependencies: clamp "^1.0.1" classnames "^2.2.3"