Merge branch 'master' of github.com:usefathom/fathom

This commit is contained in:
Danny 2018-05-25 14:04:28 +02:00
commit a853a9f831
9 changed files with 1269 additions and 1214 deletions

View File

@ -12,7 +12,35 @@ function padZero(s) {
return s < 10 ? "0" + s : s; return s < 10 ? "0" + s : s;
} }
function padData(startUnix, endUnix, data) { const timeFormats = [
[() => '', function(d, n) {
return true;
}],
[d3.timeFormat("%Y"), function (d, i, n) {
return d.getUTCMonth() === 0 && d.getUTCDate() === 1;;
}],
[d3.timeFormat("%b"), function (d, i, n) {
return ( d.getUTCMonth() > 0 && d.getUTCDate() === 1 );
}],
[d3.timeFormat("%d"), function (d, i, n) {
return ( d.getUTCDate() > 1 ) && n < 32;
}],
[d3.timeFormat("%b %d"), function (d, i, n) {
return i === 0 && d.getUTCDate() > 1;
}]
]
var timeFormatPicker = function (formats, len) {
return function (date, pos) {
var i = formats.length - 1, f = formats[i];
while (!f[1](date, pos, len)) {
f = formats[--i];
}
return f[0](date);
};
};
function prepareData(startUnix, endUnix, data) {
let startDate = new Date(startUnix * 1000); let startDate = new Date(startUnix * 1000);
let endDate = new Date(endUnix * 1000); let endDate = new Date(endUnix * 1000);
let datamap = []; let datamap = [];
@ -25,16 +53,18 @@ function padData(startUnix, endUnix, data) {
}); });
// make sure we have values for each date // make sure we have values for each date
while(startDate < endDate) { let currentDate = startDate;
let date = startDate.getFullYear() + "-" + padZero(startDate.getMonth() + 1) + "-" + padZero(startDate.getDate()); while(currentDate < endDate) {
let data = datamap[date] ? datamap[date] : { let key = currentDate.getFullYear() + "-" + padZero(currentDate.getMonth() + 1) + "-" + padZero(currentDate.getDate());
"Date": date, let data = datamap[key] ? datamap[key] : {
"Pageviews": 0, "Pageviews": 0,
"Visitors": 0, "Visitors": 0,
}; };
// replace Date property with actual date object
data.Date = new Date(currentDate);
newData.push(data); newData.push(data);
startDate.setDate(startDate.getDate() + 1); currentDate.setDate(currentDate.getDate() + 1);
} }
return newData; return newData;
@ -52,15 +82,14 @@ class Chart extends Component {
} }
} }
componentWillReceiveProps(newProps, prevState) { componentWillReceiveProps(newProps) {
if(newProps.before == prevState.before && newProps.after == prevState.after) { if(newProps.before == this.props.before && newProps.after == this.props.after) {
return; return;
} }
this.fetchData(newProps.before, newProps.after); this.fetchData(newProps.before, newProps.after);
} }
@bind @bind
redrawChart() { redrawChart() {
var data = this.state.data; var data = this.state.data;
@ -69,7 +98,7 @@ class Chart extends Component {
return; return;
} }
let padding = { top: 24, right: 12, bottom: 64, left: 40 }; let padding = { top: 12, right: 12, bottom: 24, left: 40 };
let height = Math.max( this.base.clientHeight, 240 ); let height = Math.max( this.base.clientHeight, 240 );
let width = this.base.clientWidth; let width = this.base.clientWidth;
let innerWidth = width - padding.left - padding.right; let innerWidth = width - padding.left - padding.right;
@ -89,33 +118,35 @@ class Chart extends Component {
const t = d3.transition().duration(500).ease(d3.easeQuadOut); const t = d3.transition().duration(500).ease(d3.easeQuadOut);
const max = d3.max(data, (d) => d.Pageviews); const max = d3.max(data, (d) => d.Pageviews);
// axes
let x = d3.scaleBand().range([0, innerWidth]).padding(0.1).domain(data.map((d) => d.Date)),
y = d3.scaleLinear().range([innerHeight, 0]).domain([0, (max*1.1)]),
yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth),
xAxis = d3.axisBottom().scale(x);
graph.append("g") // axes
let x = d3.scaleBand().range([0, innerWidth]).padding(0.1).domain(data.map((d) => d.Date))
let y = d3.scaleLinear().range([innerHeight, 0]).domain([0, (max*1.1)])
let yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth)
let xAxis = d3.axisBottom().scale(x).tickFormat(timeFormatPicker(timeFormats, data.length))
let yTicks = graph.append("g")
.attr("class", "y axis") .attr("class", "y axis")
.call(yAxis); .call(yAxis);
let nxTicks = Math.max(1, Math.round(data.length / 60));
let nxLabels = Math.max(1, Math.round(data.length / 15));
let xTicks = graph.append("g") let xTicks = graph.append("g")
.attr("class", "x axis") .attr("class", "x axis")
.attr('transform', 'translate(0,' + innerHeight + ')') .attr('transform', 'translate(0,' + innerHeight + ')')
.call(xAxis) .call(xAxis)
xTicks.selectAll('g text').style('display', (d, i) => {
return i % nxLabels != 0 ? 'none' : 'block'
}).attr("transform", "rotate(-60)").style("text-anchor", "end"); // hide all "day" ticks if we're watching more than 100 days of data
xTicks.selectAll('g').style('display', (d, i) => { xTicks.selectAll('g').style('display', (d, i) => {
return i % nxTicks != 0 ? 'none' : 'block'; if(data.length > 100 && d.getUTCDate() > 1 ) {
}); return 'none';
}
return '';
})
// tooltip // tooltip
const tip = d3.tip().attr('class', 'd3-tip').html((d) => (` const tip = d3.tip().attr('class', 'd3-tip').html((d) => (`
<div class="tip-heading">${d.Date}</div> <div class="tip-heading">${d.Date.toLocaleDateString()}</div>
<div class="tip-content"> <div class="tip-content">
<div class="tip-pageviews"> <div class="tip-pageviews">
<div class="tip-number">${d.Pageviews}</div> <div class="tip-number">${d.Pageviews}</div>
@ -169,10 +200,9 @@ class Chart extends Component {
this.setState({ this.setState({
loading: false, loading: false,
data: padData(after, before, d), data: prepareData(after, before, d),
}); })
this.redrawChart()
this.redrawChart();
}) })
} }

