Skip to content

Commit

Permalink
Allow build and pipeline status to be updated automatically
Browse files Browse the repository at this point in the history
Use channels to send updated status of each build, when in the build view, or of each pipeline when in the pipeline view.

Closes #40
  • Loading branch information
suprnova32 committed Jun 25, 2018
1 parent f815f13 commit 0f2fd7f
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 99 deletions.
6 changes: 5 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ import "phoenix_html"
// Stimulus data
import { Application } from "stimulus"

import BuildController from "./controllers/build_controller"
import BuildsController from "./controllers/builds_controller"
import ChartsController from "./controllers/charts_controller"
import PipelinesController from "./controllers/pipelines_controller"
import ProjectsController from "./controllers/projects_controller"
import ReposController from "./controllers/repos_controller"
import TagsController from "./controllers/tags_controller"

const application = Application.start()
application.register("build", BuildController)
application.register("builds", BuildsController)
application.register("charts", ChartsController)
application.register("pipelines", PipelinesController)
application.register("projects", ProjectsController)
application.register("repos", ReposController)
application.register("tags", TagsController)
Expand Down Expand Up @@ -61,7 +65,7 @@ if($("#aside").hasClass("page-aside")) {
$(".alert").addClass("aci-aside")
}

var
const
collapsibleSidebarClass = "aci-collapsible-sidebar",
collapsibleSidebarCollapsedClass = "aci-collapsible-sidebar-collapsed",
sidebar = $(".aci-left-sidebar"),
Expand Down
38 changes: 38 additions & 0 deletions assets/js/controllers/build_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Controller } from "stimulus"
import socket from "../socket"

export default class extends Controller {

connect() {
let Ansi = require("ansi-to-html")
const ansi = new Ansi()

const buildName = this.data.get("name")
const trace = this.data.get("trace")
const id = this.data.get("id")

if (trace == "") {
var contents = "Build is pending"
} else {
var contents = ansi.toHtml(trace)
}

$("#output").html(`<h3>${buildName}</h3>${contents.replace(/\n/g, "<br />")}`)

let channel = socket.channel(`build:${id}`, {})
channel.join()
.receive("ok", data => { console.log(`Joined successfully for build ${id}`, data) })
.receive("error", data => { console.log("Unable to join", data) })

channel.on("append_trace", data => {
$("#output").append(ansi.toHtml(data.trace).replace(/\n/g, "<br/>"))
$(window).scrollTop($(document).height())
})

channel.on("replace_trace", data => {
setTimeout(function(){
window.location.reload(true)
}, 1500)
})
}
}
31 changes: 6 additions & 25 deletions assets/js/controllers/builds_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,17 @@ import { Controller } from "stimulus"
import socket from "../socket"

