mirror of
https://github.com/status-im/fathom.git
synced 2025-03-01 03:20:27 +00:00
Merge branch 'master' into multiple-sites
* master:
use classnames to determine main-col
add actual lib 😅
use classNames for table row width
use classname for active state
use classnames for loading state
add classnames as a dependency
update README on new user command
rework user cli management
This commit is contained in:
commit
26d7ba5578
@ -26,7 +26,7 @@ For getting a development version of Fathom up & running, go through the followi
|
||||
1. Get code: `git clone https://github.com/usefathom/fathom.git $GOPATH/src/github.com/usefathom/fathom`
|
||||
1. Compile into binary & prepare assets: `make build`
|
||||
1. (Optional) Set your [custom configuration values](https://github.com/usefathom/fathom/wiki/Configuration-file).
|
||||
1. Register your user account: `fathom register --email=<email> --password=<password>`
|
||||
1. Register your user account: `fathom user add --email=<email> --password=<password>`
|
||||
1. Start the webserver: `fathom server` and then visit **http://localhost:8080** to access your analytics dashboard.
|
||||
|
||||
To install and run Fathom in production, [have a look at the installation instructions](https://github.com/usefathom/fathom/wiki/Installing-&-running-Fathom).
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { h, Component } from 'preact';
|
||||
import * as numbers from '../lib/numbers.js';
|
||||
import { bind } from 'decko';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const duration = 600;
|
||||
const easeOutQuint = function (t) { return 1+(--t)*t*t*t*t };
|
||||
@ -17,8 +18,8 @@ class CountWidget extends Component {
|
||||
}
|
||||
|
||||
// TODO: Move to component of its own
|
||||
@bind
|
||||
countUp(fromValue, toValue) {
|
||||
@bind
|
||||
countUp(fromValue, toValue) {
|
||||
const format = this.formatValue.bind(this);
|
||||
const startValue = isFinite(fromValue) ? fromValue : 0;
|
||||
const numberEl = this.numberEl;
|
||||
@ -34,7 +35,7 @@ class CountWidget extends Component {
|
||||
window.requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
@ -46,7 +47,7 @@ class CountWidget extends Component {
|
||||
switch(this.props.format) {
|
||||
case "percentage":
|
||||
formattedValue = numbers.formatPercentage(value)
|
||||
break;
|
||||
break;
|
||||
|
||||
default:
|
||||
case "number":
|
||||
@ -64,7 +65,7 @@ class CountWidget extends Component {
|
||||
|
||||
render(props, state) {
|
||||
return (
|
||||
<div class={"totals-detail " + ( props.loading ? "loading" : '')}>
|
||||
<div class={classNames("totals-detail", { loading: props.loading })}>
|
||||
<div class="total-heading">{props.title}</div>
|
||||
<div class="total-numbers" ref={(e) => { this.numberEl = e; }}>{this.formatValue(props.value)}</div>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from 'decko';
|
||||
import Pikadayer from './Pikadayer.js';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const defaultPeriod = 'last-7-days';
|
||||
const padZero = function(n){return n<10? '0'+n:''+n;}
|
||||
@ -24,7 +25,7 @@ function getNow() {
|
||||
|
||||
// today, yesterday, this week, last 7 days, last 30 days
|
||||
const availablePeriods = {
|
||||
'today': {
|
||||
'today': {
|
||||
label: 'Today',
|
||||
start: function() {
|
||||
const now = getNow();
|
||||
@ -35,7 +36,7 @@ const availablePeriods = {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
},
|
||||
},
|
||||
'last-7-days': {
|
||||
'last-7-days': {
|
||||
label: 'Last 7 days',
|
||||
start: function() {
|
||||
const now = getNow();
|
||||
@ -46,7 +47,7 @@ const availablePeriods = {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
},
|
||||
},
|
||||
'last-30-days': {
|
||||
'last-30-days': {
|
||||
label: 'Last 30 days',
|
||||
start: function() {
|
||||
const now = getNow();
|
||||
@ -57,7 +58,7 @@ const availablePeriods = {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
},
|
||||
},
|
||||
'this-year': {
|
||||
'this-year': {
|
||||
label: 'This year',
|
||||
start: function() {
|
||||
const now = getNow();
|
||||
@ -117,7 +118,7 @@ class DatePicker extends Component {
|
||||
period: period || '',
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
before: before,
|
||||
before: before,
|
||||
after: after,
|
||||
});
|
||||
|
||||
@ -127,7 +128,7 @@ class DatePicker extends Component {
|
||||
this.props.onChange(this.state);
|
||||
this.timeout = null;
|
||||
|
||||
window.localStorage.setItem('period', this.state.period)
|
||||
window.localStorage.setItem('period', this.state.period)
|
||||
window.history.replaceState(this.state, null, `#!${this.state.period}`)
|
||||
}, 2)
|
||||
}
|
||||
@ -149,12 +150,12 @@ class DatePicker extends Component {
|
||||
return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());
|
||||
}
|
||||
|
||||
@bind
|
||||
@bind
|
||||
setStartDate(date) {
|
||||
this.setDateRange(date, this.state.endDate, '')
|
||||
}
|
||||
|
||||
@bind
|
||||
@bind
|
||||
setEndDate(date) {
|
||||
this.setDateRange(this.state.startDate, date, '')
|
||||
}
|
||||
@ -189,8 +190,11 @@ class DatePicker extends Component {
|
||||
render(props, state) {
|
||||
const links = Object.keys(availablePeriods).map((id) => {
|
||||
let p = availablePeriods[id];
|
||||
let className = ( id == state.period ) ? 'active' : '';
|
||||
return <li class={className} ><a href="#" data-value={id} onClick={this.setPeriod}>{p.label}</a></li>
|
||||
return (
|
||||
<li class={classNames({ active: id == state.period })}>
|
||||
<a href="#" data-value={id} onClick={this.setPeriod}>{p.label}</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -198,7 +202,7 @@ class DatePicker extends Component {
|
||||
{links}
|
||||
<li class="custom">
|
||||
<Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} />
|
||||
<span style="margin: 0 8px"> to </span>
|
||||
<span style="margin: 0 8px"> to </span>
|
||||
<Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate} />
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -4,6 +4,7 @@ import { h, Component } from 'preact';
|
||||
import * as numbers from '../lib/numbers.js';
|
||||
import Client from '../lib/client.js';
|
||||
import { bind } from 'decko';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const dayInSeconds = 60 * 60 * 24;
|
||||
|
||||
@ -31,7 +32,7 @@ class Table extends Component {
|
||||
@bind
|
||||
fetchRecords(before, after) {
|
||||
this.setState({ loading: true });
|
||||
|
||||
|
||||
Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${this.state.limit}`)
|
||||
.then((d) => {
|
||||
// request finished; check if timestamp range is still the one user wants to see
|
||||
@ -39,7 +40,7 @@ class Table extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
this.setState({
|
||||
loading: false,
|
||||
records: d,
|
||||
});
|
||||
@ -48,7 +49,7 @@ class Table extends Component {
|
||||
// fetch totals too
|
||||
Client.request(`${this.props.endpoint}/pageviews?before=${before}&after=${after}`)
|
||||
.then((d) => {
|
||||
this.setState({
|
||||
this.setState({
|
||||
total: d
|
||||
});
|
||||
});
|
||||
@ -59,9 +60,9 @@ class Table extends Component {
|
||||
const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => {
|
||||
|
||||
let href = (p.Hostname + p.Pathname) || p.URL;
|
||||
let classes = "table-row";
|
||||
let widthClass = "";
|
||||
if(state.total > 0) {
|
||||
classes += " w" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5));
|
||||
widthClass = "w" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5));
|
||||
}
|
||||
|
||||
let label = p.Pathname
|
||||
@ -74,19 +75,19 @@ class Table extends Component {
|
||||
}
|
||||
|
||||
return(
|
||||
<div class={classes}>
|
||||
<div class={classNames("table-row", widthClass)}>
|
||||
<div class="cell main-col"><a href={href}>{label}</a></div>
|
||||
<div class="cell">{numbers.formatPretty(p.Pageviews)}</div>
|
||||
<div class="cell">{numbers.formatPretty(p.Visitors)||"-"}</div>
|
||||
<div class="cell">{numbers.formatPretty(p.Visitors)||"-"}</div>
|
||||
</div>
|
||||
)}) : <div class="table-row"><div class="cell main-col">Nothing here, yet.</div></div>;
|
||||
|
||||
return (
|
||||
<div class={(state.loading ? "loading" : '')}>
|
||||
<div class={classNames({ loading: state.loading })}>
|
||||
<div class="table-row header">
|
||||
{props.headers.map((header, i) => {
|
||||
return (<div class={i === 0 ? 'main-col cell' : 'cell'}>{header}</div>)
|
||||
})}
|
||||
return <div class={classNames("cell", { "main-col": i === 0 })}>{header}</div>
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{tableRows}
|
||||
|
@ -38,7 +38,7 @@ func main() {
|
||||
app.After = after
|
||||
app.Commands = []cli.Command{
|
||||
serverCmd,
|
||||
registerCmd,
|
||||
userCmd,
|
||||
statsCmd,
|
||||
}
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/usefathom/fathom/pkg/models"
|
||||
)
|
||||
|
||||
var registerCmd = cli.Command{
|
||||
Name: "register",
|
||||
Usage: "register a new admin user",
|
||||
Action: register,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "email, e",
|
||||
Usage: "user email",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: "user password",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-bcrypt",
|
||||
Usage: "store password string as is, skipping bcrypt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func register(c *cli.Context) error {
|
||||
email := c.String("email")
|
||||
if email == "" {
|
||||
return errors.New("Invalid arguments: missing email")
|
||||
}
|
||||
|
||||
password := c.String("password")
|
||||
if password == "" {
|
||||
return errors.New("Invalid arguments: missing password")
|
||||
}
|
||||
|
||||
user := models.NewUser(email, password)
|
||||
|
||||
// set password manually if --skip-bcrypt was given
|
||||
// this is used to supply an already encrypted password string
|
||||
if c.Bool("skip-bcrypt") {
|
||||
user.Password = password
|
||||
}
|
||||
|
||||
if err := app.database.SaveUser(&user); err != nil {
|
||||
return fmt.Errorf("Error creating user: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("Created user %s", user.Email)
|
||||
return nil
|
||||
}
|
99
cmd/fathom/user.go
Normal file
99
cmd/fathom/user.go
Normal file
@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/usefathom/fathom/pkg/datastore"
|
||||
"github.com/usefathom/fathom/pkg/models"
|
||||
)
|
||||
|
||||
var userCmd = cli.Command{
|
||||
Name: "user",
|
||||
Usage: "manage registered admin users",
|
||||
Action: userAdd,
|
||||
Subcommands: []cli.Command{
|
||||
cli.Command{
|
||||
Name: "add",
|
||||
Aliases: []string{"register"},
|
||||
Action: userAdd,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "email, e",
|
||||
Usage: "user email",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: "user password",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-bcrypt",
|
||||
Usage: "store password string as-is, skipping bcrypt",
|
||||
},
|
||||
},
|
||||
},
|
||||
cli.Command{
|
||||
Name: "delete",
|
||||
Action: userDelete,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "email, e",
|
||||
Usage: "user email",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func userAdd(c *cli.Context) error {
|
||||
email := c.String("email")
|
||||
if email == "" {
|
||||
return errors.New("Invalid arguments: missing email")
|
||||
}
|
||||
|
||||
password := c.String("password")
|
||||
if password == "" {
|
||||
return errors.New("Invalid arguments: missing password")
|
||||
}
|
||||
|
||||
user := models.NewUser(email, password)
|
||||
|
||||
// set password manually if --skip-bcrypt was given
|
||||
// this is used to supply an already encrypted password string
|
||||
if c.Bool("skip-bcrypt") {
|
||||
user.Password = password
|
||||
}
|
||||
|
||||
if err := app.database.SaveUser(&user); err != nil {
|
||||
return fmt.Errorf("Error creating user: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("Created user %s", user.Email)
|
||||
return nil
|
||||
}
|
||||
|
||||
func userDelete(c *cli.Context) error {
|
||||
email := c.String("email")
|
||||
if email == "" {
|
||||
return errors.New("Invalid arguments: missing email")
|
||||
}
|
||||
|
||||
user, err := app.database.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
if err == datastore.ErrNoResults {
|
||||
return fmt.Errorf("No user with email %s", email)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.database.DeleteUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Deleted user %s", user.Email)
|
||||
|
||||
return nil
|
||||
}
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -1649,6 +1649,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||
},
|
||||
"cliui": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
|
||||
|
@ -22,6 +22,7 @@
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"d3": "^5.4.0",
|
||||
"d3-tip": "^0.9.1",
|
||||
"d3-transition": "^1.1.1",
|
||||
|
@ -16,6 +16,7 @@ type Datastore interface {
|
||||
GetUser(int64) (*models.User, error)
|
||||
GetUserByEmail(string) (*models.User, error)
|
||||
SaveUser(*models.User) error
|
||||
DeleteUser(*models.User) error
|
||||
CountUsers() (int64, error)
|
||||
|
||||
// site stats
|
||||
|
@ -51,6 +51,13 @@ func (db *sqlstore) SaveUser(u *models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user in the datastore
|
||||
func (db *sqlstore) DeleteUser(user *models.User) error {
|
||||
query := db.Rebind("DELETE FROM users WHERE id = ?")
|
||||
_, err := db.Exec(query, user.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountUsers returns the number of users
|
||||
func (db *sqlstore) CountUsers() (int64, error) {
|
||||
var c int64
|
||||
|
Loading…
x
Reference in New Issue
Block a user