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"
|
"github.com/uber/h3-go/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var syncStatusByOrg map[int32]bool
|
||||||
|
|
||||||
// When the API responds that the token is now invalidated
|
// When the API responds that the token is now invalidated
|
||||||
type InvalidatedTokenError struct{}
|
type InvalidatedTokenError struct{}
|
||||||
|
|
||||||
|
|
@ -129,6 +131,10 @@ func generateCodeVerifier() string {
|
||||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSyncOngoing(org_id int32) bool {
|
||||||
|
return syncStatusByOrg[org_id]
|
||||||
|
}
|
||||||
|
|
||||||
// Find out what we can about this user
|
// 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) {
|
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(
|
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")
|
log.Error().Err(err).Msg("Failed to update oauth token portal data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var org *models.Organization
|
org := user.R.Organization
|
||||||
orgs, err := models.Organizations.Query(models.SelectWhere.Organizations.ArcgisName.EQ(portal.Name)).All(ctx, db.PGInstance.BobDB)
|
err = org.Update(ctx, db.PGInstance.BobDB, &models.OrganizationSetter{
|
||||||
switch len(orgs) {
|
|
||||||
case 0:
|
|
||||||
setter := models.OrganizationSetter{
|
|
||||||
Name: omitnull.From(portal.Name),
|
|
||||||
ArcgisID: omitnull.From(portal.User.OrgID),
|
ArcgisID: omitnull.From(portal.User.OrgID),
|
||||||
ArcgisName: omitnull.From(portal.Name),
|
ArcgisName: omitnull.From(portal.Name),
|
||||||
}
|
})
|
||||||
org, err = models.Organizations.Insert(&setter).One(ctx, db.PGInstance.BobDB)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create new organization")
|
log.Error().Err(err).Int32("id", user.R.Organization.ID).Msg("Failed to update organization's arcgis info")
|
||||||
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")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,6 +271,7 @@ func redirectURL() string {
|
||||||
|
|
||||||
// This is a goroutine that is in charge of getting Fieldseeker data and keeping it fresh.
|
// This is a goroutine that is in charge of getting Fieldseeker data and keeping it fresh.
|
||||||
func refreshFieldseekerData(ctx context.Context, newOauthCh <-chan struct{}) {
|
func refreshFieldseekerData(ctx context.Context, newOauthCh <-chan struct{}) {
|
||||||
|
syncStatusByOrg = make(map[int32]bool, 0)
|
||||||
for {
|
for {
|
||||||
workerCtx, cancel := context.WithCancel(context.Background())
|
workerCtx, cancel := context.WithCancel(context.Background())
|
||||||
var wg sync.WaitGroup
|
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)
|
return fmt.Errorf("Failed to get oauth for org: %w", err)
|
||||||
}
|
}
|
||||||
err = exportFieldseekerData(ctx, org, oauth)
|
err = exportFieldseekerData(ctx, org, oauth)
|
||||||
|
syncStatusByOrg[org.ID] = false
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to export Fieldseeker data: %w", err)
|
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 {
|
func exportFieldseekerData(ctx context.Context, org *models.Organization, oauth *models.OauthToken) error {
|
||||||
log.Info().Msg("Update Fieldseeker data")
|
log.Info().Msg("Update Fieldseeker data")
|
||||||
|
syncStatusByOrg[org.ID] = true
|
||||||
var err error
|
var err error
|
||||||
ar := arcgis.NewArcGIS(
|
ar := arcgis.NewArcGIS(
|
||||||
arcgis.AuthenticatorOAuth{
|
arcgis.AuthenticatorOAuth{
|
||||||
|
|
|
||||||
21
html.go
21
html.go
|
|
@ -34,7 +34,6 @@ var embeddedFiles embed.FS
|
||||||
var (
|
var (
|
||||||
cell = newBuiltTemplate("cell", "authenticated")
|
cell = newBuiltTemplate("cell", "authenticated")
|
||||||
dashboard = newBuiltTemplate("dashboard", "authenticated")
|
dashboard = newBuiltTemplate("dashboard", "authenticated")
|
||||||
dashboardLoading = newBuiltTemplate("dashboard-loading", "authenticated")
|
|
||||||
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
|
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
|
||||||
settings = newBuiltTemplate("settings", "authenticated")
|
settings = newBuiltTemplate("settings", "authenticated")
|
||||||
source = newBuiltTemplate("source", "authenticated")
|
source = newBuiltTemplate("source", "authenticated")
|
||||||
|
|
@ -142,6 +141,7 @@ type ContentDashboard struct {
|
||||||
CountMosquitoSources int
|
CountMosquitoSources int
|
||||||
CountServiceRequests int
|
CountServiceRequests int
|
||||||
Geo template.JS
|
Geo template.JS
|
||||||
|
IsSyncOngoing bool
|
||||||
LastSync *time.Time
|
LastSync *time.Time
|
||||||
MapData ComponentMap
|
MapData ComponentMap
|
||||||
Org string
|
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) {
|
func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
|
||||||
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
|
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
|
||||||
if err != nil {
|
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)
|
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +328,7 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
|
||||||
} else {
|
} else {
|
||||||
lastSync = &sync.Created
|
lastSync = &sync.Created
|
||||||
}
|
}
|
||||||
|
is_syncing := isSyncOngoing(org.ID)
|
||||||
inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB)
|
inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, "Failed to get inspection count", err, http.StatusInternalServerError)
|
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),
|
CountInspections: int(inspectionCount),
|
||||||
CountMosquitoSources: int(sourceCount),
|
CountMosquitoSources: int(sourceCount),
|
||||||
CountServiceRequests: int(serviceCount),
|
CountServiceRequests: int(serviceCount),
|
||||||
|
IsSyncOngoing: is_syncing,
|
||||||
LastSync: lastSync,
|
LastSync: lastSync,
|
||||||
MapData: ComponentMap{
|
MapData: ComponentMap{
|
||||||
MapboxToken: MapboxToken,
|
MapboxToken: MapboxToken,
|
||||||
|
|
@ -381,19 +379,6 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
|
||||||
renderOrError(w, dashboard, data)
|
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) {
|
func htmlMock(t string, w http.ResponseWriter, code string) {
|
||||||
data := ContentMock{
|
data := ContentMock{
|
||||||
DistrictName: "Delta MVCD",
|
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-size: 2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.syncing {
|
||||||
|
color: #28a745;
|
||||||
|
animation: fa-spin 2s linear infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
|
@ -174,10 +178,16 @@ body {
|
||||||
<p class="text-muted">Overview of mosquito control activities in your district</p>
|
<p class="text-muted">Overview of mosquito control activities in your district</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end">
|
<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">
|
<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>
|
<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>
|
<button class="btn btn-sm btn-outline-primary ms-3">Refresh Data</button>
|
||||||
</p>
|
</p>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue