Skip to content

Commit

Permalink
adding the ability to add one custom dashboard at a time.
Browse files Browse the repository at this point in the history
make sure source information is automatically populated for remote dashboards
adding modals to add dashboard via UI
addign modals to edit
make sure we can switch between dashboards.
  • Loading branch information
AnalogJ committed Aug 25, 2023
1 parent 31479a4 commit cb6cb1d
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 47 deletions.
64 changes: 43 additions & 21 deletions backend/pkg/web/handler/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func GetDashboard(c *gin.Context) {
return
}

dashboards, err = getDashboardFromDir(dirEntries, os.ReadFile)
dashboards, err = getDashboardFromDir(cacheDir, dirEntries, os.ReadFile)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
Expand All @@ -77,7 +77,7 @@ func GetDashboard(c *gin.Context) {
return
}

dashboards, err = getDashboardFromDir(dirEntries, dashboardFS.ReadFile)
dashboards, err = getDashboardFromDir("dashboard", dirEntries, dashboardFS.ReadFile)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
Expand All @@ -87,20 +87,28 @@ func GetDashboard(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": dashboards})
}

func SaveDashboardLocations(c *gin.Context) {
func AddDashboardLocation(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
//appConfig := c.MustGet(pkg.ContextKeyTypeConfig).(config.Interface)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)

var dashboardLocation map[string]string
if err := c.ShouldBindJSON(&dashboardLocation); err != nil {
logger.Errorln("An error occurred while parsing new dashboard location", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}

//load settings from database
logger.Infof("Loading User Settings..")
userSettings, err := databaseRepo.LoadUserSettings(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

//override locations with new locations
userSettings.DashboardLocations = c.PostFormArray("dashboardLocations")
userSettings.DashboardLocations = append(userSettings.DashboardLocations, dashboardLocation["location"])

logger.Debugf("User Settings: %v", userSettings)

Expand Down Expand Up @@ -130,11 +138,15 @@ func SaveDashboardLocations(c *gin.Context) {
}
cacheDashboards = append(cacheDashboards, filepath.Base(cacheDashboardLocation))
}

if len(cacheErrors) > 0 {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Errorf("error caching dashboards: %v", cacheErrors)})
return
}
//cleanup any files in the cache that are no longer in the dashboard locations
logger.Infof("Cleaning cache dir: %v", cacheDir)
dirEntries, err := os.ReadDir(cacheDir)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Errorf("error before cleaning cache dir: %v", err)})
return
}
for _, dirEntry := range dirEntries {
Expand All @@ -147,18 +159,13 @@ func SaveDashboardLocations(c *gin.Context) {
}
}

if len(cacheErrors) > 0 {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": cacheErrors})
err = databaseRepo.SaveUserSettings(c, userSettings)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Errorf("error saving user settings: %v", err)})
return
} else {
err = databaseRepo.SaveUserSettings(c, userSettings)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}

c.JSON(http.StatusOK, gin.H{"success": true})
return
}

//private functions
Expand Down Expand Up @@ -187,10 +194,17 @@ func cacheCustomDashboard(logger *logrus.Entry, githubClient *github.Client, cac

logger.Infof("Processing custom dashboard from %v", remoteDashboardLocation)

gist, _, err := githubClient.Gists.Get(context.Background(), remoteDashboardLocation)
gistSlug := strings.TrimPrefix(remoteDashboardLocation, "https://gist.github.com/")
gistSlugParts := strings.Split(gistSlug, "/")
if len(gistSlugParts) != 2 {
return "", fmt.Errorf("invalid gist slug: %v", gistSlug)
}

gist, _, err := githubClient.Gists.Get(context.Background(), gistSlugParts[1])
if err != nil {
return "", fmt.Errorf("error retrieving remote gist: %v", err)
}
logger.Debugf("Got Gist %v, files: %d", gist.GetID(), len(gist.GetFiles()))

//check if gist has more than 1 file
var dashboardJsonFile github.GistFile
Expand All @@ -216,28 +230,36 @@ func cacheCustomDashboard(logger *logrus.Entry, githubClient *github.Client, cac
}

//ensure that the file is valid json
logger.Debugf("validating dashboard gist json")
var dashboardJson map[string]interface{}
err = json.Unmarshal([]byte(dashboardJsonFile.GetContent()), &dashboardJson)
if err != nil {
return "", fmt.Errorf("error unmarshalling dashboard configuration (invalid JSON?): %v", err)
}

dashboardJson["source"] = remoteDashboardLocation
dashboardJson["id"] = gist.GetID()

//TODO: validate against DashboardConfigSchema

absCacheFileLocation := filepath.Join(cacheDir, gist.GetID())
absCacheFileLocation := filepath.Join(cacheDir, fmt.Sprintf("%s.json", gist.GetID()))
//write it to filesystem
logger.Infof("Writing new dashboard configuration to filesystem: %v", remoteDashboardLocation)

//write file to cache
err = os.WriteFile(absCacheFileLocation, []byte(dashboardJsonFile.GetContent()), 0644)
dashboardJsonBytes, err := json.MarshalIndent(dashboardJson, "", " ")
if err != nil {
return "", fmt.Errorf("error marshalling dashboard configuration: %v", err)
}
err = os.WriteFile(absCacheFileLocation, dashboardJsonBytes, 0644)
if err != nil {
return "", fmt.Errorf("error writing dashboard configuration to cache: %v", err)
}

return absCacheFileLocation, nil
}

