Skip to content

Commit

Permalink
Add maintenance options.
Browse files Browse the repository at this point in the history
- Add new maintenance UI with options to garbage collect (delete)
  orphan subscriber and analytics records.
  • Loading branch information
knadh committed Sep 3, 2022
1 parent 8ace258 commit 6d820f4
Show file tree
Hide file tree
Showing 34 changed files with 553 additions and 14 deletions.
4 changes: 4 additions & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
g.PUT("/api/templates/:id/default", handleTemplateSetDefault)
g.DELETE("/api/templates/:id", handleDeleteTemplate)

g.DELETE("/api/maintenance/subscribers/:type", handleGCSubscribers)
g.DELETE("/api/maintenance/analytics/:type", handleGCCampaignAnalytics)
g.DELETE("/api/maintenance/subscriptions/unconfirmed", handleGCSubscriptions)

g.POST("/api/tx", handleSendTxMessage)

if app.constants.BounceWebhooksEnabled {
Expand Down
92 changes: 92 additions & 0 deletions cmd/maintenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"net/http"
"time"

"github.com/labstack/echo/v4"
)

// handleGCSubscribers garbage collects (deletes) orphaned or blocklisted subscribers.
func handleGCSubscribers(c echo.Context) error {
var (
app = c.Get("app").(*App)
typ = c.Param("type")
)

var (
n int
err error
)

switch typ {
case "blocklisted":
n, err = app.core.DeleteBlocklistedSubscribers()
case "orphan":
n, err = app.core.DeleteOrphanSubscribers()
default:
err = echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
}

if err != nil {
return err
}

return c.JSON(http.StatusOK, okResp{struct {
Count int `json:"count"`
}{n}})
}

