diff --git a/src-ts/api/dimension/DimensionIntegrationsService.ts b/src-ts/api/dimension/DimensionIntegrationsService.ts index 1ca5b57..7bdc637 100644 --- a/src-ts/api/dimension/DimensionIntegrationsService.ts +++ b/src-ts/api/dimension/DimensionIntegrationsService.ts @@ -1,4 +1,4 @@ -import { GET, Path, PathParam, QueryParam } from "typescript-rest"; +import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import * as Promise from "bluebird"; import { ScalarService } from "../scalar/ScalarService"; import { DimensionStore } from "../../db/DimensionStore"; @@ -12,6 +12,14 @@ interface IntegrationsResponse { widgets: Widget[], } +interface SetEnabledRequest { + enabled: boolean; +} + +interface SetOptionsRequest { + options: any; +} + @Path("/api/v1/dimension/integrations") export class DimensionIntegrationsService { @@ -21,6 +29,26 @@ export class DimensionIntegrationsService { DimensionIntegrationsService.integrationCache.clear(); } + @POST + @Path(":category/:type/enabled") + public setEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetEnabledRequest): Promise { + return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => { + if (category === "widget") { + return DimensionStore.setWidgetEnabled(type, body.enabled); + } else throw new ApiError(400, "Unrecongized category"); + }).then(() => DimensionIntegrationsService.clearIntegrationCache()); + } + + @POST + @Path(":category/:type/options") + public setOptions(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetOptionsRequest): Promise { + return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => { + if (category === "widget") { + return DimensionStore.setWidgetOptions(type, body.options); + } else throw new ApiError(400, "Unrecongized category"); + }).then(() => DimensionIntegrationsService.clearIntegrationCache()); + } + @GET @Path("enabled") public getEnabledIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise { diff --git a/src-ts/db/DimensionStore.ts b/src-ts/db/DimensionStore.ts index ca326d3..6a9b97f 100644 --- a/src-ts/db/DimensionStore.ts +++ b/src-ts/db/DimensionStore.ts @@ -94,6 +94,31 @@ class _DimensionStore { if (isEnabled === true || isEnabled === false) conditions = {where: {isEnabled: isEnabled}}; return WidgetRecord.findAll(conditions).then(widgets => widgets.map(w => new Widget(w))); } + + public setWidgetEnabled(type: string, isEnabled: boolean): Promise { + return this.getWidget(type).then(widget => { + widget.isEnabled = isEnabled; + return widget.save(); + }); + } + + public setWidgetOptions(type: string, options: any): Promise { + const optionsJson = JSON.stringify(options); + return this.getWidget(type).then(widget => { + widget.optionsJson = optionsJson; + return widget.save(); + }); + } + + private getWidget(type: string): Promise { + return WidgetRecord.findAll({where: {type: type}}).then(widgets => { + if (!widgets || widgets.length !== 1) { + return Promise.reject("Widget not found or too many results"); + } + + return Promise.resolve(widgets[0]); + }); + } } export const DimensionStore = new _DimensionStore(); \ No newline at end of file diff --git a/web/app/admin/admin.component.html b/web/app/admin/admin.component.html index 0a269de..2556572 100644 --- a/web/app/admin/admin.component.html +++ b/web/app/admin/admin.component.html @@ -1,5 +1,6 @@
  • Dashboard
  • +
  • Widgets
{{ version }} diff --git a/web/app/admin/widgets/config-dialog.scss b/web/app/admin/widgets/config-dialog.scss new file mode 100644 index 0000000..dd4814b --- /dev/null +++ b/web/app/admin/widgets/config-dialog.scss @@ -0,0 +1,8 @@ +.text-muted { + display: block; + font-size: 12px; +} + +.label-block { + margin-bottom: 15px; +} \ No newline at end of file diff --git a/web/app/admin/widgets/etherpad/etherpad.component.html b/web/app/admin/widgets/etherpad/etherpad.component.html new file mode 100644 index 0000000..4bf7ec7 --- /dev/null +++ b/web/app/admin/widgets/etherpad/etherpad.component.html @@ -0,0 +1,22 @@ +
+
+

Etherpad Widget Configuration

