diff --git a/cumulocity.json b/cumulocity.json index 5d81b88..471bde3 100644 --- a/cumulocity.json +++ b/cumulocity.json @@ -1,7 +1,7 @@ { - "name": "Runtime Loaded Widget", - "contextPath": "cumulocity-runtime-widget", - "key": "cumulocity-runtime-widget-application-key", + "name": "Datahub Widget", + "contextPath": "cumulocity-datahub-widget", + "key": "cumulocity-datahub-widget-application-key", "contentSecurityPolicy": "default-src 'self'", "icon": { "class": "fa fa-puzzle-piece" diff --git a/package.json b/package.json index 9e33ac6..26e915c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "cumulocity-runtime-widget", + "name": "cumulocity-datahub-widget", "interleave": { - "dist\\bundle-src\\custom-widget.js": "cumulocity-runtime-widget-CustomWidget", - "dist/bundle-src/custom-widget.js": "cumulocity-runtime-widget-CustomWidget" + "dist\\bundle-src\\custom-widget.js": "cumulocity-datahub-widget-CustomWidget", + "dist/bundle-src/custom-widget.js": "cumulocity-datahub-widget-CustomWidget" }, "version": "0.0.0-development", - "description": "Template widget for runtime loading in Cumulocity using the cumulocity-runtime-widget-loader (written by Software AG Global Presales)", + "description": "An example widget that pulls data from Cumulocity IoT DataHub (written by Software AG Global Presales)", "scripts": { "build": "gulp" }, diff --git a/src/datahub-widget/datahub-query-wrapper-service.ts b/src/datahub-widget/datahub-query-wrapper-service.ts index 747b05e..22d5316 100644 --- a/src/datahub-widget/datahub-query-wrapper-service.ts +++ b/src/datahub-widget/datahub-query-wrapper-service.ts @@ -23,13 +23,12 @@ export interface JobResult { rows: T[] } - -@Injectable() +@Injectable({providedIn: 'root'}) export class QueryWrapperService { constructor(private queryService: QueryService) { } - async queryForResults(queryString: string, config?: QueryConfig): Promise> { + async queryForResults(queryString: string, config: QueryConfig = {}): Promise> { //post job const jobId = await this.postQuery(queryString); @@ -76,4 +75,4 @@ export class QueryWrapperService { sleep(milliseconds) { return new Promise(resolve => setTimeout(resolve, milliseconds)); } -} \ No newline at end of file +} diff --git a/src/datahub-widget/datahub-widget-config.component.ts b/src/datahub-widget/datahub-widget-config.component.ts index e73019d..1082790 100644 --- a/src/datahub-widget/datahub-widget-config.component.ts +++ b/src/datahub-widget/datahub-widget-config.component.ts @@ -16,16 +16,98 @@ * limitations under the License. */ -import {Component, Input} from '@angular/core'; +import {Component, Input, OnDestroy} from '@angular/core'; +import {switchMap} from "rxjs/operators"; +import {from, Subject, Subscription} from "rxjs"; +import {QueryWrapperService} from "./datahub-query-wrapper-service"; + +export interface IDatahubWidgetConfig { + queryString: string, + columns: { + colName: string, + displayName: string, + visibility: 'visible' | 'hidden' + }[] +} @Component({ - template: `
- - - - -
` + template: ` +
+ + + + + + + + + + + + + + + + + + + + +
VisibleDatahub ColumnLabel
{{col.colName}}
+
` }) -export class DatahubWidgetConfig { - @Input() config: any = {}; +export class DatahubWidgetConfig implements OnDestroy { + _config: IDatahubWidgetConfig = { + queryString: '', + columns: [] + }; + + @Input() set config(config: IDatahubWidgetConfig) { + this._config = Object.assign(config, { + ...this._config, + ...config + }); + }; + get config(): IDatahubWidgetConfig { + return this._config + } + + querySubject = new Subject() + subscriptions = new Subscription(); + + constructor(private queryService: QueryWrapperService) { + this.subscriptions.add( + this.querySubject + .pipe(switchMap(query => from(this.queryService.queryForResults(query)))) + .subscribe(result => { + this.config.columns = result.schema + .map(column => column.name) + .map(colName => { + let matchingColumn = this.config.columns.find(col => col.colName == colName); + if (matchingColumn) { + return matchingColumn; + } else { + return { + colName, + displayName: this.formatHeading(colName), + visibility: 'visible' + }; + } + }); + }) + ); + } + + updateColumnDefinitions() { + this.querySubject.next(this.config.queryString); + } + + formatHeading(value: string): string { + return value.replace(/_/g, " ").replace(/([^A-Z\s])(?=[A-Z])/g, "$1 ").replace(/\s+/g, " ") + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } } diff --git a/src/datahub-widget/datahub-widget.component.html b/src/datahub-widget/datahub-widget.component.html index 2aa5834..8f30058 100644 --- a/src/datahub-widget/datahub-widget.component.html +++ b/src/datahub-widget/datahub-widget.component.html @@ -1 +1,10 @@ -

{{config?.text || 'No text'}}

+ + + + + + + + + +
{{col.displayName}}
{{row[col.colName]}}
diff --git a/src/datahub-widget/datahub-widget.component.ts b/src/datahub-widget/datahub-widget.component.ts index db1b40e..2e1dd22 100644 --- a/src/datahub-widget/datahub-widget.component.ts +++ b/src/datahub-widget/datahub-widget.component.ts @@ -16,12 +16,62 @@ * limitations under the License. */ -import { Component, Input } from '@angular/core'; +import {Component, Input, OnDestroy} from '@angular/core'; +import {BehaviorSubject, from, Subscription} from "rxjs"; +import {distinctUntilChanged, switchMap} from "rxjs/operators"; +import {IDatahubWidgetConfig} from "./datahub-widget-config.component"; +import {QueryWrapperService} from "./datahub-query-wrapper-service"; @Component({ templateUrl: './datahub-widget.component.html', styles: [ `.text { transform: scaleX(-1); font-size: 3em ;}` ] }) -export class DatahubWidgetComponent { - @Input() config; +export class DatahubWidgetComponent implements OnDestroy { + _config: IDatahubWidgetConfig = { + queryString: '', + columns: [] + }; + + @Input() set config(config: IDatahubWidgetConfig) { + this._config = Object.assign(config, { + ...this._config, + ...config + }); + this.querySubject.next(this.config.queryString); + this.visibleColumns = this.config.columns.filter(col => col.visibility == 'visible') as { + colName: string, + displayName: string, + visibility: 'visible' + }[]; + }; + get config(): IDatahubWidgetConfig { + return this._config + } + + subscriptions = new Subscription(); + querySubject = new BehaviorSubject(undefined); + + visibleColumns: { + colName: string, + displayName: string, + visibility: 'visible' + }[]; + rows: string[]; + + constructor(private queryService: QueryWrapperService) { + this.subscriptions.add( + this.querySubject + .pipe( + distinctUntilChanged(), + switchMap(query => from(this.queryService.queryForResults(query))) + ) + .subscribe(results => { + this.rows = results.rows + }) + ); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } } diff --git a/src/datahub-widget/datahub-widget.module.ts b/src/datahub-widget/datahub-widget.module.ts index 154b21d..ae97da3 100644 --- a/src/datahub-widget/datahub-widget.module.ts +++ b/src/datahub-widget/datahub-widget.module.ts @@ -38,13 +38,13 @@ import {DatahubWidgetComponent} from "./datahub-widget.component"; { provide: HOOK_COMPONENTS, multi: true, - useValue: [{ + useValue: { id: 'acme.test.widget', label: 'Test widget', description: 'Displays some mirrored text', component: DatahubWidgetComponent, configComponent: DatahubWidgetConfig, - }] + } } ], }) diff --git a/src/datahub-widget/query.service.ts b/src/datahub-widget/query.service.ts index b73ea1b..4b853bd 100644 --- a/src/datahub-widget/query.service.ts +++ b/src/datahub-widget/query.service.ts @@ -1,17 +1,21 @@ -import { Injectable } from '@angular/core'; +import {Injectable, Injector} from '@angular/core'; import { FetchClient } from '@c8y/ngx-components/api'; import { IFetchOptions } from '@c8y/client'; -@Injectable() +@Injectable({providedIn: 'root'}) export class QueryService { private readonly dataHubDremioApi = '/service/datahub/dremio/api/v3'; + private readonly fetchClient: FetchClient; private fetchOptions: IFetchOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; - constructor(private fetchClient: FetchClient) { } + constructor(injector: Injector) { + // Cumulocity won't let you inject this if your @Injectable is provided in root... so this is a workaround.. + this.fetchClient = injector.get(FetchClient); + } async getJobState(jobId) { const response = await this.fetchClient.fetch(this.dataHubDremioApi + '/job/' + jobId, this.fetchOptions); @@ -39,4 +43,4 @@ export class QueryService { throw new Error(await response.text()); } } -} \ No newline at end of file +}