// handleGCSubscriptions garbage collects (deletes) orphaned or blocklisted subscribers.
func handleGCSubscriptions(c echo.Context) error {
var (
app = c.Get("app").(*App)
)

t, err := time.Parse(time.RFC3339, c.FormValue("before_date"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
}

n, err := app.core.DeleteUnconfirmedSubscriptions(t)
if err != nil {
return err
}

return c.JSON(http.StatusOK, okResp{struct {
Count int `json:"count"`
}{n}})
}

// handleGCCampaignAnalytics garbage collects (deletes) campaign analytics.
func handleGCCampaignAnalytics(c echo.Context) error {
var (
app = c.Get("app").(*App)
typ = c.Param("type")
)

t, err := time.Parse(time.RFC3339, c.FormValue("before_date"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
}

switch typ {
case "all":
if err := app.core.DeleteCampaignViews(t); err != nil {
return err
}
err = app.core.DeleteCampaignLinkClicks(t)
case "views":
err = app.core.DeleteCampaignViews(t)
case "clicks":
err = app.core.DeleteCampaignLinkClicks(t)
default:
err = echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
}

if err != nil {
return err
}

return c.JSON(http.StatusOK, okResp{true})
}
28 changes: 14 additions & 14 deletions frontend/fontello/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,20 @@
"logout-variant"
]
},
{
"uid": "10098901a143c53df6eeaeb317ae3da6",
"css": "wrench-outline",
"code": 986080,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M941.4 791L564.5 412.1Q593.8 337.9 578.1 258.8T503.9 121.1Q431.6 50.8 332 43T152.3 93.8L312.5 253.9 253.9 312.5 93.8 154.3Q35.2 234.4 42 334T121.1 503.9Q177.7 562.5 255.9 578.1T408.2 566.4L787.1 945.3Q798.8 959 816.4 959T845.7 945.3L941.4 849.6Q955.1 837.9 955.1 821.3T941.4 791ZM816.4 857.4L423.8 462.9Q384.8 492.2 340.8 498T253.9 491.2 180.7 447.3 136.7 378.9 125 302.7L253.9 431.6 429.7 253.9 300.8 125Q384.8 121.1 445.3 179.7 478.5 212.9 491.2 256.8T496.1 344.7 460.9 427.7L853.5 820.3Z",
"width": 1000
},
"search": [
"wrench-outline"
]
},
{
"uid": "f4ad3f6d071a0bfb3a8452b514ed0892",
"css": "vector-square",
Expand Down Expand Up @@ -42748,20 +42762,6 @@
"wrap-disabled"
]
},
{
"uid": "10098901a143c53df6eeaeb317ae3da6",
"css": "wrench-outline",
"code": 986080,
"src": "custom_icons",
"selected": false,
"svg": {
"path": "M941.4 791L564.5 412.1Q593.8 337.9 578.1 258.8T503.9 121.1Q431.6 50.8 332 43T152.3 93.8L312.5 253.9 253.9 312.5 93.8 154.3Q35.2 234.4 42 334T121.1 503.9Q177.7 562.5 255.9 578.1T408.2 566.4L787.1 945.3Q798.8 959 816.4 959T845.7 945.3L941.4 849.6Q955.1 837.9 955.1 821.3T941.4 791ZM816.4 857.4L423.8 462.9Q384.8 492.2 340.8 498T253.9 491.2 180.7 447.3 136.7 378.9 125 302.7L253.9 431.6 429.7 253.9 300.8 125Q384.8 121.1 445.3 179.7 478.5 212.9 491.2 256.8T496.1 344.7 460.9 427.7L853.5 820.3Z",
"width": 1000
},
"search": [
"wrench-outline"
]
},
{
"uid": "d670b0f395ba3a61b975a6387f8a2471",
"css": "access-point-network-off",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,12 @@ export const getLang = async (lang) => http.get(`/api/lang/${lang}`,
export const logout = async () => http.get('/api/logout', {
auth: { username: 'wrong', password: 'wrong' },
});

export const deleteGCCampaignAnalytics = async (typ, beforeDate) => http.delete(`/api/maintenance/analytics/${typ}`,
{ loading: models.maintenance, params: { before_date: beforeDate } });

export const deleteGCSubscribers = async (typ) => http.delete(`/api/maintenance/subscribers/${typ}`,
{ loading: models.maintenance });

export const deleteGCSubscriptions = async (beforeDate) => http.delete('/api/maintenance/subscriptions/unconfirmed',
{ loading: models.maintenance, params: { before_date: beforeDate } });
2 changes: 2 additions & 0 deletions frontend/src/assets/icons/fontello.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}


.mdi-view-dashboard-variant-outline:before { content: '\e800'; } /* '' */
.mdi-format-list-bulleted-square:before { content: '\e801'; } /* '' */
.mdi-newspaper-variant-outline:before { content: '\e802'; } /* '' */
Expand Down Expand Up @@ -80,3 +81,4 @@
.mdi-email-bounce:before { content: '\e825'; } /* '' */
.mdi-speedometer:before { content: '\e826'; } /* '' */
.mdi-logout-variant:before { content: '󰗽'; } /* '\f05fd' */
.mdi-wrench-outline:before { content: '󰯠'; } /* '\f0be0' */
Binary file modified frontend/src/assets/icons/fontello.woff2
Binary file not shown.
4 changes: 4 additions & 0 deletions frontend/src/components/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
data-cy="all-settings" icon="cog-outline" :label="$t('menu.settings')">
</b-menu-item>

<b-menu-item :to="{name: 'maintenance'}" tag="router-link" :active="activeItem.maintenance"
data-cy="maintenance" icon="wrench-outline" :label="$t('menu.maintenance')">
</b-menu-item>

<b-menu-item :to="{name: 'logs'}" tag="router-link" :active="activeItem.logs"
data-cy="logs" icon="newspaper-variant-outline" :label="$t('menu.logs')">
</b-menu-item>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const models = Object.freeze({
bounces: 'bounces',
settings: 'settings',
logs: 'logs',
maintenance: 'maintenance',
});

// Ad-hoc URIs that are used outside of vuex requests.
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ const routes = [
meta: { title: 'logs.title', group: 'settings' },
component: () => import(/* webpackChunkName: "main" */ '../views/Logs.vue'),
},
{
path: '/settings/maintenance',
name: 'maintenance',
meta: { title: 'logs.title', group: 'settings' },
component: () => import(/* webpackChunkName: "main" */ '../views/Maintenance.vue'),
},
];

