Index: /trunk/src/VBox/ValidationKit/testmanager/core/base.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/base.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/base.py	(revision 65051)
@@ -1186,9 +1186,10 @@
     ## @}
 
-    def __init__(self, sName, sVarNm = None, sTypeNm = ksType_UInt, sState = ksState_NotSelected, sKind = ksKind_AnyOf):
+    def __init__(self, sName, sVarNm = None, sType = ksType_UInt, sState = ksState_NotSelected, sKind = ksKind_AnyOf):
+        assert len(sVarNm) in (2,3); # required by wuimain.py
         self.sName      = sName;
         self.sState     = sState;
         self.sVarNm     = sVarNm if sVarNm is not None else sName;
-        self.sTypeNm    = sTypeNm;
+        self.sType      = sType;
         self.sKind      = sKind;
         self.aoSelected = []; # Single value, any type.
@@ -1201,7 +1202,7 @@
 
     Filters are used to narrow down data that is displayed in a list or
-    report.  It differs a little from ModelDataBase in that it's not tied to a
-    database table, but one or more database queries that are typically very
-    complicated.
+    report.  This class differs a little from ModelDataBase in that it is not
+    tied to a database table, but one or more database queries that are
+    typically rather complicated.
 
     The filter object has two roles:
@@ -1234,12 +1235,13 @@
             if oCriterion.sType == FilterCriterion.ksType_UInt:
                 oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []);
+            elif oCriterion.sType == FilterCriterion.ksType_String:
+                oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
             else:
-                oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
-            if len(oCriterion.aoSelected):
-                oCriterion.sState = FilterCriterion.ksState_Selected;
+                assert False;
+            if len(oCriterion.aoSelected) > 0:
+                oCriterion.sState     = FilterCriterion.ksState_Selected;
             else:
-                oCriterion.sState = FilterCriterion.ksState_NotSelected;
+                oCriterion.sState     = FilterCriterion.ksState_NotSelected;
         return self;
-
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65051)
@@ -37,6 +37,7 @@
 from common                                 import constants;
 from testmanager                            import config;
-from testmanager.core.base                  import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, \
-                                                   TMTooManyRows, TMRowNotFound;
+from testmanager.core.base                  import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, ModelFilterBase, \
+                                                   FilterCriterion, FilterCriterionValueAndDescription, \
+                                                   TMExceptionBase, TMTooManyRows, TMRowNotFound;
 from testmanager.core.testgroup             import TestGroupData;
 from testmanager.core.build                 import BuildDataEx, BuildCategoryData;
@@ -638,4 +639,37 @@
     """Hanging offence committed by test case."""
     pass;
+
+
+class TestResultFilter(ModelFilterBase):
+    """
+    Test result filter.
+    """
+
+    kiTestStatus        = 0;
+    kiSchedGroups       = 1;
+    kiTestBoxes         = 2;
+
+    def __init__(self):
+        ModelFilterBase.__init__(self);
+        oCrit = FilterCriterion('Test status', sVarNm = 'ts', sType = FilterCriterion.ksType_String);
+        oCrit.aoPossible = (
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Success,  'Success'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Running,  'Running'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Skipped,  'Skipped'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Aborted,  'Aborted'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Failure,  'Failure'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_TimedOut, 'Timed out'),
+            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Rebooted, 'Rebooted'),
+        );
+        self.aCriteria.append(oCrit);
+        assert self.aCriteria[self.kiTestStatus] is oCrit;
+
+        oCrit = FilterCriterion('Sched groups', sVarNm = 'sg');
+        self.aCriteria.append(oCrit);
+        assert self.aCriteria[self.kiSchedGroups] is oCrit;
+
+        oCrit = FilterCriterion('Testboxes', sVarNm = 'tb');
+        self.aCriteria.append(oCrit);
+        assert self.aCriteria[self.kiTestBoxes] is oCrit;
 
 
@@ -849,5 +883,5 @@
         return sRet
 
-    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy, # pylint: disable=R0913
+    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, oFilter, enmResultSortBy, # pylint: disable=R0913
                                enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
         """
@@ -864,4 +898,6 @@
         Raises exception on error.
         """
+
+        _ = oFilter;
 
         #
@@ -939,4 +975,13 @@
             sQuery += '            AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
                       '            AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+        if oFilter.aCriteria[oFilter.kiTestStatus].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.enmStatus IN (' \
