Skip to content

Commit

Permalink
Merge pull request #12123 from danidoni/implement-filters-for-a-workf…
Browse files Browse the repository at this point in the history
…low-run-index-page

Implement filters for Workflow Runs
  • Loading branch information
krauselukas committed Feb 4, 2022
2 parents d55a0f1 + 9d11375 commit ea402e1
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 34 deletions.
38 changes: 38 additions & 0 deletions src/api/app/assets/stylesheets/webui/workflow_runs.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
$workflow-runs-filter-box-height: 3.5rem;

#request-payload {
max-height: 20rem;
}
Expand All @@ -7,3 +9,39 @@
max-height: 30rem;
}
}

#workflow-runs-filter-desktop {
strong { cursor: pointer; }
.collapse {
&.show { border-top: 1px solid $gray-300;}
}
}

@include media-breakpoint-up(md) {
#workflow-runs-filter-desktop {
.collapse { display: block !important; }
}
}

@include media-breakpoint-between(xs, sm) {
#workflow-runs-filter-desktop {
&.show { border-top: 1px solid $gray-300; }
&.sticky-top { top: $top-navigation-height; }
height: $workflow-runs-filter-box-height;
// To not overlap with the notification action bar
z-index: calc(#{$zindex-sticky} + 1);

#filters {
max-height: 100vw; overflow: auto;
-webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
-moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
}
}

@media (orientation: landscape) {
#workflow-runs-filter-desktop {
#filters { max-height: 20vw; }
}
}
}
21 changes: 21 additions & 0 deletions src/api/app/components/workflow_run_filter_component.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.row.list-group-flush
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'All', amount: @count['all'],
filter_item: {}, selected_filter: @selected_filter)
.row.list-group-flush.mt-5
%h5.ml-3 Status
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Succeeded', amount: @count['success'],
filter_item: { status: 'success' }, selected_filter: @selected_filter)
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Running', amount: @count['running'],
filter_item: { status: 'running' }, selected_filter: @selected_filter)
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Failed', amount: @count['fail'],
filter_item: { status: 'fail' }, selected_filter: @selected_filter)
.row.list-group-flush.mt-5
%h5.ml-3 Event type
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Pull Requests', amount: @count['pull_request'],
filter_item: { event_type: 'pull_request' }, selected_filter: @selected_filter)
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Push', amount: @count['push'],
filter_item: { event_type: 'push' }, selected_filter: @selected_filter)
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Merge Request Hook', amount: @count['Merge Request Hook'],
filter_item: { event_type: 'Merge Request Hook' }, selected_filter: @selected_filter)
= render WorkflowRunFilterLinkComponent.new(token: @token, text: 'Push Hook', amount: @count['Push Hook'],
filter_item: { event_type: 'Push Hook' }, selected_filter: @selected_filter)
17 changes: 17 additions & 0 deletions src/api/app/components/workflow_run_filter_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class WorkflowRunFilterComponent < ApplicationComponent
def initialize(token:, selected_filter:, finder: WorkflowRunsFinder.new)
super

@count = workflow_runs_count(finder)
@selected_filter = selected_filter
@token = token
end

def workflow_runs_count(finder)
counted_workflow_runs = {}
counted_workflow_runs['success'] = finder.succeeded.count
counted_workflow_runs['running'] = finder.running.count
counted_workflow_runs['fail'] = finder.failed.count
counted_workflow_runs.merge(finder.group_by_event_type)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
= link_to(token_workflow_runs_path(@token, @filter_item), class: "list-group-item list-group-item-action #{css_for_link}") do
= @text
- if @amount.positive?
%span.badge.align-text-top.ml-2{ class: css_for_badge_color }>= @amount
31 changes: 31 additions & 0 deletions src/api/app/components/workflow_run_filter_link_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class WorkflowRunFilterLinkComponent < ApplicationComponent
def initialize(text:, filter_item:, selected_filter:, token:, amount:)
super

@text = text
@filter_item = filter_item
@selected_filter = selected_filter
@amount = amount || 0
@token = token
end

def css_for_link
workflow_run_filter_matches? ? 'active' : ''
end

def css_for_badge_color
workflow_run_filter_matches? ? 'badge-light' : 'badge-primary'
end

private