const router = new VueRouter({
Expand Down
163 changes: 163 additions & 0 deletions frontend/src/views/Maintenance.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<template>
<section class="maintenance wrap">
<h1 class="title is-4">{{ $t('maintenance.title') }}</h1>
<hr />
<p class="has-text-grey">
{{ $t('maintenance.help') }}
</p>
<br />


<div class="box">
<h4 class="is-size-5">{{ $t('globals.terms.subscribers') }}</h4><br />
<div class="columns">
<div class="column is-4">
<b-field label="Data" message="Orpans = subscribers with no lists">
<b-select v-model="subscriberType" expanded>
<option value="orphan">{{ $t('dashboard.orphanSubs') }}</option>
<option value="blocklisted">{{ $t('subscribers.status.blocklisted') }}</option>
</b-select>
</b-field>
</div>
<div class="column is-5"></div>
<div class="column">
<b-field label=".">
<b-button class="is-primary" :loading="loading.maintenance"
@click="deleteSubscribers" expanded>{{ $t('globals.buttons.delete') }}</b-button>
</b-field>
</div>
</div>
</div><!-- subscribers -->

<div class="box">
<h4 class="is-size-5">{{ $tc('globals.terms.subscriptions', 2) }}</h4><br />
<div class="columns">
<div class="column is-4">
<b-field label="Data">
<b-select v-model="subscriptionType" expanded>
<option value="optin">{{ $t('maintenance.maintenance.unconfirmedOptins') }}</option>
</b-select>
</b-field>
</div>
<div class="column is-4">
<b-field :label="$t('maintenance.olderThan')">
<b-datepicker
v-model="subscriptionDate"
required expanded
icon="calendar-clock"
:date-formatter="formatDateTime">
</b-datepicker>
</b-field>
</div>
<div class="column is-1"></div>
<div class="column">
<b-field label=".">
<b-button class="is-primary" :loading="loading.maintenance"
@click="deleteSubscriptions" expanded>{{ $t('globals.buttons.delete') }}</b-button>
</b-field>
</div>
</div>
</div><!-- subscriptions -->

<div class="box mt-6">
<h4 class="is-size-5">{{ $t('globals.terms.analytics') }}</h4><br />
<div class="columns">
<div class="column is-4">
<b-field label="Data">
<b-select v-model="analyticsType" expanded>
<option selected value="all">{{ $t('globals.terms.all') }}</option>
<option value="views">{{ $t('dashboard.campaignViews') }}</option>
<option value="clicks">{{ $t('dashboard.linkClicks') }}</option>
</b-select>
</b-field>
</div>
<div class="column is-4">
<b-field :label="$t('maintenance.olderThan')">
<b-datepicker
v-model="analyticsDate"
required expanded
icon="calendar-clock"
:date-formatter="formatDateTime">
</b-datepicker>
</b-field>
</div>
<div class="column is-1"></div>
<div class="column">
<b-field label=".">
<b-button expanded class="is-primary" :loading="loading.maintenance"
@click="deleteAnalytics">{{ $t('globals.buttons.delete') }}</b-button>
</b-field>
</div>
</div>
</div><!-- analytics -->

</section>
</template>

<script>
import Vue from 'vue';
import { mapState } from 'vuex';
import dayjs from 'dayjs';
export default Vue.extend({
components: {
},
data() {
return {
subscriberType: 'orphan',
analyticsType: 'all',
subscriptionType: 'optin',
analyticsDate: dayjs().subtract(7, 'day').toDate(),
subscriptionDate: dayjs().subtract(7, 'day').toDate(),
};
},
methods: {
formatDateTime(s) {
return dayjs(s).format('YYYY-MM-DD');
},
deleteSubscribers() {
this.$utils.confirm(
null,
() => {
this.$api.deleteGCSubscribers(this.subscriberType).then((data) => {
this.$utils.toast(this.$t('globals.messages.deletedCount',
{ name: this.$tc('globals.terms.subscribers', 2), num: data.count }));
});
},
);
},
deleteSubscriptions() {
this.$utils.confirm(
null,
() => {
this.$api.deleteGCSubscriptions(this.subscriptionDate).then((data) => {
this.$utils.toast(this.$t('globals.messages.deletedCount',
{ name: this.$tc('globals.terms.subscriptions', 2), num: data.count }));
});
},
);
},
deleteAnalytics() {
this.$utils.confirm(
null,
() => {
this.$api.deleteGCCampaignAnalytics(this.analyticsType, this.analyticsDate)
.then(() => {
this.$utils.toast(this.$t('globals.messages.done'));
});
},
);
},
},
computed: {
...mapState(['loading']),
},
});
</script>
Loading

0 comments on commit 6d820f4

Please sign in to comment.