func getDashboardFromDir(dirEntries []fs.DirEntry, fsReadFile func(name string) ([]byte, error)) ([]map[string]interface{}, error) {
func getDashboardFromDir(parentDir string, dirEntries []fs.DirEntry, fsReadFile func(name string) ([]byte, error)) ([]map[string]interface{}, error) {
dashboards := []map[string]interface{}{}

for _, file := range dirEntries {
Expand All @@ -246,7 +268,7 @@ func getDashboardFromDir(dirEntries []fs.DirEntry, fsReadFile func(name string)
}

//unmarshal file into map
embeddedFile, err := fsReadFile("dashboard/" + file.Name())
embeddedFile, err := fsReadFile(filepath.Join(parentDir, file.Name()))
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
secure.POST("/resource/composition", handler.CreateResourceComposition)

secure.GET("/dashboards", handler.GetDashboard)
secure.POST("/dashboards", handler.AddDashboardLocation)
//secure.GET("/dashboard/:dashboardId", handler.GetDashboard)

secure.POST("/query", handler.QueryResourceFhir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h2 class="az-content-title">Connected Sources</h2>
<ng-template #contentModalRef let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{modalSelectedSourceListItem?.metadata["display"]}}</h4>
<button type="button" class="btn btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<button type="button" class="btn close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/models/widget/dashboard-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class DashboardConfig {
schema_version: string //increment this number when the config schema changes, not controlled by user.
title: string
description?: string
source?: string //remote dashboard source (not present for default/embedded dashboards)

widgets: DashboardWidgetConfig[]
}
93 changes: 81 additions & 12 deletions frontend/src/app/pages/dashboard/dashboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,29 @@ <h6>All Sources</h6>

<div class="az-dashboard-nav">
<nav class="nav">
<a *ngFor="let dashboardConfig of dashboardConfigs; let i = index"
class="nav-link"
[class.active]="selectedDashboardConfigNdx === i"
(click)="changeSelectedDashboard(i)"
>{{dashboardConfig.title}}</a>
</nav>

<nav class="nav">
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-save"></i> Export</a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-file-pdf"></i> Create</a>
<a class="nav-link" (click)="toggleEditableGrid()"><i class="fas fa-edit"></i> Edit </a>
<a class="nav-link" (click)="showAddDashboardLocationModal(addDashboard)"><i class="fas fa-plus"></i> Add</a>
<a class="nav-link" (click)="showDashboardSettingsModal(dashboardSettings)"><i class="fas fa-sliders-h"></i> Settings</a>
<a class="nav-link"><i class="fas fa-ellipsis"></i></a>
</nav>
</div>

<div class="row mt-5 mb-3">
<div class="col-12">

<div class="alert alert-warning" role="alert">
<strong>Warning!</strong> Fasten Health is in the process of implementing a customizable widget based dashboard.
<br/>
<ul>
<li>Users will be able to add, remove, and re-organize widgets on their dashboard.</li>
<li>Users will be able to create multiple dashboards, and switch between them.</li>
<li>Users will be able to share their dashboards with other users.</li>
</ul>
<div class="alert alert-info" role="alert">
<strong>Description:</strong> {{dashboardConfigs?.[selectedDashboardConfigNdx]?.description}}
<br/>
<strong>This functionality is not yet fully completed</strong> but this example dashboard below will give you a sense of what this may look like. Widgets in yellow only contain placeholder data.
<span *ngIf="dashboardConfigs?.[selectedDashboardConfigNdx]?.source">
<strong>Source:</strong> <a [href]="dashboardConfigs?.[selectedDashboardConfigNdx]?.source" target="_blank">{{dashboardConfigs?.[selectedDashboardConfigNdx]?.source}}</a>
</span>
</div>
</div>
</div>
Expand All @@ -61,3 +61,72 @@ <h6>All Sources</h6>
</div><!-- az-content-body -->
</div>
</div><!-- az-content -->

<ng-template #addDashboard let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Profile update</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="remoteDashboardLocation">Remote Dashboard Location (Github Gist)<span ngbTooltip="required" class="text-danger">*</span></label>
<div class="input-group">
<input
required
id="remoteDashboardLocation"
class="form-control"
placeholder="https://gist.github.com/..."
name="remoteDashboardLocation"
type="url"
[(ngModel)]="dashboardLocation"
/>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" [disabled]="addDashboardLoading" (click)="addDashboardLocation()">Add</button>
</div>
</ng-template>


<ng-template #dashboardSettings let-modal>
<div class="modal-header">
<h4 class="modal-title">{{dashboardConfigs?.[selectedDashboardConfigNdx]?.title}} Settings</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12">
<p>{{dashboardConfigs?.[selectedDashboardConfigNdx]?.description}}</p>
</div>
</div>


<div class="row row-xs align-items-center mg-b-20">
<div class="col-12">
<h6>Editor Mode</h6>
<p>Editor mode allows you to reorganize widgets on your dashboard</p>
<div (click)="toggleEditableGrid()" [class.on]="!this.gridEditDisabled" class="az-toggle"><span></span></div>
</div><!-- col -->
</div>
<div *ngIf="dashboardConfigs?.[selectedDashboardConfigNdx]?.source" class="row row-xs align-items-center mg-b-20">
<div class="col-12">
<h6>Refresh</h6>
<p>Click to refresh your dashboard configuration. Only applicable to remote dashboards.</p>
</div><!-- col -->
<div class="col-4">
<button ngbTooltip="not yet implemented" class="btn btn-outline-indigo">Refresh</button>
</div><!-- col -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-danger mr-auto" ngbTooltip="not yet implemented">Delete</button>
<button type="button" class="btn btn-outline-dark" (click)="modal.close('Save click')">Close</button>
</div>
</ng-template>
Loading

0 comments on commit cb6cb1d

Please sign in to comment.