+                    + ', '.join('\'%s\'' % sValue for sValue in oFilter.aCriteria[oFilter.kiTestStatus].aoSelected) + ')\n';
+        if oFilter.aCriteria[oFilter.kiSchedGroups].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.idSchedGroup IN (' \
+                    + ', '.join(str(iVal) for iVal in oFilter.aCriteria[oFilter.kiSchedGroups].aoSelected) + ')\n';
+        if oFilter.aCriteria[oFilter.kiTestBoxes].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.idTestBox IN (' \
+                    + ', '.join(str(iVal) for iVal in oFilter.aCriteria[oFilter.kiTestBoxes].aoSelected) + ')\n';
         if fOnlyNeedingReason:
             sQuery += '            AND TestResultFailures.idTestSet IS NULL\n';
@@ -1070,5 +1115,6 @@
 
 
-    def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
+    def getEntriesCount(self, tsNow, sInterval, oFilter, enmResultsGroupingType, iResultsGroupingValue,
+                        fOnlyFailures, fOnlyNeedingReason):
         """
         Get number of table records.
@@ -1081,4 +1127,5 @@
         @param iResultsGroupingValue is ignored.
         """
+        _ = oFilter;
 
         #
@@ -1108,4 +1155,13 @@
             sQuery += '   AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
                       '   AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+        if oFilter.aCriteria[oFilter.kiTestStatus].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.enmStatus IN (' \
+                    + ', '.join('\'%s\'' % sValue for sValue in oFilter.aCriteria[oFilter.kiTestStatus].aoSelected) + ')\n';
+        if oFilter.aCriteria[oFilter.kiSchedGroups].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.idSchedGroup IN (' \
+                    + ', '.join(str(iVal) for iVal in oFilter.aCriteria[oFilter.kiSchedGroups].aoSelected) + ')\n';
+        if oFilter.aCriteria[oFilter.kiTestBoxes].sState == FilterCriterion.ksState_Selected:
+            sQuery += '            AND TestSets.idTestBox IN (' \
+                    + ', '.join(str(iVal) for iVal in oFilter.aCriteria[oFilter.kiTestBoxes].aoSelected) + ')\n';
         if fOnlyNeedingReason:
             sQuery += '   AND TestResultFailures.idTestSet IS NULL\n';
@@ -1298,4 +1354,46 @@
         except IndexError:
             return None
+
+
+    def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod):
+        """
+        Fetches the available filter criteria.
+        Returns oFilter.
+        """
+        assert isinstance(oFilter, TestResultFilter);
+
+        # Scheduling groups (see getSchedGroups).
+        oCrit = oFilter.aCriteria[TestResultFilter.kiSchedGroups];
+        self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName\n'
+                          'FROM   ( SELECT idSchedGroup,\n'
+                          '                MAX(TestSets.tsCreated) AS tsNow\n'
+                          '         FROM   TestSets\n'
+                          '         WHERE  ' + self._getTimePeriodQueryPart(tsNow, sPeriod, '         ') +
+                          '         GROUP BY idSchedGroup\n'
+                          '       ) AS SchedGroupIDs\n'
+                          '       INNER JOIN SchedGroups\n'
+                          '               ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
+                          '              AND SchedGroups.tsExpire     > SchedGroupIDs.tsNow\n'
+                          '              AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
+                          'ORDER BY SchedGroups.sName\n' );
+        for aoRow in self._oDb.fetchAll():
+            oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0], aoRow[1]));
+
+        # Scheduling groups (see getTestBoxes).
+        oCrit = oFilter.aCriteria[TestResultFilter.kiTestBoxes];
+        self._oDb.execute('SELECT TestBoxesWithStrings.idTestBox, TestBoxesWithStrings.sName\n'
+                          'FROM   ( SELECT idTestBox         AS idTestBox,\n'
+                          '                MAX(idGenTestBox) AS idGenTestBox\n'
+                          '         FROM   TestSets\n'
+                          '         WHERE  ' + self._getTimePeriodQueryPart(tsNow, sPeriod, '        ') +
+                          '         GROUP BY idTestBox\n'
+                          '       ) AS TestBoxIDs\n'
+                          '       LEFT OUTER JOIN TestBoxesWithStrings\n'
+                          '                    ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+                          'ORDER BY TestBoxesWithStrings.sName\n' );
+        for aoRow in self._oDb.fetchAll():
+            oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0], aoRow[1]));
+
+        return oFilter;
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65051)
@@ -303,4 +303,10 @@
 }
 
