From cb6cb1d2c56c69f50736e2ea8b38e3ba2889516a Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 24 Aug 2023 17:07:06 -0700 Subject: [PATCH] adding the ability to add one custom dashboard at a time. 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. --- backend/pkg/web/handler/dashboard.go | 64 ++++++++----- backend/pkg/web/server.go | 1 + .../medical-sources-connected.component.html | 2 +- .../src/app/models/widget/dashboard-config.ts | 1 + .../pages/dashboard/dashboard.component.html | 93 ++++++++++++++++--- .../pages/dashboard/dashboard.component.ts | 83 ++++++++++++++--- .../src/app/services/fasten-api.service.ts | 12 +++ 7 files changed, 209 insertions(+), 47 deletions(-) diff --git a/backend/pkg/web/handler/dashboard.go b/backend/pkg/web/handler/dashboard.go index 39914bd94..4eac93b0e 100644 --- a/backend/pkg/web/handler/dashboard.go +++ b/backend/pkg/web/handler/dashboard.go @@ -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 @@ -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 @@ -87,11 +87,18 @@ 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) @@ -99,8 +106,9 @@ func SaveDashboardLocations(c *gin.Context) { 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) @@ -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 { @@ -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 @@ -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 @@ -216,20 +230,28 @@ 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) } @@ -237,7 +259,7 @@ func cacheCustomDashboard(logger *logrus.Entry, githubClient *github.Client, cac 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 { @@ -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 } diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index d4b7e47f5..c7b240dc8 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -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) diff --git a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html index f05a976a5..6b84bbc88 100644 --- a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html +++ b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html @@ -14,7 +14,7 @@

Connected Sources

diff --git a/frontend/src/app/models/widget/dashboard-config.ts b/frontend/src/app/models/widget/dashboard-config.ts index 19682fb8a..a2189ba41 100644 --- a/frontend/src/app/models/widget/dashboard-config.ts +++ b/frontend/src/app/models/widget/dashboard-config.ts @@ -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[] } diff --git a/frontend/src/app/pages/dashboard/dashboard.component.html b/frontend/src/app/pages/dashboard/dashboard.component.html index d5f6337a2..3b7112069 100644 --- a/frontend/src/app/pages/dashboard/dashboard.component.html +++ b/frontend/src/app/pages/dashboard/dashboard.component.html @@ -28,29 +28,29 @@
All Sources
- -
@@ -61,3 +61,72 @@
All Sources
+ + + + + + + + + + + + + diff --git a/frontend/src/app/pages/dashboard/dashboard.component.ts b/frontend/src/app/pages/dashboard/dashboard.component.ts index 7863474db..85bb57806 100644 --- a/frontend/src/app/pages/dashboard/dashboard.component.ts +++ b/frontend/src/app/pages/dashboard/dashboard.component.ts @@ -9,6 +9,7 @@ import {LighthouseService} from '../../services/lighthouse.service'; import { GridStack, GridStackOptions, GridStackWidget } from 'gridstack'; import {GridstackComponent, NgGridStackOptions} from '../../components/gridstack/gridstack.component'; import {DashboardConfig} from '../../models/widget/dashboard-config'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; // unique ids sets for each item for correct ngFor updating @@ -30,6 +31,11 @@ export class DashboardComponent implements OnInit { metadataSource: { [name: string]: MetadataSource } dashboardConfigs: DashboardConfig[] = [] + selectedDashboardConfigNdx: number = 0 + + //dashboardLocation is used to store the location of the dashboard that we're trying to add + addDashboardLoading: boolean = false + dashboardLocation: string = '' @ViewChild(GridstackComponent) gridComp?: GridstackComponent; @@ -39,6 +45,7 @@ export class DashboardComponent implements OnInit { private router: Router, private componentFactoryResolver: ComponentFactoryResolver, private vcRef: ViewContainerRef, + private modalService: NgbModal, ) { } ngOnInit() { @@ -54,19 +61,7 @@ export class DashboardComponent implements OnInit { //setup dashboard configs console.log("DASHBOARDS!", this.dashboardConfigs) - this.dashboardConfigs?.[0].widgets.forEach((widgetConfig) => { - console.log("Adding Widgets to Dashboard Grid") - - this.gridComp?.grid?.addWidget({ - x: widgetConfig.x, - y: widgetConfig.y, - w: widgetConfig.width, - h: widgetConfig.height, - // @ts-ignore - type: widgetConfig.item_type, - widgetConfig: !!widgetConfig?.queries?.length ? widgetConfig : undefined, - }) - }) + this.changeSelectedDashboard(0) }, error => { this.loading = false @@ -116,6 +111,25 @@ export class DashboardComponent implements OnInit { children: [], } + public changeSelectedDashboard(selectedDashboardNdx: number){ + this.selectedDashboardConfigNdx = selectedDashboardNdx + this.gridComp?.grid?.removeAll() //clear the grid + + this.dashboardConfigs?.[this.selectedDashboardConfigNdx]?.widgets?.forEach((widgetConfig) => { + console.log("Adding Widgets to Dashboard Grid") + + this.gridComp?.grid?.addWidget({ + x: widgetConfig.x, + y: widgetConfig.y, + w: widgetConfig.width, + h: widgetConfig.height, + // @ts-ignore + type: widgetConfig.item_type, + widgetConfig: !!widgetConfig?.queries?.length ? widgetConfig : undefined, + }) + }) + } + public toggleEditableGrid() { this.gridEditDisabled = !this.gridEditDisabled; console.log('toggle - is disabled', this.gridEditDisabled) @@ -123,4 +137,47 @@ export class DashboardComponent implements OnInit { this.gridEditDisabled ? this.gridComp.grid?.disable(true) : this.gridComp.grid?.enable(true); } + + public addDashboardLocation(){ + this.addDashboardLoading = true + this.fastenApi.addDashboardLocation(this.dashboardLocation).subscribe((result) => { + console.log("Added Remote Dashboard", result) + this.addDashboardLoading = false + + this.modalService.dismissAll() + + //reload the page + window.location.reload() + }, (error) => { + console.log("Error Adding Remote Dashboard", error) + this.addDashboardLoading = false + }, + () => { + console.log("Completed Adding Remote Dashboard") + this.addDashboardLoading = false + }) + } + + public showAddDashboardLocationModal(content) { + this.dashboardLocation = '' + this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then( + (result) => { + console.log(`Closed with: ${result}`) + }, + (reason) => { + console.log(`Dismissed ${reason}`) + }, + ); + } + public showDashboardSettingsModal(content) { + this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then( + (result) => { + console.log(`Closed with: ${result}`) + }, + (reason) => { + console.log(`Dismissed ${reason}`) + }, + ); + } + } diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index ccbbcf6fe..d318ef5f5 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -197,6 +197,18 @@ export class FastenApiService { ); } + addDashboardLocation(location: string): Observable { + return this._httpClient.post(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/dashboards`, { + "location": location + }) + .pipe( + map((response: ResponseWrapper) => { + console.log("RESPONSE", response) + return response + }) + ); + } + //this method allows a user to manually group related FHIR resources together (conditions, encounters, etc). createResourceComposition(title: string, resources: ResourceFhir[]){ return this._httpClient.post(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/composition`, {