View File

@ -16,8 +16,8 @@ class CountWidget extends Component {
} }
} }
componentWillReceiveProps(newProps, prevState) { componentWillReceiveProps(newProps, newState) {
if(newProps.before == prevState.before && newProps.after == prevState.after) { if(newProps.before == this.props.before && newProps.after == this.props.after) {
return; return;
} }

View File

@ -21,7 +21,9 @@ const availablePeriods = [
id: 'year', id: 'year',
label: 'This year' label: 'This year'
} }
] ];
const padZero = function(n){return n<10? '0'+n:''+n;}
class DatePicker extends Component { class DatePicker extends Component {
constructor(props) { constructor(props) {
@ -31,6 +33,8 @@ class DatePicker extends Component {
period: props.value, period: props.value,
before: 0, before: 0,
after: 0, after: 0,
startDate: null,
endDate: null,
} }
this.updateDatesFromPeriod(this.state.period) this.updateDatesFromPeriod(this.state.period)
@ -87,17 +91,21 @@ class DatePicker extends Component {
endDate: endDate, endDate: endDate,
before: before, before: before,
after: after, after: after,
picking: '',
}); });
this.props.onChange(this.state); if(!this.timeout) {
this.timeout = window.setTimeout(() => {
this.props.onChange(this.state);
this.timeout = null;
}, 5)
}
} }
@bind @bind
setPeriod(e) { setPeriod(e) {
e.preventDefault(); e.preventDefault();
let newPeriod = e.target.dataset.value; let newPeriod = e.target.getAttribute('data-value');
if( newPeriod === this.state.period) { if( newPeriod === this.state.period) {
return; return;
} }
@ -106,8 +114,7 @@ class DatePicker extends Component {
} }
dateValue(date) { dateValue(date) {
const addZero = function(n){return n<10? '0'+n:''+n;} return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());
return date.getFullYear() + '-' + addZero(date.getMonth() + 1) + '-' + addZero(date.getDate());
} }
@bind @bind

View File