+#side-filters dd {
+    font-size:      0.86em;
+    max-height:     22em;
+    overflow:       auto;
+}
+
 
 #side-footer {
Index: /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 65051)
@@ -368,9 +368,13 @@
     var oParent = oAnchor.parentElement;
     var sClass  = oParent.className;
-    var oUncle  = oParent.nextSibling;
+
+    /* Find the DD sibling tag */
+    var oDdElement = oParent.nextSibling;
+    while (oDdElement != null && oDdElement.tagName != 'DD')
+        oDdElement = oDdElement.nextSibling;
+
+    /* Determin the new class and arrow char. */
     var sNewClass;
     var sNewChar;
-
-    /* Determin the new class and arrow char. */
     if (sClass.endsWith('collapsable'))
     {
@@ -393,7 +397,7 @@
     oAnchor.firstChild.textContent = sNewChar + oAnchor.firstChild.textContent.substr(1);
 
-    /* Update the uncle (DD) class.*/
-    if (oUncle)
-        oUncle.className = sNewClass;
+    /* Update the uncle (DD) class. */
+    if (oDdElement)
+        oDdElement.className = sNewClass;
     return true;
 }
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/template.html
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/template.html	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/template.html	(revision 65051)
@@ -2,4 +2,5 @@
 <html lang="en">
     <head>
+        <meta charset="UTF-8" />
         <meta http-equiv="content-type"             content="text/html; charset=UTF-8" />
         <meta http-equiv="content-language"         content="en" />
@@ -34,5 +35,5 @@
 
             <div id="side-menu">
-                <form id="side-menu-form">
+                <form id="side-menu-form"@@SIDE_MENU_FORM_ATTRS@@>
                     <ul>
                         @@SIDE_MENU_ITEMS@@
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py	(revision 65051)
@@ -386,23 +386,27 @@
                    % (sRowClass, cAttribsChanged + 1, cAttribsChanged + 1, sRowClass);
             for j, oChange in enumerate(oChangeEntry.aoChanges):
+                fLastRow = j + 1 == len(oChangeEntry.aoChanges);
                 sHtml += u'  <tr class="%s%s tmsyschlogattr%s">\n' \
-                       % ( sRowClass, 'odd' if j & 1 else 'even',
-                           ' tmsyschlogattrfinal' if j + 1 == len(oChangeEntry.aoChanges) else '',);
+                       % ( sRowClass, 'odd' if j & 1 else 'even', ' tmsyschlogattrfinal' if fLastRow else '',);
                 if j == 0:
                     sHtml += u'    <td class="%s tmsyschlogspacer" rowspan="%d"></td>\n' % (sRowClass, cAttribsChanged - 1,);
 
                 if isinstance(oChange, AttributeChangeEntryPre):
-                    sHtml += u'    <td>%s</td>\n' \
+                    sHtml += u'    <td class="%s%s">%s</td>\n' \
                              u'    <td><div class="tdpre"><pre>%s</pre></div></td>\n' \
-                             u'    <td><div class="tdpre"><pre>%s</pre></div></td>\n' \
-                           % ( webutils.escapeElem(oChange.sAttr),
+                             u'    <td class="%s%s"><div class="tdpre"><pre>%s</pre></div></td>\n' \
+                           % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+                               webutils.escapeElem(oChange.sAttr),
                                webutils.escapeElem(oChange.sOldText),
+                               ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
                                webutils.escapeElem(oChange.sNewText), );
                 else:
-                    sHtml += u'    <td>%s</td>\n' \
+                    sHtml += u'    <td class="%s%s">%s</td>\n' \
                              u'    <td>%s</td>\n' \
-                             u'    <td>%s</td>\n' \
-                           % ( webutils.escapeElem(oChange.sAttr),
+                             u'    <td class="%s%s">%s</td>\n' \
+                           % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+                               webutils.escapeElem(oChange.sAttr),
                                webutils.escapeElem(oChange.sOldText),
+                               ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
                                webutils.escapeElem(oChange.sNewText), );
                 sHtml += u'  </tr>\n';
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 65051)
@@ -118,4 +118,5 @@
         self._sPageFilter       = '';           # The filter controls (optional).
         self._sPageBody         = '$$TODO$$';   # The body text.
+        self._dSideMenuFormAttrs = {};          # key/value with attributes for the side menu <form> tag.
         self._sRedirectTo       = None;
         self._sDebug            = '';