export default class extends Controller {

connect() {
let Ansi = require("ansi-to-html")
const ansi = new Ansi()

const buildName = this.data.get("name")
const trace = this.data.get("trace")
const id = this.data.get("id")
const token = this.data.get("token")
let channel = socket.channel(`build:${id}`, {})

if (trace == "") {
var contents = "Build is pending"
} else {
var contents = ansi.toHtml(trace)
}

$("#output").html(`<h3>${buildName}</h3>${contents.replace(/\n/g, "<br />")}`)

let channel = socket.channel(`builds:${this.data.get("id")}`, {})
channel.join()
.receive("ok", data => { console.log("Joined successfully", data) })
.receive("ok", data => { console.log(`Joined successfully for build ${id}`, data) })
.receive("error", data => { console.log("Unable to join", data) })

channel.on("append_trace", data => {
$("#output").append(ansi.toHtml(data.trace).replace(/\n/g, "<br/>"))
$(window).scrollTop($(document).height())
})

channel.on("replace_trace", data => {
setTimeout(function(){
window.location.reload(true)
}, 1500)
channel.on("update_status", data => {
$(`#build-${id}`).html(data.content.replace(/data-csrf="{1}.*=="/g, `data-csrf="${token}"`))
})
}
}
16 changes: 16 additions & 0 deletions assets/js/controllers/pipelines_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Controller } from "stimulus"
import socket from "../socket"

export default class extends Controller {
connect() {
const id = this.data.get("id")
let channel = socket.channel(`pipeline:${id}`, {})
channel.join()
.receive("ok", data => { console.log(`Joined successfully for pipeline ${id}`, data) })
.receive("error", data => { console.log("Unable to join", data) })

channel.on("update_status", data => {
$(`#pipeline-${id}`).html(data.content)
})
}
}
8 changes: 5 additions & 3 deletions lib/alloy_ci/builds/builds.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule AlloyCi.Builds do
@moduledoc """
The boundary for the Builds system.
"""
alias AlloyCi.{Artifact, Build, Queuer, Pipelines, Projects, Repo, Workers}
alias AlloyCi.{Artifact, Build, Queuer, Pipelines, Projects, Repo, Workers, Web.BuildsChannel}
import Ecto.Query, warn: false

@github_api Application.get_env(:alloy_ci, :github_api)
Expand Down Expand Up @@ -60,7 +60,7 @@ defmodule AlloyCi.Builds do
end

def clean_ref(ref) do
ref |> String.replace(ref |> ref_type() |> cleanup_string(), "")
String.replace(ref, ref |> ref_type() |> cleanup_string(), "")
end

def create_builds_from_config(content, pipeline) do
Expand Down Expand Up @@ -160,7 +160,7 @@ defmodule AlloyCi.Builds do
end

def for_runner(runner) do
# Select builds whose tags are fully contained in the runner's tags
# Select a build whose tags are fully contained in the runner's tags
Build
|> where([b], b.status == "pending" and is_nil(b.runner_id))
|> where([b], fragment("? <@ ?", b.tags, ^runner.tags))
Expand Down Expand Up @@ -290,6 +290,7 @@ defmodule AlloyCi.Builds do
"created" -> update_status(build, status || "running")
"pending" -> update_status(build, status || "running")
"running" -> update_status(build, status || "success")
_ -> {:ok, build}
end
end

Expand Down Expand Up @@ -581,6 +582,7 @@ defmodule AlloyCi.Builds do
case do_update_status(build, status) do
{:ok, build} ->
Queuer.push(Workers.ProcessPipelineWorker, build.pipeline_id)
BuildsChannel.update_status(build)
build

{:error, _} ->
Expand Down
5 changes: 5 additions & 0 deletions lib/alloy_ci/lib/artifact_sweeper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,27 @@ defmodule AlloyCi.ArtifactSweeper do
GenServer.call(__MODULE__, :sweep)
end

@impl true
def init(state) do
{:ok, schedule_work(self(), state)}
end

@impl true
def handle_call(:reset_timer, _from, state) do
{:reply, :ok, schedule_work(self(), state)}
end

@impl true
def handle_call(:sweep, _from, state) do
{:reply, :ok, sweep(self(), state)}
end

@impl true
def handle_info(:sweep, state) do
{:noreply, sweep(self(), state)}
end

@impl true
def handle_info(_, state), do: {:noreply, state}

defp parse_interval(interval) do
Expand Down
11 changes: 8 additions & 3 deletions lib/alloy_ci/pipelines/pipelines.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule AlloyCi.Pipelines do
Projects,
Queuer,
Repo,
Web.PipelinesChannel,
Workers.CreateBuildsWorker
}

Expand Down Expand Up @@ -172,9 +173,13 @@ defmodule AlloyCi.Pipelines do
end

def update_pipeline(%Pipeline{} = pipeline, params) do
pipeline
|> Pipeline.changeset(params)
|> Repo.update()
with {:ok, pipeline} <-
pipeline
|> Pipeline.changeset(params)
|> Repo.update() do
PipelinesChannel.update_status(pipeline |> Repo.preload(:project))
{:ok, pipeline}
end
end

def update_status(pipeline_id) do
Expand Down
21 changes: 17 additions & 4 deletions lib/alloy_ci/web/channels/builds_channel.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule AlloyCi.Web.BuildsChannel do
@moduledoc false
alias AlloyCi.{Builds, Projects, Web.Endpoint}
alias AlloyCi.{Builds, Projects, Web.Endpoint, Web.PipelineView}
use AlloyCi.Web, :channel

# Channels can be used in a request/response fashion
Expand All @@ -16,7 +16,7 @@ defmodule AlloyCi.Web.BuildsChannel do
{:noreply, socket}
end

def join("builds:" <> build_id, _payload, socket) do
def join("build:" <> build_id, _payload, socket) do
build = Builds.get(build_id)

if Projects.can_access?(build.project_id, %{id: socket.assigns.user_id}) do
Expand All @@ -27,12 +27,25 @@ defmodule AlloyCi.Web.BuildsChannel do
end

def replace_trace(build_id, trace) do
Endpoint.broadcast("builds:#{build_id}", "replace_trace", %{trace: trace})
Endpoint.broadcast("build:#{build_id}", "replace_trace", %{trace: trace})
end

def send_trace(_build_id, ""), do: nil

def send_trace(build_id, trace) do
Endpoint.broadcast("builds:#{build_id}", "append_trace", %{trace: trace})
Endpoint.broadcast("build:#{build_id}", "append_trace", %{trace: trace})
end

def update_status(build) do
Endpoint.broadcast("build:#{build.id}", "update_status", %{content: render_build(build)})
end

