Activity Home (#56)

* home activity graph
* add crowdin file
This commit is contained in:
Tyr Mactire 2022-08-10 22:01:51 -07:00 committed by GitHub
parent 7eff78b91c
commit 515ca0fde1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 332 additions and 173 deletions

4
crowdin.yml Normal file
View File

@ -0,0 +1,4 @@
---
files:
- source: /web/locales/active.en.yaml
translation: /web/locales/active.%two_letters_code%.yaml

View File

@ -1,6 +1,7 @@
package webapp
import (
"fmt"
libhttp "github.com/feditools/go-lib/http"
libtemplate "github.com/feditools/go-lib/template"
"github.com/feditools/relay/internal/http/template"
@ -280,3 +281,62 @@ func (m *Module) displayAdminBlockList(w http.ResponseWriter, r *http.Request, c
m.executeTemplate(w, r, template.AdminBlockName, tmplVars)
}
const jsAdminBlock = `
const deleteModal = document.getElementById('deleteModal')
deleteModal.addEventListener('show.bs.modal', event => {
// Button that triggered the modal
const button = event.relatedTarget
// Extract info from data-bs-* attributes
const token = button.getAttribute('data-bs-token')
const domain = button.getAttribute('data-bs-domain')
// Update the modal's content.
const modalTitle = deleteModal.querySelector('.modal-title')
const confirmText = deleteModal.querySelector('.confirm-text')
const inputToken = deleteModal.querySelector('.modal-body #formDeleteInputToken')
modalTitle.textContent = %s
confirmText.textContent = %s
inputToken.value = token
})
const editModal = document.getElementById('editModal')
editModal.addEventListener('show.bs.modal', event => {
// Button that triggered the modal
const button = event.relatedTarget
// Extract info from data-bs-* attributes
const token = button.getAttribute('data-bs-token')
const domain = button.getAttribute('data-bs-domain')
const obfuscatedDomain = button.getAttribute('data-bs-obfuscated-domain')
const blockSubdomains = button.getAttribute('data-bs-block-subdomains')
// Update the modal's content.
const modalTitle = editModal.querySelector('.modal-title')
const inputToken = editModal.querySelector('.modal-body #formEditInputToken')
const inputDomain = editModal.querySelector('.modal-body #formEditInputDomain')
const inputObfuscatedDomain = editModal.querySelector('.modal-body #formEditInputObfuscatedDomain')
const inputSubdomain = editModal.querySelector('.modal-body #formEditInputSubdomain')
modalTitle.textContent = %s
inputToken.value = token
inputDomain.value = domain
inputObfuscatedDomain.value = obfuscatedDomain
if (blockSubdomains == "true") {
inputSubdomain.checked = true
} else {
inputSubdomain.checked = false
}
})
`
func JSAdminBlock(deleteTitleText, deleteConfirmText, editTitleText *language.LocalizedString) string {
return fmt.Sprintf(
jsAdminBlock,
"`"+deleteTitleText.String()+"`",
"`"+deleteConfirmText.String()+"`",
"`"+editTitleText.String()+"`",
)
}

View File

@ -2,15 +2,19 @@ package webapp
import (
"errors"
"fmt"
libtemplate "github.com/feditools/go-lib/template"
"github.com/feditools/relay/internal/db"
"github.com/feditools/relay/internal/http/template"
"github.com/feditools/relay/internal/language"
"github.com/feditools/relay/internal/logic"
"github.com/feditools/relay/internal/models"
"github.com/feditools/relay/internal/path"
"github.com/feditools/relay/internal/token"
"github.com/gorilla/mux"
"net/http"
"strconv"
"strings"
)
// AdminInstanceViewGetHandler serves the instance info page.
@ -111,3 +115,78 @@ func (m *Module) displayAdminInstanceView(w http.ResponseWriter, r *http.Request
m.executeTemplate(w, r, template.AdminInstanceViewName, tmplVars)
}
const jsAdminInstanceView = `
new Chart(document.getElementById("deliveryChartContainer"), {
type: 'line',
data: {
labels: [%s],
datasets: [{
data: [%s],
label: "Error",
borderColor: "#c45850",
fill: false,
lineTension: 0.2
}, {
data: [%s],
label: "Success",
borderColor: "#3cba9f",
fill: false,
lineTension: 0.2
}
]
},
options: {}
});
new Chart(document.getElementById("receiveChartContainer"), {
type: 'line',
data: {
labels: [%s],
datasets: [{
data: [%s],
label: "Received",
borderColor: "#3e95cd",
fill: false,
lineTension: 0.2
}
]
},
options: {}
});
`
func JSAdminInstanceView(deliverErrors, deliverSuccess, receive *logic.MetricsDataPointsTime) string {
labelsLen := len(*deliverErrors)
labels := make([]string, labelsLen)
for i, dp := range *deliverErrors {
labels[labelsLen-1-i] = dp.X.Format("\"Jan 02 2006\"")
}
deliverErrorsLen := len(*deliverErrors)
deliverErrorsData := make([]string, deliverErrorsLen)
for i, dp := range *deliverErrors {
deliverErrorsData[deliverErrorsLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
deliverSuccessLen := len(*deliverSuccess)
deliverSuccessData := make([]string, deliverSuccessLen)
for i, dp := range *deliverSuccess {
deliverSuccessData[deliverSuccessLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
receiveLen := len(*receive)
receiveData := make([]string, receiveLen)
for i, dp := range *receive {
receiveData[receiveLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
return fmt.Sprintf(
jsAdminInstanceView,
strings.Join(labels, ","),
strings.Join(deliverErrorsData, ","),
strings.Join(deliverSuccessData, ","),
strings.Join(labels, ","),
strings.Join(receiveData, ","),
)
}

View File

@ -1,11 +1,15 @@
package webapp
import (
"fmt"
"github.com/feditools/relay/internal/http/template"
"github.com/feditools/relay/internal/language"
"github.com/feditools/relay/internal/logic"
"github.com/feditools/relay/internal/models"
"github.com/feditools/relay/internal/path"
"net/http"
"strconv"
"strings"
)
const defaultHomeBody = `This is another Activity Relay for fediverse instances.`
@ -60,6 +64,17 @@ func (m *Module) HomeGetHandler(w http.ResponseWriter, r *http.Request) {
}
tmplVars.FollowingInstances = followingInstance
// get metrics
received, err := m.logic.MetricsGetReceivedTotalWeek(r.Context())
if err != nil {
l.Errorf("get metrics deliver error: %s", err.Error())
m.returnErrorPage(w, r, http.StatusInternalServerError, err.Error())
return
}
tmplVars.AddFooterExtraScript(JSHome(received))
tmplVars.AddFooterScript(m.footerScriptChartJS)
m.executeTemplate(w, r, template.HomeName, tmplVars)
}
@ -67,3 +82,47 @@ func (m *Module) HomeGetHandler(w http.ResponseWriter, r *http.Request) {
func (m *Module) ForwardToHomeHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path.AppHome, http.StatusFound)
}
const jsHome = `
new Chart(document.getElementById("receiveChartContainer"), {
type: 'line',
data: {
labels: [%s],
datasets: [{
data: [%s],
label: "Received",
borderColor: "#3e95cd",
fill: false,
lineTension: 0.2
}
]
},
options: {
plugins: {
legend: {
display: false
}
}
}
});
`
func JSHome(receive *logic.MetricsDataPointsTime) string {
labelsLen := len(*receive)
labels := make([]string, labelsLen)
for i, dp := range *receive {
labels[labelsLen-1-i] = dp.X.Format("\"Jan 02 2006\"")
}
receiveLen := len(*receive)
receiveData := make([]string, receiveLen)
for i, dp := range *receive {
receiveData[receiveLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
return fmt.Sprintf(
jsHome,
strings.Join(labels, ","),
strings.Join(receiveData, ","),
)
}

View File

@ -2,68 +2,8 @@ package webapp
import (
"fmt"
"github.com/feditools/relay/internal/language"
)
const jsAdminBlock = `
const deleteModal = document.getElementById('deleteModal')
deleteModal.addEventListener('show.bs.modal', event => {
// Button that triggered the modal
const button = event.relatedTarget
// Extract info from data-bs-* attributes
const token = button.getAttribute('data-bs-token')
const domain = button.getAttribute('data-bs-domain')
// Update the modal's content.
const modalTitle = deleteModal.querySelector('.modal-title')
const confirmText = deleteModal.querySelector('.confirm-text')
const inputToken = deleteModal.querySelector('.modal-body #formDeleteInputToken')
modalTitle.textContent = %s
confirmText.textContent = %s
inputToken.value = token
})
const editModal = document.getElementById('editModal')
editModal.addEventListener('show.bs.modal', event => {
// Button that triggered the modal
const button = event.relatedTarget
// Extract info from data-bs-* attributes
const token = button.getAttribute('data-bs-token')
const domain = button.getAttribute('data-bs-domain')
const obfuscatedDomain = button.getAttribute('data-bs-obfuscated-domain')
const blockSubdomains = button.getAttribute('data-bs-block-subdomains')
// Update the modal's content.
const modalTitle = editModal.querySelector('.modal-title')
const inputToken = editModal.querySelector('.modal-body #formEditInputToken')
const inputDomain = editModal.querySelector('.modal-body #formEditInputDomain')
const inputObfuscatedDomain = editModal.querySelector('.modal-body #formEditInputObfuscatedDomain')
const inputSubdomain = editModal.querySelector('.modal-body #formEditInputSubdomain')
modalTitle.textContent = %s
inputToken.value = token
inputDomain.value = domain
inputObfuscatedDomain.value = obfuscatedDomain
if (blockSubdomains == "true") {
inputSubdomain.checked = true
} else {
inputSubdomain.checked = false
}
})
`
func JSAdminBlock(deleteTitleText, deleteConfirmText, editTitleText *language.LocalizedString) string {
return fmt.Sprintf(
jsAdminBlock,
"`"+deleteTitleText.String()+"`",
"`"+deleteConfirmText.String()+"`",
"`"+editTitleText.String()+"`",
)
}
const jsOpenModal = `
var autoOpenModal = new bootstrap.Modal(document.getElementById('%s'), {})
autoOpenModal.toggle()

View File

@ -1,83 +0,0 @@
package webapp
import (
"fmt"
"github.com/feditools/relay/internal/logic"
"strconv"
"strings"
)
const jsAdminInstanceView = `
new Chart(document.getElementById("deliveryChartContainer"), {
type: 'line',
data: {
labels: [%s],
datasets: [{
data: [%s],
label: "Error",
borderColor: "#c45850",
fill: false,
lineTension: 0.2
}, {
data: [%s],
label: "Success",
borderColor: "#3cba9f",
fill: false,
lineTension: 0.2
}
]
},
options: {}
});
new Chart(document.getElementById("receiveChartContainer"), {
type: 'line',
data: {
labels: [%s],
datasets: [{
data: [%s],
label: "Received",
borderColor: "#3e95cd",
fill: false,
lineTension: 0.2
}
]
},
options: {}
});
`
func JSAdminInstanceView(deliverErrors, deliverSuccess, receive *logic.MetricsDataPointsTime) string {
labelsLen := len(*deliverErrors)
labels := make([]string, labelsLen)
for i, dp := range *deliverErrors {
labels[labelsLen-1-i] = dp.X.Format("\"Jan 02 2006\"")
}
deliverErrorsLen := len(*deliverErrors)
deliverErrorsData := make([]string, deliverErrorsLen)
for i, dp := range *deliverErrors {
deliverErrorsData[deliverErrorsLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
deliverSuccessLen := len(*deliverSuccess)
deliverSuccessData := make([]string, deliverSuccessLen)
for i, dp := range *deliverSuccess {
deliverSuccessData[deliverSuccessLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
receiveLen := len(*receive)
receiveData := make([]string, receiveLen)
for i, dp := range *receive {
receiveData[receiveLen-1-i] = strconv.FormatInt(int64(dp.Y), 10)
}
return fmt.Sprintf(
jsAdminInstanceView,
strings.Join(labels, ","),
strings.Join(deliverErrorsData, ","),
strings.Join(deliverSuccessData, ","),
strings.Join(labels, ","),
strings.Join(receiveData, ","),
)
}

View File

@ -24,6 +24,28 @@ func (l *Localizer) TextAccount(count int) *LocalizedString {
}
}
// TextActivity returns a translated phrase.
func (l *Localizer) TextActivity(count int) *LocalizedString {
lg := logger.WithField("func", "TextActivity")
text, tag, err := l.localizer.LocalizeWithTag(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "Activity",
One: "Activity",
Other: "Activities",
},
PluralCount: count,
})
if err != nil {
lg.Warningf(missingTranslationWarning, err.Error())
}
return &LocalizedString{
language: tag,
string: text,
}
}
// TextActivityLog returns a translated phrase.
func (l *Localizer) TextActivityLog(count int) *LocalizedString {
lg := logger.WithField("func", "TextActivityLog")

View File

@ -46,6 +46,45 @@ func TestLocalizer_TextAccount(t *testing.T) {
}
}
func TestLocalizer_TextActivity(t *testing.T) {
t.Parallel()
tables := []testTextTable{
{
inputLang: language.English,
inputCount: 1,
outputString: "Activity",
outputLang: language.English,
},
{
inputLang: language.English,
inputCount: 2,
outputString: "Activities",
outputLang: language.English,
},
}
langMod, _ := New()
for i, table := range tables {
i := i
table := table
name := fmt.Sprintf(testTranslatedTo, i, table.inputLang)
t.Run(name, func(t *testing.T) {
t.Parallel()
localizer, err := langMod.NewLocalizer(table.inputLang.String())
if err != nil {
t.Errorf(testCantGetLocalizer, i, table.inputLang, err.Error())
return
}
testTextWithCount(t, i, localizer.TextActivity, table)
})
}
}
func TestLocalizer_TextActivityLog(t *testing.T) {
t.Parallel()

View File

@ -68,18 +68,18 @@ func (l *Localizer) TextDeleteBlockConfirmDomain(domain string) *LocalizedString
}
}
// TextDeliveryStat returns a translated phrase.
func (l *Localizer) TextDeliveryStat(count int) *LocalizedString {
// TextDeliveredActivity returns a translated phrase.
func (l *Localizer) TextDeliveredActivity(count int) *LocalizedString {
text, tag, err := l.localizer.LocalizeWithTag(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DeliveryStat",
One: "Delivery Stat",
Other: "Delivery Stats",
ID: "DeliveredActivity",
One: "Delivered Activity",
Other: "Delivered Activities",
},
PluralCount: count,
})
if err != nil {
logger.WithField("func", "TextDeliveryStat").Warningf(missingTranslationWarning, err.Error())
logger.WithField("func", "TextDeliveredActivity").Warningf(missingTranslationWarning, err.Error())
}
return &LocalizedString{

View File

@ -117,20 +117,20 @@ func TestLocalizer_TextDeleteBlockConfirmDomain(t *testing.T) {
}
}
func TestLocalizer_TextDeliveryStat(t *testing.T) {
func TestLocalizer_TextDeliveredActivity(t *testing.T) {
t.Parallel()
tables := []testTextTable{
{
inputLang: language.English,
inputCount: 1,
outputString: "Delivery Stat",
outputString: "Delivered Activity",
outputLang: language.English,
},
{
inputLang: language.English,
inputCount: 2,
outputString: "Delivery Stats",
outputString: "Delivered Activities",
outputLang: language.English,
},
}
@ -151,7 +151,7 @@ func TestLocalizer_TextDeliveryStat(t *testing.T) {
return
}
testTextWithCount(t, i, localizer.TextDeliveryStat, table)
testTextWithCount(t, i, localizer.TextDeliveredActivity, table)
})
}
}

View File

@ -2,18 +2,18 @@ package language
import "github.com/nicksnyder/go-i18n/v2/i18n"
// TextReceivedStat returns a translated phrase.
func (l *Localizer) TextReceivedStat(count int) *LocalizedString {
// TextReceivedActivity returns a translated phrase.
func (l *Localizer) TextReceivedActivity(count int) *LocalizedString {
text, tag, err := l.localizer.LocalizeWithTag(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "ReceivedStat",
One: "Received Stat",
Other: "Received Stats",
ID: "ReceivedActivity",
One: "Received Activity",
Other: "Received Activities",
},
PluralCount: count,
})
if err != nil {
logger.WithField("func", "TextReceivedStat").Warningf(missingTranslationWarning, err.Error())
logger.WithField("func", "TextReceivedActivity").Warningf(missingTranslationWarning, err.Error())
}
return &LocalizedString{

View File

@ -7,20 +7,20 @@ import (
"golang.org/x/text/language"
)
func TestLocalizer_TextReceivedStat(t *testing.T) {
func TestLocalizer_TextReceivedActivity(t *testing.T) {
t.Parallel()
tables := []testTextTable{
{
inputLang: language.English,
inputCount: 1,
outputString: "Received Stat",
outputString: "Received Activity",
outputLang: language.English,
},
{
inputLang: language.English,
inputCount: 2,
outputString: "Received Stats",
outputString: "Received Activities",
outputLang: language.English,
},
}
@ -41,7 +41,7 @@ func TestLocalizer_TextReceivedStat(t *testing.T) {
return
}
testTextWithCount(t, i, localizer.TextReceivedStat, table)
testTextWithCount(t, i, localizer.TextReceivedActivity, table)
})
}
}

View File

@ -28,6 +28,7 @@ type Logic interface {
MetricsGetDeliverErrorWeek(ctx context.Context, instanceID int64) (*MetricsDataPointsTime, error)
MetricsGetDeliverSuccessWeek(ctx context.Context, instanceID int64) (*MetricsDataPointsTime, error)
MetricsGetReceivedWeek(ctx context.Context, instanceID int64) (*MetricsDataPointsTime, error)
MetricsGetReceivedTotalWeek(ctx context.Context) (*MetricsDataPointsTime, error)
MetricsIncDeliverError(ctx context.Context, instanceID int64)
MetricsIncDeliverSuccess(ctx context.Context, instanceID int64)
MetricsIncReceived(ctx context.Context, instanceID int64)

View File

@ -79,3 +79,27 @@ func (l *Logic) MetricsGetReceivedWeek(ctx context.Context, instanceID int64) (*
return &days, nil
}
func (l *Logic) MetricsGetReceivedTotalWeek(ctx context.Context) (*logic.MetricsDataPointsTime, error) {
now := time.Now().UTC()
now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
var days logic.MetricsDataPointsTime = make([]logic.MetricsDataPointTime, 7)
for i := 0; i < 7; i++ {
day := now.Add(-24 * time.Duration(i) * time.Hour)
count := 0
var err error
count, err = l.kv.GetMetricsReceivedTotal(ctx, day)
if err != nil && !errors.Is(err, kv.ErrNil) {
return nil, err
}
days[i] = logic.MetricsDataPointTime{
X: day,
Y: count,
}
}
return &days, nil
}

View File

@ -2,6 +2,9 @@
Account:
one: Account
other: Accounts
Activity:
one: Activity
other: Activities
ActivityLog:
one: Activity Log
other: Activity Logs
@ -34,9 +37,9 @@ Create: Create
Delete: Delete
DeleteBlockConfirmDomain: Are you sure you want to delete the block for {{.Domain}}?
DeleteBlockDomain: Delete Block {{.Domain}}
DeliveryStat:
one: Delivery Stat
other: Delivery Stats
DeliveredActivity:
one: Delivered Activity
other: Delivered Activities
Description:
one: Description
other: Descriptions
@ -78,9 +81,9 @@ OAuthConfigured: OAuth Configured
ObfuscatedDomain:
one: Obfuscated Domain
other: Obfuscated Domains
ReceivedStat:
one: Received Stat
other: Received Stats
ReceivedActivity:
one: Received Activity
other: Received Activities
Relay:
one: Relay
other: Relays

View File

@ -1,7 +1,7 @@
{{ define "admin_instance_view" -}}
{{- $textActorURI := .Localizer.TextActorURI 1 -}}
{{- $textBlock := .Localizer.TextBlock 1 -}}
{{- $textDeliveryStats := .Localizer.TextDeliveryStat 2 -}}
{{- $textDeliveredActivities := .Localizer.TextDeliveredActivity 2 -}}
{{- $textDomain := .Localizer.TextDomain 1 -}}
{{- $textFederation := .Localizer.TextFederation -}}
{{- $textFollowing := .Localizer.TextFollowing -}}
@ -10,7 +10,7 @@
{{- $textInstance := .Localizer.TextInstance 1 -}}
{{- $textMetrics := .Localizer.TextMetric 2 -}}
{{- $textOAuthConfigured := .Localizer.TextOAuthConfigured -}}
{{- $textReceivedStats := .Localizer.TextReceivedStat 2 -}}
{{- $textReceivedActivities := .Localizer.TextReceivedActivity 2 -}}
{{- $textServerHostname := .Localizer.TextServerHostname 1 -}}
{{- $textSoftware := .Localizer.TextSoftware -}}
{{- template "header" . }}
@ -118,11 +118,11 @@
<div class="col mb-3">
<div class="row mb-3">
<div class="col-lg">
<div class="fw-bold" lang="{{ $textDeliveryStats.Language }}">{{ $textDeliveryStats }}</div>
<div class="fw-bold" lang="{{ $textDeliveredActivities.Language }}">{{ $textDeliveredActivities }}</div>
<canvas id="deliveryChartContainer" style="height: 300px; width: 100%;"></canvas>
</div>
<div class="col-lg">
<div class="fw-bold" lang="{{ $textReceivedStats.Language }}">{{ $textReceivedStats }}</div>
<div class="fw-bold" lang="{{ $textReceivedActivities.Language }}">{{ $textReceivedActivities }}</div>
<canvas id="receiveChartContainer" style="height: 300px; width: 100%;"></canvas>
</div>
</div>

View File

@ -2,6 +2,7 @@
{{- $textBlockedDomains := .Localizer.TextBlockedDomain 2 -}}
{{- $textHowToJoin := .Localizer.TextHowToJoin -}}
{{- $textInstances := .Localizer.TextInstance 2 -}}
{{- $extReceivedActivities := .Localizer.TextReceivedActivity 2 -}}
{{- $textRelay := .Localizer.TextRelay 1 -}}
{{- template "header" . }}
<div class="container">
@ -50,6 +51,16 @@
</ul>
</div>
</div>
<div class="row">
<div class="col mt-4">
<h2 lang="{{ $extReceivedActivities.Language }}"><i class="fa-solid fa-inbox"></i> {{ $extReceivedActivities }}</h2>
</div>
</div>
<div class="row">
<div class="col mt-4">
<canvas id="receiveChartContainer" style="height: 350px; width: 100%;"></canvas>
</div>
</div>
</div><!-- /.container -->
{{ template "footer" . }}
{{ end }}