diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Analyze.js (renamed from src/CustomAnalyzer.js) | 111 | ||||
-rw-r--r-- | src/Chart.js | 113 | ||||
-rw-r--r-- | src/Dashboard.js | 4 | ||||
-rw-r--r-- | src/Settings.js | 158 | ||||
-rw-r--r-- | src/background.js | 39 | ||||
-rw-r--r-- | src/duration.js | 21 | ||||
-rw-r--r-- | src/msg.js | 8 | ||||
-rw-r--r-- | src/pattern.js | 23 | ||||
-rw-r--r-- | src/popup.js | 114 |
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); +} @@ -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> ); } |