From feb5ba09be1f223c5787f5b913d73e1f1313dd99 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sat, 4 Jul 2020 22:25:02 +0530 Subject: [PATCH] Add new dashboard (with new metrics) --- admin.go | 29 +- frontend/package.json | 2 + frontend/src/api/index.js | 7 + frontend/src/assets/style.scss | 77 +++++- frontend/src/components/EmptyPlaceholder.vue | 20 ++ frontend/src/constants.js | 5 + frontend/src/utils.js | 4 + frontend/src/views/Dashboard.vue | 220 +++++++++++++-- frontend/src/views/Lists.vue | 4 +- frontend/yarn.lock | 277 ++++++++++++++++++- handlers.go | 3 +- init.go | 3 + queries.go | 3 +- queries.sql | 51 ++-- 14 files changed, 623 insertions(+), 82 deletions(-) create mode 100644 frontend/src/components/EmptyPlaceholder.vue diff --git a/admin.go b/admin.go index cf832a5..8d7aa44 100644 --- a/admin.go +++ b/admin.go @@ -17,10 +17,6 @@ type configScript struct { Messengers []string `json:"messengers"` } -type dashboardStats struct { - Stats types.JSONText `db:"stats"` -} - // handleGetConfigScript returns general configuration as a Javascript // variable that can be included in an HTML page directly. func handleGetConfigScript(c echo.Context) error { @@ -41,17 +37,32 @@ func handleGetConfigScript(c echo.Context) error { return c.Blob(http.StatusOK, "application/javascript", b.Bytes()) } -// handleGetDashboardStats returns general states for the dashboard. -func handleGetDashboardStats(c echo.Context) error { +// handleGetDashboardCharts returns chart data points to render ont he dashboard. +func handleGetDashboardCharts(c echo.Context) error { var ( app = c.Get("app").(*App) - out dashboardStats + out types.JSONText ) - if err := app.queries.GetDashboardStats.Get(&out); err != nil { + if err := app.queries.GetDashboardCharts.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}) + return c.JSON(http.StatusOK, okResp{out}) +} + +// handleGetDashboardCounts returns stats counts to show on the dashboard. +func handleGetDashboardCounts(c echo.Context) error { + var ( + app = c.Get("app").(*App) + out types.JSONText + ) + + if err := app.queries.GetDashboardCounts.Get(&out); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, + fmt.Sprintf("Error fetching dashboard statsc counts: %s", pqErrMsg(err))) + } + + return c.JSON(http.StatusOK, okResp{out}) } diff --git a/frontend/package.json b/frontend/package.json index 4ae2c58..6022e94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "axios": "^0.19.2", "buefy": "^0.8.20", + "c3": "^0.7.18", "core-js": "^3.6.5", "dayjs": "^1.8.28", "humps": "^2.0.1", @@ -20,6 +21,7 @@ "quill-delta": "^4.2.2", "sass-loader": "^8.0.2", "vue": "^2.6.11", + "vue-c3": "^1.2.11", "vue-quill-editor": "^3.0.6", "vue-router": "^3.2.0", "vuex": "^3.4.0" diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 482efaa..bfe0205 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -79,6 +79,13 @@ http.interceptors.response.use((resp) => { // loading: modelName (set's the loading status in the global store: eg: store.loading.lists = true) // store: modelName (set's the API response in the global store. eg: store.lists: { ... } ) +// Dashboard +export const getDashboardCounts = () => http.get('/api/dashboard/counts', + { loading: models.dashboard }); + +export const getDashboardCharts = () => http.get('/api/dashboard/charts', + { loading: models.dashboard }); + // Lists. export const getLists = () => http.get('/api/lists', { loading: models.lists, store: models.lists }); diff --git a/frontend/src/assets/style.scss b/frontend/src/assets/style.scss index daf6183..a4cd40a 100644 --- a/frontend/src/assets/style.scss +++ b/frontend/src/assets/style.scss @@ -111,7 +111,7 @@ section { margin-right: 0; } > li { - margin-bottom: 5px; + margin-bottom: 15px; } } .logo { @@ -229,20 +229,34 @@ section { /* Dashboard */ section.dashboard { - .counts { - .title { - margin-bottom: 1rem; - } - .level-item { - background-color: $white-bis; - padding: 30px; - margin: 10px; + .title { + margin-bottom: 0.5rem; + } - &:first-child, &:last-child { - margin: 0; - } + .level-item { + background-color: $white-bis; + padding: 30px; + margin: 10px; + + &:first-child, &:last-child { + margin: 0; } } + + label { + font-weight: bold; + display: inline-block; + min-width: 50px; + text-align: right; + } + + .tile { + position: relative; + } + + .charts { + min-height: 200px; + } } /* Lists page */ @@ -429,6 +443,39 @@ section.campaign { } } +.c3 { + .c3-chart-lines .c3-line { + stroke-width: 2px; + } + .c3-axis-x .tick line, + .c3-axis-y .tick line { + display: none; + } + text { + fill: $grey; + font-family: $body-family; + font-size: 11px; + } + .c3-axis path, .c3-axis line { + stroke: #eee; + } + + .c3-tooltip { + border: 0; + background-color: #fff; + empty-cells: show; + box-shadow: none; + opacity: 0.9; + + tr { + border: 0; + } + th { + background: $white; + } + } +} + @media screen and (max-width: 1450px) and (min-width: 769px) { section.campaigns { /* Fold the stats labels until the card view */ @@ -478,3 +525,9 @@ section.campaign { margin: 15px; } } + +@media screen and (max-width: 840px) { + section.dashboard label { + min-width: auto; + } +} \ No newline at end of file diff --git a/frontend/src/components/EmptyPlaceholder.vue b/frontend/src/components/EmptyPlaceholder.vue new file mode 100644 index 0000000..2b97248 --- /dev/null +++ b/frontend/src/components/EmptyPlaceholder.vue @@ -0,0 +1,20 @@ + + + + diff --git a/frontend/src/views/Lists.vue b/frontend/src/views/Lists.vue index a4b921d..b3437bc 100644 --- a/frontend/src/views/Lists.vue +++ b/frontend/src/views/Lists.vue @@ -40,7 +40,7 @@ - + {{ props.row.subscriberCount }} @@ -78,7 +78,7 @@

