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:
Danny van Kooten 2018-10-02 11:02:19 +02:00
commit 26d7ba5578
11 changed files with 147 additions and 85 deletions

View File

@ -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. 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. Compile into binary & prepare assets: `make build`
1. (Optional) Set your [custom configuration values](https://github.com/usefathom/fathom/wiki/Configuration-file). 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. 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). To install and run Fathom in production, [have a look at the installation instructions](https://github.com/usefathom/fathom/wiki/Installing-&-running-Fathom).

View File

@ -3,6 +3,7 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as numbers from '../lib/numbers.js'; import * as numbers from '../lib/numbers.js';
import { bind } from 'decko'; import { bind } from 'decko';
import classNames from 'classnames';
const duration = 600; const duration = 600;
const easeOutQuint = function (t) { return 1+(--t)*t*t*t*t }; const easeOutQuint = function (t) { return 1+(--t)*t*t*t*t };
@ -64,7 +65,7 @@ class CountWidget extends Component {
render(props, state) { render(props, state) {
return ( 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-heading">{props.title}</div>
<div class="total-numbers" ref={(e) => { this.numberEl = e; }}>{this.formatValue(props.value)}</div> <div class="total-numbers" ref={(e) => { this.numberEl = e; }}>{this.formatValue(props.value)}</div>
</div> </div>

View File

@ -3,6 +3,7 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from 'decko'; import { bind } from 'decko';
import Pikadayer from './Pikadayer.js'; import Pikadayer from './Pikadayer.js';
import classNames from 'classnames';
const defaultPeriod = 'last-7-days'; const defaultPeriod = 'last-7-days';
const padZero = function(n){return n<10? '0'+n:''+n;} const padZero = function(n){return n<10? '0'+n:''+n;}
@ -189,8 +190,11 @@ class DatePicker extends Component {
render(props, state) { render(props, state) {
const links = Object.keys(availablePeriods).map((id) => { const links = Object.keys(availablePeriods).map((id) => {
let p = availablePeriods[id]; let p = availablePeriods[id];
let className = ( id == state.period ) ? 'active' : ''; return (
return <li class={className} ><a href="#" data-value={id} onClick={this.setPeriod}>{p.label}</a></li> <li class={classNames({ active: id == state.period })}>
<a href="#" data-value={id} onClick={this.setPeriod}>{p.label}</a>
</li>
);
}); });
return ( return (

View File

@ -4,6 +4,7 @@ import { h, Component } from 'preact';
import * as numbers from '../lib/numbers.js'; import * as numbers from '../lib/numbers.js';
import Client from '../lib/client.js'; import Client from '../lib/client.js';
import { bind } from 'decko'; import { bind } from 'decko';
import classNames from 'classnames';
const dayInSeconds = 60 * 60 * 24; const dayInSeconds = 60 * 60 * 24;
@ -59,9 +60,9 @@ class Table extends Component {
const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => { const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => {
let href = (p.Hostname + p.Pathname) || p.URL; let href = (p.Hostname + p.Pathname) || p.URL;
let classes = "table-row"; let widthClass = "";
if(state.total > 0) { 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 let label = p.Pathname
@ -74,7 +75,7 @@ class Table extends Component {
} }
return( return(
<div class={classes}> <div class={classNames("table-row", widthClass)}>
<div class="cell main-col"><a href={href}>{label}</a></div> <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.Pageviews)}</div>
<div class="cell">{numbers.formatPretty(p.Visitors)||"-"}</div> <div class="cell">{numbers.formatPretty(p.Visitors)||"-"}</div>
@ -82,10 +83,10 @@ class Table extends Component {
)}) : <div class="table-row"><div class="cell main-col">Nothing here, yet.</div></div>; )}) : <div class="table-row"><div class="cell main-col">Nothing here, yet.</div></div>;
return ( return (
<div class={(state.loading ? "loading" : '')}> <div class={classNames({ loading: state.loading })}>
<div class="table-row header"> <div class="table-row header">
{props.headers.map((header, i) => { {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>
<div> <div>

View File

@ -38,7 +38,7 @@ func main() {
app.After = after app.After = after
app.Commands = []cli.Command{ app.Commands = []cli.Command{
serverCmd, serverCmd,
registerCmd, userCmd,
statsCmd, statsCmd,
} }

View File

@ -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
View 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
View File

@ -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": { "cliui": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",

View File

@ -22,6 +22,7 @@
"vinyl-source-stream": "^2.0.0" "vinyl-source-stream": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6",
"d3": "^5.4.0", "d3": "^5.4.0",
"d3-tip": "^0.9.1", "d3-tip": "^0.9.1",
"d3-transition": "^1.1.1", "d3-transition": "^1.1.1",

View File

@ -16,6 +16,7 @@ type Datastore interface {
GetUser(int64) (*models.User, error) GetUser(int64) (*models.User, error)
GetUserByEmail(string) (*models.User, error) GetUserByEmail(string) (*models.User, error)
SaveUser(*models.User) error SaveUser(*models.User) error
DeleteUser(*models.User) error
CountUsers() (int64, error) CountUsers() (int64, error)
// site stats // site stats

View File

@ -51,6 +51,13 @@ func (db *sqlstore) SaveUser(u *models.User) error {
return nil 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 // CountUsers returns the number of users
func (db *sqlstore) CountUsers() (int64, error) { func (db *sqlstore) CountUsers() (int64, error) {
var c int64 var c int64