Remove legacy configurations for now

This commit is contained in:
Travis Ralston 2017-12-23 13:15:54 -07:00
parent 111423cc7d
commit 7fd674a8ff
47 changed files with 19 additions and 2444 deletions

View File

@ -26,7 +26,6 @@ import { SpinnerComponent } from "./elements/spinner/spinner.component";
import { BreadcrumbsModule } from "ng2-breadcrumbs";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
import { IntegrationBagComponent } from "./integration-bag/integration-bag.component";
import { IntegrationComponent } from "./integration/integration.component";
import { ScalarServerApiService } from "./shared/services/scalar-server-api.service";
import { DimensionApiService } from "./shared/services/dimension-api.service";
import { AdminApiService } from "./shared/services/admin-api.service";
@ -53,7 +52,6 @@ import { ConfigScreenWidgetComponent } from "./configs/widget/config_screen/conf
AppComponent,
HomeComponent,
RiotComponent,
IntegrationComponent,
IntegrationBagComponent,
PageHeaderComponent,
SpinnerComponent,

View File

@ -1,63 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure CircleCI hooks</h4>
</div>
<div class="config-content">
<form (submit)="addRepository()" novalidate name="addRepoForm">
<div class="row">
<div class="col-md-12" style="margin-bottom: 12px;">
<h6>.circleci/config.yml configuration</h6>
The following will need to be added to your .circleci/config.yml file:
<pre class="yaml">{{ circleYaml }}</pre>
</div>
<div class="col-md-8" style="margin-bottom: 12px;">
<h6>Your CircleCI hooks</h6>
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="owner/repo-name"
[(ngModel)]="repoKey" name="repoKey"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Repository
</button>
</span>
</div>
</div>
<div class="col-md-12 removable" *ngFor="let repo of integration.repoTemplates trackById">
{{ repo.repoKey }}
<button type="button" class="btn btn-outline-info btn-sm" (click)="editTemplate(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating" *ngIf="!isTemplateToggled(repo.repoKey)">
<i class="fa fa-pencil"></i> Edit
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeRepository(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Repository
</button>
<div *ngIf="isTemplateToggled(repo.repoKey)">
<textarea [(ngModel)]="repo.newTemplate" name="template-{{repo.repoKey}}"
style="width: 100%; height: 100px; margin-top: 5px;"></textarea>
<button type="button" class="btn btn-primary btn-sm" (click)="saveTemplate(repo.repoKey)">Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleTemplate(repo.repoKey)">
Cancel
</button>
</div>
</div>
<div class="col-md-12" *ngIf="integration.immutableRepoTemplates.length > 0">
<h6 class="other-items-title">Hooks from other users in the room</h6>
</div>
<div class="col-md-12 list" *ngFor="let repo of integration.immutableRepoTemplates trackById">
{{ repo.repoKey }} <span class="text-muted">(added by {{ repo.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="toggleTemplate(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating">
{{ isTemplateToggled(repo.repoKey) ? "Hide" : "Show" }} Template
</button>
<pre *ngIf="isTemplateToggled(repo.repoKey)">{{ repo.template }}</pre>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,19 +0,0 @@
// component styles are encapsulated and only applied to their components
.list {
margin-top: 5px;
}
.removable {
margin-top: 3px;
}
.other-items-title {
margin-top: 25px;
margin-bottom: 0;
}
.yaml {
border: 1px solid #ccc;
background: #eee;
padding: 5px;
}

View File

@ -1,116 +0,0 @@
import { Component } from "@angular/core";
import { CircleCiIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-circleci-config",
templateUrl: "./circleci-config.component.html",
styleUrls: ["./circleci-config.component.scss", "./../config.component.scss"],
})
export class CircleCiConfigComponent implements ModalComponent<ConfigModalContext> {
public integration: CircleCiIntegration;
public isUpdating = false;
public repoKey = "";
public repoTemplate = "";
public circleYaml = "";
private roomId: string;
private scalarToken: string;
private knownRepos: string[] = [];
private visibleTemplates = [];
constructor(public dialog: DialogRef<ConfigModalContext>,
private toaster: ToasterService,
private api: ApiService) {
this.integration = <CircleCiIntegration>dialog.context.integration;
this.roomId = dialog.context.roomId;
this.scalarToken = dialog.context.scalarToken;
this.circleYaml = "notify:\n webhooks:\n - url: " + this.integration.webhookUrl;
this.calculateKnownRepos();
this.reset();
}
private calculateKnownRepos() {
for (let repo of this.integration.repoTemplates)
this.knownRepos.push(repo.repoKey);
for (let immutableRepo of this.integration.immutableRepoTemplates)
this.knownRepos.push(immutableRepo.repoKey);
}
public toggleTemplate(repoKey: string) {
let idx = this.visibleTemplates.indexOf(repoKey);
if (idx === -1) this.visibleTemplates.push(repoKey);
else this.visibleTemplates.splice(idx, 1);
}
public isTemplateToggled(repoKey: string) {
return this.visibleTemplates.indexOf(repoKey) !== -1;
}
public editTemplate(repoKey: string) {
this.toggleTemplate(repoKey);
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
repoConfig.newTemplate = repoConfig.template;
}
public saveTemplate(repoKey: string) {
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
repoConfig.template = repoConfig.newTemplate;
this.updateTemplates().then(() => this.toggleTemplate(repoKey));
}
public addRepository() {
if (!this.repoKey || this.repoKey.trim().length === 0) {
this.toaster.pop("warning", "Please enter a repository");
return;
}
if (this.knownRepos.indexOf(this.repoKey) !== -1) {
this.toaster.pop("error", "Repository " + this.repoKey + " is already being tracked");
return;
}
this.integration.repoTemplates.push({repoKey: this.repoKey, template: this.repoTemplate, newTemplate: ""});
this.updateTemplates().then(() => this.reset());
}
private reset() {
this.repoKey = "";
this.repoTemplate = "%{build_num}#%{build_num} (%{branch} - %{commit} : %{committer_name}): %{outcome}\n Build details : %{build_url}\n";
}
public removeRepository(repoKey: string) {
for (let i = 0; i < this.integration.repoTemplates.length; i++) {
if (this.integration.repoTemplates[i].repoKey === repoKey) {
this.integration.repoTemplates.splice(i, 1);
this.updateTemplates().then(() => this.reset());
return;
}
}
this.toaster.pop("error", "Could not find target repository");
}
public updateTemplates() {
this.isUpdating = true;
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
repoTemplates: this.integration.repoTemplates
}).then(response => {
this.integration.repoTemplates = response.repoTemplates;
this.integration.immutableRepoTemplates = response.immutableRepoTemplates;
this.calculateKnownRepos();
this.isUpdating = false;
this.toaster.pop("success", "Repositories updated");
}).catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
}

View File

@ -1,32 +0,0 @@
// shared styling for all config screens
.config-wrapper {
padding: 25px;
}
.config-header {
padding-bottom: 8px;
margin-bottom: 14px;
border-bottom: 1px solid #dadada;
}
.config-header h4 {
display: inline-block;
vertical-align: middle;
}
.config-header img {
margin-right: 7px;
width: 35px;
height: 35px;
border-radius: 35px;
}
.close-icon {
float: right;
margin: -17px;
cursor: pointer;
}
.config-content {
display: block;
}

View File

@ -1,75 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img src="/img/avatars/irc.png">
<h4>Configure IRC Bridge</h4>
</div>
<div class="config-content">
<div class="alert alert-info">
Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will
join the channel to ensure it exists and has operators available.
</div>
<div *ngIf="!opsLoaded">
<h6>Add a channel</h6>
<div class="row form-group" style="margin-bottom: 0.1rem;">
<label class="col-sm-2 col-form-label">Network</label>
<div class="col-md-4">
<select class="form-control form-control-sm" [(ngModel)]="newChannel.network" [disabled]="loadingOps">
<option *ngFor="let network of integration.availableNetworks" [ngValue]="network.id">
{{ network.name }}
</option>
</select>
</div>
</div>
<div class="row form-group" style="margin-bottom: 0.1rem;">
<label class="col-sm-2 col-form-label">Channel</label>
<div class="col-md-4 input-group input-group-sm">
<div class="input-group-addon">#</div>
<input type="text" class="form-control form-control-sm" [(ngModel)]="newChannel.channel" [disabled]="loadingOps">
</div>
</div>
<div class="row">
<div class="col-md-12">
<p *ngIf="opsError" class="text-danger">{{ opsError }}</p>
<button type="button" class="btn btn-primary btn-sm" (click)="checkOps()" [disabled]="loadingOps">
Next <i class="fa fa-arrow-right"></i>
</button>
</div>
</div>
</div>
<div *ngIf="opsLoaded">
<h6>Add a channel</h6>
<div class="row form-group" style="margin-bottom: 0.1rem;">
<label class="col-sm-2 col-form-label">Operator</label>
<div class="col-md-4">
<select class="form-control form-control-sm" [(ngModel)]="newChannel.op" [disabled]="addingChannel">
<option *ngFor="let op of channelOps" [ngValue]="op">{{ op }}</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>A request to bridge #{{ newChannel.channel }} on {{ getNewChannelNetworkName() }} will be sent to {{ newChannel.op }}. Once they accept, the channel will show up below.</p>
<button type="button" class="btn btn-primary btn-sm" (click)="addChannel()" [disabled]="addingChannel">
<i class="fa fa-plus-circle"></i> Request Bridge
</button>
</div>
</div>
</div>
<div class="row" style="margin-top: 40px;">
<div class="col-md-12">
<h6>Linked channels</h6>
<p *ngIf="channelLinks.length <= 0">No linked channels.</p>
</div>
<div class="col-md-12" *ngFor="let link of channelLinks">
<strong>{{ link.displayName }}</strong>
<div *ngFor="let channel of link.channels" style="padding-left: 10px;">
{{ channel }}
<button type="button" class="btn btn-outline-danger btn-sm" (click)="removeChannelLink(link, channel)" [disabled]="link.beingRemoved.indexOf(channel) !== -1">
<i class="fa fa-times"></i> Unlink
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -1 +0,0 @@
// component styles are encapsulated and only applied to their components

View File

@ -1,168 +0,0 @@
import { Component, OnDestroy } from "@angular/core";
import { IRCIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { IrcApiService } from "../../shared/services/legacy/irc-api.service";
import { ToasterService } from "angular2-toaster";
import { IntervalObservable } from "rxjs/observable/IntervalObservable";
import { ApiService } from "../../shared/services/legacy/api.service";
import { Subscription } from "rxjs";
@Component({
selector: "my-irc-config",
templateUrl: "./irc-config.component.html",
styleUrls: ["./irc-config.component.scss", "./../config.component.scss"],
})
export class IrcConfigComponent implements ModalComponent<ConfigModalContext>, OnDestroy {
public integration: IRCIntegration;
public loadingOps = false;
public opsLoaded = false;
public addingChannel = false;
public newChannel = {
network: "",
channel: "",
op: ""
};
public channelOps: string[];
public opsError: string = null;
public channelLinks: ChannelLink[];
private roomId: string;
private scalarToken: string;
private stateTimer: Subscription;
constructor(public dialog: DialogRef<ConfigModalContext>,
private ircApi: IrcApiService,
private toaster: ToasterService,
private api: ApiService) {
this.integration = <IRCIntegration>dialog.context.integration;
this.roomId = dialog.context.roomId;
this.scalarToken = dialog.context.scalarToken;
this.newChannel.network = this.integration.availableNetworks[0].id;
this.buildChannelLinks();
this.stateTimer = IntervalObservable.create(5000).subscribe(() => {
this.api.getIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken)
.then(state => {
for (let key in state) {
this.integration[key] = state[key];
}
this.buildChannelLinks();
});
});
}
public ngOnDestroy(): void {
this.stateTimer.unsubscribe();
}
public checkOps(): void {
if (this.newChannel.channel.trim().length === 0) {
this.toaster.pop("warning", "Please enter a channel name");
return;
}
this.loadingOps = true;
this.ircApi.getChannelOps(this.roomId, this.newChannel.network, this.newChannel.channel, this.scalarToken).then(ops => {
this.channelOps = ops;
if (this.channelOps.length === 0) {
this.opsError = "No channel operators available";
} else {
this.newChannel.op = this.channelOps[0];
this.loadingOps = false;
this.opsLoaded = true;
}
}).catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.loadingOps = false;
});
}
public getNewChannelNetworkName(): string {
for (let network of this.integration.availableNetworks) {
if (network.id === this.newChannel.network) {
return network.name;
}
}
return "Unknown";
}
public addChannel(): void {
this.addingChannel = true;
this.ircApi.linkChannel(
this.roomId,
this.newChannel.network,
this.newChannel.channel,
this.newChannel.op,
this.scalarToken)
.then(() => {
this.newChannel = {
network: this.integration.availableNetworks[0].id,
channel: "",
op: ""
};
this.channelOps = [];
this.addingChannel = false;
this.opsLoaded = false;
this.toaster.pop("success", "Channel bridge requested");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.addingChannel = false;
});
}
private buildChannelLinks(): void {
this.channelLinks = [];
for (let network in this.integration.channels) {
if (this.integration.channels[network].length <= 0) continue;
let displayName = "Unknown Network";
for (let parentNetwork of this.integration.availableNetworks) {
if (parentNetwork.id === network) {
displayName = parentNetwork.name;
break;
}
}
this.channelLinks.push({
displayName: displayName,
network: network,
channels: this.integration.channels[network],
beingRemoved: []
});
}
}
public removeChannelLink(link: ChannelLink, channel: string): void {
link.beingRemoved.push(channel);
this.ircApi.unlinkChannel(this.roomId, link.network, channel.substring(1), this.scalarToken).then(() => {
this.toaster.pop("success", "Channel " + channel + " unlinked");
link.channels.splice(link.channels.indexOf(channel), 1);
link.beingRemoved.splice(link.beingRemoved.indexOf(channel), 1);
if (link.channels.length === 0) {
this.channelLinks.splice(this.channelLinks.indexOf(link), 1);
}
}).catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
link.beingRemoved.splice(link.beingRemoved.indexOf(channel), 1);
});
}
}
interface ChannelLink {
displayName: string;
channels: string[];
network: string;
beingRemoved: string[];
}