@@ -266,7 +267,13 @@
             '@@SIDE_MENU_ITEMS@@':      sSideMenuItems,
             '@@SIDE_FILTER_CONTROL@@':  self._sPageFilter,
+            '@@SIDE_MENU_FORM_ATTRS@@': '',
             '@@PAGE_BODY@@':            self._sPageBody,
             '@@DEBUG@@':                '',
         };
+
+        # Side menu form attributes.
+        if len(self._dSideMenuFormAttrs) > 0:
+            dReplacements['@@SIDE_MENU_FORM_ATTRS@@'] = ' '.join(['%s="%s"' % (sKey, webutils.escapeAttr(sValue))
+                                                                  for sKey, sValue in self._dSideMenuFormAttrs.iteritems()]);
 
         # Special current user handling.
@@ -746,6 +753,11 @@
         for sKey, oValue in self._dParams.iteritems():
             if sKey not in self.kasDbgParams:
-                sHtml += '  <input type="hidden" name="%s" value="%s"/>\n' \
-                       % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oValue),);
+                if hasattr(oValue, 'startswith'):
+                    sHtml += '  <input type="hidden" name="%s" value="%s"/>\n' \
+                           % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oValue),);
+                else:
+                    for oSubValue in oValue:
+                        sHtml += '  <input type="hidden" name="%s" value="%s"/>\n' \
+                               % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oSubValue),);
 
         for aoCheckBox in (
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 65051)
@@ -84,5 +84,5 @@
                 if dParams[sKey] is None:
                     dParams[sKey] = '';
-            self.sUrl += '?' + webutils.encodeUrlParams(dParams)
+            self.sUrl += '?' + webutils.encodeUrlParams(dParams);
 
         if sFragmentId is not None:
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65050)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65051)
@@ -335,9 +335,8 @@
         """Show the default admin page."""
         from testmanager.webui.wuitestresult import WuiGroupedResultList;
-        from testmanager.core.testresults    import TestResultLogic;
+        from testmanager.core.testresults    import TestResultLogic, TestResultFilter;
         self._sAction = self.ksActionResultsUnGrouped
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
-                                                 TestResultLogic,
-                                                 WuiGroupedResultList)
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _isMenuMatch(self, sMenuUrl, sActionParam):
@@ -722,10 +721,12 @@
             enmResultsGroupingType,
             oResultsLogicType,
+            oResultFilterType,
             oResultsListContentType):
         """
         Override generic listing action.
 
-        oLogicType implements fetchForListing.
-        oListContentType is a child of WuiListContentBase.
+        oResultsLogicType implements getEntriesCount, fetchResultsForListing and more.
+        oResultFilterType is a child of ModelFilterBase.
+        oResultsListContentType is a child of WuiListContentBase.
         """
         from testmanager.core.testresults    import TestResultLogic;
@@ -740,4 +741,5 @@
                                                   asValidValues = TestResultLogic.kasResultsSortBy,
                                                   sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
+        oFilter = oResultFilterType().initFromParams(self);
 
         # Get testing results period and validate it
