]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3825 new page for measure filters
authorSimon Brandhof <simon.brandhof@gmail.com>
Wed, 21 Nov 2012 12:37:45 +0000 (13:37 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Wed, 21 Nov 2012 12:49:22 +0000 (13:49 +0100)
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java
sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterDecoderTest.java
sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb [new file with mode: 0644]

index e3e8e475c6b1db1b615a6b7e55482e15135aec2b..c58cd5f6dc582ccc3e52b3f7c75d4d0b9661de48 100644 (file)
@@ -403,7 +403,7 @@ filters.display_form.as=Display as
 filters.display_form.table=Table
 filters.display_form.treemap=Treemap
 filters.build_date=Build date
-filters.col.date=Build date
+filters.col.date=Date
 filters.col.language=Language
 filters.col.name=Name
 filters.col.links=Links
index 9b9c124aa13dfd6a473bab116477f9b90f0175bc..42a4f3e0f0aecd053e2f8ce292cceee6990e7658 100644 (file)
 package org.sonar.core.measure;
 
 import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
 import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.Nullable;
-import java.util.Collections;
+
+import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 public class MeasureFilter {
+  private static final String[] EMPTY = {};
+
   // conditions on resources
   private String baseResourceKey;
   private boolean onBaseResourceChildren = false; // only if getBaseResourceKey is set
-  private List<String> resourceScopes = Lists.newArrayList();
-  private List<String> resourceQualifiers = Lists.newArrayList();
-  private List<String> resourceLanguages = Lists.newArrayList();
+  private String[] resourceScopes = EMPTY;
+  private String[] resourceQualifiers = EMPTY;
+  private String[] resourceLanguages = EMPTY;
   private String resourceKeyRegexp;
   private String resourceName;
   private Date fromDate = null, toDate = null;
@@ -63,28 +69,33 @@ public class MeasureFilter {
     return onBaseResourceChildren;
   }
 
-  public MeasureFilter setResourceScopes(@Nullable List<String> l) {
-    this.resourceScopes = (l != null ? l : Collections.<String>emptyList());
+  public MeasureFilter setResourceScopes(String... l) {
+    this.resourceScopes = (l != null ? l : EMPTY);
     return this;
   }
 
-  public MeasureFilter setResourceQualifiers(List<String> l) {
-    this.resourceQualifiers = (l != null ? l : Collections.<String>emptyList());
+  public MeasureFilter setResourceQualifiers(String... l) {
+    this.resourceQualifiers = l;
     return this;
   }
 
-  public MeasureFilter setResourceQualifiers(String... l) {
-    this.resourceQualifiers = Lists.newArrayList(l);
+  public MeasureFilter setResourceLanguages(String... l) {
+    this.resourceLanguages = (l != null ? l : EMPTY);
     return this;
   }
 
-  public MeasureFilter setResourceLanguages(List<String> l) {
-    this.resourceLanguages = (l != null ? l : Collections.<String>emptyList());
+  public MeasureFilter setResourceScopes(@Nullable List<String> l) {
+    this.resourceScopes = (l != null ? l.toArray(new String[l.size()]) : EMPTY);
     return this;
   }
 
-  public MeasureFilter setResourceLanguages(String... l) {
-    this.resourceLanguages = Lists.newArrayList(l);
+  public MeasureFilter setResourceQualifiers(@Nullable List<String> l) {
+    this.resourceQualifiers = (l != null ? l.toArray(new String[l.size()]) : EMPTY);
+    return this;
+  }
+
+  public MeasureFilter setResourceLanguages(@Nullable List<String> l) {
+    this.resourceLanguages = (l != null ? l.toArray(new String[l.size()]) : EMPTY);
     return this;
   }
 
@@ -159,15 +170,15 @@ public class MeasureFilter {
     return toDate;
   }
 
-  public List<String> getResourceScopes() {
+  public String[] getResourceScopes() {
     return resourceScopes;
   }
 
-  public List<String> getResourceQualifiers() {
+  public String[] getResourceQualifiers() {
     return resourceQualifiers;
   }
 
-  public List<String> getResourceLanguages() {
+  public String[] getResourceLanguages() {
     return resourceLanguages;
   }
 
@@ -179,4 +190,56 @@ public class MeasureFilter {
     return sort;
   }
 
+  public static MeasureFilter create(Map<String, String> properties) {
+    MeasureFilter filter = new MeasureFilter();
+    filter.setBaseResourceKey(properties.get("base"));
+    filter.setResourceScopes(toArray(properties.get("scopes")));
+    filter.setResourceQualifiers(toArray(properties.get("qualifiers")));
+    filter.setResourceLanguages(toArray(properties.get("languages")));
+    if (properties.containsKey("onBaseChildren")) {
+      filter.setOnBaseResourceChildren(Boolean.valueOf(properties.get("onBaseChildren")));
+    }
+    filter.setResourceName(properties.get("nameRegexp"));
+    filter.setResourceKeyRegexp(properties.get("keyRegexp"));
+    if (properties.containsKey("fromDate")) {
+      filter.setFromDate(toDate(properties.get("fromDate")));
+    } else if (properties.containsKey("afterDays")) {
+      filter.setFromDate(toDays(properties.get("afterDays")));
+    }
+    if (properties.containsKey("toDate")) {
+      filter.setToDate(toDate(properties.get("toDate")));
+    } else if (properties.containsKey("beforeDays")) {
+      filter.setToDate(toDays(properties.get("beforeDays")));
+    }
+
+    if (properties.containsKey("favourites")) {
+      filter.setUserFavourites(Boolean.valueOf(properties.get("favourites")));
+    }
+    return filter;
+  }
+
+  private static String[] toArray(@Nullable String s) {
+    if (s == null) {
+      return EMPTY;
+    }
+    return StringUtils.split(s, ",");
+  }
+
+  private static Date toDate(@Nullable String date) {
+    if (date != null) {
+      return DateUtils.parseDate(date);
+    }
+    return null;
+  }
+
+  private static Date toDays(@Nullable String s) {
+    if (s != null) {
+      int days = Integer.valueOf(s);
+      Date date = org.apache.commons.lang.time.DateUtils.truncate(new Date(), Calendar.DATE);
+      date = org.apache.commons.lang.time.DateUtils.addDays(date, -days);
+      return date;
+    }
+    return null;
+  }
+
 }
index 253c2875d6f7dd214cee1dbb46b368cf999a239c..fcebe106ea9b0e4a076ab3ab4e0a712db7e161fa 100644 (file)
@@ -27,7 +27,9 @@ import org.slf4j.LoggerFactory;
 import org.sonar.api.ServerComponent;
 
 import javax.annotation.Nullable;
+
 import java.util.List;
+import java.util.Map;
 
 public class MeasureFilterEngine implements ServerComponent {
   private static final Logger FILTER_LOG = LoggerFactory.getLogger("org.sonar.MEASURE_FILTER");
@@ -44,6 +46,22 @@ public class MeasureFilterEngine implements ServerComponent {
     return execute(filterJson, userId, FILTER_LOG);
   }
 
+  public List<MeasureFilterRow> execute2(Map<String, String> filterMap, @Nullable Long userId) throws ParseException {
+    Logger logger = FILTER_LOG;
+    MeasureFilterContext context = new MeasureFilterContext();
+    context.setJson(filterMap.toString());
+    context.setUserId(userId);
+    try {
+      long start = System.currentTimeMillis();
+      MeasureFilter filter = MeasureFilter.create(filterMap);
+      List<MeasureFilterRow> rows = executor.execute(filter, context);
+      log(context, rows, (System.currentTimeMillis() - start), logger);
+      return rows;
+    } catch (Exception e) {
+      throw new IllegalStateException("Fail to execute filter: " + context, e);
+    }
+  }
+
   @VisibleForTesting
   List<MeasureFilterRow> execute(String filterJson, @Nullable Long userId, Logger logger) {
     MeasureFilterContext context = new MeasureFilterContext();
index c95717e2c2a37f40a634bd219f8d5782b5ce9f25..bb41a9c77ed324c5439f5a0085ad0d55704f3437 100644 (file)
@@ -37,7 +37,6 @@ import java.sql.Date;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.Collection;
 import java.util.List;
 
 class MeasureFilterSql {
@@ -150,15 +149,15 @@ class MeasureFilterSql {
     if (context.getBaseSnapshot() == null) {
       sql.append(" AND p.copy_resource_id IS NULL ");
     }
-    if (!filter.getResourceQualifiers().isEmpty()) {
+    if (filter.getResourceQualifiers().length > 0) {
       sql.append(" AND s.qualifier IN ");
       appendInStatement(filter.getResourceQualifiers(), sql);
     }
-    if (!filter.getResourceScopes().isEmpty()) {
+    if (filter.getResourceScopes().length > 0) {
       sql.append(" AND s.scope IN ");
       appendInStatement(filter.getResourceScopes(), sql);
     }
-    if (!filter.getResourceLanguages().isEmpty()) {
+    if (filter.getResourceLanguages().length > 0) {
       sql.append(" AND p.language IN ");
       appendInStatement(filter.getResourceLanguages(), sql);
     }
@@ -217,7 +216,7 @@ class MeasureFilterSql {
       sql.append(" AND s.project_id IN (SELECT rindex.resource_id FROM resource_index rindex WHERE rindex.kee like '");
       sql.append(StringEscapeUtils.escapeSql(StringUtils.lowerCase(filter.getResourceName())));
       sql.append("%'");
-      if (!filter.getResourceQualifiers().isEmpty()) {
+      if (filter.getResourceQualifiers().length > 0) {
         sql.append(" AND rindex.qualifier IN ");
         appendInStatement(filter.getResourceQualifiers(), sql);
       }
@@ -259,7 +258,7 @@ class MeasureFilterSql {
   }
 
 
-  private static void appendInStatement(Collection<String> values, StringBuilder to) {
+  private static void appendInStatement(String[] values, StringBuilder to) {
     to.append(" ('");
     to.append(StringUtils.join(values, "','"));
     to.append("') ");
index 3dedb2d09a114a60f7813cf3e9e945a4d5f2ec24..ce7ed550464eaec583257c23638014837e3a3d73 100644 (file)
@@ -62,9 +62,9 @@ public class MeasureFilterDecoderTest {
 
     assertThat(filter.getBaseResourceKey()).isEqualTo("org.struts");
     assertThat(filter.isOnBaseResourceChildren()).isTrue();
-    assertThat(filter.getResourceScopes()).containsExactly("PRJ");
-    assertThat(filter.getResourceQualifiers()).containsExactly("TRK", "CLA");
-    assertThat(filter.getResourceLanguages()).containsExactly("java", "php");
+    assertThat(filter.getResourceScopes()).containsOnly("PRJ");
+    assertThat(filter.getResourceQualifiers()).containsOnly("TRK", "CLA");
+    assertThat(filter.getResourceLanguages()).containsOnly("java", "php");
     assertThat(filter.getResourceName()).isEqualTo("Struts");
     assertThat(filter.getResourceKeyRegexp()).isEqualTo("*foo*");
     assertThat(filter.getFromDate().getYear()).isEqualTo(2012 - 1900);
index 8fb4fc40ae0261aa3e341472f724340930a72109..cbedd251521afc4a68910036548a3061b376da6c 100644 (file)
@@ -102,6 +102,10 @@ public final class JRubyFacade {
     return get(MeasureFilterEngine.class).execute(json, userId);
   }
 
+  public List<MeasureFilterRow> executeMeasureFilter2(Map<String,String> map, @Nullable Long userId) throws ParseException {
+    return get(MeasureFilterEngine.class).execute2(map, userId);
+  }
+
   public Collection<ResourceType> getResourceTypesForFilter() {
     return get(ResourceTypes.class).getAll(ResourceTypes.AVAILABLE_FOR_FILTERS);
   }
@@ -314,7 +318,7 @@ public final class JRubyFacade {
 
   public void ruleSeverityChanged(int parentProfileId, int activeRuleId, int oldSeverityId, int newSeverityId, String userName) {
     getProfilesManager().ruleSeverityChanged(parentProfileId, activeRuleId, RulePriority.values()[oldSeverityId],
-        RulePriority.values()[newSeverityId], userName);
+      RulePriority.values()[newSeverityId], userName);
   }
 
   public void ruleDeactivated(int parentProfileId, int deactivatedRuleId, String userName) {
@@ -510,10 +514,10 @@ public final class JRubyFacade {
     // notifier is null when creating the administrator in the migration script 011.
     if (notifier != null) {
       notifier.onNewUser(NewUserHandler.Context.builder()
-          .setLogin(fields.get("login"))
-          .setName(fields.get("name"))
-          .setEmail(fields.get("email"))
-          .build());
+        .setLogin(fields.get("login"))
+        .setName(fields.get("name"))
+        .setEmail(fields.get("email"))
+        .build());
     }
   }
 
index 0b6662b207f5e076b9ef5635a7624c28945a9c77..25a0248df1ca39ff4f51d3c74a8351ef2c26ebb6 100644 (file)
 #
 class MeasuresController < ApplicationController
 
+  SECTION=Navigation::SECTION_HOME
+
   def index
-    
+    render :action => 'search'
   end
 
+  def search
+    options = {
+      :user => current_user,
+      :page => (params[:page] || 1)
+    }
+    @filter = MeasureFilter.new(params)
+    @filter.execute(self, options)
+  end
 
 end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb
new file mode 100644 (file)
index 0000000..225f233
--- /dev/null
@@ -0,0 +1,207 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2012 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# Sonar is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# Sonar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with {library}; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+class MeasureFilter
+  # Row in the table of results
+  class Data
+    attr_reader :snapshot, :measures_by_metric, :links
+
+    def initialize(snapshot)
+      @snapshot = snapshot
+      @measures_by_metric = {}
+      @links = nil
+    end
+
+    def add_measure(measure)
+      @measures_by_metric[measure.metric] = measure
+    end
+
+    def add_link(link)
+      @links ||= []
+      @links << link
+    end
+
+    def measure(metric)
+      @measures_by_metric[metric]
+    end
+  end
+
+  # Column to be displayed
+  class Column
+    attr_reader :key, :metric
+
+    def initialize(key)
+      @key = key
+      metric_key = @key.split(':')[1]
+      @metric = Metric.by_key(metric_key) if metric_key
+    end
+
+    def name?
+      @key == 'name'
+    end
+
+    def links?
+      @key == 'links'
+    end
+  end
+
+  class Display
+    def init_filter(filter)
+
+    end
+  end
+
+  class ListDisplay < Display
+    def key
+      'list'
+    end
+
+    def init_filter(filter)
+
+    end
+  end
+
+  class TreemapDisplay < Display
+    def key
+      'treemap'
+    end
+  end
+
+  DEFAULT_OPTIONS = {:page => 1, :page_size => 50}
+  DEFAULT_COLUMNS = [Column.new('name'), Column.new('date'), Column.new('metric:ncloc'), Column.new('metric:violations')]
+  DISPLAYS = [ListDisplay.new, TreemapDisplay.new]
+
+  # Simple hash {string key => fixnum or boolean or string}
+  attr_accessor :criteria
+
+  # Configuration available after call to execute()
+  attr_reader :pagination, :security_exclusions, :columns, :display
+
+  # Results : sorted array of Data
+  attr_reader :data
+
+  def initialize(criteria={})
+    @criteria = criteria
+  end
+
+  # ==== Options
+  # 'page' : page id starting with 1. Used on table display.
+  # 'page_size' : number of results per page.
+  # 'user' : the authenticated user
+  # 'period' : index of the period between 1 and 5
+  #
+  def execute(controller, options={})
+    return reset_results if @criteria.empty?
+    init_columns
+    init_display(options)
+
+    opts = DEFAULT_OPTIONS.merge(options)
+    user = opts[:user]
+
+    rows=Api::Utils.java_facade.executeMeasureFilter2(@criteria, (user ? user.id : nil))
+    snapshot_ids = filter_authorized_snapshot_ids(rows, controller)
+    snapshot_ids = paginate_snapshot_ids(snapshot_ids, opts)
+    init_data(snapshot_ids)
+
+    self
+  end
+
+  private
+
+  def init_columns
+    fields = @criteria['columns']
+    if fields.present?
+      @columns = fields.split(',').map { |field| Column.new(field) }
+    else
+      @columns = DEFAULT_COLUMNS.clone
+    end
+  end
+
+  def init_display(options)
+    key = @criteria['display']
+    if key.present?
+      @display = DISPLAYS.find { |display| display.key==key }
+    end
+    @display ||= DISPLAYS.first
+  end
+
+  def reset_results
+    @pagination = nil
+    @security_exclusions = nil
+    @data = nil
+    self
+  end
+
+  def filter_authorized_snapshot_ids(rows, controller)
+    project_ids = rows.map { |row| row.getResourceRootId() }.compact.uniq
+    authorized_project_ids = controller.select_authorized(:user, project_ids)
+    snapshot_ids = rows.map { |row| row.getSnapshotId() if authorized_project_ids.include?(row.getResourceRootId()) }.compact
+    @security_exclusions = (snapshot_ids.size<rows.size)
+    snapshot_ids
+  end
+
+  def paginate_snapshot_ids(snapshot_ids, options)
+    @pagination = Api::Pagination.new({
+                                        :per_page => options[:page_size],
+                                        :page => options[:page],
+                                        :count => snapshot_ids.size})
+    from = (@pagination.page - 1) * @pagination.per_page
+    to = (@pagination.page * @pagination.per_page) - 1
+    to = snapshot_ids.size - 1 if to >= snapshot_ids.size
+    snapshot_ids[from..to]
+  end
+
+  def init_data(snapshot_ids)
+    @data = []
+    if !snapshot_ids.empty?
+      data_by_snapshot_id = {}
+      snapshots = Snapshot.find(:all, :include => ['project'], :conditions => ['id in (?)', snapshot_ids])
+      snapshots.each do |snapshot|
+        data = Data.new(snapshot)
+        data_by_snapshot_id[snapshot.id] = data
+        @data << data
+      end
+
+      metric_ids = @columns.map { |column| column.metric }.compact.uniq.map { |metric| metric.id }
+      unless metric_ids.empty?
+        measures = ProjectMeasure.find(:all, :conditions =>
+          ['rule_priority is null and rule_id is null and characteristic_id is null and person_id is null and snapshot_id in (?) and metric_id in (?)', snapshot_ids, metric_ids]
+        )
+        measures.each do |measure|
+          data = data_by_snapshot_id[measure.snapshot_id]
+          data.add_measure measure
+        end
+      end
+
+      if @columns.index { |column| column.links? }
+        project_ids = []
+        data_by_project_id = {}
+        snapshots.each do |snapshot|
+          project_ids << snapshot.project_id
+          data_by_project_id[snapshot.project_id] = data_by_snapshot_id[snapshot.id]
+        end
+        links = ProjectLink.find(:all, :conditions => {:project_id => project_ids}, :order => 'link_type')
+        links.each do |link|
+          data_by_project_id[link.project_id].add_link(link)
+        end
+      end
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb
new file mode 100644 (file)
index 0000000..fd24b72
--- /dev/null
@@ -0,0 +1,41 @@
+<table class="data">
+  <thead>
+  <tr>
+    <% @filter.columns.each do |column| %>
+      <% if column.metric %>
+        <th class="right"><%= Api::Utils.message("metric.#{column.metric.key}.name", :default => column.metric.short_name) -%></th>
+      <% elsif column.key=='name' %>
+        <th class="left"><%= Api::Utils.message("filters.col.name") -%></th>
+      <% else %>
+        <th class="right"><%= Api::Utils.message("filters.col.#{column.key}", :default => column.key) -%></th>
+      <% end %>
+    <% end %>
+  </tr>
+  </thead>
+  <tbody>
+  <% @filter.data.each do |data| %>
+    <tr class="<%= cycle 'even', 'odd' -%>">
+      <% @filter.columns.each do |column| %>
+        <% if column.metric %>
+          <td class="right">
+            <%= format_measure(data.measure(column.metric)) -%>
+          </td>
+        <% elsif column.key=='name' %>
+          <td class="left">
+            <%= qualifier_icon(data.snapshot) %> <%= link_to_resource(data.snapshot.resource, h(data.snapshot.resource.name(true)), {:title => data.snapshot.resource.key}) -%>
+          </td>
+        <% elsif column.key=='date' %>
+          <td class="right">
+            <%= human_short_date(data.snapshot.created_at) -%>
+          </td>
+        <% elsif column.key=='key' %>
+          <td class="left"><span class="small"><%= data.snapshot.resource.kee -%></span></td>
+        <% else %>
+          <td></td>
+        <% end %>
+      <% end %>
+    </tr>
+  <% end %>
+  </tbody>
+  <%= render :partial => 'utils/tfoot_pagination', :locals => {:pagination => @filter.pagination, :colspan => @filter.columns.size} %>
+</table>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb
new file mode 100644 (file)
index 0000000..fd1fc74
--- /dev/null
@@ -0,0 +1,72 @@
+<% content_for :style do %>
+  <style type="text/css">
+    #filter-form {
+      float: left;
+      width: 240px;
+    }
+
+    #filter-result {
+      padding-left: 250px;
+    }
+  </style>
+<% end %>
+
+<% url_options = params.reject { |k, v| v.empty? || k=='search' }.merge({:controller => 'measures', :action => 'search'}) %>
+
+<div id="measure-filter">
+  <div id="filter-form">
+    <form method="POST" action="<%= ApplicationController.root_context -%>/measures/search">
+      <table>
+        <tbody>
+        <tr>
+          <td>Base:</td>
+          <td><input type="text" name="base"></td>
+        </tr>
+        <tr>
+          <td>Qualifiers:</td>
+          <td>
+            <select name="qualifiers">
+              <option value="">Any</option>
+              <option value="TRK">Project</option>
+              <option value="TRK,BRC">Project</option>
+              <option value="DIR,PAC">Directory/Package</option>
+              <option value="FIL,CLA">File</option>
+              <option value="UTS">Unit Test File</option>
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <td>Language:</td>
+          <td><input type="text" name="language"></td>
+        </tr>
+        <tr>
+          <td>Name:</td>
+          <td><input type="text" name="nameRegexp"></td>
+        </tr>
+        <tr>
+          <td>Key:</td>
+          <td><input type="text" name="keyRegexp"></td>
+        </tr>
+        <tr>
+          <td><input type="submit" name="search" value="Search"></td>
+          <td></td>
+        </tr>
+        </tbody>
+      </table>
+    </form>
+  </div>
+  <% if @filter %>
+    <div id="filter-result">
+      <% MeasureFilter::DISPLAYS.each do |display| %>
+        <%= link_to display.key, url_options.merge(:display => display.key) -%>
+      <% end %>
+
+      <%= render :partial => "measures/display_#{@filter.display.key}.html.erb" -%>
+      <p>
+        <% permalink = url_for(url_options) %>
+        <a href="<%= permalink -%>">Permalink</a>: <input type="text" value="<%= permalink -%>" size="100">
+      </p>
+    </div>
+  <% end %>
+
+</div>
\ No newline at end of file