Skip to content

Commit

Permalink
Nextflow Samplesheet: File Selector (#879)
Browse files Browse the repository at this point in the history
* start file selector

* tie in params for selector dialog and rendering file cell

* streamline param passing between actions, add paired attachment auto selection

* add ability to handle no attachment selection

* fix regex filtering, more cleanup

* move filtering to sample rather than samplesheet

* rework logic to use pipeline for pattern

* streamline params, fix ordering

* start migrating logic to accept non-fastq files as well

* make other file types useable with file selector

* start adding translations, add empty state for file selector

* add frontend validation for samplesheet file cells

* make full div clickable on samplesheet, add hover state

* fix tailwind classes for error submission state

* fix some older arguments

* cleanup

* further cleanup, fixing styling

* add ui tests

* some cleanup

* cleanup

* fix tests, fix required headers, move file selector under WE module

* generalize nextflow component error

* cleanup

* run normalize

* add controller tests

* fix translation in test

* fix translations, move file filtering methods to concern, add unrelated asserts for flakes

* change required_properties default value

* run normalize

* fix test

* cleanup

* rework some of the required_properties logic

* add wrapper for file selector dialog

* further fix file selector dialog

* make submit btn of file selector dialog part of dialog, add stimulus logic, make radio buttons sticky for horizontal scrolling

* fix dialog id ambiguity, fix tests with now moved submit button out of original selector

* add type and format to file selector, add non-pe selection autopopulate fastq_2 to no file

* revert form button changes
  • Loading branch information
ChrisHuynh333 authored Jan 13, 2025
1 parent a6a42bf commit ef00f5a
Show file tree
Hide file tree
Showing 25 changed files with 1,096 additions and 176 deletions.
16 changes: 11 additions & 5 deletions app/components/nextflow/samplesheet/column_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<div
class="table-column flex-grow text-sm text-left whitespace-nowrap bg-slate-50 text-slate-500 dark:text-white"
class="
table-column flex-grow text-sm text-left whitespace-nowrap bg-slate-50
text-slate-500 dark:text-white
"
>
<% if property["cell_type"] == "metadata_cell" %>
<div
class="sticky top-0 z-10 border table-header dark:border-slate-600 border-slate-100 dark:bg-slate-700 bg-slate-50"
class="
sticky top-0 z-10 border table-header dark:border-slate-600 border-slate-100
dark:bg-slate-700 bg-slate-50
"
>
<%= form_with url: fields_workflow_executions_metadata_path do |f| %>
<%= f.hidden_field :format, value: "turbo_stream", id: "field_#{header}_turbo" %>
Expand All @@ -21,7 +27,7 @@
<% end %>
<%= f.select :field,
options_for_select(metadata_fields_for_field(header), header),
{ include_blank: !@required },
{ include_blank: !@required_properties.include?(header) },
{
id: "field-#{header}",
"aria-label": header,
Expand All @@ -40,7 +46,7 @@
>
<div class="flex items-center space-x-2 uppercase">
<%= header %>
<% if @required %>
<% if @required_properties.include?(header) %>
<span class="ml-1 text-red-600 dark:text-red-300">(<%= t(".required") %>)</span>
<% end %>
</div>
Expand All @@ -61,7 +67,7 @@
<%= s.hidden_field :sample_id, value: sample.id %>
<% end %>
<%= s.fields_for "samplesheet_params" do |fields| %>
<%= render_cell_type(header, property, sample, fields, index) %>
<%= render_cell_type(header, property, sample, fields, index, workflow_params) %>
<% end %>
<% end %>
</div>
Expand Down
94 changes: 30 additions & 64 deletions app/components/nextflow/samplesheet/column_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,32 @@ module Nextflow
module Samplesheet
# Renders a column in the sample sheet table
class ColumnComponent < Component
attr_reader :namespace_id, :header, :property, :samples
attr_reader :namespace_id, :header, :property, :samples, :metadata_fields, :required_properties, :workflow_params

# rubocop:disable Metrics/ParameterLists
def initialize(namespace_id:, header:, property:, samples:, metadata_fields:, required:)
def initialize(namespace_id:, header:, property:, samples:, metadata_fields:, required_properties:,
workflow_params:)
@namespace_id = namespace_id
@header = header
@property = property
@samples = samples
@metadata_fields = metadata_fields
@required = required
@required_properties = required_properties
@workflow_params = workflow_params
end

# rubocop:enable Metrics/ParameterLists

def render_cell_type(property, entry, sample, fields, index) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
def render_cell_type(property, entry, sample, fields, index, workflow_params) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/ParameterLists
case entry['cell_type']
when 'sample_cell'
render_sample_cell(sample, fields)
when 'sample_name_cell'
render_sample_name_cell(sample, fields)
when 'fastq_cell'
render_fastq_cell(sample, property, entry, fields, index)
render_fastq_cell(sample, property, index, workflow_params)
when 'file_cell'
render_other_file_cell(sample, property, entry, fields)
render_other_file_cell(sample, property, index, entry)
when 'metadata_cell'
render_metadata_cell(sample, property, fields)
when 'dropdown_cell'
Expand All @@ -37,56 +39,20 @@ def render_cell_type(property, entry, sample, fields, index) # rubocop:disable M
end
end

def render_fastq_cell(sample, property, entry, fields, index)
direction = get_fastq_direction(property)
files = get_fastq_files(entry, sample, direction, pe_only: property['pe_only'].present?)
data = get_fastq_data(files, direction, index, property)
render_file_cell(property, entry, fields, files, @required, data, files&.first)
def render_fastq_cell(sample, property, index, workflow_params)
selected_file = sample.most_recent_file('fastq', property:, workflow_params:)
render_file_cell(sample, property, index, selected_file, 'fastq', workflow_params:)
end

private

def get_fastq_direction(property)
property.match(/fastq_(\d+)/)[1].to_i == 1 ? :pe_forward : :pe_reverse
end

def get_fastq_files(entry, sample, direction, pe_only: false)
singles = filter_files_by_pattern(sample.sorted_files[:singles] || [],
entry['pattern'] || "/^\S+.f(ast)?q(.gz)?$/")

files = []
if sample.sorted_files[direction].present?
files = sample.sorted_files[direction] || []
files.concat(singles) unless pe_only
else
files = singles
end
files
end

def get_fastq_data(files, direction, index, property)
return {} if files.empty? && property == 'fastq_2'

{
'data-action' => 'change->nextflow--samplesheet#file_selected',
'data-nextflow--samplesheet-target' => "select#{direction.to_s.sub!('pe_', '').capitalize}",
'data-direction' => direction.to_s,
'data-index' => index
}
end

def render_other_file_cell(sample, property, entry, fields)
files = if entry['pattern']
filter_files_by_pattern(sample.sorted_files[:singles] || [], entry['pattern'])
else
sample.sorted_files[:singles] || []
end
render_file_cell(property, entry, fields,
files, @required, {}, nil)
def render_other_file_cell(sample, property, index, entry)
selected_file = sample.most_recent_file('other', autopopulate: entry['autopopulate'], pattern: entry['pattern'])
render_file_cell(sample, property, index, selected_file, 'other', pattern: entry['pattern'])
end

def filter_files_by_pattern(files, pattern)
files.select { |file| file.first[Regexp.new(pattern)] }
files.select { |file| file[:filename] =~ Regexp.new(pattern) }
end

def render_sample_cell(sample, fields)
Expand All @@ -98,24 +64,20 @@ def render_sample_name_cell(sample, fields)
end

def render_metadata_cell(sample, name, fields)
render(Samplesheet::MetadataCellComponent.new(sample:, name:, form: fields, required: @required))
render(Samplesheet::MetadataCellComponent.new(sample:, name:, form: fields, required: required?))
end

# rubocop:disable Metrics/ParameterLists
def render_file_cell(property, entry, fields, files, is_required, data, selected)
selected_item = if selected.present?
selected
else
entry['autopopulate'] && files.present? ? files[0] : nil
end

render(Samplesheet::DropdownCellComponent.new(
def render_file_cell(sample, property, index, selected, file_type, **file_selector_arguments)
render(Samplesheet::FileCellComponent.new(
sample,
property,
files,
selected_item,
fields,
is_required,
data
selected,
index,
@required_properties,
file_type,
**file_selector_arguments
))
end

Expand All @@ -127,15 +89,15 @@ def render_dropdown_cell(property, entry, fields)
entry['enum'],
nil,
fields,
@required
required?
))
end

def render_input_cell(property, fields)
render(Samplesheet::TextCellComponent.new(
property,
fields:,
required: @required
required: required?
))
end

Expand All @@ -144,6 +106,10 @@ def metadata_fields_for_field(field)
label = t('.default', label: field)
options.map { |f| [f.eql?(field) ? label : f, f] }
end

def required?
@required_properties.include?(@header)
end
end
end
end
33 changes: 33 additions & 0 deletions app/components/nextflow/samplesheet/file_cell_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<%= link_to new_workflow_executions_file_selector_path(
"file_selector[attachable_id]": @attachable.id,
"file_selector[attachable_type]": @attachable.class.to_s,
"file_selector[index]": @index,
"file_selector[selected_id]": @selected.empty? ? nil : @selected[:id],
"file_selector[property]": @property,
"file_selector[required_properties]": @required_properties,
"file_selector[file_type]": @file_type,
"file_selector[file_selector_arguments]": @file_selector_arguments
),
data: {
turbo_stream: "true",
} do %>
<div
id="<%="#{@attachable.id}_#{@property}" %>"
class="
w-full focus:ring-primary-500 focus:border-primary-500 p-2 dark:bg-inherit
bg-inherit dark:placeholder-slate-400 text-inherit
text-sm dark:focus:ring-primary-500 dark:focus:border-primary-500 cursor-pointer
hover:border-slate-300 border-transparent border-2
"
data-file-cell-required="<%= @required_properties.present? && @required_properties.include?(@property) ? "true" : "false" %>"
>
<% unless @selected.empty? %>
<input
type="hidden"
name="workflow_execution[samples_workflow_executions_attributes][<%= @index%>][samplesheet_params][<%= @property %>]"
value= <%= @selected[:global_id]%>
>
<% end %>
<%= @selected.empty? ? t(".no_selected_file") : @selected[:filename] %>
</div>
<% end %>
22 changes: 22 additions & 0 deletions app/components/nextflow/samplesheet/file_cell_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Nextflow
module Samplesheet
# Render a single cell of a Nextflow samplesheet for a property that requires a file
class FileCellComponent < Component
attr_reader :attachable, :property, :selected, :index, :required_properties, :file_type, :file_selector_arguments

# rubocop: disable Metrics/ParameterLists
def initialize(attachable, property, selected, index, required_properties, file_type, **file_selector_arguments)
@attachable = attachable
@property = property
@selected = selected
@index = index
@required_properties = required_properties
@file_type = file_type
@file_selector_arguments = file_selector_arguments
end
# rubocop: enable Metrics/ParameterLists
end
end
end
41 changes: 21 additions & 20 deletions app/components/nextflow/samplesheet_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
<div class="sample-sheet">

<label class="block mb-2 text-sm font-medium text-slate-900 dark:text-white"><%= t(".label") %></label>

<div
role="alert"
class="
hidden flex items-center p-4 border-l-4 text-red-800 border-red-300 bg-red-50
dark:text-red-400 dark:bg-slate-800 dark:border-red-800 mb-2
"
data-nextflow--samplesheet-target="error"
>
<span class="flex-shrink-0 inline">
<%= viral_icon(name: "exclamation_circle", classes: "w-5 h-5 mr-3 stroke-2") %>
</span>
<div>
<div class="font-medium" data-nextflow--samplesheet-target="errorMessage"></div>
</div>
</div>
<div
data-nextflow--samplesheet-target="table"
class="
samplesheet-table
flex
border
dark:border-slate-600
border-slate-100
max-h-72
overflow-y-auto
relative
samplesheet-table flex border dark:border-slate-600 border-slate-100 max-h-72
overflow-y-auto relative
"
>
<% properties.each do |header, property| %>
Expand All @@ -22,23 +30,16 @@
property:,
samples: @samples,
metadata_fields:,
required: required_properties.include?(header)
required_properties:,
workflow_params:,
) %>
<% end %>
<template data-nextflow--samplesheet-target="loading">
<div
role="status"
class="
absolute
h-full
w-full
text-black
dark:text-white
backdrop-blur-sm
flex
items-center
justify-center
z-20
absolute h-full w-full text-black dark:text-white backdrop-blur-sm flex
items-center justify-center z-20
"
>
<span class="flex items-center p-4 space-x-4">
Expand Down
7 changes: 4 additions & 3 deletions app/components/nextflow/samplesheet_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
module Nextflow
# Render the contents of a Nextflow samplesheet to a table
class SamplesheetComponent < Component
attr_reader :properties, :samples, :required_properties, :metadata_fields, :namespace_id
attr_reader :properties, :samples, :required_properties, :metadata_fields, :namespace_id, :workflow_params

FILE_CELL_TYPES = %w[fastq_cell file_cell].freeze

def initialize(schema:, samples:, fields:, namespace_id:)
def initialize(schema:, samples:, fields:, namespace_id:, workflow_params:)
@samples = samples
@namespace_id = namespace_id
@metadata_fields = fields
@required_properties = schema['items']['required']
@required_properties = schema['items']['required'] || []
@workflow_params = workflow_params
extract_properties(schema)
end

Expand Down
Loading

0 comments on commit ef00f5a

Please sign in to comment.