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 (
-
-
+
+
+
+
+
+
+
);
}
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"