-

Nothing here.

+

Nothing here yet.

diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 11fd8b6..246eee0 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1992,6 +1992,13 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +c3@^0.7.18: + version "0.7.18" + resolved "https://registry.yarnpkg.com/c3/-/c3-0.7.18.tgz#a94228191b6178288fa416a6135c9624abd8dc45" + integrity sha512-ioiqCvET2sjAn80V3qVBEkyAtCH3tktZsz9SylGmUeeGEfqZxZfq9qRCxfgl64LpA6d9/Oz4C8oYEIXHbMX/Ow== + dependencies: + d3 "^5.8.0" + cacache@^12.0.2, cacache@^12.0.3: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -2414,16 +2421,16 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@2, commander@^2.18.0, commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.18.0, commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -2863,6 +2870,254 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-axis@1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" + integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== + +d3-brush@1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.5.tgz#066b8e84d17b192986030446c97c0fba7e1bacdc" + integrity sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" + integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== + +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + +d3-dispatch@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" + integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== + +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" + integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0" + integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ== + +d3-fetch@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7" + integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + +d3-format@1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.4.tgz#356925f28d0fd7c7983bfad593726fce46844030" + integrity sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw== + +d3-geo@1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f" + integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg== + dependencies: + d3-array "1" + +d3-hierarchy@1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" + integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== + +d3-interpolate@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-polygon@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + +d3-quadtree@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" + integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== + +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + +d3-scale-chromatic@1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" + integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-selection@1, d3-selection@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.1.tgz#98eedbbe085fbda5bafa2f9e3f3a2f4d7d622a98" + integrity sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA== + +d3-shape@1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb" + integrity sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +d3-timer@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-transition@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" + integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +d3-zoom@1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^5.8.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -4504,7 +4759,7 @@ humps@^2.0.1: resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -7397,6 +7652,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= + rxjs@^6.5.3: version "6.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" @@ -8646,6 +8906,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vue-c3@^1.2.11: + version "1.2.11" + resolved "https://registry.yarnpkg.com/vue-c3/-/vue-c3-1.2.11.tgz#6937f0dd54addab2b76de74cd30c0ab9ad788080" + integrity sha512-jxYZ726lKO1Qa+CHOcekPD4ZIwcMQy2LYDafYy2jYD1oswAo/4SnEJmbwp9X+NWzZg/KIAijeB9ImS7Gfvhceg== + vue-eslint-parser@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.1.0.tgz#9cdbcc823e656b087507a1911732b867ac101e83" diff --git a/handlers.go b/handlers.go index 454e292..8aa00a3 100644 --- a/handlers.go +++ b/handlers.go @@ -38,7 +38,8 @@ var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[ func registerHTTPHandlers(e *echo.Echo) { e.GET("/", handleIndexPage) e.GET("/api/config.js", handleGetConfigScript) - e.GET("/api/dashboard/stats", handleGetDashboardStats) + e.GET("/api/dashboard/charts", handleGetDashboardCharts) + e.GET("/api/dashboard/counts", handleGetDashboardCounts) e.GET("/api/subscribers/:id", handleGetSubscriber) e.GET("/api/subscribers/:id/export", handleExportSubscriberData) diff --git a/init.go b/init.go index 62d3187..8319d01 100644 --- a/init.go +++ b/init.go @@ -87,10 +87,13 @@ func initFS(staticDir string) stuffbin.FileSystem { // initDB initializes the main DB connection pool and parse and loads the app's // SQL queries into a prepared query map. func initDB() *sqlx.DB { + var dbCfg dbConf if err := ko.Unmarshal("db", &dbCfg); err != nil { lo.Fatalf("error loading db config: %v", err) } + + lo.Printf("connecting to db: %s:%d/%s", dbCfg.Host, dbCfg.Port, dbCfg.DBName) db, err := connectDB(dbCfg) if err != nil { lo.Fatalf("error connecting to DB: %v", err) diff --git a/queries.go b/queries.go index eab93f8..28cb505 100644 --- a/queries.go +++ b/queries.go @@ -11,7 +11,8 @@ import ( // Queries contains all prepared SQL queries. type Queries struct { - GetDashboardStats *sqlx.Stmt `query:"get-dashboard-stats"` + GetDashboardCharts *sqlx.Stmt `query:"get-dashboard-charts"` + GetDashboardCounts *sqlx.Stmt `query:"get-dashboard-counts"` InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"` UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"` diff --git a/queries.sql b/queries.sql index 01d7277..49c95ea 100644 --- a/queries.sql +++ b/queries.sql @@ -678,21 +678,8 @@ INSERT INTO link_clicks (campaign_id, subscriber_id, link_id) RETURNING (SELECT url FROM link); --- 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 ( +-- name: get-dashboard-charts +WITH 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 @@ -706,9 +693,31 @@ views AS ( 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; +SELECT JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'), + 'campaign_views', COALESCE((SELECT * FROM views), '[]')); + +-- name: get-dashboard-counts +SELECT JSON_BUILD_OBJECT('subscribers', JSON_BUILD_OBJECT( + 'total', (SELECT COUNT(*) FROM subscribers), + 'blacklisted', (SELECT COUNT(*) FROM subscribers WHERE status='blacklisted'), + 'orphans', ( + SELECT COUNT(id) FROM subscribers + LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id) + WHERE subscriber_lists.subscriber_id IS NULL + ) + ), + 'lists', JSON_BUILD_OBJECT( + 'total', (SELECT COUNT(*) FROM lists), + 'private', (SELECT COUNT(*) FROM lists WHERE type='private'), + 'public', (SELECT COUNT(*) FROM lists WHERE type='public'), + 'optin_single', (SELECT COUNT(*) FROM lists WHERE optin='single'), + 'optin_double', (SELECT COUNT(*) FROM lists WHERE optin='double') + ), + 'campaigns', JSON_BUILD_OBJECT( + 'total', (SELECT COUNT(*) FROM campaigns), + 'by_status', ( + SELECT JSON_OBJECT_AGG (status, num) FROM + (SELECT status, COUNT(*) AS num FROM campaigns GROUP BY status) r + ) + ), + 'messages', (SELECT SUM(sent) AS messages FROM campaigns));