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 @@ +
+ +
+ +

Configure CircleCI hooks

+
+
+
+
+
+
.circleci/config.yml configuration
+ The following will need to be added to your .circleci/config.yml file: +
{{ circleYaml }}
+
+
+
Your CircleCI hooks
+
+ + + + +
+
+
+ {{ repo.repoKey }} + + +
+ + + +
+
+
+
Hooks from other users in the room
+
+
+ {{ repo.repoKey }} (added by {{ repo.ownerId }}) + +
{{ repo.template }}
+
+
+
+
+
\ 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