diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/App.js | 233 | ||||
-rw-r--r-- | src/Chart.js | 80 | ||||
-rw-r--r-- | src/RegexField.js | 79 | ||||
-rw-r--r-- | src/gapi.js | 39 | ||||
-rw-r--r-- | src/pattern.js | 24 |
5 files changed, 246 insertions, 209 deletions
@@ -4,11 +4,10 @@ import 'typeface-roboto'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; import { DateRangePicker } from 'react-dates'; -import { withStyles } from '@material-ui/core/styles'; +import { withStyles, withTheme } 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'; @@ -28,16 +27,15 @@ import Grid from '@material-ui/core/Grid'; import DeleteOutlinedIcon from '@material-ui/icons/DeleteOutlined'; import AddCircleIcon from '@material-ui/icons/AddCircle'; import IconButton from '@material-ui/core/IconButton'; -import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; -import { PieChart, Pie, Cell, Tooltip } from 'recharts'; import Logo from './Logo'; +import * as gapi from './gapi'; +import { Pattern, PatternEntry } from './pattern'; +import PieChart from './Chart'; +import { CalendarField, EventField } from './RegexField'; 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: { @@ -46,52 +44,15 @@ const theme = createMuiTheme({ dark: orange[700], contrastText: "#fff" } + }, + typography: { + useNextVariants: true, } }); -/* 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 => { - if (response.status == 200) - return response.json() - else throw `got response ${response.status}`; - }) - .catch(e => { console.log(e); return []; }) - .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.regex ? p.cal.value : `^${p.cal.value}$`); - return re.test(calName); + return p.cal.regex.test(calName); }); } @@ -129,9 +90,6 @@ const styles = theme => ({ patternTable: { minWidth: 600 }, - pieChart: { - margin: '0 auto', - }, fab: { margin: theme.spacing.unit, }, @@ -143,143 +101,6 @@ const styles = theme => ({ } }); -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}> - <div className={props.classes.patternTableWrapper}> - <PieChart width={400} height={250} className={props.classes.pieChart}> - <Pie data={props.patternGraphData} - cx={200} - cy={125} - outerRadius={60} - fill={deepOrange[300]} - label={customizedLabel}/> - <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/> - </PieChart> - </div> - </Grid> - <Grid item xs={12} lg={6}> - <div className={props.classes.patternTableWrapper}> - <PieChart width={400} height={250} className={props.classes.pieChart}> - <Pie data={props.calendarGraphData} - cx={200} - cy={125} - 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> - </div> - </Grid> - </Grid>); -} - -ChromiclePieChart.propTypes = { - patternGraphData: PropTypes.object.isRequired, - patterncalendarData: PropTypes.object.isRequired, -}; - -class RegexField extends React.Component { - render() { - var pitems = this.props.items; - let items = []; - pitems['0'] = {regex: true, label: null, id: '0'}; - for (let id in pitems) - { - items.push( - <MenuItem key={id} value={id}> - {pitems[id].label != null ? pitems[id].label : - <span style={{color: theme.palette.primary.dark}}>Custom</span>} - </MenuItem>); - } - return ( - <FormControl> - <span> - <Select - value={this.props.value.id} - onChange={event => { - let value; - if (pitems[event.target.value].label == null) { - if (this.props.value.regex) - value = {regex: true, value: this.props.value.value, label: null, id: '0'}; - else - value = {regex: true, value: `^${this.props.value.value}$`, label: null, id: '0'}; - } else { - value = pitems[event.target.value]; - } - this.props.onChange({target: {value}}); - }} - className={this.props.value.regex ? - this.props.fieldStyles.regex : - this.props.fieldStyles.noRegex}>{items}</Select> - {this.props.value.label == null && ( - <TextField - value={this.props.value.value} - onChange={event => - this.props.onChange({target: { value: {regex: true, value: event.target.value, label: null, id: '0'}}})} /> - )} - </span> - </FormControl> - ); - } -} - -function CalendarField(props) { - let items = {}; - for (let id in props.cached.calendars) { - items[id] = { - regex: false, - value: props.cached.calendars[id].name, - label: props.cached.calendars[id].name, id} - } - return ( - <RegexField - value={props.value} - items={items} - fieldStyles={props.fieldStyles} - onChange={props.onChange} />); -} - -function EventField(props) { - let items = {'any': { - regex: true, - value: '.*', - label: 'Any', id: 'any'}}; - return ( - <RegexField - value={props.value} - items={items} - fieldStyles={props.fieldStyles} - onChange={props.onChange} />); -} - class Dashboard extends React.Component { state = { open: true, @@ -287,7 +108,7 @@ class Dashboard extends React.Component { page: 0, rowsPerPage: 5, timeRange: null, - token: getAuthToken(), + token: gapi.getAuthToken(), patternGraphData: default_chart_data, calendarGraphData: default_chart_data, activePattern: null @@ -299,8 +120,8 @@ class Dashboard extends React.Component { static patternHead = [ {label: "Name", field: "name", elem: TextField}, - {label: "Calendar", field: "cal", elem: CalendarField}, - {label: "Event", field: 'event', elem: EventField}]; + {label: "Calendar", field: "cal", elem: withTheme(theme)(CalendarField)}, + {label: "Event", field: 'event', elem: withTheme(theme)(EventField)}]; handleChangePage = (event, page) => { this.setState({ page }); @@ -325,10 +146,7 @@ class Dashboard extends React.Component { }; newPattern = () => { - let patterns = [{name: '', - cal: { regex: true, label: null, id: '0' }, - event: { regex: true, value: '.*', label: 'Any', id: 'any' }, - idx: 0 }, ...this.state.patterns]; + let patterns = [PatternEntry.defaultPatternEntry(), ...this.state.patterns]; for (let i = 1; i < patterns.length; i++) patterns[i].idx = i; this.setState({ patterns }); @@ -345,7 +163,7 @@ class Dashboard extends React.Component { for (let id in this.cached.calendars) { event_pms.push( this.state.token - .then(genEventsGetter(id, start, end)) + .then(gapi.genEventsGetter(id, start, end)) .then(items => this.cached.calendars[id].events = items)); } @@ -355,13 +173,12 @@ class Dashboard extends React.Component { 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.value), idx: p.idx } }); + let patterns = filterPatterns(this.state.patterns, this.cached.calendars[id].name); 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 (!p.event.regex.test(event.summary)) return; if (cal_results[id] === undefined) { cal_results[id] = 0; } @@ -388,10 +205,10 @@ class Dashboard extends React.Component { loadPatterns = () => { let token = this.state.token; - let colors = token.then(getColors).then(color => { + let colors = token.then(gapi.getColors).then(color => { return color.calendar; }); - let cals = token.then(getCalendars); + let cals = token.then(gapi.getCalendars); Promise.all([colors, cals]).then(([colors, items]) => { items.forEach(item => { this.cached.calendars[item.id] = { @@ -401,10 +218,9 @@ class Dashboard extends React.Component { }; }); this.setState({ patterns: items.map((item, idx) => { - return { name: item.summary, - cal: { regex: false, value: item.summary, label: item.summary, id: item.id }, - event: { regex: true, value: '.*', label: 'Any', id: 'any' }, - idx } + return new PatternEntry(item.summary, idx, + new Pattern(item.id, false, item.summary, item.summary), + Pattern.anyPattern()); })}); }); }; @@ -442,7 +258,7 @@ class Dashboard extends React.Component { <div className={classes.patternTableWrapper}> <Table className={classes.patternTable}> <TableHead> - <TableRow>{Dashboard.patternHead.map(s => (<TableCell>{s.label}</TableCell>))}</TableRow> + <TableRow>{Dashboard.patternHead.map((s, i) => (<TableCell key={i}>{s.label}</TableCell>))}</TableRow> </TableHead> <TableBody> {patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(p => ( @@ -524,10 +340,9 @@ class Dashboard extends React.Component { <Typography variant="h6" component="h1" gutterBottom> Graph </Typography> - <ChromiclePieChart + <PieChart patternGraphData={this.state.patternGraphData} - calendarGraphData={this.state.calendarGraphData} - classes={classes}/> + calendarGraphData={this.state.calendarGraphData}/> </Grid> </Grid> </main> diff --git a/src/Chart.js b/src/Chart.js new file mode 100644 index 0000000..b41b17e --- /dev/null +++ b/src/Chart.js @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import deepOrange from '@material-ui/core/colors/deepOrange'; +import cyan from '@material-ui/core/colors/cyan'; +import { PieChart, Pie, Cell, Tooltip } from 'recharts'; + +const styles = theme => ({ + pieChart: { + margin: '0 auto', + } +}); + +function customizedLabel(props) { + const {cx, cy, x, y, fill, name} = 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}> + <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> + <Grid item xs={12} lg={6}> + <div className={props.classes.patternTableWrapper}> + <PieChart width={400} height={250} className={props.classes.pieChart}> + <Pie data={props.calendarGraphData} + dataKey='value' + cx={200} + cy={125} + innerRadius={40} + outerRadius={70} + fill={cyan[300]} + label={customizedLabel}> + {props.calendarGraphData.map((d, i) => <Cell key={i} fill={d.color}/>)} + </Pie> + <Tooltip formatter={(value) => `${value.toFixed(2)} hr`}/> + </PieChart> + </div> + </Grid> + </Grid>); +} + +ChromiclePieChart.propTypes = { + patternGraphData: PropTypes.array.isRequired, + calendarGraphData: PropTypes.array.isRequired, +}; + +export default withStyles(styles)(ChromiclePieChart); diff --git a/src/RegexField.js b/src/RegexField.js new file mode 100644 index 0000000..104512e --- /dev/null +++ b/src/RegexField.js @@ -0,0 +1,79 @@ +import React from 'react'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; +import TextField from '@material-ui/core/TextField'; +import FormControl from '@material-ui/core/FormControl'; +import { Pattern } from './pattern'; + +class RegexField extends React.Component { + render() { + var pitems = this.props.options; + var p0 = new Pattern.emptyPattern(); + let items = []; + pitems[p0.id] = p0; + for (let id in pitems) + items.push( + <MenuItem key={id} value={id}> + {!pitems[id].isEmpty ? + pitems[id].label : + <span style={{color: this.props.theme.palette.primary.dark}}>Custom</span>} + </MenuItem>); + return ( + <FormControl> + <span> + <Select + value={this.props.value.id} + onChange={event => { + let value; + if (pitems[event.target.value].label == null) { + value = new Pattern(0, true, + this.props.value.isRegex ? + this.props.value.value : + `^${this.props.value.value}$`, null); + } else { + value = pitems[event.target.value]; + } + this.props.onChange({target: {value}}); + }} + className={this.props.value.isRegex ? + this.props.fieldStyles.regex : + this.props.fieldStyles.noRegex}>{items}</Select> + {this.props.value.label == null && ( + <TextField + value={this.props.value.value} + onChange={event => + this.props.onChange({target: { value: new Pattern(0, true, event.target.value, null)}})} /> + )} + </span> + </FormControl>); + } +} + +export function CalendarField(props) { + let options = {}; + for (let id in props.cached.calendars) { + options[id] = new Pattern(id, false, + props.cached.calendars[id].name, + props.cached.calendars[id].name); + } + return ( + <RegexField + value={props.value} + options={options} + fieldStyles={props.fieldStyles} + onChange={props.onChange} + theme={props.theme} />); +} + +export function EventField(props) { + let any = Pattern.anyPattern(); + let options = {}; + options[any.id] = any; + return ( + <RegexField + value={props.value} + options={options} + fieldStyles={props.fieldStyles} + onChange={props.onChange} + theme={props.theme}/>); +} diff --git a/src/gapi.js b/src/gapi.js new file mode 100644 index 0000000..975d4d9 --- /dev/null +++ b/src/gapi.js @@ -0,0 +1,39 @@ +/* global chrome */ +const gapi_base = 'https://www.googleapis.com/calendar/v3'; + +function to_params(dict) { + return Object.entries(dict).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); +} + +export function getAuthToken() { + return new Promise(resolver => + chrome.identity.getAuthToken( + {interactive: true}, token => resolver(token))); +} + +export 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); +} + +export 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 => { + if (response.status === 200) + return response.json() + else throw `got response ${response.status}`; + }) + .catch(e => { console.log(e); return []; }) + .then(data => data.items); +} + +export function getColors(token) { + return fetch(gapi_base + '/colors?' + to_params({access_token: token}), { method: 'GET', async: true }) + .then(response => response.json()); +} diff --git a/src/pattern.js b/src/pattern.js new file mode 100644 index 0000000..ad94253 --- /dev/null +++ b/src/pattern.js @@ -0,0 +1,24 @@ +export class Pattern { + constructor(id, isRegex, value, label) { + this.id = id; + this.isRegex = isRegex; + this.value = value; + this.label = label; + } + + get regex() { return new RegExp(this.isRegex ? this.value : `^${this.value}$`); } + get isEmpty() { return this.label === null; } + static emptyPattern = () => new Pattern(0, true, '', null); + static anyPattern = () => new Pattern('any', true, '.*', 'Any'); +} + +export class PatternEntry { + constructor(name, idx, calPattern, eventPattern) { + this.name = name; + this.idx = idx; + this.cal = calPattern; + this.event = eventPattern; + } + + static defaultPatternEntry = (idx) => new PatternEntry('', idx, Pattern.emptyPattern(), Pattern.anyPattern()); +} |