View File

@ -1,38 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure RSS/Atom Feeds</h4>
</div>
<div class="config-content">
<form (submit)="addFeed()" novalidate name="addFeedForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
[(ngModel)]="feedUrl" name="feedUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Feed
</button>
</span>
</div>
</div>
<div class="col-md-12 removable-feed" *ngFor="let feed of integration.feeds trackById">
<a [href]="feed" rel="noopener" target="_blank">{{ feed }}</a>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeFeed(feed)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Feed
</button>
</div>
<div class="col-md-12" *ngIf="integration.immutableFeeds.length > 0">
<h6 class="other-feeds-title">Feeds from other users in the room</h6>
</div>
<div class="col-md-12 feed-list" *ngFor="let feed of integration.immutableFeeds trackById">
<a [href]="feed.url" rel="noopener" target="_blank">{{ feed.url }}</a> <span class="text-muted">(added by {{ feed.ownerId }})</span>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,13 +0,0 @@
// component styles are encapsulated and only applied to their components
.feed-list {
margin-top: 5px;
}
.removable-feed {
margin-top: 3px;
}
.other-feeds-title {
margin-top: 25px;
margin-bottom: 0;
}

View File

@ -1,68 +0,0 @@
import { Component } from "@angular/core";
import { RSSIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-rss-config",
templateUrl: "./rss-config.component.html",
styleUrls: ["./rss-config.component.scss", "./../config.component.scss"],
})
export class RssConfigComponent implements ModalComponent<ConfigModalContext> {
public integration: RSSIntegration;
public isUpdating = false;
public feedUrl = "";
private roomId: string;
private scalarToken: string;
constructor(public dialog: DialogRef<ConfigModalContext>,
private toaster: ToasterService,
private api: ApiService) {
this.integration = <RSSIntegration>dialog.context.integration;
this.roomId = dialog.context.roomId;
this.scalarToken = dialog.context.scalarToken;
}
public addFeed() {
if (!this.feedUrl || this.feedUrl.trim().length === 0) {
this.toaster.pop("warning", "Please enter a feed URL");
return;
}
if (this.integration.feeds.indexOf(this.feedUrl) !== -1) {
this.toaster.pop("error", "This feed has already been added");
return;
}
let feedCopy = JSON.parse(JSON.stringify(this.integration.feeds));
feedCopy.push(this.feedUrl);
this.updateFeeds(feedCopy).then(() => this.feedUrl = "");
}
public removeFeed(feedUrl) {
let feedCopy = JSON.parse(JSON.stringify(this.integration.feeds));
const idx = feedCopy.indexOf(feedUrl);
feedCopy.splice(idx, 1);
this.updateFeeds(feedCopy);
}
private updateFeeds(newFeeds) {
this.isUpdating = true;
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
feeds: newFeeds
}).then(response => {
this.integration.feeds = response.feeds;
this.integration.immutableFeeds = response.immutableFeeds;
this.isUpdating = false;
this.toaster.pop("success", "Feeds updated");
}).catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
}