def workflow_run_filter_matches?
if @selected_filter[:status].present?
@filter_item[:status] == @selected_filter[:status]
elsif @selected_filter[:event_type].present?
@filter_item[:event_type] == @selected_filter[:event_type]
elsif @selected_filter.empty?
@filter_item.empty?
end
end
end
26 changes: 8 additions & 18 deletions src/api/app/components/workflow_run_row_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,21 @@ class WorkflowRunRowComponent < ApplicationComponent
'Push Hook' => ['commits', 0, 'url']
}.freeze

attr_reader :workflow_run, :status
attr_reader :workflow_run, :status, :hook_event

def initialize(workflow_run:)
super

@workflow_run = workflow_run
@status = workflow_run.status
@hook_event = workflow_run.hook_event
end

def hook_action
return payload['action'] if pull_request_with_allowed_action
return payload.dig('object_attributes', 'action') if merge_request_with_allowed_action
end

def hook_event
parsed_request_headers['HTTP_X_GITHUB_EVENT'] ||
parsed_request_headers['HTTP_X_GITLAB_EVENT']
end

def repository_name
payload.dig('repository', 'full_name') || # For GitHub
payload.dig('repository', 'name') # For GitLab
Expand All @@ -43,16 +39,17 @@ def repository_url
end

def event_source_name
path = SOURCE_NAME_PAYLOAD_MAPPING[hook_event]
path = SOURCE_NAME_PAYLOAD_MAPPING[@hook_event]
payload.dig(*path) if path
end

def event_source_url
payload.dig(*SOURCE_URL_PAYLOAD_MAPPING[hook_event])
mapped_source_url = SOURCE_URL_PAYLOAD_MAPPING[@hook_event]
payload.dig(*mapped_source_url) if mapped_source_url
end

def formatted_event_source_name
case hook_event
case @hook_event
when 'pull_request', 'Merge Request Hook'
"##{event_source_name}"
else
Expand Down Expand Up @@ -85,26 +82,19 @@ def status_icon

private

def parsed_request_headers
workflow_run.request_headers.split("\n").each_with_object({}) do |h, headers|
k, v = h.split(':')
headers[k] = v.strip
end
end

def payload
@payload ||= JSON.parse(workflow_run.request_payload)
rescue JSON::ParserError
{ payload: 'unparseable' }
end

def pull_request_with_allowed_action
hook_event == 'pull_request' &&
@hook_event == 'pull_request' &&
ScmWebhookEventValidator::ALLOWED_PULL_REQUEST_ACTIONS.include?(payload['action'])
end

def merge_request_with_allowed_action
hook_event == 'Merge Request Hook' &&
@hook_event == 'Merge Request Hook' &&
ScmWebhookEventValidator::ALLOWED_MERGE_REQUEST_ACTIONS.include?(payload.dig('object_attributes', 'action'))
end
end
19 changes: 18 additions & 1 deletion src/api/app/controllers/webui/workflow_runs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
class Webui::WorkflowRunsController < Webui::WebuiController
def index
@workflow_runs = WorkflowRunPolicy::Scope.new(User.session, WorkflowRun, { token_id: params[:token_id] }).resolve.page(params[:page])
relation = WorkflowRunPolicy::Scope.new(User.session, WorkflowRun, { token_id: params[:token_id] })
workflow_runs_finder = WorkflowRunsFinder.new(relation.resolve)

@workflow_runs = if params[:status]
workflow_runs_finder.with_status(params[:status])
elsif params[:event_type]
workflow_runs_finder.with_event_type(params[:event_type])
else
workflow_runs_finder.all
end

@workflow_runs = @workflow_runs.page(params[:page])

@selected_filter = selected_filter
@token = Token::Workflow.find(params[:token_id])
end

Expand All @@ -10,4 +23,8 @@ def show

@token = @workflow_run.token
end

def selected_filter
{ event_type: params[:event_type], status: params[:status] }.compact
end
end
14 changes: 14 additions & 0 deletions src/api/app/models/workflow_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ class WorkflowRun < ApplicationRecord
def update_to_fail(message)
update(response_body: message, status: 'fail')
end

def hook_event
parsed_request_headers['HTTP_X_GITHUB_EVENT'] ||
parsed_request_headers['HTTP_X_GITLAB_EVENT']
end

private

def parsed_request_headers
request_headers.split("\n").each_with_object({}) do |h, headers|
k, v = h.split(':')
headers[k] = v.strip
end
end
end