+
+
+ +
+ +
\ No newline at end of file diff --git a/web/app/admin/widgets/etherpad/etherpad.component.scss b/web/app/admin/widgets/etherpad/etherpad.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/widgets/etherpad/etherpad.component.ts b/web/app/admin/widgets/etherpad/etherpad.component.ts new file mode 100644 index 0000000..f2b31fc --- /dev/null +++ b/web/app/admin/widgets/etherpad/etherpad.component.ts @@ -0,0 +1,35 @@ +import { Component } from "@angular/core"; +import { AdminApiService } from "../../../shared/services/admin-api.service"; +import { EtherpadWidget } from "../../../shared/models/integration"; +import { ToasterService } from "angular2-toaster"; +import { DialogRef, ModalComponent } from "ngx-modialog"; +import { WidgetConfigDialogContext } from "../widgets.component"; + +@Component({ + templateUrl: "./etherpad.component.html", + styleUrls: ["./etherpad.component.scss", "../config-dialog.scss"], +}) +export class AdminWidgetEtherpadConfigComponent implements ModalComponent { + + public isUpdating = false; + public widget: EtherpadWidget; + private originalWidget: EtherpadWidget; + + constructor(public dialog: DialogRef, private adminApi: AdminApiService, private toaster: ToasterService) { + this.originalWidget = dialog.context.widget; + this.widget = JSON.parse(JSON.stringify(this.originalWidget)); + } + + public save() { + this.isUpdating = true; + this.adminApi.setWidgetOptions(this.widget.category, this.widget.type, this.widget.options).then(() => { + this.originalWidget.options = this.widget.options; + this.toaster.pop("success", "Widget updated"); + this.dialog.close(); + }).catch(err => { + this.isUpdating = false; + console.error(err); + this.toaster.pop("error", "Error updating widget"); + }); + } +} diff --git a/web/app/admin/widgets/jitsi/jitsi.component.html b/web/app/admin/widgets/jitsi/jitsi.component.html new file mode 100644 index 0000000..43f7978 --- /dev/null +++ b/web/app/admin/widgets/jitsi/jitsi.component.html @@ -0,0 +1,29 @@ +
+
+

Jitsi Widget Configuration

+
+
+ + +
+ +
\ No newline at end of file diff --git a/web/app/admin/widgets/jitsi/jitsi.component.scss b/web/app/admin/widgets/jitsi/jitsi.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/widgets/jitsi/jitsi.component.ts b/web/app/admin/widgets/jitsi/jitsi.component.ts new file mode 100644 index 0000000..bdd6804 --- /dev/null +++ b/web/app/admin/widgets/jitsi/jitsi.component.ts @@ -0,0 +1,35 @@ +import { Component } from "@angular/core"; +import { AdminApiService } from "../../../shared/services/admin-api.service"; +import { JitsiWidget } from "../../../shared/models/integration"; +import { ToasterService } from "angular2-toaster"; +import { DialogRef, ModalComponent } from "ngx-modialog"; +import { WidgetConfigDialogContext } from "../widgets.component"; + +@Component({ + templateUrl: "./jitsi.component.html", + styleUrls: ["./jitsi.component.scss", "../config-dialog.scss"], +}) +export class AdminWidgetJitsiConfigComponent implements ModalComponent { + + public isUpdating = false; + public widget: JitsiWidget; + private originalWidget: JitsiWidget; + + constructor(public dialog: DialogRef, private adminApi: AdminApiService, private toaster: ToasterService) { + this.originalWidget = dialog.context.widget; + this.widget = JSON.parse(JSON.stringify(this.originalWidget)); + } + + public save() { + this.isUpdating = true; + this.adminApi.setWidgetOptions(this.widget.category, this.widget.type, this.widget.options).then(() => { + this.originalWidget.options = this.widget.options; + this.toaster.pop("success", "Widget updated"); + this.dialog.close(); + }).catch(err => { + this.isUpdating = false; + console.error(err); + this.toaster.pop("error", "Error updating widget"); + }); + } +} diff --git a/web/app/admin/widgets/widgets.component.html b/web/app/admin/widgets/widgets.component.html new file mode 100644 index 0000000..2beebd6 --- /dev/null +++ b/web/app/admin/widgets/widgets.component.html @@ -0,0 +1,34 @@ +
+ +
+
+ +
+

Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets + Dimension will offer to users.

