Kaynağa Gözat

Fix merge conflicts

Kailash Nadh 6 yıl önce
ebeveyn
işleme
b333d05609
8 değiştirilmiş dosya ile 198 ekleme ve 34 silme
  1. 19 4
      admin.go
  2. 1 0
      frontend/my/package.json
  3. 118 9
      frontend/my/src/Dashboard.js
  4. 1 0
      frontend/my/src/constants.js
  5. 22 2
      frontend/my/src/index.css
  6. 1 0
      main.go
  7. 2 0
      queries.go
  8. 34 19
      queries.sql

+ 19 - 4
admin.go

@@ -3,8 +3,10 @@ package main
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"net/http"
 
+	"github.com/jmoiron/sqlx/types"
 	"github.com/labstack/echo"
 )
 
@@ -15,10 +17,8 @@ type configScript struct {
 	Messengers []string `json:"messengers"`
 }
 
-// handleGetStats returns a collection of general statistics.
-func handleGetStats(c echo.Context) error {
-	app := c.Get("app").(*App)
-	return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
+type dashboardStats struct {
+	Stats types.JSONText `db:"stats"`
 }
 
 // handleGetConfigScript returns general configuration as a Javascript
@@ -41,3 +41,18 @@ func handleGetConfigScript(c echo.Context) error {
 	j.Encode(out)
 	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})
+}

+ 1 - 0
frontend/my/package.json

@@ -5,6 +5,7 @@
   "dependencies": {
     "antd": "^3.6.5",
     "axios": "^0.18.0",
+    "bizcharts": "^3.2.5-beta.4",
     "dayjs": "^1.7.5",
     "react": "^16.4.1",
     "react-app-rewire-less": "^2.1.3",

+ 118 - 9
frontend/my/src/Dashboard.js

@@ -1,14 +1,123 @@
-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 {
-  componentDidMount = () => {
-    this.props.pageTitle("Dashboard")
-  }
-  render() {
-    return (
-        <h1>Welcome</h1>
-    );
-  }
+    state = {
+        stats: null
+    }
+
+    campaignTypes = ["running", "finished", "paused", "draft", "scheduled", "cancelled"]
+
+    componentDidMount = () => {
+        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() {
+        return (
+            <section className = "dashboard">
+                <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>
+        );
+    }
 }
 
 export default Dashboard;

+ 1 - 0
frontend/my/src/constants.js

@@ -52,6 +52,7 @@ export const SubscriptionStatusUnsubscribed = "unsubscribed"
 
 // API routes.
 export const Routes = {
+    GetDashboarcStats: "/api/dashboard/stats",
     GetUsers: "/api/users",
     
     GetLists: "/api/lists",

+ 22 - 2
frontend/my/src/index.css

@@ -48,11 +48,14 @@ body {
 }
 
 .content-body {
-  background: #fff;
-  padding: 24px;
   min-height: 90vh;
 }
 
+section.content {
+  padding: 24px;
+  background: #fff;
+}
+
 .logo {
   padding: 30px;
 }
@@ -69,10 +72,19 @@ body {
   width: 20px;
 }
 
+.ant-card-head-title {
+  font-size: .85em !important;
+  color: #999 !important;
+}
+
 .broken {
   margin: 100px;
 }
 
+.hidden {
+  display: none;
+}
+
 /* Form */
 
 
@@ -90,6 +102,14 @@ td .ant-tag {
   margin-top: 5px;
 }
 
+/* Dashboard */
+.dashboard {
+  margin: 24px;
+}
+  .dashboard .campaign-counts .name {
+    text-transform: capitalize;
+  }
+
 /* Templates */
 .wysiwyg {
   padding: 30px;

+ 1 - 0
main.go

@@ -80,6 +80,7 @@ func init() {
 func registerHandlers(e *echo.Echo) {
 	e.GET("/", handleIndexPage)
 	e.GET("/api/config.js", handleGetConfigScript)
+	e.GET("/api/dashboard/stats", handleGetDashboardStats)
 	e.GET("/api/users", handleGetUsers)
 	e.POST("/api/users", handleCreateUser)
 	e.DELETE("/api/users/:id", handleDeleteUser)

+ 2 - 0
queries.go

@@ -8,6 +8,8 @@ import (
 
 // Queries contains all prepared SQL queries.
 type Queries struct {
+	GetDashboardStats *sqlx.Stmt `query:"get-dashboard-stats"`
+
 	InsertSubscriber            *sqlx.Stmt `query:"insert-subscriber"`
 	UpsertSubscriber            *sqlx.Stmt `query:"upsert-subscriber"`
 	BlacklistSubscriber         *sqlx.Stmt `query:"blacklist-subscriber"`

+ 34 - 19
queries.sql

@@ -416,22 +416,37 @@ INSERT INTO link_clicks (campaign_id, subscriber_id, link_id)
     RETURNING (SELECT url FROM link);
 
 
--- -- name: get-stats
--- WITH lists AS (
---     SELECT type, COUNT(id) AS num FROM lists GROUP BY type
--- ),
--- subs AS (
---     SELECT status, COUNT(id) AS num FROM subscribers GROUP by status
--- ),
--- orphans AS (
---     SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
---     WHERE subscriber_lists.subscriber_id IS NULL
--- ),
--- camps AS (
---     SELECT status, COUNT(id) AS num FROM campaigns GROUP by status
--- )
--- SELECT JSON_BUILD_OBJECT('lists', lists);
--- row_to_json(t)
--- from (
---   select type, num from lists
--- ) t,
+-- name: get-dashboard-stats
+WITH lists AS (
+    SELECT JSON_OBJECT_AGG(type, num) FROM (SELECT type, COUNT(id) AS num FROM lists GROUP BY type) row
+),
+subs AS (
+    SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM subscribers GROUP by status) row
+),
+orphans AS (
+    SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
+    WHERE subscriber_lists.subscriber_id IS NULL
+),
+camps AS (
+    SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM campaigns GROUP by status) row
+),
+clicks AS (
+    -- Clicks 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 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;