Fix merge conflicts
This commit is contained in:
commit
b333d05609
23
admin.go
23
admin.go
|
@ -3,8 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx/types"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,10 +17,8 @@ type configScript struct {
|
||||||
Messengers []string `json:"messengers"`
|
Messengers []string `json:"messengers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetStats returns a collection of general statistics.
|
type dashboardStats struct {
|
||||||
func handleGetStats(c echo.Context) error {
|
Stats types.JSONText `db:"stats"`
|
||||||
app := c.Get("app").(*App)
|
|
||||||
return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetConfigScript returns general configuration as a Javascript
|
// handleGetConfigScript returns general configuration as a Javascript
|
||||||
|
@ -41,3 +41,18 @@ func handleGetConfigScript(c echo.Context) error {
|
||||||
j.Encode(out)
|
j.Encode(out)
|
||||||
return c.Blob(http.StatusOK, "application/javascript", b.Bytes())
|
return c.Blob(http.StatusOK, "application/javascript", b.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetDashboardStats returns general states for the dashboard.
|
||||||
|
func handleGetDashboardStats(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
out dashboardStats
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := app.Queries.GetDashboardStats.Get(&out); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, okResp{out.Stats})
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"antd": "^3.6.5",
|
"antd": "^3.6.5",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"bizcharts": "^3.2.5-beta.4",
|
||||||
"dayjs": "^1.7.5",
|
"dayjs": "^1.7.5",
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"react-app-rewire-less": "^2.1.3",
|
"react-app-rewire-less": "^2.1.3",
|
||||||
|
|
|
@ -1,12 +1,121 @@
|
||||||
import React from 'react';
|
import { Col, Row, notification, Card, Tooltip, Icon } from "antd"
|
||||||
|
import React from "react";
|
||||||
|
import { Chart, Axis, Geom, Tooltip as BizTooltip } from 'bizcharts';
|
||||||
|
|
||||||
|
import * as cs from "./constants"
|
||||||
|
|
||||||
class Dashboard extends React.PureComponent {
|
class Dashboard extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
stats: null
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignTypes = ["running", "finished", "paused", "draft", "scheduled", "cancelled"]
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
this.props.pageTitle("Dashboard")
|
this.props.pageTitle("Dashboard")
|
||||||
|
|
||||||
|
this.props.request(cs.Routes.GetDashboarcStats, cs.MethodGet).then((resp) => {
|
||||||
|
this.setState({ stats: resp.data.data })
|
||||||
|
}).catch(e => {
|
||||||
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orZero(v) {
|
||||||
|
return v ? v : 0
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
<section className = "dashboard">
|
||||||
<h1>Welcome</h1>
|
<h1>Welcome</h1>
|
||||||
|
<hr />
|
||||||
|
{ this.state.stats &&
|
||||||
|
<div className="stats">
|
||||||
|
<Row>
|
||||||
|
<Col span={ 16 }>
|
||||||
|
<Row gutter={ 24 }>
|
||||||
|
<Col span={ 8 }>
|
||||||
|
<Card title="Active subscribers" bordered={ false }>
|
||||||
|
<h1 className="count">{ this.orZero(this.state.stats.subscribers.enabled) }</h1>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={ 8 }>
|
||||||
|
<Card title="Blacklisted subscribers" bordered={ false }>
|
||||||
|
<h1 className="count">{ this.orZero(this.state.stats.subscribers.blacklisted) }</h1>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={ 8 }>
|
||||||
|
<Card title="Orphaned subscribers" bordered={ false }>
|
||||||
|
<h1 className="count">{ this.orZero(this.state.stats.orphan_subscribers) }</h1>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={ 6 } offset={ 2 }>
|
||||||
|
<Row gutter={ 24 }>
|
||||||
|
<Col span={ 12 }>
|
||||||
|
<Card title="Public lists" bordered={ false }>
|
||||||
|
<h1 className="count">{ this.orZero(this.state.stats.lists.public) }</h1>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={ 12 }>
|
||||||
|
<Card title="Private lists" bordered={ false }>
|
||||||
|
<h1 className="count">{ this.orZero(this.state.stats.lists.private) }</h1>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<hr />
|
||||||
|
<Row>
|
||||||
|
<Col span={ 16 }>
|
||||||
|
<Row gutter={ 24 }>
|
||||||
|
<Col span={ 12 }>
|
||||||
|
<Card title="Campaign views (last 3 months)" bordered={ false }>
|
||||||
|
<h1 className="count">
|
||||||
|
{ this.state.stats.campaign_views.reduce((total, v) => total + v.count, 0) }
|
||||||
|
{' '}
|
||||||
|
views
|
||||||
|
</h1>
|
||||||
|
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.campaign_views } forceFit>
|
||||||
|
<BizTooltip crosshairs={{ type : "y" }} />
|
||||||
|
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
||||||
|
<Geom type='point' position="date*count" size={ 0 } />
|
||||||
|
</Chart>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={ 12 }>
|
||||||
|
<Card title="Link clicks (last 3 months)" bordered={ false }>
|
||||||
|
<h1 className="count">
|
||||||
|
{ this.state.stats.link_clicks.reduce((total, v) => total + v.count, 0) }
|
||||||
|
{' '}
|
||||||
|
clicks
|
||||||
|
</h1>
|
||||||
|
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.link_clicks } forceFit>
|
||||||
|
<BizTooltip crosshairs={{ type : "y" }} />
|
||||||
|
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
||||||
|
<Geom type='point' position="date*count" size={ 0 } />
|
||||||
|
</Chart>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={ 6 } offset={ 2 }>
|
||||||
|
<Card title="Campaigns" bordered={ false } className="campaign-counts">
|
||||||
|
{ this.campaignTypes.map((key, count) =>
|
||||||
|
<Row key={ `stats-campaigns-${ key }` }>
|
||||||
|
<Col span={ 18 }><h1 className="name">{ key }</h1></Col>
|
||||||
|
<Col span={ 6 }><h1 className="count">{ count }</h1></Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const SubscriptionStatusUnsubscribed = "unsubscribed"
|
||||||
|
|
||||||
// API routes.
|
// API routes.
|
||||||
export const Routes = {
|
export const Routes = {
|
||||||
|
GetDashboarcStats: "/api/dashboard/stats",
|
||||||
GetUsers: "/api/users",
|
GetUsers: "/api/users",
|
||||||
|
|
||||||
GetLists: "/api/lists",
|
GetLists: "/api/lists",
|
||||||
|
|
|
@ -48,11 +48,14 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body {
|
.content-body {
|
||||||
background: #fff;
|
|
||||||
padding: 24px;
|
|
||||||
min-height: 90vh;
|
min-height: 90vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section.content {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
@ -69,10 +72,19 @@ body {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
font-size: .85em !important;
|
||||||
|
color: #999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.broken {
|
.broken {
|
||||||
margin: 100px;
|
margin: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form */
|
/* Form */
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,6 +102,14 @@ td .ant-tag {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dashboard */
|
||||||
|
.dashboard {
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
.dashboard .campaign-counts .name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
/* Templates */
|
/* Templates */
|
||||||
.wysiwyg {
|
.wysiwyg {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
|
1
main.go
1
main.go
|
@ -80,6 +80,7 @@ func init() {
|
||||||
func registerHandlers(e *echo.Echo) {
|
func registerHandlers(e *echo.Echo) {
|
||||||
e.GET("/", handleIndexPage)
|
e.GET("/", handleIndexPage)
|
||||||
e.GET("/api/config.js", handleGetConfigScript)
|
e.GET("/api/config.js", handleGetConfigScript)
|
||||||
|
e.GET("/api/dashboard/stats", handleGetDashboardStats)
|
||||||
e.GET("/api/users", handleGetUsers)
|
e.GET("/api/users", handleGetUsers)
|
||||||
e.POST("/api/users", handleCreateUser)
|
e.POST("/api/users", handleCreateUser)
|
||||||
e.DELETE("/api/users/:id", handleDeleteUser)
|
e.DELETE("/api/users/:id", handleDeleteUser)
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
|
|
||||||
// Queries contains all prepared SQL queries.
|
// Queries contains all prepared SQL queries.
|
||||||
type Queries struct {
|
type Queries struct {
|
||||||
|
GetDashboardStats *sqlx.Stmt `query:"get-dashboard-stats"`
|
||||||
|
|
||||||
InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"`
|
InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"`
|
||||||
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
|
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
|
||||||
BlacklistSubscriber *sqlx.Stmt `query:"blacklist-subscriber"`
|
BlacklistSubscriber *sqlx.Stmt `query:"blacklist-subscriber"`
|
||||||
|
|
53
queries.sql
53
queries.sql
|
@ -416,22 +416,37 @@ INSERT INTO link_clicks (campaign_id, subscriber_id, link_id)
|
||||||
RETURNING (SELECT url FROM link);
|
RETURNING (SELECT url FROM link);
|
||||||
|
|
||||||
|
|
||||||
-- -- name: get-stats
|
-- name: get-dashboard-stats
|
||||||
-- WITH lists AS (
|
WITH lists AS (
|
||||||
-- SELECT type, COUNT(id) AS num FROM lists GROUP BY type
|
SELECT JSON_OBJECT_AGG(type, num) FROM (SELECT type, COUNT(id) AS num FROM lists GROUP BY type) row
|
||||||
-- ),
|
),
|
||||||
-- subs AS (
|
subs AS (
|
||||||
-- SELECT status, COUNT(id) AS num FROM subscribers GROUP by status
|
SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM subscribers GROUP by status) row
|
||||||
-- ),
|
),
|
||||||
-- orphans AS (
|
orphans AS (
|
||||||
-- SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
|
SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
|
||||||
-- WHERE subscriber_lists.subscriber_id IS NULL
|
WHERE subscriber_lists.subscriber_id IS NULL
|
||||||
-- ),
|
),
|
||||||
-- camps AS (
|
camps AS (
|
||||||
-- SELECT status, COUNT(id) AS num FROM campaigns GROUP by status
|
SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM campaigns GROUP by status) row
|
||||||
-- )
|
),
|
||||||
-- SELECT JSON_BUILD_OBJECT('lists', lists);
|
clicks AS (
|
||||||
-- row_to_json(t)
|
-- Clicks by day for the last 3 months
|
||||||
-- from (
|
SELECT JSON_AGG(ROW_TO_JSON(row))
|
||||||
-- select type, num from lists
|
FROM (SELECT COUNT(*) AS count, created_at::DATE as date
|
||||||
-- ) t,
|
FROM link_clicks GROUP by date ORDER BY date DESC LIMIT 100
|
||||||
|
) row
|
||||||
|
),
|
||||||
|
views AS (
|
||||||
|
-- Views by day for the last 3 months
|
||||||
|
SELECT JSON_AGG(ROW_TO_JSON(row))
|
||||||
|
FROM (SELECT COUNT(*) AS count, created_at::DATE as date
|
||||||
|
FROM campaign_views GROUP by date ORDER BY date DESC LIMIT 100
|
||||||
|
) row
|
||||||
|
)
|
||||||
|
SELECT JSON_BUILD_OBJECT('lists', COALESCE((SELECT * FROM lists), '[]'),
|
||||||
|
'subscribers', COALESCE((SELECT * FROM subs), '[]'),
|
||||||
|
'orphan_subscribers', (SELECT * FROM orphans),
|
||||||
|
'campaigns', COALESCE((SELECT * FROM camps), '[]'),
|
||||||
|
'link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
|
||||||
|
'campaign_views', COALESCE((SELECT * FROM views), '[]')) AS stats;
|
||||||
|
|
Loading…
Reference in New Issue