+ + + + + + + + + + + + + + + + +
NameDescriptionActions
{{ widget.displayName }}{{ widget.description }} + + + + +
+
+
+
\ No newline at end of file diff --git a/web/app/admin/widgets/widgets.component.scss b/web/app/admin/widgets/widgets.component.scss new file mode 100644 index 0000000..65b9b23 --- /dev/null +++ b/web/app/admin/widgets/widgets.component.scss @@ -0,0 +1,13 @@ +ul { + padding-left: 25px; +} + +.editButton { + cursor: pointer; + position: relative; + top: -5px; +} + +tr td:last-child { + vertical-align: middle; +} \ No newline at end of file diff --git a/web/app/admin/widgets/widgets.component.ts b/web/app/admin/widgets/widgets.component.ts new file mode 100644 index 0000000..286c5b1 --- /dev/null +++ b/web/app/admin/widgets/widgets.component.ts @@ -0,0 +1,69 @@ +import { Component } from "@angular/core"; +import { AdminApiService } from "../../shared/services/admin-api.service"; +import { Widget } from "../../shared/models/integration"; +import { ToasterService } from "angular2-toaster"; +import { AdminWidgetEtherpadConfigComponent } from "./etherpad/etherpad.component"; +import { Modal, overlayConfigFactory } from "ngx-modialog"; +import { BSModalContext } from "ngx-modialog/plugins/bootstrap"; +import { AdminWidgetJitsiConfigComponent } from "./jitsi/jitsi.component"; + +export class WidgetConfigDialogContext extends BSModalContext { + public widget: Widget; +} + +@Component({ + templateUrl: "./widgets.component.html", + styleUrls: ["./widgets.component.scss"], +}) +export class AdminWidgetsComponent { + + public isLoading = true; + public isUpdating = false; + public widgets: Widget[]; + + constructor(private adminApi: AdminApiService, private toaster: ToasterService, private modal: Modal) { + adminApi.getAllIntegrations().then(integrations => { + this.isLoading = false; + this.widgets = integrations.widgets; + }); + } + + public disableWidget(widget: Widget) { + widget.isEnabled = !widget.isEnabled; + this.isUpdating = true; + this.adminApi.toggleIntegration(widget.category, widget.type, widget.isEnabled).then(() => { + this.isUpdating = false; + this.toaster.pop("success", "Widget updated"); + }).catch(err => { + console.error(err); + widget.isEnabled = !widget.isEnabled; // revert change + this.isUpdating = false; + this.toaster.pop("error", "Error updating widget"); + }) + } + + public editWidget(widget: Widget) { + let component = null; + + if (widget.type === "etherpad") component = AdminWidgetEtherpadConfigComponent; + if (widget.type === "jitsi") component = AdminWidgetJitsiConfigComponent; + + if (!component) { + console.error("No known dialog component for " + widget.type); + this.toaster.pop("error", "Error opening configuration page"); + return; + } + + this.modal.open(component, overlayConfigFactory({ + widget: widget, + + isBlocking: true, + size: 'lg', + }, WidgetConfigDialogContext)); + } + + public hasConfiguration(widget: Widget) { + // Currently only Jitsi and Etherpad have additional configuration + return widget.type === "jitsi" || widget.type === "etherpad"; + } +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 2749a40..59cfc39 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -42,6 +42,9 @@ import { TwitchWidgetConfigComponent } from "./configs/widget/twitch/twitch.widg import { YoutubeWidgetConfigComponent } from "./configs/widget/youtube/youtube.widget.component"; import { AdminComponent } from "./admin/admin.component"; import { AdminHomeComponent } from "./admin/home/home.component"; +import { AdminWidgetsComponent } from "./admin/widgets/widgets.component"; +import { AdminWidgetEtherpadConfigComponent } from "./admin/widgets/etherpad/etherpad.component"; +import { AdminWidgetJitsiConfigComponent } from "./admin/widgets/jitsi/jitsi.component"; @NgModule({ imports: [ @@ -83,6 +86,9 @@ import { AdminHomeComponent } from "./admin/home/home.component"; YoutubeWidgetConfigComponent, AdminComponent, AdminHomeComponent, + AdminWidgetsComponent, + AdminWidgetEtherpadConfigComponent, + AdminWidgetJitsiConfigComponent, // Vendor ], @@ -97,7 +103,10 @@ import { AdminHomeComponent } from "./admin/home/home.component"; // Vendor ], bootstrap: [AppComponent], - entryComponents: [] + entryComponents: [ + AdminWidgetEtherpadConfigComponent, + AdminWidgetJitsiConfigComponent, + ] }) export class AppModule { constructor(public appRef: ApplicationRef, injector: Injector) { diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 0cf9e20..dda540f 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -15,6 +15,7 @@ import { TwitchWidgetConfigComponent } from "./configs/widget/twitch/twitch.widg import { YoutubeWidgetConfigComponent } from "./configs/widget/youtube/youtube.widget.component"; import { AdminComponent } from "./admin/admin.component"; import { AdminHomeComponent } from "./admin/home/home.component"; +import { AdminWidgetsComponent } from "./admin/widgets/widgets.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -37,6 +38,11 @@ const routes: Routes = [ path: "", component: AdminHomeComponent, }, + { + path: "widgets", + component: AdminWidgetsComponent, + data: {breadcrumb: "Widgets", name: "Widgets"}, + }, ], }, { diff --git a/web/app/shared/services/AuthedApi.ts b/web/app/shared/services/AuthedApi.ts index eb6161c..8334134 100644 --- a/web/app/shared/services/AuthedApi.ts +++ b/web/app/shared/services/AuthedApi.ts @@ -11,4 +11,10 @@ export class AuthedApi { qs["scalar_token"] = SessionStorage.scalarToken; return this.http.get(url, {params: qs}); } + + protected authedPost(url: string, body?: any): Observable { + if (!body) body = {}; + const qs = {scalar_token: SessionStorage.scalarToken}; + return this.http.post(url, body, {params: qs}); + } } diff --git a/web/app/shared/services/admin-api.service.ts b/web/app/shared/services/admin-api.service.ts index 8404220..5003211 100644 --- a/web/app/shared/services/admin-api.service.ts +++ b/web/app/shared/services/admin-api.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { Http } from "@angular/http"; import { AuthedApi } from "./AuthedApi"; import { DimensionConfigResponse, DimensionVersionResponse } from "../models/admin_responses"; +import { DimensionIntegrationsResponse } from "../models/dimension_responses"; @Injectable() export class AdminApiService extends AuthedApi { @@ -20,4 +21,16 @@ export class AdminApiService extends AuthedApi { public getVersion(): Promise { return this.authedGet("/api/v1/dimension/admin/version").map(r => r.json()).toPromise(); } + + public getAllIntegrations(): Promise { + return this.authedGet("/api/v1/dimension/integrations/all").map(r => r.json()).toPromise(); + } + + public toggleIntegration(category: string, type: string, enabled: boolean): Promise { + return this.authedPost("/api/v1/dimension/integrations/" + category + "/" + type + "/enabled", {enabled: enabled}).map(r => r.json()).toPromise(); + } + + public setWidgetOptions(category: string, type: string, options: any): Promise { + return this.authedPost("/api/v1/dimension/integrations/" + category + "/" + type + "/options", {options: options}).map(r => r.json()).toPromise(); + } } diff --git a/web/style/app.scss b/web/style/app.scss index 159ce79..28c6b31 100644 --- a/web/style/app.scss +++ b/web/style/app.scss @@ -2,6 +2,7 @@ @import url('https://fonts.googleapis.com/css?family=Open+Sans:100|Roboto:300'); @import '../../node_modules/angular2-toaster/toaster'; @import "components/ibox"; +@import "components/dialog"; @import "riot"; body { diff --git a/web/style/components/dialog.scss b/web/style/components/dialog.scss new file mode 100644 index 0000000..3e1ec50 --- /dev/null +++ b/web/style/components/dialog.scss @@ -0,0 +1,21 @@ +.dialog { + .dialog-header { + border-bottom: 1px solid #ddd; + padding: 20px; + + h4 { + margin: 0; + } + } + + .dialog-content { + padding: 20px; + + } + + .dialog-footer { + padding: 20px; + border-top: 1px solid #bbb; + background-color: #ddd; + } +} \ No newline at end of file