- {name}
+
+
+ {name}
{this.props.strategyDefinition.description}
{inputFields && (
diff --git a/frontend/src/component/feature/variant/update-variant-component.jsx b/frontend/src/component/feature/variant/update-variant-component.jsx
index cc9ed390bd..dc0e290c94 100644
--- a/frontend/src/component/feature/variant/update-variant-component.jsx
+++ b/frontend/src/component/feature/variant/update-variant-component.jsx
@@ -106,7 +106,8 @@ class UpdateVariantComponent extends Component {
supports variants. You should read more about variants in the
user documentation
- .
+
+ .
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/drawer-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/drawer-test.jsx.snap
index ac7b1d6398..8d2dfeff86 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/drawer-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/drawer-test.jsx.snap
@@ -25,7 +25,7 @@ exports[`should render DrawerMenu 1`] = `
>
@@ -38,7 +38,7 @@ exports[`should render DrawerMenu 1`] = `
@@ -51,7 +51,7 @@ exports[`should render DrawerMenu 1`] = `
@@ -64,7 +64,7 @@ exports[`should render DrawerMenu 1`] = `
@@ -77,7 +77,7 @@ exports[`should render DrawerMenu 1`] = `
@@ -90,7 +90,7 @@ exports[`should render DrawerMenu 1`] = `
@@ -107,7 +107,7 @@ exports[`should render DrawerMenu 1`] = `
className="navigation"
>
@@ -118,14 +118,14 @@ exports[`should render DrawerMenu 1`] = `
User documentation
- GitHub
+ GitHub
@@ -156,7 +156,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
>
@@ -183,7 +183,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
@@ -196,7 +196,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
@@ -209,7 +209,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
@@ -222,7 +222,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
@@ -239,7 +239,7 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
className="navigation"
>
@@ -250,14 +250,14 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
User documentation
- GitHub
+ GitHub
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap
index 455e938e14..8a25ea33f1 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap
@@ -50,6 +50,12 @@ exports[`should render DrawerMenu 1`] = `
>
Sign out
+
+ GitHub
+
Sign out
+
+ GitHub
+
(
@@ -31,16 +31,16 @@ export const DrawerMenu = () => (
User documentation
- GitHub
+ GitHub
diff --git a/frontend/src/component/menu/footer.jsx b/frontend/src/component/menu/footer.jsx
index a263c2bf4a..ab8d4895be 100644
--- a/frontend/src/component/menu/footer.jsx
+++ b/frontend/src/component/menu/footer.jsx
@@ -13,6 +13,9 @@ export const FooterMenu = () => (
{item.title}
))}
+
+ GitHub
+
diff --git a/frontend/src/component/menu/header.jsx b/frontend/src/component/menu/header.jsx
new file mode 100644
index 0000000000..7685ac9b8d
--- /dev/null
+++ b/frontend/src/component/menu/header.jsx
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { Header, Navigation } from 'react-mdl';
+import { Route } from 'react-router-dom';
+import { DrawerMenu } from './drawer';
+import Breadcrum from './breadcrumb';
+import ShowUserContainer from '../user/show-user-container';
+import { fetchUIConfig } from './../../store/ui-config/actions';
+
+class HeaderComponent extends PureComponent {
+ static propTypes = {
+ uiConfig: PropTypes.object.isRequired,
+ fetchUIConfig: PropTypes.func.isRequired,
+ location: PropTypes.object.isRequired,
+ };
+
+ componentDidMount() {
+ this.props.fetchUIConfig();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.location.pathname !== nextProps.location.pathname) {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(() => {
+ const layout = document.querySelector('.mdl-js-layout');
+ const drawer = document.querySelector('.mdl-layout__drawer');
+ // hack, might get a built in alternative later
+ if (drawer.classList.contains('is-visible')) {
+ layout.MaterialLayout.toggleDrawer();
+ }
+ }, 10);
+ }
+ }
+
+ render() {
+ const { headerBackground } = this.props.uiConfig;
+ const style = headerBackground ? { background: headerBackground } : {};
+ return (
+
+ } style={style}>
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(state => ({ uiConfig: state.uiConfig.toJS() }), { fetchUIConfig })(HeaderComponent);
diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx
index 140f7af47d..e84b3d85ac 100644
--- a/frontend/src/component/user/show-user-component.jsx
+++ b/frontend/src/component/user/show-user-component.jsx
@@ -46,7 +46,8 @@ export default class ShowUserComponent extends React.Component {

-
+
+
diff --git a/frontend/src/data/config-api.js b/frontend/src/data/config-api.js
new file mode 100644
index 0000000000..2879e62361
--- /dev/null
+++ b/frontend/src/data/config-api.js
@@ -0,0 +1,13 @@
+import { throwIfNotSuccess } from './helper';
+
+const URI = 'api/admin/ui-config';
+
+function fetchConfig() {
+ return fetch(URI, { credentials: 'include' })
+ .then(throwIfNotSuccess)
+ .then(response => response.json());
+}
+
+export default {
+ fetchConfig,
+};
diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx
index 33104de9b9..c6aaa4d955 100644
--- a/frontend/src/index.jsx
+++ b/frontend/src/index.jsx
@@ -1,7 +1,8 @@
import 'whatwg-fetch';
-import 'react-mdl/extra/material.css';
import 'react-mdl/extra/material.js';
+import 'react-mdl/extra/css/material.blue_grey-pink.min.css';
+
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route } from 'react-router-dom';
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
index 278cea3ab9..7bb506be41 100644
--- a/frontend/src/store/index.js
+++ b/frontend/src/store/index.js
@@ -11,6 +11,7 @@ import settings from './settings';
import user from './user';
import api from './api';
import applications from './application';
+import uiConfig from './ui-config';
const unleashStore = combineReducers({
features,
@@ -24,6 +25,7 @@ const unleashStore = combineReducers({
settings,
user,
applications,
+ uiConfig,
api,
});
diff --git a/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap b/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap
new file mode 100644
index 0000000000..c62af75765
--- /dev/null
+++ b/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be default state 1`] = `
+Object {
+ "environment": undefined,
+ "headerBackground": undefined,
+ "slogan": "A product originally created by FINN.no.",
+}
+`;
+
+exports[`should be merged state all 1`] = `
+Object {
+ "environment": "dev",
+ "headerBackground": "red",
+ "slogan": "hello",
+}
+`;
+
+exports[`should only update headerBackground 1`] = `
+Object {
+ "environment": undefined,
+ "headerBackground": "black",
+ "slogan": "A product originally created by FINN.no.",
+}
+`;
diff --git a/frontend/src/store/ui-config/__tests__/ui-config-store.test.js b/frontend/src/store/ui-config/__tests__/ui-config-store.test.js
new file mode 100644
index 0000000000..cf765a1616
--- /dev/null
+++ b/frontend/src/store/ui-config/__tests__/ui-config-store.test.js
@@ -0,0 +1,27 @@
+import reducer from '../index';
+import { receiveConfig } from '../actions';
+
+test('should be default state', () => {
+ const state = reducer(undefined, {});
+ expect(state.toJS()).toMatchSnapshot();
+});
+
+test('should be merged state all', () => {
+ const uiConfig = {
+ headerBackground: 'red',
+ slogan: 'hello',
+ environment: 'dev',
+ };
+
+ const state = reducer(undefined, receiveConfig(uiConfig));
+ expect(state.toJS()).toMatchSnapshot();
+});
+
+test('should only update headerBackground', () => {
+ const uiConfig = {
+ headerBackground: 'black',
+ };
+
+ const state = reducer(undefined, receiveConfig(uiConfig));
+ expect(state.toJS()).toMatchSnapshot();
+});
diff --git a/frontend/src/store/ui-config/actions.js b/frontend/src/store/ui-config/actions.js
new file mode 100644
index 0000000000..a7979d6ced
--- /dev/null
+++ b/frontend/src/store/ui-config/actions.js
@@ -0,0 +1,18 @@
+import api from '../../data/config-api';
+import { dispatchAndThrow } from '../util';
+
+export const RECEIVE_CONFIG = 'RECEIVE_CONFIG';
+export const ERROR_RECEIVE_CONFIG = 'ERROR_RECEIVE_CONFIG';
+
+export const receiveConfig = json => ({
+ type: RECEIVE_CONFIG,
+ value: json,
+});
+
+export function fetchUIConfig() {
+ return dispatch =>
+ api
+ .fetchConfig()
+ .then(json => dispatch(receiveConfig(json)))
+ .catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONFIG));
+}
diff --git a/frontend/src/store/ui-config/index.js b/frontend/src/store/ui-config/index.js
new file mode 100644
index 0000000000..59937e8be9
--- /dev/null
+++ b/frontend/src/store/ui-config/index.js
@@ -0,0 +1,41 @@
+import { Map as $Map } from 'immutable';
+import { RECEIVE_CONFIG } from './actions';
+
+const localStorage = window.localStorage || {
+ setItem: () => {},
+ getItem: () => {},
+};
+
+const basePath = location ? location.pathname : '/';
+const UI_CONFIG = `${basePath}:ui_config`;
+
+const DEFAULT = new $Map({
+ headerBackground: undefined,
+ environment: undefined,
+ slogan: 'A product originally created by FINN.no.',
+});
+
+function getInitState() {
+ try {
+ const state = JSON.parse(localStorage.getItem(UI_CONFIG));
+ return state ? DEFAULT.merge(state) : DEFAULT;
+ } catch (e) {
+ return DEFAULT;
+ }
+}
+
+function updateConfig(state, config) {
+ localStorage.setItem(UI_CONFIG, JSON.stringify(config));
+ return state.merge(config);
+}
+
+const strategies = (state = getInitState(), action) => {
+ switch (action.type) {
+ case RECEIVE_CONFIG:
+ return updateConfig(state, action.value);
+ default:
+ return state;
+ }
+};
+
+export default strategies;