aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDeterminant <[email protected]>2019-02-10 20:01:42 -0500
committerDeterminant <[email protected]>2019-02-10 20:01:42 -0500
commit8a0f7d58a136e87f71b790bbbb489af111472796 (patch)
tree3619963a184bf48f56601cee783eceda861e484f /src
parentbc097c38fa76563e7361b2193508a8ce13d73cae (diff)
render graph data in background; other improvements
Diffstat (limited to 'src')
-rw-r--r--src/Chart.js1
-rw-r--r--src/Dashboard.js3
-rw-r--r--src/PatternTable.js40
-rw-r--r--src/Settings.js29
-rw-r--r--src/background.js159
-rw-r--r--src/msg.js6
-rw-r--r--src/popup.js146
7 files changed, 259 insertions, 125 deletions
diff --git a/src/Chart.js b/src/Chart.js
index 88ab72c..983436a 100644
--- a/src/Chart.js
+++ b/src/Chart.js
@@ -11,6 +11,7 @@ export function getChartData(start, end, patterns, calendars, calEventsGetter) {
let event_pms = [];
for (let id in calendars)
{
+ if (!calendars[id].enabled) continue;
let filtered = patterns.filter(p => p.cal.regex.test(calendars[id].name));
if (filtered.length > 0)
event_pms.push(calEventsGetter(id, start, end)
diff --git a/src/Dashboard.js b/src/Dashboard.js
index 748d117..6c17d2c 100644
--- a/src/Dashboard.js
+++ b/src/Dashboard.js
@@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'typeface-roboto';
-import { withStyles } from '@material-ui/core/styles';
-import { MuiThemeProvider } from '@material-ui/core/styles';
+import { withStyles, MuiThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
diff --git a/src/PatternTable.js b/src/PatternTable.js
index 3e16bbf..e054f9e 100644
--- a/src/PatternTable.js
+++ b/src/PatternTable.js
@@ -13,15 +13,19 @@ import { CalendarField, EventField } from './RegexField';
import theme from './theme';
const styles = theme => ({
- deleteButtonShow: {
+ deleteButton: {
+ width: '100%',
position: 'absolute',
+ marginRight: '2em',
right: 0,
- height: 48
+ height: 48,
},
deleteButtonHide: {
display: 'none'
},
+ deleteButtonShow: {},
deleteIcon: {
+ position: 'absolute',
height: '100%',
cursor: 'pointer'
},
@@ -58,27 +62,35 @@ class PatternTable extends React.Component {
const { classes, calendars, patterns } = this.props;
const { rowsPerPage, page } = this.state;
const nDummy = rowsPerPage - Math.min(rowsPerPage, patterns.length - page * rowsPerPage);
- let rows = patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(p => (
- <TableRow
- onMouseOver={() => this.setState({ activePattern: p.idx })}
- onMouseOut={() => this.setState({ activePattern: null })}>
+ let rows = patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map((p, i) => {
+ let setActive = () => this.setState({ activePattern: p.idx });
+ let unsetActive = () => this.setState({ activePattern: null });
+ return [<TableRow key={i * 2}
+ onMouseOver={setActive} onMouseOut={unsetActive}
+ className={classes.deleteButton}>
+ <td>
+ <span className={this.state.activePattern !== p.idx ? classes.deleteButtonHide : classes.deleteButtonShow}>
+ <DeleteOutlinedIcon
+ className={classes.deleteIcon}
+ onClick={() => this.props.onRemovePattern(p.idx)} />
+ </span>
+ </td>
+ </TableRow>,
+ <TableRow key={i * 2 + 1} onMouseOver={setActive} onMouseOut={unsetActive}>
{
- patternHead.map(s => {
+ patternHead.map((s, i) => {
const CustomText = s.elem;
return (
- <TableCell>
+ <TableCell key={i}>
<CustomText
value={p[s.field]}
calendars={calendars}
onChange={event => this.props.onUpdatePattern(s.field, p.idx, event.target.value)}/>
</TableCell>)})
}
- <span className={this.state.activePattern === p.idx ? classes.deleteButtonShow : classes.deleteButtonHide}>
- <DeleteOutlinedIcon
- className={classes.deleteIcon}
- onClick={() => this.props.onRemovePattern(p.idx)} />
- </span>
- </TableRow>));
+ </TableRow>]
+ });
+ rows.flat();
return (
<div>
diff --git a/src/Settings.js b/src/Settings.js
index 0951e27..9358eee 100644
--- a/src/Settings.js
+++ b/src/Settings.js
@@ -78,6 +78,13 @@ class TrackedPeriod extends React.Component {
}
};
+ static toValue(value) {
+ if (isNaN(value)) return null;
+ let v = parseInt(value, 10);
+ if (v < 0 || v > 999) return null;
+ return v;
+ }
+
render() {
let { classes, fromDuration, toDuration, nameOnChange, fromOnChange, toOnChange, name } = this.props;
let units = [
@@ -92,12 +99,14 @@ class TrackedPeriod extends React.Component {
value={name}
onChange={event => nameOnChange(event.target.value)}/>:
from <TextField
+ error={TrackedPeriod.toValue(fromDuration.value) === null}
inputProps={{style: TrackedPeriod.styles.periodValue}}
value={fromDuration.value}
onChange={this.valueOnChange(fromDuration, fromOnChange)} />
<Select value={fromDuration.unit}
onChange={this.unitOnChange(fromDuration, fromOnChange)}>{units}</Select> ago
to <TextField
+ error={TrackedPeriod.toValue(toDuration.value) === null}
inputProps={{style: TrackedPeriod.styles.periodValue}}
value={toDuration.value}
onChange={this.valueOnChange(toDuration, toOnChange)} />
@@ -118,6 +127,7 @@ class Settings extends React.Component {
snackBarMsg: 'unknown',
dialogOpen: false,
dialogMsg: {title: '', message: ''},
+ calendarsLoading: false,
};
constructor(props) {
@@ -186,12 +196,14 @@ class Settings extends React.Component {
}).then(() => this.setState({ calendars }));
}
- loadAll = loadDefaultPatterns => {
+ async loadAll(loadDefaultPatterns) {
+ await new Promise(resolver => (this.setState({ calendarsLoading: true }, resolver)));
+
let colors = gapi.getAuthToken().then(gapi.getColors).then(color => {
return color.calendar;
});
let cals = gapi.getAuthToken().then(gapi.getCalendars);
- Promise.all([colors, cals]).then(([colors, items]) => {
+ await Promise.all([colors, cals]).then(([colors, items]) => {
var cals = {};
items.forEach(item => {
cals[item.id] = {
@@ -210,6 +222,7 @@ class Settings extends React.Component {
}), 'main');
}
});
+ this.setState({ calendarsLoading: false });
};
loadCalendars = calendars => {
@@ -276,7 +289,11 @@ class Settings extends React.Component {
updateTrackedPeriods = trackedPeriods => {
this.msgClient.sendMsg({
type: msgType.updateConfig,
- data: { trackedPeriods: trackedPeriods.map(p => p.deflate()) }
+ data: { trackedPeriods: trackedPeriods.map(p => ({
+ name: p.name,
+ start: p.start.deflate(),
+ end: p.end.deflate()
+ })) }
}).then(() => this.setState({...this.state.config, trackedPeriods }));
}
@@ -332,7 +349,9 @@ class Settings extends React.Component {
<IconButton
style={{marginBottom: '0.12em', marginRight: '0.5em'}}
onClick={() => this.loadAll(false)}
- disabled={!this.state.isLoggedIn}><RefreshIcon /></IconButton>
+ disabled={this.state.calendarsLoading || !this.state.isLoggedIn}>
+ <RefreshIcon />
+ </IconButton>
Calendars
</STableCell>
<STableCell className={classes.tableContent}>
@@ -378,7 +397,7 @@ class Settings extends React.Component {
<STableCell className={classes.tableContent}>
{this.state.config.trackedPeriods &&
this.state.config.trackedPeriods.map((p, idx) =>
- <FormGroup>
+ <FormGroup key={idx}>
<TrackedPeriod
name={p.name}
fromDuration={p.start}
diff --git a/src/background.js b/src/background.js
index 4bee605..1907a30 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,6 +1,9 @@
import * as gapi from './gapi';
import { msgType, Msg } from './msg';
import { Duration } from './duration';
+import moment from 'moment';
+import { getChartData } from './Chart';
+import { PatternEntry } from './pattern';
let mainPatterns = [];
let analyzePatterns = [];
@@ -13,6 +16,118 @@ let config = {
{name: 'This Week', start: Duration.weeks(1), end: Duration.weeks(0)},
{name: 'This Month', start: Duration.months(1), end: Duration.months(0)}]
};
+let mainGraphData = [{}, {}, {}, {}];
+let dirtyMetadata = false;
+
+function loadMetadata() {
+ return new Promise(resolver => chrome.storage.local.get([
+ 'calendars', 'config', 'mainPatterns', 'analyzePatterns',
+ ], function(items) {
+ if (chrome.runtime.lastError)
+ {
+ console.error("error while loading saved metadata");
+ return;
+ }
+ if (!items.hasOwnProperty('config'))
+ {
+ console.log("no saved metadata");
+ return;
+ }
+ console.log('metadata loaded');
+ config = {
+ trackedPeriods: items.config.trackedPeriods.map(p => ({
+ name: p.name,
+ start: Duration.inflate(p.start),
+ end: Duration.inflate(p.end),
+ }))
+ };
+ calendars = items.calendars;
+ mainPatterns = items.mainPatterns.map(p => PatternEntry.inflate(p));
+ analyzePatterns = items.analyzePatterns.map(p => PatternEntry.inflate(p));
+ resolver();
+ }));
+}
+
+function saveMetadata() {
+ return new Promise(resolver => chrome.storage.local.set({
+ calendars,
+ config: {
+ trackedPeriods: config.trackedPeriods.map(p => ({
+ name: p.name,
+ start: p.start.deflate(),
+ end: p.end.deflate()
+ }))
+ },
+ mainPatterns: mainPatterns.map(p => p.deflate()),
+ analyzePatterns: analyzePatterns.map(p => p.deflate())
+ }, function() {
+ console.log('metadata saved');
+ resolver();
+ }));
+}
+
+function getCalEvents(id, start, end) {
+ if (!calData.hasOwnProperty(id))
+ calData[id] = new gapi.GCalendar(id, calendars[id].summary);
+ return calData[id].getEvents(new Date(start), new Date(end))
+ .catch(e => {
+ console.log(`cannot load calendar ${id}`, e);
+ calendars[id].enabled = false;
+ return [];
+ });
+}
+
+function updateMainGraphData() {
+ console.log('refreshing graph data');
+ console.log(mainGraphData);
+ let pms = [];
+ for (let i = 0; i < config.trackedPeriods.length; i++)
+ {
+ let p = config.trackedPeriods[i];
+ let startD = p.start.toMoment();
+ let endD = p.end.toMoment();
+ if (!(startD && endD)) return;
+ let start = moment().endOf('day');
+ if (endD.valueOf() == 0) {
+ switch (p.start.unit) {
+ case 'days': start = moment().endOf('day'); break;
+ case 'weeks': start = moment().endOf('week'); break;
+ case 'months': start = moment().endOf('month'); break;
+ default:
+ }
+ }
+ let end = start.clone();
+ start.subtract(startD);
+ end.subtract(endD);
+ pms.push(getChartData(
+ start.toDate(),
+ end.toDate(),
+ mainPatterns,
+ calendars,
+ (id, start,end) => getCalEvents(id, start, end).then(d => d.map(e => ({
+ id: e.id,
+ start: e.start.getTime(),
+ end: e.end.getTime()
+ })))).then(results => {
+ mainGraphData[i] = {
+ name: p.name, start, end,
+ data: results.patternGraphData
+ };
+ }));
+ }
+ return Promise.all(pms);
+}
+
+async function pollSync() {
+ await updateMainGraphData();
+ if (dirtyMetadata)
+ await saveMetadata().then(() => dirtyMetadata = false);
+ return new Promise(resolver => (
+ window.setTimeout(() => { resolver(); pollSync();}, 10000)
+ ));
+}
+
+loadMetadata().then(() => pollSync());
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name == 'main');
@@ -21,10 +136,12 @@ chrome.runtime.onConnect.addListener(function(port) {
console.log(msg);
switch (msg.type) {
case msgType.updatePatterns: {
+ let patterns = msg.data.patterns.map(p => PatternEntry.inflate(p));
if (msg.data.id == 'analyze')
- analyzePatterns = msg.data.patterns;
+ analyzePatterns = patterns;
else
- mainPatterns = msg.data.patterns;
+ mainPatterns = patterns;
+ dirtyMetadata = true;
port.postMessage(msg.genResp(null));
break;
}
@@ -34,15 +151,12 @@ chrome.runtime.onConnect.addListener(function(port) {
patterns = analyzePatterns;
else
patterns = mainPatterns;
- port.postMessage(msg.genResp(patterns));
+ port.postMessage(msg.genResp(patterns.map(p => p.deflate())));
break;
}
case msgType.updateCalendars: {
calendars = msg.data;
- for (let id in calendars) {
- if (!calData.hasOwnProperty(id))
- calData[id] = new gapi.GCalendar(id, calendars[id].summary);
- }
+ dirtyMetadata = true;
port.postMessage(msg.genResp(null));
break;
}
@@ -58,12 +172,7 @@ chrome.runtime.onConnect.addListener(function(port) {
break;
}
case msgType.getCalEvents: {
- calData[msg.data.id].getEvents(new Date(msg.data.start), new Date(msg.data.end))
- .catch(e => {
- console.log(`cannot load calendar ${msg.data.id}`, e);
- return [];
- })
- .then(data => {
+ getCalEvents(msg.data.id, msg.data.start, msg.data.end).then(data => {
console.log(data);
let resp = msg.genResp(data.map(e => {
return {
@@ -78,8 +187,12 @@ chrome.runtime.onConnect.addListener(function(port) {
break;
}
case msgType.updateConfig: {
- for (let prop in msg.data)
- config[prop] = msg.data[prop];
+ config.trackedPeriods = msg.data.trackedPeriods.map(p => ({
+ name: p.name,
+ start: Duration.inflate(p.start),
+ end: Duration.inflate(p.end)
+ }));
+ dirtyMetadata = true;
port.postMessage(msg.genResp(null));
break;
}
@@ -89,12 +202,18 @@ chrome.runtime.onConnect.addListener(function(port) {
port.postMessage(msg.genResp(res));
break;
}
+ case msgType.getGraphData: {
+ (msg.data.sync ? updateMainGraphData() : Promise.resolve()).then(() => (
+ port.postMessage(msg.genResp(mainGraphData.map(d => ({
+ name: d.name,
+ start: d.start.toISOString(),
+ end: d.end.toISOString(),
+ data: d.data
+ }))))
+ ));
+ break;
+ }
default: console.error("unknown msg type");
}
});
});
-
-chrome.browserAction.onClicked.addListener(function() {
- chrome.tabs.create({url: 'index.html'});
-});
-
diff --git a/src/msg.js b/src/msg.js
index ce83eb8..2e72ea7 100644
--- a/src/msg.js
+++ b/src/msg.js
@@ -6,6 +6,7 @@ const _getCalendars = "getCalendars";
const _getCalEvents = "getCalEvents";
const _updateConfig = "updateConfig";
const _getConfig = "getConfig";
+const _getGraphData = "getGraphData";
export const msgType = Object.freeze({
updatePatterns: Symbol(_updatePatterns),
@@ -14,7 +15,8 @@ export const msgType = Object.freeze({
getCalendars: Symbol(_getCalendars),
getCalEvents: Symbol(_getCalEvents),
updateConfig: Symbol(_updateConfig),
- getConfig: Symbol(_getConfig)
+ getConfig: Symbol(_getConfig),
+ getGraphData: Symbol(_getGraphData),
});
function stringifyMsgType(mt) {
@@ -26,6 +28,7 @@ function stringifyMsgType(mt) {
case msgType.getCalEvents: return _getCalEvents;
case msgType.updateConfig: return _updateConfig;
case msgType.getConfig: return _getConfig;
+ case msgType.getGraphData: return _getGraphData;
default: console.error("unreachable");
}
}
@@ -39,6 +42,7 @@ function parseMsgType(s) {
case _getCalEvents: return msgType.getCalEvents;
case _updateConfig: return msgType.updateConfig;
case _getConfig: return msgType.getConfig;
+ case _getGraphData: return msgType.getGraphData;
default: console.error("unreachable");
}
}
diff --git a/src/popup.js b/src/popup.js
index d8e045c..f13688c 100644
--- a/src/popup.js
+++ b/src/popup.js
@@ -1,135 +1,115 @@
import React from 'react';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
-import { MuiThemeProvider } from '@material-ui/core/styles';
+import { withStyles, MuiThemeProvider } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
+import IconButton from '@material-ui/core/IconButton';
+import RefreshIcon from '@material-ui/icons/Refresh';
+import Logo from './Logo';
import Typography from '@material-ui/core/Typography';
import theme from './theme';
+import CssBaseline from '@material-ui/core/CssBaseline';
import { PatternEntry } from './pattern';
import { Duration } from './duration';
import { msgType, MsgClient } from './msg';
-import { getChartData, StyledPatternPieChart } from './Chart';
+import { StyledPatternPieChart } from './Chart';
+import Divider from '@material-ui/core/Divider';
import moment from 'moment';
function openOptions() {
chrome.tabs.create({ url: "index.html" });
}
+const styles = theme => ({
+ content: {
+ padding: theme.spacing.unit * 1,
+ overflow: 'auto',
+ },
+ buttons: {
+ height: 48,
+ lineHeight: '48px'
+ },
+ buttonSpacer: {
+ marginBottom: theme.spacing.unit * 2,
+ },
+});
+
class Popup extends React.Component {
state = {
patternGraphData: [],
+ loading: false,
};
constructor(props) {
super(props);
this.msgClient = new MsgClient('main');
-
- let pm1 = this.msgClient.sendMsg({
- type: msgType.getPatterns,
- data: { id: 'main' }
- }).then(msg => {
- this.patterns = msg.data.map(p => PatternEntry.inflate(p));
- });
-
- let pm2 = this.msgClient.sendMsg({
- type: msgType.getCalendars,
- data: { enabledOnly: false }
- }).then(msg => {
- this.calendars = msg.data;
- });
-
- let pm3 = this.msgClient.sendMsg({
- type: msgType.getConfig,
- data: ['trackedPeriods']
- }).then(msg => {
- this.trackedPeriods = msg.data.trackedPeriods.map(p => {
- return {
- start: Duration.inflate(p.start),
- end: Duration.inflate(p.end),
- name: p.name
- };
- });
- });
-
- // initial update
- Promise.all([pm1, pm2, pm3]).then(() => {
- for (let i = 0; i < this.trackedPeriods.length; i++)
- this.renderChartData(i);
- });
- }
-
- getCalEvents = (id, start, end) => {
- return this.msgClient.sendMsg({ type: msgType.getCalEvents, data: { id,
- start: start.getTime(),
- end: end.getTime() } })
- .then(({ data }) => data.map(e => {
- return {
- id: e.id,
- start: new Date(e.start),
- end: new Date(e.end) }
- }));
+ this.loading = true;
+ this.loadGraphData(false).then(() => this.setState({ loading: false }));
}
- renderChartData(idx) {
- let p = this.trackedPeriods[idx];
- console.log(this.trackedPeriods);
- let startD = p.start.toMoment();
- let endD = p.end.toMoment();
- if (!(startD && endD)) return;
- let start = moment().endOf('day');
- if (endD.valueOf() == 0) {
- switch (p.start.unit) {
- case 'days': start = moment().endOf('day'); break;
- case 'weeks': start = moment().endOf('week'); break;
- case 'months': start = moment().endOf('month'); break;
- default:
- }
- }
- let end = start.clone();
- start.subtract(startD);
- end.subtract(endD);
- console.log(start, end);
- return getChartData(start.toDate(),
- end.toDate(),
- this.patterns, this.calendars, this.getCalEvents).then(results => {
- let patternGraphData = this.state.patternGraphData;
- patternGraphData[idx] = {
- start: moment(results.start),
- end: moment(results.end),
- data: results.patternGraphData
- };
- this.setState({ patternGraphData });
+ loadGraphData(sync) {
+ return this.msgClient.sendMsg({
+ type: msgType.getGraphData,
+ data: { sync }
+ }).then(msg => {
+ this.setState({ patternGraphData: msg.data.map(d => ({
+ name: d.name,
+ data: d.data,
+ start: new Date(d.start),
+ end: new Date(d.end)
+ }))});
});
}
render() {
- console.log(this.state.patternGraphData);
+ let { classes } = this.props;
+ let data = this.state.patternGraphData;
return (
<MuiThemeProvider theme={theme}>
- <Button variant="contained" color="primary" onClick={openOptions}>Dashboard</Button>
+ <CssBaseline />
+ <main className={classes.content}>
+ <div className={classes.buttons}>
+ <Logo style={{height: '100%', verticalAlign: 'bottom', marginRight: '1em'}}/>
+ <Button variant="contained" color="primary" onClick={openOptions}>Settings</Button>
+ <IconButton
+ disabled={this.state.loading}
+ style={{float: 'right'}}
+ onClick={() => (
+ new Promise(resolver => (
+ this.setState({ loading: true }, resolver)))
+ .then(() => this.loadGraphData(true))
+ .then(() => this.setState({ loading: false }))
+ )}><RefreshIcon />
+ </IconButton>
+ </div>
+ <div className={classes.buttonSpacer} />
{
- this.state.patternGraphData.map((d, idx) => (
+ data.map((d, idx) => (
<div key={idx}>
<Typography variant="subtitle1" align="center" color="textPrimary">
- {this.trackedPeriods[idx].name}
+ {d.name}
</Typography>
<Typography variant="caption" align="center">
- {`${d.start.format('ddd, MMM Do, YYYY')} -
- ${d.end.format('ddd, MMM Do, YYYY')}`}
+ {`${moment(d.start).format('ddd, MMM Do, YYYY')} -
+ ${moment(d.end).format('ddd, MMM Do, YYYY')}`}
</Typography>
{(d.data.some(dd => dd.value > 1e-3) &&
<StyledPatternPieChart data={d.data} />) ||
<Typography variant="subtitle1" align="center" color="textSecondary">
No data available
</Typography>}
+ {idx + 1 < data.length && <Divider />}
</div>
))
}
+ </main>
</MuiThemeProvider>
);
}
}
-ReactDOM.render(<Popup />, document.getElementById('root'));
+const StyledPopup = withStyles(styles)(Popup);
+
+ReactDOM.render(<StyledPopup />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.