View File

@ -1,63 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure Travis CI hooks</h4>
</div>
<div class="config-content">
<form (submit)="addRepository()" novalidate name="addRepoForm">
<div class="row">
<div class="col-md-12" style="margin-bottom: 12px;">
<h6>.travis.yml configuration</h6>
The following will need to be added to your .travis.yml file:
<pre class="yaml">{{ travisYaml }}</pre>
</div>
<div class="col-md-8" style="margin-bottom: 12px;">
<h6>Your Travis CI hooks</h6>
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="owner/repo-name"
[(ngModel)]="repoKey" name="repoKey"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Repository
</button>
</span>
</div>
</div>
<div class="col-md-12 removable" *ngFor="let repo of integration.repoTemplates trackById">
{{ repo.repoKey }}
<button type="button" class="btn btn-outline-info btn-sm" (click)="editTemplate(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating" *ngIf="!isTemplateToggled(repo.repoKey)">
<i class="fa fa-pencil"></i> Edit
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeRepository(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Repository
</button>
<div *ngIf="isTemplateToggled(repo.repoKey)">
<textarea [(ngModel)]="repo.newTemplate" name="template-{{repo.repoKey}}"
style="width: 100%; height: 100px; margin-top: 5px;"></textarea>
<button type="button" class="btn btn-primary btn-sm" (click)="saveTemplate(repo.repoKey)">Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleTemplate(repo.repoKey)">
Cancel
</button>
</div>
</div>
<div class="col-md-12" *ngIf="integration.immutableRepoTemplates.length > 0">
<h6 class="other-items-title">Hooks from other users in the room</h6>
</div>
<div class="col-md-12 list" *ngFor="let repo of integration.immutableRepoTemplates trackById">
{{ repo.repoKey }} <span class="text-muted">(added by {{ repo.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="toggleTemplate(repo.repoKey)"
style="margin-top: -5px;" [disabled]="isUpdating">
{{ isTemplateToggled(repo.repoKey) ? "Hide" : "Show" }} Template
</button>
<pre *ngIf="isTemplateToggled(repo.repoKey)">{{ repo.template }}</pre>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,19 +0,0 @@
// component styles are encapsulated and only applied to their components
.list {
margin-top: 5px;
}
.removable {
margin-top: 3px;
}
.other-items-title {
margin-top: 25px;
margin-bottom: 0;
}
.yaml {
border: 1px solid #ccc;
background: #eee;
padding: 5px;
}

View File

@ -1,116 +0,0 @@
import { Component } from "@angular/core";
import { TravisCiIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-travisci-config",
templateUrl: "./travisci-config.component.html",
styleUrls: ["./travisci-config.component.scss", "./../config.component.scss"],
})
export class TravisCiConfigComponent implements ModalComponent<ConfigModalContext> {
public integration: TravisCiIntegration;
public isUpdating = false;
public repoKey = "";
public repoTemplate = "";
public travisYaml = "";
private roomId: string;
private scalarToken: string;
private knownRepos: string[] = [];
private visibleTemplates = [];
constructor(public dialog: DialogRef<ConfigModalContext>,
private toaster: ToasterService,
private api: ApiService) {
this.integration = <TravisCiIntegration>dialog.context.integration;
this.roomId = dialog.context.roomId;
this.scalarToken = dialog.context.scalarToken;
this.travisYaml = "notifications:\n webhooks:\n urls:\n - " + this.integration.webhookUrl + "\n on_success: change # always | never | change\n on_failure: always\n on_start: never";
this.calculateKnownRepos();
this.reset();
}
private calculateKnownRepos() {
for (let repo of this.integration.repoTemplates)
this.knownRepos.push(repo.repoKey);
for (let immutableRepo of this.integration.immutableRepoTemplates)
this.knownRepos.push(immutableRepo.repoKey);
}
public toggleTemplate(repoKey: string) {
let idx = this.visibleTemplates.indexOf(repoKey);
if (idx === -1) this.visibleTemplates.push(repoKey);
else this.visibleTemplates.splice(idx, 1);
}
public isTemplateToggled(repoKey: string) {
return this.visibleTemplates.indexOf(repoKey) !== -1;
}
public editTemplate(repoKey: string) {
this.toggleTemplate(repoKey);
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
repoConfig.newTemplate = repoConfig.template;
}
public saveTemplate(repoKey: string) {
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
repoConfig.template = repoConfig.newTemplate;
this.updateTemplates().then(() => this.toggleTemplate(repoKey));
}
public addRepository() {
if (!this.repoKey || this.repoKey.trim().length === 0) {
this.toaster.pop("warning", "Please enter a repository");
return;
}
if (this.knownRepos.indexOf(this.repoKey) !== -1) {
this.toaster.pop("error", "Repository " + this.repoKey + " is already being tracked");
return;
}
this.integration.repoTemplates.push({repoKey: this.repoKey, template: this.repoTemplate, newTemplate: ""});
this.updateTemplates().then(() => this.reset());
}
private reset() {
this.repoKey = "";
this.repoTemplate = "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n Change view : %{compare_url}\n Build details : %{build_url}\n";
}
public removeRepository(repoKey: string) {
for (let i = 0; i < this.integration.repoTemplates.length; i++) {
if (this.integration.repoTemplates[i].repoKey === repoKey) {
this.integration.repoTemplates.splice(i, 1);
this.updateTemplates().then(() => this.reset());
return;
}
}
this.toaster.pop("error", "Could not find target repository");
}
public updateTemplates() {
this.isUpdating = true;
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
repoTemplates: this.integration.repoTemplates
}).then(response => {
this.integration.repoTemplates = response.repoTemplates;
this.integration.immutableRepoTemplates = response.immutableRepoTemplates;
this.calculateKnownRepos();
this.isUpdating = false;
this.toaster.pop("success", "Repositories updated");
}).catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
}

View File

@ -1,65 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure custom widgets</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="addWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Custom widget URL"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label>
Widget Name
<input type="text" class="form-control"
placeholder="Custom Widget"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Widget URL
<input type="text" class="form-control"
placeholder="Custom widget URL"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,32 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_CUSTOM } from "../../../shared/models/widget";
@Component({
selector: "my-customwidget-config",
templateUrl: "custom_widget-config.component.html",
styleUrls: ["custom_widget-config.component.scss", "./../../config.component.scss"],
})
export class CustomWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_CUSTOM,
"Custom Widget",
"generic" // wrapper
);
}
}

