Skip to content

Commit

Permalink
feat: Add option to change timezone (#84)
Browse files Browse the repository at this point in the history
Fixes #79
  • Loading branch information
vivekratnavel committed Apr 3, 2019
1 parent a4f73d8 commit 95488e0
Show file tree
Hide file tree
Showing 22 changed files with 3,299 additions and 110 deletions.
Binary file added docs/assets/screenshots/settings-menu.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/settings-timezone.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ If there is any issue with the Table's column display or if the newly added
config parameters don't show up, then try resetting the browser cache for Omniboard.

To reset cache:
- Click on Cog/Settings icon found on the top right corner
- Click the `Cog` icon on the top right corner
- Click on "Reset Cache"

![Reset Cache](https://raw.githubusercontent.com/vivekratnavel/omniboard/master/docs/assets/screenshots/reset-cache.png)
15 changes: 15 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ and then click on `Source Files` or `Artifacts` from the side menu.

You can then choose to either `Download All` files or `Download` each file individually.

## Change Timezone

All the timestamps are stored in UTC timezone in database and Omniboard
will try to guess the user timezone and set it as default timezone for
`start_time`, `stop_time` and `heartbeat` columns.

To change the default timezone:
- Click the `Cog` icon on the top right corner and select `Settings` from the menu

![Settings Menu](https://raw.githubusercontent.com/vivekratnavel/omniboard/master/docs/assets/screenshots/settings-menu.png)

- Select the desired timezone and save the settings

![Settings Timezone](https://raw.githubusercontent.com/vivekratnavel/omniboard/master/docs/assets/screenshots/settings-timezone.png)

## Delete an experiment run

To delete an unwanted experiment run, hover over its `Id` column and click on the delete icon as shown below.
Expand Down
2 changes: 2 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import RunsModel from './models/runs';
import MetricsModel from './models/metrics';
import OmniboardColumnsModel from './models/omniboard.columns';
import OmniboardConfigColumnsModel from './models/omniboard.config.columns';
import OmniboardSettingsModel from './models/omniboard.settings';
import FilesModel from './models/fs.files';
import ChunksModel from './models/fs.chunks';
import archiver from 'archiver';
Expand Down Expand Up @@ -43,6 +44,7 @@ restify.serve(router, RunsModel);
restify.serve(router, MetricsModel);
restify.serve(router, OmniboardColumnsModel);
restify.serve(router, OmniboardConfigColumnsModel);
restify.serve(router, OmniboardSettingsModel);
restify.serve(router, FilesModel);
restify.serve(router, ChunksModel);
app.use(router);
Expand Down
14 changes: 14 additions & 0 deletions server/models/omniboard.settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoose from 'mongoose';
import {databaseConn} from '../config/database';

const Schema = mongoose.Schema;
mongoose.Promise = Promise;

export const OmniboardSettingsSchema = new Schema({
name: {type: String},
value: {}
}, {
strict: false
});

export default databaseConn.model('omniboard.settings', OmniboardSettingsSchema);
10 changes: 10 additions & 0 deletions web/__mocks__/reactn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// ./__mocks__/reactn.js

import React from 'reactn';

const Component = React.Component;
const PureComponent = React.PureComponent;

export default React;

export {Component, PureComponent};
16 changes: 9 additions & 7 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"ify-loader": "^1.1.0",
"jest": "^24.1.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"ms": "^2.1.1",
"object-assign": "4.1.1",
"plotly.js": "^1.33.1",
Expand All @@ -52,13 +53,13 @@
"raf": "3.4.0",
"rc-slider": "^8.6.4",
"rc-util": "^4.6.0",
"react": "^16.2.0",
"react": "^16.8.6",
"react-accessible-accordion": "^2.4.4",
"react-bootstrap": "^0.31.5",
"react-bootstrap-multiselect": "^2.4.1",
"react-bootstrap-xeditable": "^0.2.10",
"react-dev-utils": "^4.2.1",
"react-dom": "^16.2.0",
"react-dom": "16.8.6",
"react-json-view": "^1.16.0",
"react-list": "^0.8.11",
"react-localstorage": "^0.3.1",
Expand All @@ -71,6 +72,7 @@
"react-syntax-highlighter": "^9.0.0",
"react-table": "^6.7.6",
"react-toastify": "^4.2.0",
"reactn": "^1.0.0",
"style-loader": "0.19.0",
"url-loader": "0.6.2",
"webpack": "3.8.1",
Expand Down Expand Up @@ -108,7 +110,7 @@
"<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}",
"<rootDir>/src/**/?(*.)(spec|test).{js,jsx,mjs}"
],
"testEnvironment": "node",
"testEnvironment": "jsdom",
"testURL": "http://localhost",
"transform": {
"^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest",
Expand Down Expand Up @@ -147,11 +149,11 @@
},
"proxy": "http://localhost:9000/",
"devDependencies": {
"@types/enzyme": "^3.1.13",
"@types/enzyme": "^3.9.1",
"@types/jest": "^23.3.1",
"@types/react": "^16.4.10",
"enzyme": "^3.4.4",
"enzyme-adapter-react-16": "^1.2.0",
"@types/react": "^16.8.10",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.11.2",
"enzyme-to-json": "^3.3.4",
"jest-canvas-mock": "^2.0.0-alpha.3",
"jest-fetch-mock": "^2.1.1",
Expand Down
19 changes: 19 additions & 0 deletions web/src/components/App/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ exports[`App component should render 1`] = `
>
+/- Config Columns
</MenuItem>
<MenuItem
bsClass="dropdown"
disabled={false}
divider={false}
eventKey={1.3}
header={false}
onClick={[Function]}
test-attr="settings-button"
>
<Glyphicon
bsClass="glyphicon"
glyph="wrench"
/>
  Settings
</MenuItem>
</NavDropdown>
</Nav>
</NavbarCollapse>
Expand Down Expand Up @@ -126,5 +141,9 @@ exports[`App component should render 1`] = `
}
/>
</div>
<SettingsModal
handleClose={[Function]}
show={false}
/>
</div>
`;
81 changes: 74 additions & 7 deletions web/src/components/App/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component } from 'reactn';
import { Navbar, Nav, MenuItem, NavDropdown, Glyphicon, NavItem } from 'react-bootstrap';
import RunsTable from '../RunsTable/runsTable';
import axios from 'axios';
Expand All @@ -7,13 +7,19 @@ import { parseServerError } from "../Helpers/utils";
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
import './style.scss';
import { SettingsModal } from "../SettingsModal/settingsModal";
import moment from 'moment-timezone';

export const SERVER_TIMEZONE = 'Atlantic/Reykjavik';
export const SETTING_TIMEZONE = 'timezone';

class App extends Component {
constructor(props) {
super(props);
this.state = {
showConfigColumnModal: false,
dbName: ''
dbName: '',
showSettingsModal: false
}
}

Expand All @@ -34,20 +40,76 @@ class App extends Component {
});
};

componentDidMount() {
axios.get('/api/v1/database').then(dbResponse => {
_showSettingsModal = () => {
this.setState({
showSettingsModal: true
});
};

_handleSettingsModalClose = () => {
this.setState({
showSettingsModal: false
});
};

/**
* settingsResponse is of type
* [
* {
* "_id": "5ca1ce94a686ac25c4a78eea",
* "name": "timezone",
* "value": "America/Los_Angeles"
* }
* ]
* @param settingsResponse
* @private
*/
_updateGlobalSettings = (settingsResponse) => {
const settings = settingsResponse.reduce( (acc, current) => {
return Object.assign({}, acc, {[current.name]: current});
}, this.global.settings);

this.setGlobal({
settings
});
};

_fetchData = () => {
axios.all([
axios.get('/api/v1/database'),
axios.get('/api/v1/Omniboard.Settings')
]).then(axios.spread((dbResponse, settingsResponse) => {
if (dbResponse && dbResponse.data && dbResponse.data.name) {
this.setState({
dbName: dbResponse.data.name
})
});
}
if (settingsResponse && settingsResponse.data && settingsResponse.data.length) {
this._updateGlobalSettings(settingsResponse.data);
} else {
// Write default settings to the database for the first time
// Guess the client timezone and set it as default
const userTimezone = moment.tz.guess();
axios.post('/api/v1/Omniboard.Settings', {
name: SETTING_TIMEZONE,
value: userTimezone
}).then(response => {
if (response.status === 201) {
this._updateGlobalSettings(response.data);
}
});
}
}).catch(error => {
})).catch(error => {
toast.error(parseServerError(error));
});
};

componentDidMount() {
this._fetchData();
}

render() {
const {showConfigColumnModal, dbName} = this.state;
const {showConfigColumnModal, showSettingsModal, dbName} = this.state;
const localStorageKey = 'RunsTable|1';
return (
<div className="App">
Expand All @@ -70,6 +132,10 @@ class App extends Component {
<MenuItem test-attr="manage-config-columns-button" eventKey={1.2} onClick={this._showConfigColumnModal}>
+/- Config Columns
</MenuItem>
<MenuItem test-attr="settings-button" eventKey={1.3} onClick={this._showSettingsModal}>
<Glyphicon glyph="wrench"/>
&nbsp; Settings
</MenuItem>
</NavDropdown>
</Nav>
</Navbar.Collapse>
Expand All @@ -79,6 +145,7 @@ class App extends Component {
<RunsTable localStorageKey={localStorageKey} showConfigColumnModal={showConfigColumnModal}
handleConfigColumnModalClose={this._handleConfigColumnModalClose} />
</div>
<SettingsModal show={showSettingsModal} handleClose={this._handleSettingsModalClose}/>
</div>
);
}
Expand Down
33 changes: 30 additions & 3 deletions web/src/components/App/index.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React from 'reactn';
import App from './index';
import mockAxios from 'jest-mock-axios';
import { toast } from "react-toastify";
Expand Down Expand Up @@ -50,19 +50,46 @@ describe('App component', () => {
expect(wrapper.state().showConfigColumnModal).toBeFalsy();
});

describe('should fetch database name on mount', () => {
it('should show/hide Settings modal', () => {
wrapper.find('[test-attr="settings-button"]').simulate('click');

expect(wrapper.state().showSettingsModal).toBeTruthy();
wrapper.instance()._handleSettingsModalClose();

expect(wrapper.state().showSettingsModal).toBeFalsy();
});

describe('should fetch database name on mount', async () => {
it('and handle success', async () => {
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/database');
mockAxios.mockResponse({status: 200, data: {name: 'test_db'}});
mockAxios.mockResponse({status: 200, data: [{name: 'timezone', value: 'Atlantic/Reykjavik', _id: 1}]});
await tick();

expect(wrapper.update().state().dbName).toEqual('test_db');
});

it('and handle error', () => {
it('and handle error', async () => {
const error = {status: 500, message: 'Unknown error'};
mockAxios.mockError(error);
mockAxios.mockError(error);
await tick();

expect(toast.error).toHaveBeenCalledWith(parseServerError(error));
});
});

it('should write default settings for the first time', async () => {
const setting = {name: 'timezone', value: 'Atlantic/Reykjavik', _id: 1};
mockAxios.mockResponse({status: 200, data: {name: 'test_db'}});
mockAxios.mockResponse({status: 200, data: []});
await tick();

expect(mockAxios.post).toHaveBeenCalledTimes(1);
mockAxios.mockResponse({status: 201, data: [setting]});

await tick();

expect(wrapper.update().instance().global.settings.timezone).toEqual(setting);
});
});
48 changes: 48 additions & 0 deletions web/src/components/Helpers/__snapshots__/cells.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Cells Date Cell should render correctly 1`] = `
<DateCell
columnKey="start_time"
data={
DataListWrapper {
"_data": Array [
Object {
"start_time": "2019-04-01T23:59:59",
},
],
"_indexMap": Array [
0,
],
}
}
rowIndex={0}
>
<FixedDataTableCellDefault
test-attr="date-cell"
>
<div
className="fixedDataTableCellLayout_wrap1 public_fixedDataTableCell_wrap1"
style={
Object {
"height": undefined,
"width": undefined,
}
}
test-attr="date-cell"
>
<div
className="fixedDataTableCellLayout_wrap2 public_fixedDataTableCell_wrap2"
>
<div
className="fixedDataTableCellLayout_wrap3 public_fixedDataTableCell_wrap3"
>
<div
className="public_fixedDataTableCell_cellContent"
>
2019-04-01T23:59:59
</div>
</div>
</div>
</div>
</FixedDataTableCellDefault>
</DateCell>
`;

exports[`Cells Editable Cell should render correctly 1`] = `
<EditableCell
changeHandler={
Expand Down

0 comments on commit 95488e0

Please sign in to comment.