@@ -833,4 +835,5 @@
             cEntries = oResultLogic.getEntriesCount(tsNow = tsEffective,
                                                     sInterval = sCurPeriod,
+                                                    oFilter = oFilter,
                                                     enmResultsGroupingType = enmResultsGroupingType,
                                                     iResultsGroupingValue = idMember,
@@ -843,4 +846,5 @@
                                                             tsNow = tsEffective,
                                                             sInterval = sCurPeriod,
+                                                            oFilter = oFilter,
                                                             enmResultSortBy = enmResultSortBy,
                                                             enmResultsGroupingType = enmResultsGroupingType,
@@ -890,7 +894,5 @@
         # Now, generate a filter control panel for the side bar.
         #
-        self._sPageFilter = self._generateResultFilter(oResultLogic,
-                                                       tsNow = tsEffective,
-                                                       sInterval = sCurPeriod,
+        self._sPageFilter = self._generateResultFilter(oFilter, oResultLogic, tsEffective, sCurPeriod,
                                                        enmResultsGroupingType = enmResultsGroupingType,
                                                        aoGroupMembers = aoGroupMembers,
@@ -899,22 +901,51 @@
         return True;
 
-    def _generateResultFilter(self, oResultLogic, tsNow, sInterval, enmResultsGroupingType, aoGroupMembers,
+    def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType, aoGroupMembers,
                               fOnlyFailures, fOnlyNeedingReason):
         """
         Generates the result filter for the left hand side.
         """
-        sHtml  = u'<div id="side-filters">\n' \
+        _ = enmResultsGroupingType; _ = aoGroupMembers; _ = fOnlyFailures; _ = fOnlyNeedingReason;
+        oResultLogic.fetchPossibleFilterOptions(oFilter, tsNow, sPeriod)
+
+        # Add non-filter parameters as hidden fields so we can use 'GET' and have URLs to bookmark.
+        self._dSideMenuFormAttrs['method'] = 'GET';
+        sHtml = u'';
+        for sKey, oValue in self._oSrvGlue.getParameters().iteritems():
+            if len(sKey) > 3:
+                if hasattr(oValue, 'startswith'):
+                    sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+                           % (webutils.escapeAttr(sKey), webutils.escapeAttr(oValue),);
+                else:
+                    for oSubValue in oValue:
+                        sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+                               % (webutils.escapeAttr(sKey), webutils.escapeAttr(oSubValue),);
+
+        # Generate the filter panel.
+        sHtml += u'<div id="side-filters">\n' \
                  u' <p>Filters</p>\n' \
                  u' <dl>\n';
 
-        sHtml += u'  <dt class="sf-collapsable"><a href="javascript:void(0)" onclick="toggleCollapsableDtDd(this);">&#9660; Test filter 1</a></dd>\n';
-        sHtml += u'  <dd class="sf-collapsable"><ul><li>stuff 1</li><li>stuff 2</li><ul></dd>\n'
-
-        sHtml += u'  <dt class="sf-expandable"><a href="javascript:void(0)" onclick="toggleCollapsableDtDd(this);">&#9654; Test filter 2</a></dd>\n';
-        sHtml += u'  <dd class="sf-expandable"><ul><li>stuff 3</li><li>stuff 4</li><ul></dd>\n'
-
-
-        sHtml += u' </dl>\n' \
-                 u'</div>\n';
+        for iCrit, oCrit in enumerate(oFilter.aCriteria):
+            sClass = 'sf-collapsable' if oCrit.sState == oCrit.ksState_Selected else 'sf-expandable';
+            sChar  = '&#9660;'        if oCrit.sState == oCrit.ksState_Selected else '&#9654;';
+            sHtml += u'  <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsableDtDd(this);">%s'\
+                     u' %s</a></dt>\n' \
+                     u'  <dd class="%s">\n' \
+                     u'   <ul>\n' \
+                     % (sClass, sChar, webutils.escapeElem(oCrit.sName), sClass);
+            for oDesc in oCrit.aoPossible:
+                fChecked = oDesc.oValue in oCrit.aoSelected;
+                sHtml += u'    <li><input type="checkbox" name="%s" value="%s"%s/>%s</li>\n' \
+                       % (oCrit.sVarNm, oDesc.oValue, ' checked' if fChecked else '', webutils.escapeElem(oDesc.sDesc),);
+
+            sHtml += u'   </ul>\n';
+            if iCrit + 1 < len(oFilter.aCriteria):                    ## @todo fix me.
+                sHtml += u'   <div class="filterend">&nbsp;</div>\n'; ## @todo fix me.
+            sHtml += u'  </dd>\n';
+
+        sHtml += u' </dl>\n';
+        sHtml += u' <input type="submit" value="Apply"/>\n';
+        sHtml += u'</div>\n';
         return sHtml;
 
@@ -922,64 +953,64 @@
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         #return self._actionResultsListing(TestResultLogic, WuiGroupedResultList)?
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByTestGroup(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestGroup,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByBuildRev(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildRev,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByBuildCat(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildCat,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByTestBox(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestBox,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByTestCase(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestCase,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByOS(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeOS,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedByArch(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeArch,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
     def _actionResultsGroupedBySchedGroup(self):
         """ Action wrapper. """
         from testmanager.webui.wuitestresult        import WuiGroupedResultList;
-        from testmanager.core.testresults           import TestResultLogic;
+        from testmanager.core.testresults           import TestResultLogic, TestResultFilter;
         return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeSchedGroup,
-                                                 TestResultLogic, WuiGroupedResultList);
+                                                 TestResultLogic, TestResultFilter, WuiGroupedResultList);
 
 
