Add ongoing sync indicator to dashboard
This means I can remove the "loading" state of the dashboard.
This commit is contained in:
parent
53ee020fe0
commit
05b3caaa73
4 changed files with 37 additions and 86 deletions
44
arcgis.go
44
arcgis.go
|
|
@ -39,6 +39,8 @@ import (
|
|||
"github.com/uber/h3-go/v4"
|
||||
)
|
||||
|
||||
var syncStatusByOrg map[int32]bool
|
||||
|
||||
// When the API responds that the token is now invalidated
|
||||
type InvalidatedTokenError struct{}
|
||||
|
||||
|
|
@ -129,6 +131,10 @@ func generateCodeVerifier() string {
|
|||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func isSyncOngoing(org_id int32) bool {
|
||||
return syncStatusByOrg[org_id]
|
||||
}
|
||||
|
||||
// Find out what we can about this user
|
||||
func updateArcgisUserData(ctx context.Context, user *models.User, access_token string, access_token_expires time.Time, refresh_token string, refresh_token_expires time.Time) {
|
||||
client := arcgis.NewArcGIS(
|
||||
|
|
@ -151,40 +157,13 @@ func updateArcgisUserData(ctx context.Context, user *models.User, access_token s
|
|||
log.Error().Err(err).Msg("Failed to update oauth token portal data")
|
||||
return
|
||||
}
|
||||
var org *models.Organization
|
||||
orgs, err := models.Organizations.Query(models.SelectWhere.Organizations.ArcgisName.EQ(portal.Name)).All(ctx, db.PGInstance.BobDB)
|
||||
switch len(orgs) {
|
||||
case 0:
|
||||
setter := models.OrganizationSetter{
|
||||
Name: omitnull.From(portal.Name),
|
||||
org := user.R.Organization
|
||||
err = org.Update(ctx, db.PGInstance.BobDB, &models.OrganizationSetter{
|
||||
ArcgisID: omitnull.From(portal.User.OrgID),
|
||||
ArcgisName: omitnull.From(portal.Name),
|
||||
}
|
||||
org, err = models.Organizations.Insert(&setter).One(ctx, db.PGInstance.BobDB)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create new organization")
|
||||
return
|
||||
}
|
||||
log.Info().Int("org_id", int(org.ID)).Msg("Created new organization")
|
||||
case 1:
|
||||
org = orgs[0]
|
||||
log.Info().Msg("Organization already exists")
|
||||
default:
|
||||
log.Error().Msg("Got too many organizations, bailing")
|
||||
return
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
debug.LogErrorTypeInfo(err)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
} else {
|
||||
log.Error().Err(err).Msg("Failed to query for existing org")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = org.AttachUser(ctx, db.PGInstance.BobDB, user)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Int("user_id", int(user.ID)).Int("org_id", int(org.ID)).Msg("Failed to attach user to organization")
|
||||
log.Error().Err(err).Int32("id", user.R.Organization.ID).Msg("Failed to update organization's arcgis info")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -292,6 +271,7 @@ func redirectURL() string {
|
|||
|
||||
// This is a goroutine that is in charge of getting Fieldseeker data and keeping it fresh.
|
||||
func refreshFieldseekerData(ctx context.Context, newOauthCh <-chan struct{}) {
|
||||
syncStatusByOrg = make(map[int32]bool, 0)
|
||||
for {
|
||||
workerCtx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -448,6 +428,7 @@ func periodicallyExportFieldseeker(ctx context.Context, org *models.Organization
|
|||
return fmt.Errorf("Failed to get oauth for org: %w", err)
|
||||
}
|
||||
err = exportFieldseekerData(ctx, org, oauth)
|
||||
syncStatusByOrg[org.ID] = false
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to export Fieldseeker data: %w", err)
|
||||
}
|
||||
|
|
@ -458,6 +439,7 @@ func periodicallyExportFieldseeker(ctx context.Context, org *models.Organization
|
|||
}
|
||||
func exportFieldseekerData(ctx context.Context, org *models.Organization, oauth *models.OauthToken) error {
|
||||
log.Info().Msg("Update Fieldseeker data")
|
||||
syncStatusByOrg[org.ID] = true
|
||||
var err error
|
||||
ar := arcgis.NewArcGIS(
|
||||
arcgis.AuthenticatorOAuth{
|
||||
|
|
|
|||
21
html.go
21
html.go
|
|
@ -34,7 +34,6 @@ var embeddedFiles embed.FS
|
|||
var (
|
||||
cell = newBuiltTemplate("cell", "authenticated")
|
||||
dashboard = newBuiltTemplate("dashboard", "authenticated")
|
||||
dashboardLoading = newBuiltTemplate("dashboard-loading", "authenticated")
|
||||
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
|
||||
settings = newBuiltTemplate("settings", "authenticated")
|
||||
source = newBuiltTemplate("source", "authenticated")
|
||||
|
|
@ -142,6 +141,7 @@ type ContentDashboard struct {
|
|||
CountMosquitoSources int
|
||||
CountServiceRequests int
|
||||
Geo template.JS
|
||||
IsSyncOngoing bool
|
||||
LastSync *time.Time
|
||||
MapData ComponentMap
|
||||
Org string
|
||||
|
|
@ -315,10 +315,6 @@ func htmlCell(ctx context.Context, w http.ResponseWriter, user *models.User, c i
|
|||
func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
|
||||
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
htmlDashboardLoading(ctx, w, user)
|
||||
return
|
||||
}
|
||||
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
@ -332,6 +328,7 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
|
|||
} else {
|
||||
lastSync = &sync.Created
|
||||
}
|
||||
is_syncing := isSyncOngoing(org.ID)
|
||||
inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
respondError(w, "Failed to get inspection count", err, http.StatusInternalServerError)
|
||||
|
|
@ -370,6 +367,7 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
|
|||
CountInspections: int(inspectionCount),
|
||||
CountMosquitoSources: int(sourceCount),
|
||||
CountServiceRequests: int(serviceCount),
|
||||
IsSyncOngoing: is_syncing,
|
||||
LastSync: lastSync,
|
||||
MapData: ComponentMap{
|
||||
MapboxToken: MapboxToken,
|
||||
|
|
@ -381,19 +379,6 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
|
|||
renderOrError(w, dashboard, data)
|
||||
}
|
||||
|
||||
// A dashboard to show while we are still downloading information about their organization
|
||||
func htmlDashboardLoading(ctx context.Context, w http.ResponseWriter, user *models.User) {
|
||||
userContent, err := contentForUser(ctx, user)
|
||||
if err != nil {
|
||||
respondError(w, "Failed to get user context", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := ContentDashboardLoading{
|
||||
User: userContent,
|
||||
}
|
||||
renderOrError(w, dashboardLoading, data)
|
||||
}
|
||||
|
||||
func htmlMock(t string, w http.ResponseWriter, code string) {
|
||||
data := ContentMock{
|
||||
DistrictName: "Delta MVCD",
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
{{template "authenticated.html" .}}
|
||||
|
||||
{{define "title"}}Dash{{end}}
|
||||
{{define "extraheader"}}
|
||||
<script src='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js'></script>
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' />
|
||||
<script>
|
||||
function onLoad() {
|
||||
console.log("Map init done.");
|
||||
}
|
||||
window.addEventListener("load", onLoad);
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.dashboard-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container dashboard-container">
|
||||
<p>We're downloading the data we need, hold on. This page will refresh every 10 seconds automatically until we've got it.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -163,6 +163,10 @@ body {
|
|||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.syncing {
|
||||
color: #28a745;
|
||||
animation: fa-spin 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
|
|
@ -174,10 +178,16 @@ body {
|
|||
<p class="text-muted">Overview of mosquito control activities in your district</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end">
|
||||
{{ if .IsSyncOngoing }}
|
||||
<p class="last-refreshed mb-0">
|
||||
<i class="fas fa-sync-alt me-2 syncing"></i>Syncing now...
|
||||
</p>
|
||||
{{ else }}
|
||||
<p class="last-refreshed mb-0">
|
||||
<i class="fas fa-sync-alt me-2"></i>Last updated: <span id="last-refreshed-time">{{ .LastSync | timeSince }}</span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-3">Refresh Data</button>
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue