aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Analyze.js (renamed from src/CustomAnalyzer.js)111
-rw-r--r--src/Chart.js113
-rw-r--r--src/Dashboard.js4
-rw-r--r--src/Settings.js158
-rw-r--r--src/background.js39
-rw-r--r--src/duration.js21
-rw-r--r--src/msg.js8
-rw-r--r--src/pattern.js23
-rw-r--r--src/popup.js114
9 files changed, 457 insertions, 134 deletions
diff --git a/src/CustomAnalyzer.js b/src/Analyze.js
index 53fa7ba..0d01210 100644
--- a/src/CustomAnalyzer.js
+++ b/src/Analyze.js
@@ -16,7 +16,7 @@ import IconButton from '@material-ui/core/IconButton';
import * as gapi from './gapi';
import { msgType, MsgClient } from './msg';
import { Pattern, PatternEntry } from './pattern';
-import PieChart from './Chart';
+import { AnalyzePieChart, getChartData } from './Chart';
import PatternTable from './PatternTable';
import Snackbar from './Snackbar';
import AlertDialog from './Dialog';
@@ -25,19 +25,13 @@ const default_chart_data = [
{name: 'Work', value: 10, color: cyan[300]},
{name: 'Wasted', value: 10, color: cyan[300]}];
-function filterPatterns(patterns, calName) {
- return patterns.filter(p => {
- return p.cal.regex.test(calName);
- });
-}
-
const styles = theme => ({
buttonSpacer: {
marginBottom: theme.spacing.unit * 4,
},
});
-class CustomAnalyzer extends React.Component {
+class Analyze extends React.Component {
state = {
patterns: [],
calendars: {},
@@ -55,55 +49,53 @@ class CustomAnalyzer extends React.Component {
constructor(props) {
super(props);
this.msgClient = new MsgClient('main');
+
this.msgClient.sendMsg({
type: msgType.getPatterns,
data: { id: 'analyze' }
}).then(msg => {
- this.setState({ patterns: msg.data.map(p => PatternEntry.revive(p)) });
+ this.setState({ patterns: msg.data.map(p => PatternEntry.inflate(p)) });
});
- this.msgClient.sendMsg({ type: msgType.getCalendars, data: { enabledOnly: true }}).then(msg => {
+
+ this.msgClient.sendMsg({
+ type: msgType.getCalendars,
+ data: { enabledOnly: true }
+ }).then(msg => {
this.setState({ calendars: msg.data });
});
+
gapi.getLoggedIn().then(b => !b &&
this.handleSnackbarOpen('Not logged in. Operating in offline mode.', 'warning'));
+
this.dialogPromiseResolver = null;
}
- updatePattern = (field, idx, value) => {
- let patterns = this.state.patterns;
- patterns[idx][field] = value;
+ loadPatterns = patterns => {
this.msgClient.sendMsg({
type: msgType.updatePatterns,
- data: { id: 'analyze', patterns }
+ data: { id: 'analyze', patterns: patterns.map(p => p.deflate()) }
}).then(() => this.setState({ patterns }));
};
+ updatePattern = (field, idx, value) => {
+ let patterns = this.state.patterns;
+ patterns[idx][field] = value;
+ this.loadPatterns(patterns);
+ };
+
removePattern = idx => {
let patterns = this.state.patterns;
patterns.splice(idx, 1);
for (let i = 0; i < patterns.length; i++)
patterns[i].idx = i;
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'analyze', patterns }
- }).then(() => this.setState({ patterns }));
+ this.loadPatterns(patterns);
};
newPattern = () => {
let patterns = [PatternEntry.defaultPatternEntry(0), ...this.state.patterns];
for (let i = 1; i < patterns.length; i++)
patterns[i].idx = i;
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'analyze', patterns }
- }).then(() => this.setState({ patterns }));
- };
-
- loadPatterns = patterns => {
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'analyze', patterns }
- }).then(() => this.setState({ patterns }));
+ this.loadPatterns(patterns);
};
getCalEvents = (id, start, end) => {
@@ -125,56 +117,13 @@ class CustomAnalyzer extends React.Component {
}
let start = this.state.startDate.startOf('day').toDate();
let end = this.state.endDate.startOf('day').toDate();
- let event_pms = [];
- let cals = this.state.calendars;
- for (let id in cals)
- {
- let patterns = filterPatterns(this.state.patterns, cals[id].name);
- if (patterns.length > 0)
- event_pms.push(this.getCalEvents(id, start, end)
- .then(r => { return { id, events: r, patterns }; }));
- }
- Promise.all(event_pms).then(all_events => {
- console.log(all_events);
- let events = {};
- let patterns = {};
- let results = {}; // pattern idx => time
- let cal_results = {}; // cal id => time
- all_events.forEach(e => {
- events[e.id] = e.events;
- patterns[e.id] = e.patterns;
- });
- for (let i = 0; i < this.state.patterns.length; i++)
- results[i] = 0;
- for (let id in cals) {
- if (!events[id]) continue;
- events[id].forEach(event => {
- patterns[id].forEach(p => {
- if (!p.event.regex.test(event.summary)) return;
- if (!cal_results.hasOwnProperty(id)) {
- cal_results[id] = 0;
- }
- let duration = (event.end - event.start) / 60000;
- results[p.idx] += duration;
- cal_results[id] += duration;
- });
- });
- }
- let patternGraphData = [];
- let calendarGraphData = [];
- for (let i = 0; i < this.state.patterns.length; i++) {
- patternGraphData.push({ name: this.state.patterns[i].name, value: results[i] / 60.0 });
- }
- for (let id in cal_results) {
- calendarGraphData.push({
- name: cals[id].name,
- value: (cal_results[id] / 60.0),
- color: cals[id].color.background});
- }
- console.log(patternGraphData, calendarGraphData);
- this.setState({ patternGraphData, calendarGraphData });
+ getChartData(start, end,
+ this.state.patterns,
+ this.state.calendars,
+ this.getCalEvents).then(results => {
+ this.setState(results);
});
- };
+ }
reset = () => {
this.handleDialogOpen("Reset", "Are you sure to reset the patterns?").then(ans => {
@@ -290,7 +239,7 @@ class CustomAnalyzer extends React.Component {
<Typography variant="h6" component="h1" gutterBottom>
Results
</Typography>
- <PieChart
+ <AnalyzePieChart
patternGraphData={this.state.patternGraphData}
calendarGraphData={this.state.calendarGraphData}/>
</Grid>
@@ -299,8 +248,8 @@ class CustomAnalyzer extends React.Component {
}
}
-CustomAnalyzer.propTypes = {
+Analyze.propTypes = {
classes: PropTypes.object.isRequired,
};
-export default withStyles(styles)(CustomAnalyzer);
+export default withStyles(styles)(Analyze);
diff --git a/src/Chart.js b/src/Chart.js
index b41b17e..88ab72c 100644
--- a/src/Chart.js
+++ b/src/Chart.js
@@ -6,6 +6,75 @@ import deepOrange from '@material-ui/core/colors/deepOrange';
import cyan from '@material-ui/core/colors/cyan';
import { PieChart, Pie, Cell, Tooltip } from 'recharts';
+export function getChartData(start, end, patterns, calendars, calEventsGetter) {
+ if (start >= end) return Promise.resolve({ patternGraphData: [], calendarGraphData: [] });
+ let event_pms = [];
+ for (let id in calendars)
+ {
+ let filtered = patterns.filter(p => p.cal.regex.test(calendars[id].name));
+ if (filtered.length > 0)
+ event_pms.push(calEventsGetter(id, start, end)
+ .then(r => { return { id, events: r, filtered }; }));
+ }
+ return Promise.all(event_pms).then(all_events => {
+ let events = {};
+ let patternsByCal = {};
+ let results = {}; // pattern idx => time
+ let cal_results = {}; // cal id => time
+ all_events.forEach(e => {
+ events[e.id] = e.events;
+ patternsByCal[e.id] = e.filtered;
+ });
+ for (let i = 0; i < patterns.length; i++)
+ results[i] = 0;
+ for (let id in calendars) {
+ if (!events[id]) continue;
+ events[id].forEach(event => {
+ patternsByCal[id].forEach(p => {
+ if (!p.event.regex.test(event.summary)) return;
+ if (!cal_results.hasOwnProperty(id)) {
+ cal_results[id] = 0;
+ }
+ let duration = (event.end - event.start) / 60000;
+ results[p.idx] += duration;
+ cal_results[id] += duration;
+ });
+ });
+ }
+ let patternGraphData = [];
+ let calendarGraphData = [];
+ const filterMarginal = data => {
+ let sum = 0;
+ let majorParts = [];
+ let minorSum = 0;
+ data.forEach(d => sum += d.value);
+ data.forEach(d => {
+ let ratio = d.value / sum;
+ if (ratio < 1e-2) minorSum += d.value;
+ else majorParts.push(d);
+ });
+ majorParts.push({
+ name: 'Other',
+ value: minorSum,
+ color: '#fff',
+ });
+ return majorParts;
+ };
+ for (let i = 0; i < patterns.length; i++) {
+ patternGraphData.push({ name: patterns[i].name, value: results[i] / 60.0 });
+ }
+ for (let id in cal_results) {
+ calendarGraphData.push({
+ name: calendars[id].name,
+ value: (cal_results[id] / 60.0),
+ color: calendars[id].color.background});
+ }
+ return {start, end,
+ patternGraphData: filterMarginal(patternGraphData),
+ calendarGraphData: filterMarginal(calendarGraphData) };
+ });
+}
+
const styles = theme => ({
pieChart: {
margin: '0 auto',
@@ -35,23 +104,32 @@ function customizedLabel(props) {
return (<text x={x} y={y} dx={dx} dy={dy} fill={fill} textAnchor={anchor}>{`${name}`}</text>);
}
-function ChromiclePieChart(props) {
+function PatternPieChart(props) {
+ return (
+ <Grid item xs={12} lg={6}>
+ <div className={props.classes.patternTableWrapper}>
+ <PieChart width={400} height={250} className={props.classes.pieChart}>
+ <Pie data={props.data}
+ dataKey='value'
+ cx={200}
+ cy={125}
+ outerRadius={60}
+ fill={deepOrange[300]}
+ isAnimationActive={false}
+ label={customizedLabel}/>
+ <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/>
+ </PieChart>
+ </div>
+ </Grid>
+ );
+}
+
+export const StyledPatternPieChart = withStyles(styles)(PatternPieChart);
+
+function DoublePieChart(props) {
return (
<Grid container spacing={0}>
- <Grid item xs={12} lg={6}>
- <div className={props.classes.patternTableWrapper}>
- <PieChart width={400} height={250} className={props.classes.pieChart}>
- <Pie data={props.patternGraphData}
- dataKey='value'
- cx={200}
- cy={125}
- outerRadius={60}
- fill={deepOrange[300]}
- label={customizedLabel}/>
- <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/>
- </PieChart>
- </div>
- </Grid>
+ <StyledPatternPieChart data={props.patternGraphData} />
<Grid item xs={12} lg={6}>
<div className={props.classes.patternTableWrapper}>
<PieChart width={400} height={250} className={props.classes.pieChart}>
@@ -62,6 +140,7 @@ function ChromiclePieChart(props) {
innerRadius={40}
outerRadius={70}
fill={cyan[300]}
+ isAnimationActive={false}
label={customizedLabel}>
{props.calendarGraphData.map((d, i) => <Cell key={i} fill={d.color}/>)}
</Pie>
@@ -72,9 +151,9 @@ function ChromiclePieChart(props) {
</Grid>);
}
-ChromiclePieChart.propTypes = {
+DoublePieChart.propTypes = {
patternGraphData: PropTypes.array.isRequired,
calendarGraphData: PropTypes.array.isRequired,
};
-export default withStyles(styles)(ChromiclePieChart);
+export const AnalyzePieChart = withStyles(styles)(DoublePieChart);
diff --git a/src/Dashboard.js b/src/Dashboard.js
index bd17cec..748d117 100644
--- a/src/Dashboard.js
+++ b/src/Dashboard.js
@@ -15,7 +15,7 @@ import { HashRouter as Router, withRouter, Route, Link, Redirect, Switch } from
import { hashHistory } from 'react-router';
import Logo from './Logo';
import theme from './theme';
-import CustomAnalyzer from './CustomAnalyzer';
+import Analyze from './Analyze';
import Settings from './Settings';
const styles = theme => ({
@@ -73,7 +73,7 @@ class DashboardTabs extends React.Component {
<main className={classes.content}>
<div className={classes.appBarSpacer} />
<Route exact path="/settings" component={Settings} />
- <Route exact path="/analyze" component={CustomAnalyzer} />
+ <Route exact path="/analyze" component={Analyze} />
<Route exact path="/" render={() => <Redirect to="/settings" />}/>
</main>
</div>
diff --git a/src/Settings.js b/src/Settings.js
index 3865438..0951e27 100644
--- a/src/Settings.js
+++ b/src/Settings.js
@@ -25,6 +25,10 @@ import { Pattern, PatternEntry } from './pattern';
import PatternTable from './PatternTable';
import Snackbar from './Snackbar';
import AlertDialog from './Dialog';
+import TextField from '@material-ui/core/TextField';
+import MenuItem from '@material-ui/core/MenuItem';
+import Select from '@material-ui/core/Select';
+import { Duration } from './duration';
const styles = theme => ({
tableHead: {
@@ -39,7 +43,7 @@ const styles = theme => ({
calendarList: {
maxHeight: 400,
overflowY: 'auto'
- }
+ },
});
const STableCell = withStyles(theme => ({
@@ -55,11 +59,61 @@ const CompactListItem = withStyles(theme => ({
},
}))(ListItem);
+class TrackedPeriod extends React.Component {
+ valueOnChange = (old, onChange) => event => {
+ onChange(new Duration(event.target.value, old.unit));
+ }
+
+ unitOnChange = (old, onChange) => event => {
+ onChange(new Duration(old.value, event.target.value));
+ }
+
+ static styles = {
+ periodName: {
+ textAlign: 'right'
+ },
+ periodValue: {
+ width: 30,
+ textAlign: 'center'
+ }
+ };
+
+ render() {
+ let { classes, fromDuration, toDuration, nameOnChange, fromOnChange, toOnChange, name } = this.props;
+ let units = [
+ <MenuItem key='days' value='days'>Day(s)</MenuItem>,
+ <MenuItem key='weeks' value='weeks'>Week(s)</MenuItem>,
+ <MenuItem key='months' value='months'>Month(s)</MenuItem>
+ ];
+ return (
+ <span>
+ <TextField
+ inputProps={{ style: TrackedPeriod.styles.periodName}}
+ value={name}
+ onChange={event => nameOnChange(event.target.value)}/>:
+ from <TextField
+ 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
+ inputProps={{style: TrackedPeriod.styles.periodValue}}
+ value={toDuration.value}
+ onChange={this.valueOnChange(toDuration, toOnChange)} />
+ <Select value={toDuration.unit}
+ onChange={this.unitOnChange(toDuration, toOnChange)}>{units}</Select> ago
+ </span>
+ );
+ }
+}
+
class Settings extends React.Component {
state = {
isLoggedIn: false,
patterns: [],
calendars: {},
+ config: {},
snackBarOpen: false,
snackBarMsg: 'unknown',
dialogOpen: false,
@@ -68,17 +122,41 @@ class Settings extends React.Component {
constructor(props) {
super(props);
- this.msgClient = new MsgClient('main');
gapi.getLoggedIn().then(b => this.setState({ isLoggedIn: b }));
+
+ this.msgClient = new MsgClient('main');
+
this.msgClient.sendMsg({
type: msgType.getPatterns,
data: { id: 'main' }
}).then(msg => {
- this.setState({ patterns: msg.data.map(p => PatternEntry.revive(p)) });
+ this.setState({ patterns: msg.data.map(p => PatternEntry.inflate(p)) });
});
- this.msgClient.sendMsg({ type: msgType.getCalendars, data: { enabledOnly: false } }).then(msg => {
+
+ this.msgClient.sendMsg({
+ type: msgType.getCalendars,
+ data: { enabledOnly: false }
+ }).then(msg => {
this.setState({ calendars: msg.data });
});
+
+ this.msgClient.sendMsg({
+ type: msgType.getConfig,
+ data: ['trackedPeriods']
+ }).then(msg => {
+ let config = {
+ trackedPeriods: msg.data.trackedPeriods.map(p => {
+ return {
+ start: Duration.inflate(p.start),
+ end: Duration.inflate(p.end),
+ name: p.name
+ };
+ })
+ };
+ console.log(msg.data.trackedPeriods);
+ this.setState({ config });
+ });
+
this.dialogPromiseResolver = null;
}
@@ -102,8 +180,10 @@ class Settings extends React.Component {
handleToggleCalendar = id => {
var calendars = {...this.state.calendars};
calendars[id].enabled = !calendars[id].enabled;
- this.msgClient.sendMsg({ type: msgType.updateCalendars, data: calendars }).then(() =>
- this.setState({ calendars }));
+ this.msgClient.sendMsg({
+ type: msgType.updateCalendars,
+ data: calendars
+ }).then(() => this.setState({ calendars }));
}
loadAll = loadDefaultPatterns => {
@@ -137,24 +217,23 @@ class Settings extends React.Component {
if (calendars.hasOwnProperty(id))
calendars[id].enabled = this.state.calendars[id].enabled;
}
- this.msgClient.sendMsg({ type: msgType.updateCalendars, data: calendars }).then(() =>
- this.setState({ calendars }));
+ this.msgClient.sendMsg({
+ type: msgType.updateCalendars,
+ data: calendars
+ }).then(() => this.setState({ calendars }));
};
loadPatterns = (patterns, id) => {
this.msgClient.sendMsg({
type: msgType.updatePatterns,
- data: { id, patterns }
+ data: { id, patterns: patterns.map(p => p.deflate()) }
}).then(() => this.setState({ patterns }));
};
updatePattern = (field, idx, value) => {
let patterns = this.state.patterns;
patterns[idx][field] = value;
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'main', patterns }
- }).then(() => this.setState({ patterns }));
+ this.loadPatterns(patterns);
};
removePattern = idx => {
@@ -162,20 +241,14 @@ class Settings extends React.Component {
patterns.splice(idx, 1);
for (let i = 0; i < patterns.length; i++)
patterns[i].idx = i;
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'main', patterns }
- }).then(() => this.setState({ patterns }));
+ this.loadPatterns(patterns);
};
newPattern = () => {
let patterns = [PatternEntry.defaultPatternEntry(0), ...this.state.patterns];
for (let i = 1; i < patterns.length; i++)
patterns[i].idx = i;
- this.msgClient.sendMsg({
- type: msgType.updatePatterns,
- data: { id: 'main', patterns }
- }).then(() => this.setState({ patterns }));
+ this.loadPatterns(patterns);
};
handleSnackbarClose = (event, reason) => {
@@ -200,6 +273,31 @@ class Settings extends React.Component {
this.setState({ dialogOpen: false });
}
+ updateTrackedPeriods = trackedPeriods => {
+ this.msgClient.sendMsg({
+ type: msgType.updateConfig,
+ data: { trackedPeriods: trackedPeriods.map(p => p.deflate()) }
+ }).then(() => this.setState({...this.state.config, trackedPeriods }));
+ }
+
+ handlePeriodNameChange = idx => name => {
+ let trackedPeriods = [...this.state.config.trackedPeriods];
+ trackedPeriods[idx].name = name;
+ this.updateTrackedPeriods(trackedPeriods);
+ }
+
+ handlePeriodFromChange = idx => duration => {
+ let trackedPeriods = [...this.state.config.trackedPeriods];
+ trackedPeriods[idx].start = duration;
+ this.updateTrackedPeriods(trackedPeriods);
+ }
+
+ handlePeriodToChange = idx => duration => {
+ let trackedPeriods = [...this.state.config.trackedPeriods];
+ trackedPeriods[idx].end = duration;
+ this.updateTrackedPeriods(trackedPeriods);
+ }
+
render() {
const { classes } = this.props;
return (
@@ -273,6 +371,24 @@ class Settings extends React.Component {
</FormControl>) || 'Please Login.'}
</STableCell>
</TableRow>
+ <TableRow>
+ <STableCell className={classes.tableHead}>
+ Tracked Time Range
+ </STableCell>
+ <STableCell className={classes.tableContent}>
+ {this.state.config.trackedPeriods &&
+ this.state.config.trackedPeriods.map((p, idx) =>
+ <FormGroup>
+ <TrackedPeriod
+ name={p.name}
+ fromDuration={p.start}
+ toDuration={p.end}
+ nameOnChange={this.handlePeriodNameChange(idx)}
+ fromOnChange={this.handlePeriodFromChange(idx)}
+ toOnChange={this.handlePeriodToChange(idx)}/>
+ </FormGroup>)}
+ </STableCell>
+ </TableRow>
</TableBody>
</Table>
</div>
diff --git a/src/background.js b/src/background.js
index e9b0f60..4bee605 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,40 +1,52 @@
import * as gapi from './gapi';
import { msgType, Msg } from './msg';
+import { Duration } from './duration';
let mainPatterns = [];
let analyzePatterns = [];
let calendars = {};
let calData = {};
+let config = {
+ trackedPeriods: [
+ {name: 'Today', start: Duration.days(1), end: Duration.days(0)},
+ {name: 'Yesterday', start: Duration.days(2), end: Duration.days(1)},
+ {name: 'This Week', start: Duration.weeks(1), end: Duration.weeks(0)},
+ {name: 'This Month', start: Duration.months(1), end: Duration.months(0)}]
+};
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name == 'main');
port.onMessage.addListener(function(_msg) {
let msg = Msg.inflate(_msg);
console.log(msg);
- if (msg.type == msgType.updatePatterns) {
+ switch (msg.type) {
+ case msgType.updatePatterns: {
if (msg.data.id == 'analyze')
analyzePatterns = msg.data.patterns;
else
mainPatterns = msg.data.patterns;
port.postMessage(msg.genResp(null));
+ break;
}
- else if (msg.type == msgType.getPatterns) {
+ case msgType.getPatterns: {
let patterns;
if (msg.data.id == 'analyze')
patterns = analyzePatterns;
else
patterns = mainPatterns;
port.postMessage(msg.genResp(patterns));
+ break;
}
- else if (msg.type == msgType.updateCalendars) {
+ case msgType.updateCalendars: {
calendars = msg.data;
for (let id in calendars) {
if (!calData.hasOwnProperty(id))
calData[id] = new gapi.GCalendar(id, calendars[id].summary);
}
port.postMessage(msg.genResp(null));
+ break;
}
- else if (msg.type == msgType.getCalendars) {
+ case msgType.getCalendars: {
let cals = calendars;
if (msg.data.enabledOnly)
{
@@ -43,8 +55,9 @@ chrome.runtime.onConnect.addListener(function(port) {
.reduce((res, id) => (res[id] = calendars[id], res), {});
}
port.postMessage(msg.genResp(cals));
+ break;
}
- else if (msg.type == msgType.getCalEvents) {
+ 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);
@@ -62,9 +75,21 @@ chrome.runtime.onConnect.addListener(function(port) {
console.log(resp);
port.postMessage(resp);
});
+ break;
}
- else {
- console.error("unknown msg type");
+ case msgType.updateConfig: {
+ for (let prop in msg.data)
+ config[prop] = msg.data[prop];
+ port.postMessage(msg.genResp(null));
+ break;
+ }
+ case msgType.getConfig: {
+ let res = {};
+ msg.data.forEach(prop => res[prop] = config[prop]);
+ port.postMessage(msg.genResp(res));
+ break;
+ }
+ default: console.error("unknown msg type");
}
});
});
diff --git a/src/duration.js b/src/duration.js
new file mode 100644
index 0000000..53de0ad
--- /dev/null
+++ b/src/duration.js
@@ -0,0 +1,21 @@
+import moment from 'moment';
+
+export class Duration {
+ constructor(value, unit) {
+ this.value = value
+ this.unit = unit
+ }
+
+ toMoment() {
+ let m = moment.duration(this.value, this.unit);
+ if (m.isValid()) return m;
+ return null;
+ }
+
+ static days(n) { return new Duration(n, 'days'); }
+ static weeks(n) { return new Duration(n, 'weeks'); }
+ static months(n) { return new Duration(n, 'months'); }
+
+ deflate() { return { value: this.value, unit: this.unit }; }
+ static inflate = obj => new Duration(obj.value, obj.unit);
+}
diff --git a/src/msg.js b/src/msg.js
index 25cd53c..ce83eb8 100644
--- a/src/msg.js
+++ b/src/msg.js
@@ -4,6 +4,8 @@ const _getPatterns = "getPatterns";
const _updateCalendars = "updateCalendars";
const _getCalendars = "getCalendars";
const _getCalEvents = "getCalEvents";
+const _updateConfig = "updateConfig";
+const _getConfig = "getConfig";
export const msgType = Object.freeze({
updatePatterns: Symbol(_updatePatterns),
@@ -11,6 +13,8 @@ export const msgType = Object.freeze({
updateCalendars: Symbol(_updateCalendars),
getCalendars: Symbol(_getCalendars),
getCalEvents: Symbol(_getCalEvents),
+ updateConfig: Symbol(_updateConfig),
+ getConfig: Symbol(_getConfig)
});
function stringifyMsgType(mt) {
@@ -20,6 +24,8 @@ function stringifyMsgType(mt) {
case msgType.updateCalendars: return _updateCalendars;
case msgType.getCalendars: return _getCalendars;
case msgType.getCalEvents: return _getCalEvents;
+ case msgType.updateConfig: return _updateConfig;
+ case msgType.getConfig: return _getConfig;
default: console.error("unreachable");
}
}
@@ -31,6 +37,8 @@ function parseMsgType(s) {
case _updateCalendars: return msgType.updateCalendars;
case _getCalendars: return msgType.getCalendars;
case _getCalEvents: return msgType.getCalEvents;
+ case _updateConfig: return msgType.updateConfig;
+ case _getConfig: return msgType.getConfig;
default: console.error("unreachable");
}
}
diff --git a/src/pattern.js b/src/pattern.js
index c7dafbd..b4100e2 100644
--- a/src/pattern.js
+++ b/src/pattern.js
@@ -8,9 +8,17 @@ export class Pattern {
get regex() { return new RegExp(this.isRegex ? this.value : `^${this.value}$`); }
get isEmpty() { return this.label === null; }
+ deflate() {
+ return {
+ id: this.id,
+ isRegex: this.isRegex,
+ value: this.value,
+ label: this.label
+ };
+ }
static emptyPattern = () => new Pattern(0, true, '', null);
static anyPattern = () => new Pattern('any', true, '.*', 'Any');
- static revive = obj => new Pattern(obj.id, obj.isRegex, obj.value, obj.label);
+ static inflate = obj => new Pattern(obj.id, obj.isRegex, obj.value, obj.label);
}
export class PatternEntry {
@@ -21,8 +29,17 @@ export class PatternEntry {
this.event = eventPattern;
}
+ deflate() {
+ return {
+ name: this.name,
+ idx: this.idx,
+ cal: this.cal.deflate(),
+ event: this.event.deflate()
+ };
+ }
+
static defaultPatternEntry = (idx) => new PatternEntry('', idx, Pattern.emptyPattern(), Pattern.anyPattern());
- static revive = obj => new PatternEntry(
+ static inflate = obj => new PatternEntry(
obj.name, obj.idx,
- Pattern.revive(obj.cal), Pattern.revive(obj.event));
+ Pattern.inflate(obj.cal), Pattern.inflate(obj.event));
}
diff --git a/src/popup.js b/src/popup.js
index c4daf81..d8e045c 100644
--- a/src/popup.js
+++ b/src/popup.js
@@ -3,19 +3,127 @@ import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
import { MuiThemeProvider } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
+import Typography from '@material-ui/core/Typography';
import theme from './theme';
+import { PatternEntry } from './pattern';
+import { Duration } from './duration';
+import { msgType, MsgClient } from './msg';
+import { getChartData, StyledPatternPieChart } from './Chart';
+import moment from 'moment';
function openOptions() {
chrome.tabs.create({ url: "index.html" });
}
class Popup extends React.Component {
+ state = {
+ patternGraphData: [],
+ };
+ 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) }
+ }));
+ }
+
+ 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 });
+ });
+ }
+
render() {
+ console.log(this.state.patternGraphData);
return (
<MuiThemeProvider theme={theme}>
- <span>No data available.
- <Button variant="contained" color="primary" onClick={openOptions}>Go to Dashboard</Button>
- </span>
+ <Button variant="contained" color="primary" onClick={openOptions}>Dashboard</Button>
+ {
+ this.state.patternGraphData.map((d, idx) => (
+ <div key={idx}>
+ <Typography variant="subtitle1" align="center" color="textPrimary">
+ {this.trackedPeriods[idx].name}
+ </Typography>
+ <Typography variant="caption" align="center">
+ {`${d.start.format('ddd, MMM Do, YYYY')} -
+ ${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>}
+ </div>
+ ))
+ }
</MuiThemeProvider>
);
}