View File

@ -1,66 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure Etherpad widgets</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Etherpad name or URL"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label>
Pad Name
<input type="text" class="form-control"
placeholder="Etherpad Widget"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Pad URL
<input type="text" class="form-control"
placeholder="https://your-pad-url"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,61 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_ETHERPAD } from "../../../shared/models/widget";
import { EtherpadWidgetIntegration } from "../../../shared/models/legacyintegration";
@Component({
selector: "my-etherpadwidget-config",
templateUrl: "etherpad-config.component.html",
styleUrls: ["etherpad-config.component.scss", "./../../config.component.scss"],
})
export class EtherpadWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
private widgetOpts: EtherpadWidgetIntegration;
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_ETHERPAD,
"Etherpad Widget",
"generic", // wrapper
"etherpad" // scalar wrapper
);
this.widgetOpts = <EtherpadWidgetIntegration>dialog.context.integration;
}
public validateAndAddWidget() {
if (this.newWidget.dimension.newUrl.startsWith("http://") || this.newWidget.dimension.newUrl.startsWith("https://")) {
this.newWidget.dimension.newName = "Etherpad";
} else {
this.newWidget.dimension.newName = this.newWidget.dimension.newUrl;
this.newWidget.dimension.newUrl = this.generatePadUrl(this.newWidget.dimension.newName);
}
this.addWidget();
}
private generatePadUrl(forName: string): string {
let template = "https://demo.riot.im/etherpad/p/$roomId_$padName";
if (this.widgetOpts && this.widgetOpts.defaultUrl) {
template = this.widgetOpts.defaultUrl;
}
template = template.replace("$roomId", encodeURIComponent(this.roomId));
template = template.replace("$padName", encodeURIComponent(forName));
return template;
}
}

View File

@ -1,59 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure Google Calendars</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Calendar ID, example: en.uk#holiday@group.v.calendar.google.com"
[(ngModel)]="newWidget.dimension.newData.src" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
Shared Calendar ID
<input type="text" class="form-control"
placeholder="en.uk#holiday@group.v.calendar.google.com"
[(ngModel)]="widget.dimension.newData.src" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,57 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_GOOGLE_CALENDAR } from "../../../shared/models/widget";
@Component({
selector: "my-googlecalendarwidget-config",
templateUrl: "googlecalendar-config.component.html",
styleUrls: ["googlecalendar-config.component.scss", "./../../config.component.scss"],
})
export class GoogleCalendarWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_GOOGLE_CALENDAR,
"Google Calendar",
"", // we intentionally don't specify the wrapper so we can control the behaviour
"googleCalendar" // scalar wrapper
);
}
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
if (widget.data.dimSrc) {
// Convert legacy Dimension widgets to use src
widget.data.src = widget.data.dimSrc;
}
}
}
public validateAndAddWidget() {
this.setCalendarUrl(this.newWidget);
this.addWidget();
}
public validateAndSaveWidget(widget: EditableWidget) {
this.setCalendarUrl(widget);
this.saveWidget(widget);
}
private setCalendarUrl(widget: EditableWidget) {
const encodedId = encodeURIComponent(widget.dimension.newData.src);
widget.dimension.newUrl = window.location.origin + "/widget/gcal?calendarId=" + encodedId;
}
}

View File

@ -1,59 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure embedded Google Docs</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="addWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Link to Google Doc"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
Google Doc
<input type="text" class="form-control"
placeholder="Link to Google Doc"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,33 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_GOOGLE_DOCS } from "../../../shared/models/widget";
@Component({
selector: "my-googledocswidget-config",
templateUrl: "googledocs-config.component.html",
styleUrls: ["googledocs-config.component.scss", "./../../config.component.scss"],
})
export class GoogleDocsWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_GOOGLE_DOCS,
"Google Docs",
"generic", // wrapper
"googleDocs" // scalar wrapper
);
}
}

View File

