From 17d6ab83671eb5d0014a7bd299d0f6c53db50801 Mon Sep 17 00:00:00 2001 From: MTRNord Date: Fri, 15 Dec 2017 14:12:25 +0100 Subject: [PATCH] Add CircleCI Integration Signed-off-by: MTRNord --- config/integrations/circleci.yaml | 8 ++ .../circleci/circleci-config.component.html | 63 ++++++++++ .../circleci/circleci-config.component.scss | 19 +++ .../circleci/circleci-config.component.ts | 116 ++++++++++++++++++ web/app/shared/integration.service.ts | 4 + web/app/shared/models/integration.ts | 6 + web/public/img/avatars/circleci.png | Bin 0 -> 4381 bytes 7 files changed, 216 insertions(+) create mode 100644 config/integrations/circleci.yaml create mode 100644 web/app/configs/circleci/circleci-config.component.html create mode 100644 web/app/configs/circleci/circleci-config.component.scss create mode 100644 web/app/configs/circleci/circleci-config.component.ts create mode 100644 web/public/img/avatars/circleci.png 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 0000000000000000000000000000000000000000..1e9ba51a4c024cc125969d4adc3d7f4122c2e04f GIT binary patch literal 4381 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