# == Schema Information
Expand Down
37 changes: 37 additions & 0 deletions src/api/app/queries/workflow_runs_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class WorkflowRunsFinder
def initialize(relation = WorkflowRun.all)
@relation = relation.order(created_at: :desc)
end

def all
@relation.all
end

def group_by_event_type
@relation.all.each_with_object(Hash.new(0)) do |workflow_run, grouped_workflows|
grouped_workflows[workflow_run.hook_event] += 1
end
end

def with_event_type(event_type)
allowed_events = ScmWebhookEventValidator::ALLOWED_GITHUB_EVENTS + ScmWebhookEventValidator::ALLOWED_GITLAB_EVENTS
filtered_event_type = '%' + ([event_type] & allowed_events).first + '%'
@relation.where('request_headers LIKE ?', filtered_event_type)
end

def with_status(status)
@relation.where(status: status)
end

def succeeded
with_status('success')
end

def running
with_status('running')
end

def failed
with_status('fail')
end
end
40 changes: 25 additions & 15 deletions src/api/app/views/webui/workflow_runs/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
- @pagetitle = 'Workflow Runs'

.card
.card-body
%h3= @pagetitle
- if @workflow_runs.blank?
%p There are no workflow runs for this token yet
- else
.text-center
%span.ml-3= page_entries_info(@workflow_runs)
.accordion.pt-3#workflow-runs-accordion
- @workflow_runs.each do |workflow_run|
.card-header{ id: "workflow-run-heading#{workflow_run.id}" }
.mb-0
.row
= render(WorkflowRunRowComponent.new(workflow_run: workflow_run))
= paginate @workflow_runs, views_prefix: 'webui'
.row
.col-md-4.col-lg-3.px-0.px-md-3.sticky-top#workflow-runs-filter-desktop
.card.mb-3
%strong.d-block.d-md-none.p-3{ data: { toggle: 'collapse', target: '#filters' },
aria: { expanded: true, controls: 'filters' } }
Filtered by: #{params[:status]&.humanize}
%i.float-right.mt-1.fa.fa-chevron-down#workflow-runs-dropdown-trigger
.card-body.collapse#filters
= render WorkflowRunFilterComponent.new(token: @token, selected_filter: @selected_filter)
.col-md-8.col-lg-9.px-0.px-md-3#workflow-run-list
.card
- if @workflow_runs.blank?
.card-body
%p There are no workflow runs for this token yet
- else
.card-body
.text-center
%span.ml-3= page_entries_info(@workflow_runs)
.accordion.pt-3#workflow-runs-accordion
- @workflow_runs.each do |workflow_run|
.card-header{ id: "workflow-run-heading#{workflow_run.id}" }
.mb-0
.row
= render(WorkflowRunRowComponent.new(workflow_run: workflow_run))
= paginate @workflow_runs, views_prefix: 'webui'
47 changes: 47 additions & 0 deletions src/api/spec/components/workflow_run_filter_component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'rails_helper'

RSpec.describe WorkflowRunFilterComponent, type: :component do
let(:token) { create(:workflow_token) }
let(:stub_finder) do
instance_double('WorkflowRunsFinder',
succeeded: [:workflow_run],
running: [:workflow_run, :workflow_run],
failed: [:workflow_run, :workflow_run, :workflow_run],
group_by_event_type: { pull_request: [:workflow_run] })
end
let(:selected_filter) { {} }

before do
render_inline(described_class.new(token: token, selected_filter: selected_filter, finder: stub_finder))
end

it 'renders a link to receive all workflow runs' do
expect(rendered_component).to have_css('a.active', text: 'All')
end

context 'status filter links' do
it 'renders the succeeded filter' do
expect(rendered_component).to have_css('a', text: 'Succeeded')
end

it 'renders the failed filter' do
expect(rendered_component).to have_css('a', text: 'Failed')
end

it 'renders the running filter' do
expect(rendered_component).to have_css('a', text: 'Running')
end
end

context 'event type filter links' do
it 'renders the push event filters' do
expect(rendered_component).to have_css('a', text: 'Push')
expect(rendered_component).to have_css('a', text: 'Push Hook')
end

it 'renders the pull request event filters' do
expect(rendered_component).to have_css('a', text: 'Pull Requests')
expect(rendered_component).to have_css('a', text: 'Merge Request Hook')
end
end
end

0 comments on commit ea402e1

Please sign in to comment.