diff --git a/config/integrations/circleci.yaml b/config/integrations/circleci.yaml
new file mode 100644
index 0000000..31de701
--- /dev/null
+++ b/config/integrations/circleci.yaml
@@ -0,0 +1,8 @@
+type: "complex-bot"
+integrationType: "circleci"
+enabled: true
+name: "CircleCI"
+about: "Sends CircleCI build results into the room"
+avatar: "img/avatars/circleci.png"
+upstream:
+ type: "vector"
diff --git a/web/app/configs/circleci/circleci-config.component.html b/web/app/configs/circleci/circleci-config.component.html
new file mode 100644
index 0000000..4685cec
--- /dev/null
+++ b/web/app/configs/circleci/circleci-config.component.html
@@ -0,0 +1,63 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/configs/circleci/circleci-config.component.scss b/web/app/configs/circleci/circleci-config.component.scss
new file mode 100644
index 0000000..0430df4
--- /dev/null
+++ b/web/app/configs/circleci/circleci-config.component.scss
@@ -0,0 +1,19 @@
+// 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;
+}
\ No newline at end of file
diff --git a/web/app/configs/circleci/circleci-config.component.ts b/web/app/configs/circleci/circleci-config.component.ts
new file mode 100644
index 0000000..c524063
--- /dev/null
+++ b/web/app/configs/circleci/circleci-config.component.ts
@@ -0,0 +1,116 @@
+import { Component } from "@angular/core";
+import { CircleCiIntegration } from "../../shared/models/integration";
+import { ModalComponent, DialogRef } from "ngx-modialog";
+import { ConfigModalContext } from "../../integration/integration.component";
+import { ToasterService } from "angular2-toaster";
+import { ApiService } from "../../shared/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 {
+
+ 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,
+ private toaster: ToasterService,
+ private api: ApiService) {
+ this.integration = 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;
+ });
+ }
+}
diff --git a/web/app/shared/integration.service.ts b/web/app/shared/integration.service.ts
index c4fe7d2..67c5e5e 100644
--- a/web/app/shared/integration.service.ts
+++ b/web/app/shared/integration.service.ts
@@ -4,6 +4,7 @@ import { RssConfigComponent } from "../configs/rss/rss-config.component";
import { ContainerContent } from "ngx-modialog";
import { IrcConfigComponent } from "../configs/irc/irc-config.component";
import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component";
+import { CircleCiConfigComponent } from "../configs/circleci/circleci-config.component";
import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../configs/widget/youtube/youtube-config.component";
import { TwitchWidgetConfigComponent } from "../configs/widget/twitch/twitch-config.component";
@@ -28,6 +29,9 @@ export class IntegrationService {
"travisci": {
component: TravisCiConfigComponent,
},
+ "circleci": {
+ component: CircleCiConfigComponent,
+ },
},
"bridge": {
"irc": {
diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts
index 8f02e8c..c28eaeb 100644
--- a/web/app/shared/models/integration.ts
+++ b/web/app/shared/models/integration.ts
@@ -26,6 +26,12 @@ export interface TravisCiIntegration extends Integration {
webhookUrl: string; // immutable
}
+export interface CircleCiIntegration extends Integration {
+ repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
+ immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
+ webhookUrl: string; // immutable
+}
+
export interface IRCIntegration extends Integration {
availableNetworks: { name: string, id: string }[];
channels: { [networkId: string]: string[] };
diff --git a/web/public/img/avatars/circleci.png b/web/public/img/avatars/circleci.png
new file mode 100644
index 0000000..1e9ba51
Binary files /dev/null and b/web/public/img/avatars/circleci.png differ