@ -14,7 +14,7 @@ class Notification extends Component {
this.timeout = 0 this.timeout = 0
} }
componentWillReceiveProps(newProps, prevState) { componentWillReceiveProps(newProps) {
if(newProps.message === this.state.message) { if(newProps.message === this.state.message) {
return; return;
} }

View File

@ -5,14 +5,26 @@ import { h, Component } from 'preact';
class Pikadayer extends Component { class Pikadayer extends Component {
componentDidMount() { componentDidMount() {
new Pikaday({ this.pikaday = new Pikaday({
field: this.base, field: this.base,
onSelect: this.props.onSelect, onSelect: this.props.onSelect,
position: 'bottom right', position: 'bottom right',
}) })
} }
componentWillReceiveProps(newProps) {
// make sure pikaday updates if we set a date using one of our presets
if(this.pikaday && newProps.value !== this.props.value) {
this.pikaday.setDate(newProps.value, true)
}
}
componentWillUnmount() {
this.pikaday.destroy()
}
render(props) { render(props) {
return <input {...props} /> return <input />
} }
} }

View File

@ -16,34 +16,26 @@ class Table extends Component {
records: [], records: [],
limit: 15, limit: 15,
loading: true, loading: true,
before: props.before,
after: props.after,
total: 0, total: 0,
} }
} }
componentWillReceiveProps(newProps, prevState) { componentWillReceiveProps(newProps) {
if(newProps.before == prevState.before && newProps.after == prevState.after) { if(newProps.before == this.props.before && newProps.after == this.props.after) {
return; return;
} }
this.setState({ this.fetchRecords(newProps.before, newProps.after);
before: newProps.before,
after: newProps.after,
});
this.fetchRecords();
} }
@bind @bind
fetchRecords() { fetchRecords(before, after) {
this.setState({ loading: true }); this.setState({ loading: true });
let before = this.state.before;
let after = this.state.after;
Client.request(`${this.props.endpoint}?before=${this.state.before}&after=${this.state.after}&limit=${this.state.limit}`) Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${this.state.limit}`)
.then((d) => { .then((d) => {
// request finished; check if timestamp range is still the one user wants to see // request finished; check if timestamp range is still the one user wants to see
if( this.state.before != before || this.state.after != after ) { if( this.props.before != before || this.props.after != after ) {
return; return;
} }
@ -54,7 +46,7 @@ class Table extends Component {
}); });
// fetch totals too // fetch totals too
Client.request(`${this.props.endpoint}/pageviews?before=${this.state.before}&after=${this.state.after}`) Client.request(`${this.props.endpoint}/pageviews?before=${before}&after=${after}`)
.then((d) => { .then((d) => {
this.setState({ this.setState({
total: d total: d

View File

@ -21,9 +21,9 @@ class App extends Component {
}) })
} }
render() { render(props, state) {
// logged-in // logged-in
if( this.state.authenticated ) { if( state.authenticated ) {
return <Dashboard onLogout={this.toggleAuth} /> return <Dashboard onLogout={this.toggleAuth} />
} }

View File

@ -16,12 +16,14 @@ echo "Welcome to the Fathom quick installer. Press CTRL-C at any time to abort."
function download_fathom() { function download_fathom() {
# Download latest version of the Fathom application # Download latest version of the Fathom application
echo "Downloading Fathom" echo "Downloading latest Fathom"
wget -O fathom https://usesfathom.com/downloads/fathom-latest wget -O fathom https://usesfathom.com/downloads/fathom-latest
# Move Fathom to $PATH so we can run the command from anywhere # Move Fathom to $PATH so we can run the command from anywhere
echo "Moving Fathom to /usr/local/bin/fathom"
chmod +x fathom chmod +x fathom
mv fathom /usr/local/bin/fathom mv fathom /usr/local/bin/fathom
FATHOM_PATH=$(command -v fathom) FATHOM_PATH=$(command -v fathom)
echo "Fathom installed to $FATHOM_PATH" echo "Fathom installed to $FATHOM_PATH"
echo "" echo ""
@ -40,6 +42,7 @@ function new_site_dir() {
fi; fi;
if [ ! -e "$SITE_DIR" ]; then if [ ! -e "$SITE_DIR" ]; then
echo "Creating directory $SITE_DIR_ABS"
mkdir -p "$SITE_DIR" mkdir -p "$SITE_DIR"
chmod 755 "$SITE_DIR" chmod 755 "$SITE_DIR"
fi; fi;
@ -83,7 +86,7 @@ function setup_config() {
DATABASE_NAME="fathom" DATABASE_NAME="fathom"
fi; fi;
echo "Creating database $DATABASE_NAME" echo "Creating $DATABASE database $DATABASE_NAME"
mysql --user="$DATABASE_USER" --password="$DATABASE_PASSWORD" --execute="CREATE DATABASE $DATABASE_NAME;" mysql --user="$DATABASE_USER" --password="$DATABASE_PASSWORD" --execute="CREATE DATABASE $DATABASE_NAME;"
# TODO: Add Postgres support # TODO: Add Postgres support
fi; fi;
@ -138,9 +141,14 @@ server {
} }
END END
) )
echo "Creating file /etc/nginx/sites-available/$SERVER_NAME"
echo "$TEMPLATE" > "/etc/nginx/sites-available/$SERVER_NAME" echo "$TEMPLATE" > "/etc/nginx/sites-available/$SERVER_NAME"
ln -s "/etc/nginx/sites-available/$SERVER_NAME" "/etc/nginx/sites-enabled/$SERVER_NAME" || true ln -s "/etc/nginx/sites-available/$SERVER_NAME" "/etc/nginx/sites-enabled/$SERVER_NAME" || true
echo "Testing NGINX configuration"
nginx -t nginx -t
echo "Reloading NGINX"
service nginx reload service nginx reload
echo "" echo ""
} }
@ -178,13 +186,19 @@ WantedBy=multi-user.target
END END
) )
echo "Creating file /etc/systemd/system/$SERVICE_NAME.service"
echo "$TEMPLATE" > "/etc/systemd/system/$SERVICE_NAME.service" echo "$TEMPLATE" > "/etc/systemd/system/$SERVICE_NAME.service"
echo "Reloading systemd service files"
systemctl daemon-reload systemctl daemon-reload
echo "Enabling $SERVICE_NAME at system boot"
systemctl enable "$SERVICE_NAME" systemctl enable "$SERVICE_NAME"
echo "Starting service $SERVICE_NAME"
systemctl start "$SERVICE_NAME" systemctl start "$SERVICE_NAME"
echo "Success! Service $SERVICE_NAME created & started." echo "Success! You can manually start the service using \`systemctl start $SERVICE_NAME\`"
echo "You can manually start the service using systemctl start $SERVICE_NAME"
echo "" echo ""
} }

2304
package-lock.json generated

File diff suppressed because it is too large Load Diff