@ -1,60 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure Jitsi Conferences</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<span class="input-group-addon">https://{{ newWidget.dimension.newData.domain }}/</span>
<input type="text" class="form-control"
placeholder="MyConferenceName"
[(ngModel)]="newWidget.dimension.newData.conferenceId" name="newWidgetName"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.data.conferenceUrl }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove
</button>
<div *ngIf="isWidgetToggled(widget)">
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
Conference URL
<input type="text" class="form-control"
placeholder="https://jitsi.riot.im/MyConference"
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,140 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_JITSI } from "../../../shared/models/widget";
import { JitsiWidgetIntegration } from "../../../shared/models/legacyintegration";
import * as gobyInit from "goby";
import * as url from "url";
const goby = gobyInit.init({
// Converts words to a url-safe name
// Ie: "hello world how-are you" becomes "HelloWorldHowAreYou"
decorator: parts => parts.map(p => p ? p.split('-').map(p2 => p2 ? p2[0].toUpperCase() + p2.substring(1).toLowerCase() : '').join('') : '').join(''),
});
@Component({
selector: "my-jitsi-config",
templateUrl: "jitsi-config.component.html",
styleUrls: ["jitsi-config.component.scss", "./../../config.component.scss"],
})
export class JitsiWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
public integration: JitsiWidgetIntegration;
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_JITSI,
"Jitsi Video Conference",
"" // we intentionally don't specify the wrapper so we can control the behaviour
);
this.integration = <JitsiWidgetIntegration>dialog.context.integration;
}
protected onNewWidgetPrepared() {
this.newWidget.dimension.newData.conferenceId = this.generateConferenceId();
this.newWidget.dimension.newData.domain = this.integration.jitsiDomain;
this.newWidget.dimension.newData.isAudioConf = false;
}
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
const parsedUrl = url.parse(widget.url, true);
const conferenceId = parsedUrl.query["conferenceId"];
const confId = parsedUrl.query["confId"];
const domain = parsedUrl.query["domain"];
let isAudioConf = parsedUrl.query["isAudioConf"];
// Convert isAudioConf to boolean
if (isAudioConf === "true") isAudioConf = true;
else if (isAudioConf === "false") isAudioConf = false;
else if (isAudioConf && isAudioConf[0] === '$') isAudioConf = widget.data[isAudioConf];
else isAudioConf = false; // default
if (conferenceId) {
// It's a legacy Dimension widget
widget.data.conferenceId = conferenceId;
} else widget.data.conferenceId = confId;
if (domain) widget.data.domain = domain;
else widget.data.domain = "jitsi.riot.im";
widget.data.isAudioConf = isAudioConf;
if (!widget.data.conferenceUrl) {
widget.data.conferenceUrl = "https://" + widget.data.domain + "/" + widget.data.conferenceId;
}
}
}
protected onWidgetPreparedForEdit(widget: EditableWidget) {
if (!widget.dimension.newData.conferenceUrl) {
const conferenceId = widget.dimension.newData.conferenceId;
const domain = widget.dimension.newData.domain;
widget.dimension.newData.conferenceUrl = "https://" + domain + "/" + conferenceId;
}
}
public validateAndAddWidget() {
if (!this.newWidget.dimension.newData.conferenceId) {
this.toaster.pop("warning", "Please enter a conference name");
return;
}
this.setJitsiUrl(this.newWidget);
this.addWidget();
}
public validateAndSaveWidget(widget: EditableWidget) {
if (!widget.dimension.newData.conferenceUrl) {
this.toaster.pop("warning", "Please enter a conference URL");
return;
}
const jitsiUrl = url.parse(widget.dimension.newData.conferenceUrl);
widget.dimension.newData.domain = jitsiUrl.host;
widget.dimension.newData.conferenceId = jitsiUrl.path.substring(1);
widget.dimension.newData.isAudioConf = false;
this.setJitsiUrl(widget);
this.saveWidget(widget);
}
private setJitsiUrl(widget: EditableWidget) {
const conferenceId = widget.dimension.newData.conferenceId;
const domain = widget.dimension.newData.domain;
const isAudioConf = widget.dimension.newData.isAudioConf;
let widgetQueryString = url.format({
query: {
// TODO: Use templating when mobile riot supports it
"confId": conferenceId, // named confId for compatibility with mobile clients
"domain": domain,
"isAudioConf": isAudioConf,
"displayName": "$matrix_display_name",
"avatarUrl": "$matrix_avatar_url",
"userId": "$matrix_user_id",
},
});
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
widget.dimension.newUrl = window.location.origin + "/widgets/jitsi" + widgetQueryString;
widget.dimension.newData.conferenceUrl = "https://" + domain + "/" + conferenceId;
}
private generateConferenceId() {
return goby.generate(["adj", "pre", "suf"]);
}
}

View File

@ -1,66 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure Twitch Livestream widgets</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Twitch Channel Name"
[(ngModel)]="newWidget.dimension.newData.channelName" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label>
Widget Name
<input type="text" class="form-control"
placeholder="Twitch Widget"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Channel Name
<input type="text" class="form-control"
placeholder="Twitch Channel Name"
[(ngModel)]="widget.dimension.newData.channelName" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,72 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_TWITCH } from "../../../shared/models/widget";
@Component({
selector: "my-twitchwidget-config",
templateUrl: "twitch-config.component.html",
styleUrls: ["twitch-config.component.scss", "./../../config.component.scss"],
})
export class TwitchWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_TWITCH,
"Twitch Widget",
"video", // wrapper
"twitch" // scalar wrapper
);
}
protected onNewWidgetPrepared() {
this.newWidget.dimension.newData.channelName = "";
}
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
// Convert dimChannelName to channelName
if (!widget.data.channelName) {
widget.data.channelName = widget.data.dimChannelName;
}
}
}
public validateAndAddWidget() {
if (!this.newWidget.dimension.newData.channelName) {
this.toaster.pop("warning", "Please enter a Twitch Livestream channel name");
return;
}
this.setTwitchUrl(this.newWidget);
this.addWidget();
}
public validateAndSaveWidget(widget: EditableWidget) {
if (!widget.dimension.newData.channelName) {
this.toaster.pop("warning", "Please enter a Twitch Livestream channel name");
return;
}
this.setTwitchUrl(widget);
this.saveWidget(widget);
}
private setTwitchUrl(widget: EditableWidget) {
// TODO: This should use templating when mobile riot supports it
widget.dimension.newUrl = "https://player.twitch.tv/?channel=" + widget.dimension.newData.channelName;
}
}

View File

