aboutsummaryrefslogtreecommitdiff
path: root/src/App.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/App.js')
-rwxr-xr-xsrc/App.js432
1 files changed, 432 insertions, 0 deletions
diff --git a/src/App.js b/src/App.js
new file mode 100755
index 0000000..f0023f1
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,432 @@
+import 'typeface-roboto';
+import 'react-dates/initialize';
+import 'react-dates/lib/css/_datepicker.css';
+import { DateRangePicker, DayPickerRangeController } from 'react-dates';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { withStyles } from '@material-ui/core/styles';
+import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
+import orange from '@material-ui/core/colors/orange';
+import cyan from '@material-ui/core/colors/cyan';
+import deepOrange from '@material-ui/core/colors/deepOrange';
+import CssBaseline from '@material-ui/core/CssBaseline';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import TextField from '@material-ui/core/TextField';
+import Typography from '@material-ui/core/Typography';
+import Divider from '@material-ui/core/Divider';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableRow from '@material-ui/core/TableRow';
+import TableCell from '@material-ui/core/TableCell';
+import TableHead from '@material-ui/core/TableHead';
+import TablePagination from '@material-ui/core/TablePagination';
+import Paper from '@material-ui/core/Paper';
+import Button from '@material-ui/core/Button';
+import FormControl from '@material-ui/core/FormControl';
+import FormGroup from '@material-ui/core/FormGroup';
+import Grid from '@material-ui/core/Grid';
+import DeleteOutlinedIcon from '@material-ui/icons/DeleteOutlined';
+import { PieChart, Pie, Cell, Tooltip } from 'recharts';
+
+const default_chart_data = [{name: 'Work', value: 10, color: cyan[300]},
+ {name: 'Wasted', value: 10, color: cyan[300]}];
+
+const gapi_base = 'https://www.googleapis.com/calendar/v3';
+
+const theme = createMuiTheme({
+ palette: {
+ primary: {
+ light: orange[300],
+ main: orange[500],
+ dark: orange[700],
+ contrastText: "#fff"
+ }
+ }
+});
+
+/* eslint-disable no-undef */
+
+function to_params(dict) {
+ return Object.entries(dict).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
+}
+
+function getAuthToken() {
+ return new Promise(resolver =>
+ chrome.identity.getAuthToken(
+ {interactive: true}, token => resolver(token)));
+}
+
+function getCalendars(token) {
+ return fetch(gapi_base + '/users/me/calendarList?' + to_params({access_token: token}),
+ { method: 'GET', async: true })
+ .then(response => response.json())
+ .then(data => data.items);
+}
+
+function genEventsGetter(calId, timeMin, timeMax) {
+ return token => fetch(gapi_base + '/calendars/' + calId + '/events?' + to_params({
+ access_token: token,
+ timeMin,
+ timeMax
+ }), { method: 'GET', async: true })
+ .then(response => response.json())
+ .then(data => data.items);
+}
+
+function getColors(token) {
+ return fetch(gapi_base + '/colors?' + to_params({access_token: token}), { method: 'GET', async: true })
+ .then(response => response.json());
+}
+
+function filterPatterns(patterns, calName) {
+ return patterns.filter(p => {
+ let re = new RegExp(p.cal);
+ return re.test(calName);
+ });
+}
+
+const styles = theme => ({
+ root: {
+ display: 'flex',
+ height: '100vh',
+ },
+ icon: {
+ margin: theme.spacing.unit * 2,
+ },
+ appBar: {
+ zIndex: theme.zIndex.drawer + 1,
+ transition: theme.transitions.create(['width', 'margin'], {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ },
+ title: {
+ flexGrow: 1,
+ },
+ sectionTitle: {
+ flex: '0 0 auto'
+ },
+ appBarSpacer: theme.mixins.toolbar,
+ content: {
+ flexGrow: 1,
+ padding: theme.spacing.unit * 3,
+ overflow: 'auto',
+ },
+ buttonSpacer: {
+ marginBottom: theme.spacing.unit * 4,
+ },
+ patternTable: {
+ overflowX: 'auto'
+ },
+ pieChart: {
+ margin: '0 auto'
+ },
+ fab: {
+ margin: theme.spacing.unit,
+ }
+});
+
+function customizedLabel(props) {
+ const {cx, cy, x, y, stroke, fill, name, value} = props;
+ let anchor = "middle";
+ const EPS = 2;
+ let dx = 0;
+ let dy = 0;
+ if (x < cx - EPS) {
+ dx = -5;
+ anchor = "end"
+ } else if (x > cx + EPS) {
+ dx = 5;
+ anchor = "start";
+ }
+
+ if (y < cy - EPS) {
+ dy = -5;
+ } else if (y > cy + EPS) {
+ dy = 10;
+ }
+
+ return (<text x={x} y={y} dx={dx} dy={dy} fill={fill} textAnchor={anchor}>{`${name}`}</text>);
+}
+
+function ChromiclePieChart(props) {
+ return (
+ <Grid container spacing={0}>
+ <Grid item xs={12} lg={6}>
+ <PieChart width={400} height={300} className={props.classes.pieChart}>
+ <Pie data={props.patternGraphData}
+ cx={200}
+ cy={150}
+ outerRadius={60}
+ fill={deepOrange[300]}
+ label={customizedLabel}/>
+ <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/>
+ </PieChart>
+ </Grid>
+ <Grid item xs={12} lg={6}>
+ <PieChart width={400} height={300} className={props.classes.pieChart}>
+ <Pie data={props.calendarGraphData}
+ cx={200}
+ cy={150}
+ innerRadius={40}
+ outerRadius={70}
+ fill={cyan[300]}
+ label={customizedLabel}>
+ {props.calendarGraphData.map(d => <Cell fill={d.color}/>)}
+ </Pie>
+ <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/>
+ </PieChart>
+ </Grid>
+ </Grid>);
+}
+
+ChromiclePieChart.propTypes = {
+ patternGraphData: PropTypes.object.isRequired,
+ patterncalendarData: PropTypes.object.isRequired,
+};
+
+class Dashboard extends React.Component {
+ state = {
+ open: true,
+ patterns: [],
+ page: 0,
+ rowsPerPage: 5,
+ timeRange: null,
+ token: getAuthToken(),
+ patternGraphData: default_chart_data,
+ calendarGraphData: default_chart_data,
+ activePattern: null
+ };
+
+ cached = {
+ calendars: {}
+ };
+
+ static patternHead = [
+ {label: "Name", field: "name"},
+ {label: "Calendar", field: "cal"},
+ {label: "Event", field: 'event'}];
+
+ handleChangePage = (event, page) => {
+ this.setState({ page });
+ };
+
+ handleChangeRowsPerPage = event => {
+ this.setState({ rowsPerPage: event.target.value });
+ };
+
+ updatePattern = (field, idx, value) => {
+ let patterns = this.state.patterns;
+ patterns[idx][field] = value;
+ this.setState({ 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.setState({ patterns });
+ };
+
+ analyze = () => {
+ if (!(this.state.startDate && this.state.endDate)) {
+ alert("Please choose a valid time range.");
+ return;
+ }
+ let start = this.state.startDate.toISOString();
+ let end = this.state.endDate.toISOString();
+ let event_pms = [];
+ for (let id in this.cached.calendars) {
+ event_pms.push(
+ this.state.token
+ .then(genEventsGetter(id, start, end))
+ .then(items => this.cached.calendars[id].events = items));
+ }
+
+ Promise.all(event_pms).then(() => {
+ let results = {}; // pattern idx => time
+ let cal_results = {}; // cal id => time
+ for (let i = 0; i < this.state.patterns.length; i++)
+ results[i] = 0;
+ for (let id in this.cached.calendars) {
+ let patterns = filterPatterns(this.state.patterns, this.cached.calendars[id].name)
+ .map(p => { return { regex: new RegExp(p.event), idx: p.idx } });
+ if (!this.cached.calendars[id].events) continue;
+ this.cached.calendars[id].events.forEach(event => {
+ if (event.status != "confirmed") return;
+ patterns.forEach(p => {
+ if (!p.regex.test(event.summary)) return;
+ if (cal_results[id] === undefined) {
+ cal_results[id] = 0;
+ }
+ let duration = (new Date(event.end.dateTime) - new Date(event.start.dateTime)) / 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: this.cached.calendars[id].name,
+ value: (cal_results[id] / 60.0),
+ color: this.cached.calendars[id].color.background});
+ }
+ this.setState({ patternGraphData, calendarGraphData });
+ });
+ };
+
+ loadPatterns = () => {
+ let token = this.state.token;
+ let colors = token.then(getColors).then(color => {
+ return color.calendar;
+ });
+ let cals = token.then(getCalendars);
+ Promise.all([colors, cals]).then(([colors, items]) => {
+ items.forEach(item => {
+ this.cached.calendars[item.id] = {
+ name: item.summary,
+ events: {},
+ color: colors[item.colorId]
+ };
+ });
+ this.setState({ patterns: items.map((item, idx) => {
+ return { name: item.summary,
+ cal: item.summary,
+ event: '.*',
+ idx }
+ })});
+ });
+ };
+
+ render() {
+ const { classes } = this.props;
+ const { patterns, rows, rowsPerPage, page } = this.state;
+ const nDummy = rowsPerPage - Math.min(rowsPerPage, patterns.length - page * rowsPerPage);
+
+ return (
+ <MuiThemeProvider theme={theme}>
+ <div className={classes.root}>
+ <AppBar
+ position="absolute"
+ className={classes.appBar}>
+ <Toolbar disableGutters={!this.state.open} className={classes.toolbar}>
+ <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}>
+ Chromicle
+ </Typography>
+ </Toolbar>
+ </AppBar>
+ <main className={classes.content}>
+ <div className={classes.appBarSpacer} />
+ <Grid container spacing={16}>
+ <CssBaseline />
+ <Grid item md={6} xs={12}>
+ <FormControl fullWidth={true}>
+ <FormGroup>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Event Patterns
+ </Typography>
+ <Table>
+ <TableHead>
+ <TableRow>{Dashboard.patternHead.map(s => (<TableCell>{s.label}</TableCell>))}</TableRow>
+ </TableHead>
+ <TableBody>
+ {patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(p => (
+ <TableRow
+ onMouseOver={() => this.setState({ activePattern: p.idx })}
+ onMouseOut={() => this.setState({ activePattern: null })}>
+ {Dashboard.patternHead.map(s => (
+ <TableCell>
+ <TextField
+ value={p[s.field]}
+ onChange={event => this.updatePattern(s.field, p.idx, event.target.value)}/>
+ </TableCell>))}
+ <span style={(this.state.activePattern == p.idx &&
+ { position: 'absolute', right: 0, height: 48 }) ||
+ { display: 'none' }}>
+ <DeleteOutlinedIcon
+ className={classes.icon}
+ style={{ height: '100%', cursor: 'pointer' }}
+ onClick={() => this.removePattern(p.idx)} />
+ </span>
+ </TableRow>))}
+ {nDummy > 0 && (
+ <TableRow style={{ height: 48 * nDummy }}>
+ <TableCell colSpan={Dashboard.patternHead.length} />
+ </TableRow>)}
+ </TableBody>
+ </Table>
+ <TablePagination
+ rowsPerPageOptions={[5, 10, 25]}
+ component="div"
+ count={patterns.length}
+ rowsPerPage={rowsPerPage}
+ page={page}
+ backIconButtonProps={{'aria-label': 'Previous Page'}}
+ nextIconButtonProps={{'aria-label': 'Next Page'}}
+ onChangePage={this.handleChangePage}
+ onChangeRowsPerPage={this.handleChangeRowsPerPage}/>
+ </FormGroup>
+ <FormGroup>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Time Range
+ </Typography>
+ <div style={{textAlign: 'center'}}>
+ <DateRangePicker
+ startDate={this.state.startDate} // momentPropTypes.momentObj or null,
+ startDateId="your_unique_start_date_id" // PropTypes.string.isRequired,
+ endDate={this.state.endDate} // momentPropTypes.momentObj or null,
+ endDateId="your_unique_end_date_id" // PropTypes.string.isRequired,
+ onDatesChange={({ startDate, endDate }) => {
+ //if (startDate && endDate)
+ // this.setState({ timeRange: [startDate.toISOString(), endDate.toISOString()]});
+ this.setState({ startDate, endDate });
+ }} // PropTypes.func.isRequired,
+ focusedInput={this.state.focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null,
+ onFocusChange={focusedInput => this.setState({ focusedInput })} // PropTypes.func.isRequired,
+ isOutsideRange={() => false}/>
+ </div>
+ </FormGroup>
+ <div className={classes.buttonSpacer} />
+ <Grid container spacing={16}>
+ <Grid item md={6} xs={12}>
+ <FormGroup>
+ <Button variant="contained" color="primary" onClick={this.loadPatterns}>Load</Button>
+ </FormGroup>
+ </Grid>
+ <Grid item md={6} xs={12}>
+ <FormGroup>
+ <Button variant="contained" color="primary" onClick={this.analyze}>Analyze</Button>
+ </FormGroup>
+ </Grid>
+ </Grid>
+ </FormControl>
+ </Grid>
+ <Grid item md={6} xs={12}>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Graph
+ </Typography>
+ <ChromiclePieChart
+ patternGraphData={this.state.patternGraphData}
+ calendarGraphData={this.state.calendarGraphData}
+ classes={classes}/>
+ </Grid>
+ </Grid>
+ </main>
+ </div>
+ </MuiThemeProvider>);
+ }
+}
+
+Dashboard.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(Dashboard);