def render_build(build) do
Phoenix.View.render_to_string(
PipelineView,
"build.html",
build: build,
conn: %Plug.Conn{}
)
end
end
44 changes: 44 additions & 0 deletions lib/alloy_ci/web/channels/pipelines_channel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule AlloyCi.Web.PipelinesChannel do
@moduledoc false
alias AlloyCi.{Pipelines, Projects, Web.Endpoint, Web.ProjectView}
use AlloyCi.Web, :channel

# Channels can be used in a request/response fashion
# by sending replies to requests from the client
def handle_in("ping", payload, socket) do
{:reply, {:ok, payload}, socket}
end

# It is also common to receive messages from the client and
# broadcast to everyone in the current topic (pipelines:lobby).
def handle_in("shout", payload, socket) do
broadcast(socket, "shout", payload)
{:noreply, socket}
end

def join("pipeline:" <> pipeline_id, _payload, socket) do
pipeline = Pipelines.get(pipeline_id)

if Projects.can_access?(pipeline.project_id, %{id: socket.assigns.user_id}) do
{:ok, socket}
else
{:error, %{reason: "Unauthorized"}}
end
end

def update_status(pipeline) do
Endpoint.broadcast("pipeline:#{pipeline.id}", "update_status", %{
content: render_pipeline(pipeline)
})
end

defp render_pipeline(pipeline) do
Phoenix.View.render_to_string(
ProjectView,
"pipeline.html",
pipeline: pipeline,
project: pipeline.project,
conn: %Plug.Conn{}
)
end
end
3 changes: 2 additions & 1 deletion lib/alloy_ci/web/channels/user_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ defmodule AlloyCi.Web.UserSocket do
alias AlloyCi.Accounts

## Channels
channel("builds:*", AlloyCi.Web.BuildsChannel)
channel("build:*", AlloyCi.Web.BuildsChannel)
channel("pipeline:*", AlloyCi.Web.PipelinesChannel)
channel("repos:*", AlloyCi.Web.ReposChannel)

## Transports
Expand Down
4 changes: 2 additions & 2 deletions lib/alloy_ci/web/templates/build/show.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
<div class="row">
<div class="col-xl-12 col-sm-12">
<div class="card card-border-color card-border-color-<%= panel_status(@build.status)%>"
data-controller="builds" data-builds-id="<%= @build.id %>" data-builds-trace="<%= @build.trace %>"
data-builds-name="<%= @build.name %>">
data-controller="build" data-build-id="<%= @build.id %>" data-build-trace="<%= @build.trace %>"
data-build-name="<%= @build.name %>">
<div class="card-header"></div>
<div class="card-body">
<div class="terminal-container">
Expand Down
22 changes: 22 additions & 0 deletions lib/alloy_ci/web/templates/pipeline/build.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div id="build-<%= @build.id %>">
<span class="user-timeline <%= timeline_status(@build.status) %>">
<li class="latest">
<div class="user-timeline-date">
<%= icon("clock-o") %>
<%= build_duration(@build) %>
</div>
<div class="user-timeline-title">
<%= icon("terminal") %>
<%= link @build.name, to: project_build_path(@conn, :show, @build.project_id, @build.id) %>
</div>
<div class="user-timeline-description">
<span data-toggle="tooltip"
data-placement="bottom"
title="Status: <%= String.capitalize(@build.status) %>">
<%= build_status_icon(@build.status) %>
</span>
<%= build_actions(@conn, @build, "fa-lg") %>
</div>
</li>
</span>
</div>
23 changes: 3 additions & 20 deletions lib/alloy_ci/web/templates/pipeline/show.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,9 @@
<div class="card-body">
<ul class="user-timeline">
<%= for build <- builds do %>
<span class="user-timeline <%= timeline_status(build.status) %>">
<li class="latest">
<div class="user-timeline-date">
<%= icon("clock-o") %>
<%= build_duration(build) %>
</div>
<div class="user-timeline-title">
<%= icon("terminal") %>
<%= link build.name, to: project_build_path(@conn, :show, build.project_id, build.id) %>
</div>
<div class="user-timeline-description">
<span data-toggle="tooltip"
data-placement="bottom"
title="Status: <%= String.capitalize(build.status) %>">
<%= build_status_icon(build.status) %>
</span>
<%= build_actions(@conn, build, "fa-lg") %>
</div>
</li>
</span>
<div data-controller="builds" data-builds-id="<%= build.id %>" data-builds-token="<%= get_csrf_token() %>">
<%= render "build.html", build: build, conn: @conn %>
</div>
<% end %>
</ul>
</div>
Expand Down
Loading

0 comments on commit 0f2fd7f

Please sign in to comment.