@ -1,395 +0,0 @@
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
import { convertScalarWidgetsToDtos, EditableWidget } from "../../shared/models/widget";
import { ToasterService } from "angular2-toaster";
import { LegacyIntegration } from "../../shared/models/legacyintegration";
const SCALAR_WIDGET_LINKS = [
"https://scalar-staging.riot.im/scalar/api/widgets/__TYPE__.html?url=",
"https://scalar-staging.vector.im/scalar/api/widgets/__TYPE__.html?url=",
"https://scalar-develop.riot.im/scalar/api/widgets/__TYPE__.html?url=",
"https://demo.riot.im/scalar/api/widgets/__TYPE__.html?url=",
];
export class WidgetComponent {
public isLoading = true;
public isUpdating = false;
public widgets: EditableWidget[];
public newWidget: EditableWidget;
private toggledWidgetIds: string[] = [];
private scalarWrapperUrls: string[] = [];
private wrapperUrl = "";
constructor(window: Window,
protected toaster: ToasterService,
protected scalarApi: ScalarClientApiService,
public roomId: string,
public integration: LegacyIntegration,
editWidgetId: string,
private widgetIds: string[],
private defaultName: string,
private wrapperId = "generic",
private scalarWrapperId = null) {
this.isLoading = true;
this.isUpdating = false;
if (wrapperId) {
this.wrapperUrl = window.location.origin + "/widgets/" + wrapperId + "?url=";
if (!scalarWrapperId) scalarWrapperId = wrapperId;
for (let widgetLink of SCALAR_WIDGET_LINKS) {
this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", scalarWrapperId));
}
}
this.prepareNewWidget();
this.getWidgetsOfType(widgetIds).then(widgets => {
this.widgets = widgets;
for (let widget of this.widgets) {
this.unpackWidget(widget);
}
this.onWidgetsDiscovered();
this.isLoading = false;
this.isUpdating = false;
// See if we should request editing a particular widget
if (editWidgetId) {
for (let widget of this.widgets) {
if (widget.id === editWidgetId) {
console.log("Requesting edit for " + widget.id);
this.editWidget(widget);
}
}
}
});
}
/**
* Populates the Dimension-specific fields of the widget, overwriting any values that
* were there previously.
* @param {EditableWidget} widget The widget to unpack.
*/
private unpackWidget(widget: EditableWidget) {
widget.dimension = {
newUrl: this.unwrapUrl(widget.url),
newName: widget.name,
newTitle: widget.data ? widget.data.title : null,
newData: JSON.parse(JSON.stringify(widget.data || "{}")),
};
}
/**
* Packs a widget, converting the Dimension-specific parameters into a widget. This will
* overwrite any values in the widget which need to be committed to from the Dimension
* fields.
* @param {EditableWidget} widget The widget to pack.
*/
private packWidget(widget: EditableWidget) {
// The widget already has an ID and type, we just need to fill in the bits
widget.name = widget.dimension.newName || this.defaultName;
widget.data = widget.dimension.newData || {};
widget.url = this.wrapUrl(widget.dimension.newUrl, Object.keys(widget.data).map(k => "$" + k));
widget.type = this.widgetIds[0]; // always set the type to be the latest type
// Populate our stuff
widget.data["dimension:app:metadata"] = {
inRoomId: this.roomId,
wrapperUrlBase: this.wrapperUrl,
wrapperId: this.wrapperId,
scalarWrapperId: this.scalarWrapperId,
integration: {
type: this.integration.type,
integrationType: this.integration.integrationType,
},
lastUpdatedTs: new Date().getTime(),
};
// Set the title to an appropriate value. Empty strings are valid titles, but other falsey
// values should be considered as "no title".
if (widget.dimension.newTitle || widget.dimension.newTitle === "")
widget.data["title"] = widget.dimension.newTitle;
else widget.data["title"] = undefined;
}
/**
* Builds a new widget and assigns it to the newWidget property. This also expands the
* Dimension-specific fields for the widget with reasonable defaults.
*/
protected prepareNewWidget() {
this.newWidget = <EditableWidget>{
id: "dimension-" + this.widgetIds[0] + "-" + (new Date().getTime()),
type: this.widgetIds[0],
name: this.defaultName,
url: window.location.origin,
//ownerId: this.userId, // we don't have a user id
dimension: {
newUrl: "",
newName: "",
newTitle: null, // by default we don't offer a title (empty strings are valid titles)
newData: {},
},
};
this.onNewWidgetPrepared();
}
/**
* Gets widgets from the Scalar API. The widgets returned from here are not unpacked and
* may not have the Dimension-specific fields set.
* @param {string[]} types The widget types to look for
* @return {Promise<EditableWidget[]>} The widgets discovered
*/
private getWidgetsOfType(types: string[]): Promise<EditableWidget[]> {
return this.scalarApi.getWidgets(this.roomId)
.then(resp => convertScalarWidgetsToDtos(resp))
.then(widgets => widgets.filter(w => types.indexOf(w.type) !== -1));
}
/**
* Gets the actual URL from known wrappers. If the URL is found to be not wrapped, or no
* wrappers are applicable, then the given URL is returned.
* @param {string} url The URL to unwrap
* @return {string} The unwrapped URL
*/
private unwrapUrl(url: string): string {
console.log(this.scalarWrapperUrls);
if (!this.wrapperUrl) return url;
const urls = [this.wrapperUrl].concat(this.scalarWrapperUrls);
for (let scalarUrl of urls) {
if (url.startsWith(scalarUrl)) {
return decodeURIComponent(url.substring(scalarUrl.length));
}
}
return url;
}
/**
* Wraps the URL with an appropriate wrapper. If no wrapper is defined for this component
* then the given URL is returned.
* @param {string} url The URL to wrap
* @param {string[]} customVars The values in the URL which should not be encoded
* @return {string} The wrapped URL
*/
private wrapUrl(url: string, customVars: string[]): string {
if (!this.wrapperUrl) return url;
let encodedURL = this.wrapperUrl + encodeURIComponent(url);
encodedURL = this.decodeParams(encodedURL, customVars);
return encodedURL;
}
/**
* Decodes the given URL to make the parameters safe for interpretation by clients. The variables
* specified in the widget spec will be decoded automatically.
* @param {string} encodedURL The encoded URL
* @param {string[]} customVars The custom variables to decode
* @returns {string} The URL with variables decoded accordingly
*/
protected decodeParams(encodedURL: string, customVars: string[]): string {
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_user_id"), "$matrix_user_id");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_room_id"), "$matrix_room_id");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_display_name"), "$matrix_display_name");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_avatar_url"), "$matrix_avatar_url");
for (const key of customVars) {
encodedURL = encodedURL.replace(encodeURIComponent(key), key);
}
return encodedURL;
}
/**
* Adds the widget stored in newWidget to the room.
* @returns {Promise<*>} Resolves when the widget has been added and newWidget is populated
* with a new widget.
*/
public addWidget(): Promise<any> {
this.packWidget(this.newWidget);
this.isUpdating = true;
this.onWidgetBeforeAdd();
return this.scalarApi.setWidget(this.roomId, this.newWidget)
.then(() => this.widgets.push(this.newWidget))
.then(() => {
this.isUpdating = false;
this.onWidgetAfterAdd();
this.prepareNewWidget();
this.toaster.pop("success", "Widget added!");
})
.catch(err => {
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
/**
* Saves a widget, persisting the changes to the room.
* @param {EditableWidget} widget The widget to save.
* @returns {Promise<any>} Resolves when the widget has been updated in the room.
*/
public saveWidget(widget: EditableWidget): Promise<any> {
if (!widget.dimension.newUrl || widget.dimension.newUrl.trim().length === 0) {
this.toaster.pop("warning", "Please enter a URL for the widget");
return;
}
this.packWidget(widget);
this.isUpdating = true;
this.onWidgetBeforeEdit(widget);
return this.scalarApi.setWidget(this.roomId, widget)
.then(() => this.toggleWidget(widget))
.then(() => {
this.isUpdating = false;
this.onWidgetAfterEdit(widget);
this.toaster.pop("success", "Widget updated!");
})
.catch(err => {
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
/**
* Removes a widget from the room
* @param {EditableWidget} widget The widget to remove
* @returns {Promise<*>} Resolves when the widget has been removed.
*/
public removeWidget(widget: EditableWidget): Promise<any> {
this.isUpdating = true;
this.onWidgetBeforeDelete(widget);
return this.scalarApi.deleteWidget(this.roomId, widget)
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
.then(() => {
this.isUpdating = false;
this.onWidgetAfterDelete(widget);
this.toaster.pop("success", "Widget deleted!");
})
.catch(err => {
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
/**
* Puts a widget in the edit state.
* @param {EditableWidget} widget
*/
public editWidget(widget: EditableWidget) {
this.toggleWidget(widget, "edit");
}
/**
* Toggles a widget between the "edit" and "canceled" state. If a targetState is
* defined, the widget is forced into that state.
* @param {EditableWidget} widget The widget to set the state of.
* @param {"edit"|"cancel"|null} targetState The target state, optional
*/
public toggleWidget(widget: EditableWidget, targetState: "edit" | "cancel" | null = null) {
let idx = this.toggledWidgetIds.indexOf(widget.id);
if (targetState === null) targetState = idx === -1 ? "edit" : "cancel";
if (targetState === "edit") {
this.unpackWidget(widget);
this.onWidgetPreparedForEdit(widget);
if (idx === -1) this.toggledWidgetIds.push(widget.id);
}
else this.toggledWidgetIds.splice(idx, 1);
}
/**
* Determines if a widget is in the edit state
* @param {EditableWidget} widget The widget to check
* @returns {boolean} true if the widget is in the edit state
*/
public isWidgetToggled(widget: EditableWidget) {
return this.toggledWidgetIds.indexOf(widget.id) !== -1;
}
// Component hooks below here
// ------------------------------------------------------------------
/**
* Called when a new widget has been created in the newWidget field
*/
protected onNewWidgetPrepared(): void {
// Component hook
}
/**
* Called after all the widgets have been discovered and unpacked for the room.
*/
protected onWidgetsDiscovered(): void {
// Component hook
}
/**
* Called before the widget is added to the room, but after the Dimension-specific
* settings have been copied over to the primary fields.
*/
protected onWidgetBeforeAdd(): void {
// Component hook
}
/**
* Called after the widget has been added to the room, but before the newWidget field
* has been set to a new widget.
*/
protected onWidgetAfterAdd(): void {
// Component hook
}
/**
* Called when the given widget has been asked to be prepared for editing. At this point
* the widget is not being persisted to the room, it is just updating the EditingWidget's
* properties for the user's ability to edit it.
* @param {EditableWidget} _widget The widget that has been prepared for editing
*/
protected onWidgetPreparedForEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called before the given widget has been updated in the room, but after the
* Dimension-specific settings have been copied over to the primary fields.
* This is not called for widgets being deleted.
* @param {EditableWidget} _widget The widget about to be edited
*/
protected onWidgetBeforeEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called after a given widget has been updated in the room. This is not called for
* widgets being deleted.
* @param {EditableWidget} _widget The widget that has been updated.
*/
protected onWidgetAfterEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called before the given widget has been removed from the room. No changes to the
* widget have been made at this point.
* @param {EditableWidget} _widget The widget about to be deleted.
*/
protected onWidgetBeforeDelete(_widget: EditableWidget): void {
// Component hook
}
/**
* Called after a given widget has been deleted from the room. The widget will be in
* the deleted state and will no longer be tracked anywhere on the component.
* @param {EditableWidget} _widget The widget that has been deleted.
*/
protected onWidgetAfterDelete(_widget: EditableWidget): void {
// Component hook
}
}

View File

@ -1,66 +0,0 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img [src]="integration.avatar">
<h4>Configure video widgets</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label>
Widget Name
<input type="text" class="form-control"
placeholder="YouTube Widget"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Video URL
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,4 +0,0 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -1,70 +0,0 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_YOUTUBE } from "../../../shared/models/widget";
import * as embed from "embed-video";
import * as $ from "jquery";
@Component({
selector: "my-youtubewidget-config",
templateUrl: "youtube-config.component.html",
styleUrls: ["youtube-config.component.scss", "./../../config.component.scss"],
})
export class YoutubeWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_YOUTUBE,
"Youtube Widget",
"video", // wrapper
"youtube" // scalar wrapper
);
}
public validateAndAddWidget() {
const url = this.getRealVideoUrl(this.newWidget.dimension.newUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
this.newWidget.dimension.newUrl = url;
this.addWidget();
}
public validateAndSaveWidget(widget: EditableWidget) {
const url = this.getRealVideoUrl(widget.dimension.newUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
widget.dimension.newUrl = url;
this.saveWidget(widget);
}
private getRealVideoUrl(url) {
const embedCode = embed(url);
if (!embedCode) {
return null;
}
// HACK: Grab the video URL from the iframe embed code
url = $(embedCode).attr("src");
if (url.startsWith("//")) url = "https:" + url;
return url;
}
}

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { LegacyIntegration } from "../shared/models/legacyintegration";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
import { Integration } from "../shared/models/integration";
@ -10,7 +9,7 @@ import { Integration } from "../shared/models/integration";
})
export class IntegrationBagComponent {
@Input() integrations: LegacyIntegration[];
@Input() integrations: Integration[];
@Output() integrationClicked: EventEmitter<Integration> = new EventEmitter<Integration>();
constructor(private sanitizer: DomSanitizer) {

View File

@ -1,5 +0,0 @@
<div class="integration">
<img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
<div class="integration-name">{{ integration.name }}</div>
<div class="integration-description">{{ integration.about }}</div>
</div>

View File

@ -1,31 +0,0 @@
// component styles are encapsulated and only applied to their components
.integration {
border: 1px solid #eee;
border-radius: 3px;
margin-left: 7px;
margin-right: 7px;
padding: 5px;
width: calc(325px - 14px);
position: relative;
flex: 1;
}
.integration .integration-avatar {
width: 50px;
height: 50px;
float: left;
}
.integration .integration-name {
display: inline-block;
font-size: 1.1em;
font-weight: 100;
padding-left: 5px;
}
.integration .integration-description {
display: block;
font-size: 0.8em;
margin-left: 55px;
color: #999;
}

View File

@ -1,30 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { LegacyIntegration } from "../shared/models/legacyintegration";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
export class ConfigModalContext extends BSModalContext {
public integration: LegacyIntegration;
public roomId: string;
public userId: string;
public scalarToken: string;
public integrationId: string;
}
@Component({
selector: "my-integration",
templateUrl: "./integration.component.html",
styleUrls: ["./integration.component.scss"],
})
export class IntegrationComponent {
@Input() integration: LegacyIntegration;
@Output() selected: EventEmitter<any> = new EventEmitter<any>();
constructor(private sanitizer: DomSanitizer) {
}
public getSafeUrl(url: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}

View File

@ -1,49 +0,0 @@
export interface LegacyIntegration {
// These are from the server
type: string;
integrationType: string;
userId: string;
name: string;
avatar: string;
about: string; // nullable
supportsEncryptedRooms: boolean;
requirements: any; // nullable
// These are set in the UI
isSupported: boolean;
notSupportedReason: string;
hasAdditionalConfig: boolean;
isEnabled: boolean; // for the flip-a-bit integrations
isUpdating: boolean;
}
export interface RSSIntegration extends LegacyIntegration {
feeds: string[];
immutableFeeds: { url: string, ownerId: string }[];
}
export interface TravisCiIntegration extends LegacyIntegration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
}
export interface CircleCiIntegration extends LegacyIntegration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
}
export interface IRCIntegration extends LegacyIntegration {
availableNetworks: { name: string, id: string }[];
channels: { [networkId: string]: string[] };
}
export interface EtherpadWidgetIntegration extends LegacyIntegration {
defaultUrl: string;
}
export interface JitsiWidgetIntegration extends LegacyIntegration {
jitsiDomain: string;
scriptUrl: string
}

View File

@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "./AuthedApi";
import { DimensionIntegrationsResponse } from "../models/dimension_responses";
import { Widget } from "../models/integration";
@Injectable()
export class DimensionApiService extends AuthedApi {
@ -12,4 +13,13 @@ export class DimensionApiService extends AuthedApi {
public getIntegrations(roomId: string): Promise<DimensionIntegrationsResponse> {
return this.authedGet("/api/v1/dimension/integrations/room/" + roomId).map(r => r.json()).toPromise();
}
public getWidget(type: string): Promise<Widget> {
return this.http.get("/api/v1/dimension/widget/" + type).map(r => r.json()).toPromise();
}
public isEmbeddable(url: string): Promise<any> { // 200 = success, anything else = error
return this.http.get("/api/v1/dimension/widgets/embeddable", {params: {url: url}})
.map(r => r.json()).toPromise();
}
}

View File

@ -1,53 +0,0 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { LegacyIntegration } from "../../models/legacyintegration";
@Injectable()
export class ApiService {
constructor(private http: Http) {
}
checkScalarToken(scalarToken: string): Promise<boolean> {
return this.http.get("/api/v1/scalar/checkToken", {params: {scalar_token: scalarToken}})
.map(res => res.status === 200).toPromise();
}
getTokenOwner(scalarToken: String): Promise<string> {
return this.http.get("/api/v1/dimension/whoami", {params: {scalar_token: scalarToken}})
.map(res => res.status === 200 ? res.json()["userId"] : null).toPromise();
}
getIntegrations(roomId: string, scalarToken: string): Promise<LegacyIntegration[]> {
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
removeIntegration(roomId: string, type: string, integrationType: string, scalarToken: string): Promise<any> {
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType;
return this.http.delete(url, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
updateIntegrationState(roomId: string, type: string, integrationType: string, scalarToken: string, newState: any): Promise<any> {
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType + "/state";
return this.http.put(url, {scalar_token: scalarToken, state: newState})
.map(res => res.json()).toPromise();
}
getIntegrationState(roomId: string, type: string, integrationType: string, scalarToken: string): Promise<any> {
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType + "/state";
return this.http.get(url, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
isEmbeddable(checkUrl: string): Promise<any> {
const url = "/api/v1/dimension/widgets/embeddable";
return this.http.get(url, {params: {url: checkUrl}})
.map(res => res.json()).toPromise();
}
getIntegration(type: string, integrationType: string): Promise<LegacyIntegration> {
const url = "/api/v1/dimension/integration/" + type + "/" + integrationType;
return this.http.get(url).map(res => res.json()).toPromise();
}
}

View File

@ -1,117 +0,0 @@
import { Injectable } from "@angular/core";
import { LegacyIntegration } from "../../models/legacyintegration";
import { RssConfigComponent } from "../../../configs-old/rss/rss-config.component";
import { ContainerContent } from "ngx-modialog";
import { IrcConfigComponent } from "../../../configs-old/irc/irc-config.component";
import { TravisCiConfigComponent } from "../../../configs-old/travisci/travisci-config.component";
import { CustomWidgetConfigComponent } from "../../../configs-old/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../../../configs-old/widget/youtube/youtube-config.component";
import { TwitchWidgetConfigComponent } from "../../../configs-old/widget/twitch/twitch-config.component";
import { EtherpadWidgetConfigComponent } from "../../../configs-old/widget/etherpad/etherpad-config.component";
import { JitsiWidgetConfigComponent } from "../../../configs-old/widget/jitsi/jitsi-config.component";
import {
WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, WIDGET_JITSI, WIDGET_TWITCH,
WIDGET_YOUTUBE
} from "../../models/widget";
import { GoogleDocsWidgetConfigComponent } from "../../../configs-old/widget/googledocs/googledocs-config.component";
import { GoogleCalendarWidgetConfigComponent } from "../../../configs-old/widget/googlecalendar/googlecalendar-config.component";
import { CircleCiConfigComponent } from "../../../configs-old/circleci/circleci-config.component";
@Injectable()
export class LegacyIntegrationService {
private static supportedIntegrationsMap = {
"bot": {}, // empty == supported
"complex-bot": {
"rss": {
component: RssConfigComponent,
},
"travisci": {
component: TravisCiConfigComponent,
},
"circleci": {
component: CircleCiConfigComponent,
},
},
"bridge": {
"irc": {
component: IrcConfigComponent,
},
},
"widget": {
"customwidget": {
component: CustomWidgetConfigComponent,
types: WIDGET_CUSTOM,
},
"youtube": {
component: YoutubeWidgetConfigComponent,
types: WIDGET_YOUTUBE
},
"etherpad": {
component: EtherpadWidgetConfigComponent,
types: WIDGET_ETHERPAD,
},
"twitch": {
component: TwitchWidgetConfigComponent,
types: WIDGET_TWITCH,
},
"jitsi": {
component: JitsiWidgetConfigComponent,
types: WIDGET_JITSI,
},
"googledocs": {
component: GoogleDocsWidgetConfigComponent,
types: WIDGET_GOOGLE_DOCS,
},
"googlecalendar": {
component: GoogleCalendarWidgetConfigComponent,
types: WIDGET_GOOGLE_CALENDAR,
},
},
};
static getAllConfigComponents(): ContainerContent[] {
const components = [];
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
const component = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].component;
if (component) components.push(component);
}
}
return components;
}
static isSupported(integration: LegacyIntegration): boolean {
const forType = LegacyIntegrationService.supportedIntegrationsMap[integration.type];
if (!forType) return false;
if (Object.keys(forType).length === 0) return true;
return forType[integration.integrationType]; // has sub type
}
static hasConfig(integration: LegacyIntegration): boolean {
return integration.type !== "bot";
}
static getConfigComponent(integration: LegacyIntegration): ContainerContent {
return LegacyIntegrationService.supportedIntegrationsMap[integration.type][integration.integrationType].component;
}
static getIntegrationForScreen(screen: string): { type: string, integrationType: string } {
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
const integrationTypes = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].types;
const integrationScreens = integrationTypes.map(t => "type_" + t);
if (integrationScreens.includes(screen)) return {type: iType, integrationType: iiType};
}
}
return null;
}
constructor() {
}
}

View File

@ -1,27 +0,0 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
@Injectable()
export class IrcApiService {
constructor(private http: Http) {
}
getChannelOps(roomId: string, networkId: string, channel: string, scalarToken: string): Promise<string[]> {
return this.http.get("/api/v1/irc/" + roomId + "/ops/" + networkId + "/" + channel, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
linkChannel(roomId: string, networkId: string, channel: string, op: string, scalarToken: string): Promise<any> {
return this.http.put("/api/v1/irc/" + roomId + "/channels/" + networkId + "/" + channel, {}, {
params: {
scalar_token: scalarToken,
op: op
}
}).map(res => res.json()).toPromise();
}
unlinkChannel(roomId: string, networkId: string, channel: string, scalarToken: string): Promise<any> {
return this.http.delete("/api/v1/irc/" + roomId + "/channels/" + networkId + "/" + channel, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
}

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { ApiService } from "../../shared/services/legacy/api.service";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { DimensionApiService } from "../../shared/services/dimension-api.service";
@Component({
selector: "my-generic-widget-wrapper",
@ -14,7 +14,7 @@ export class GenericWidgetWrapperComponent {
public canEmbed = false;
public embedUrl: SafeUrl = null;
constructor(api: ApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
constructor(api: DimensionApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
let params: any = activatedRoute.snapshot.queryParams;
api.isEmbeddable(params.url).then(result => {

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import * as $ from "jquery";
import { ApiService } from "../../shared/services/legacy/api.service";
import { JitsiWidgetIntegration } from "../../shared/models/legacyintegration";
import { DimensionApiService } from "../../shared/services/dimension-api.service";
import { JitsiWidget } from "../../shared/models/integration";
declare var JitsiMeetExternalAPI: any;
@ -22,7 +22,7 @@ export class JitsiWidgetWrapperComponent implements OnInit {
private userId: string;
private jitsiApiObj: any;
constructor(activatedRoute: ActivatedRoute, private api: ApiService) {
constructor(activatedRoute: ActivatedRoute, private api: DimensionApiService) {
let params: any = activatedRoute.snapshot.queryParams;
this.domain = params.domain;
@ -33,9 +33,9 @@ export class JitsiWidgetWrapperComponent implements OnInit {
}
public ngOnInit() {
this.api.getIntegration("widget", "jitsi").then(integration => {
const widget = <JitsiWidgetIntegration>integration;
$.getScript(widget.scriptUrl);
this.api.getWidget("jitsi").then(integration => {
const widget = <JitsiWidget>integration;
$.getScript(widget.options.scriptUrl);
});
}