Index: /trunk/src/VBox/ValidationKit/testmanager/core/build.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/build.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/build.py	(revision 65040)
@@ -479,4 +479,8 @@
     Build database logic (covers build categories as well as builds).
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.dCache = None;
 
     #
@@ -609,4 +613,41 @@
         self._oDb.maybeCommit(fCommit);
         return True;
+
+    def cachedLookup(self, idBuild):
+        """
+        Looks up the most recent BuildDataEx object for idBuild
+        via an object cache.
+
+        Returns a shared BuildDataEx object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('BuildDataEx');
+        oEntry = self.dCache.get(idBuild, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   Builds.*, BuildCategories.*\n'
+                              'FROM     Builds, BuildCategories\n'
+                              'WHERE    Builds.idBuild         = %s\n'
+                              '     AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , (idBuild, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   Builds.*, BuildCategories.*\n'
+                                  'FROM     Builds, BuildCategories\n'
+                                  'WHERE    Builds.idBuild         = %s\n'
+                                  '     AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idBuild, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuild));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = BuildDataEx();
+                oEntry.initFromDbRow(aaoRow);
+                self.dCache[idBuild] = oEntry;
+        return oEntry;
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/buildblacklist.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/buildblacklist.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/buildblacklist.py	(revision 65040)
@@ -123,4 +123,8 @@
     Build Back List logic.
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.dCache = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -223,4 +227,40 @@
         self._oDb.maybeCommit(fCommit);
         return True;
+
+
+    def cachedLookup(self, idBlacklisting):
+        """
+        Looks up the most recent BuildBlacklistData object for idBlacklisting
+        via an object cache.
+
+        Returns a shared BuildBlacklistData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('BuildBlacklistData');
+        oEntry = self.dCache.get(idBlacklisting, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     BuildBlacklist\n'
+                              'WHERE    idBlacklisting = %s\n'
+                              '     AND tsExpire   = \'infinity\'::TIMESTAMP\n'
+                              , (idBlacklisting, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     BuildBlacklist\n'
+                                  'WHERE    idBlacklisting = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idBlacklisting, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBlacklisting));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = BuildBlacklistData();
+                oEntry.initFromDbRow(aaoRow);
+                self.dCache[idBlacklisting] = oEntry;
+        return oEntry;
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/buildsource.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/buildsource.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/buildsource.py	(revision 65040)
@@ -154,4 +154,8 @@
     Build source database logic.
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.dCache = None;
 
     #
@@ -325,4 +329,38 @@
         return True;
 
+    def cachedLookup(self, idBuildSrc):
+        """
+        Looks up the most recent BuildSourceData object for idBuildSrc
+        via an object cache.
+
+        Returns a shared BuildSourceData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('BuildSourceData');
+        oEntry = self.dCache.get(idBuildSrc, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     BuildSources\n'
+                              'WHERE    idBuildSrc = %s\n'
+                              '     AND tsExpire   = \'infinity\'::TIMESTAMP\n'
+                              , (idBuildSrc, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     BuildSources\n'
+                                  'WHERE    idBuildSrc = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idBuildSrc, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuildSrc));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = BuildSourceData();
+                oEntry.initFromDbRow(aaoRow);
+                self.dCache[idBuildSrc] = oEntry;
+        return oEntry;
 
     #
Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 65040)
@@ -213,5 +213,5 @@
                               'ORDER BY sShort ASC\n'
                               'LIMIT %s OFFSET %s\n'
-                              , ( tsNow, tsNow, idFailureCategory, cMaxRows, iStart,));
+                              , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,));
 
         aoRows = []
Index: /trunk/src/VBox/ValidationKit/testmanager/core/globalresource.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/globalresource.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/globalresource.py	(revision 65040)
@@ -120,4 +120,8 @@
     """
 
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.dCache = None;
+
     def fetchForListing(self, iStart, cMaxRows, tsNow):
         """
@@ -146,4 +150,41 @@
             aoRows.append(GlobalResourceData().initFromDbRow(aoRow))
         return aoRows
+
+
+    def cachedLookup(self, idGlobalRsrc):
+        """
+        Looks up the most recent GlobalResourceData object for idGlobalRsrc
+        via an object cache.
+
+        Returns a shared GlobalResourceData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('GlobalResourceData');
+        oEntry = self.dCache.get(idGlobalRsrc, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     GlobalResources\n'
+                              'WHERE    idGlobalRsrc = %s\n'
+                              '     AND tsExpire     = \'infinity\'::TIMESTAMP\n'
+                              , (idGlobalRsrc, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     GlobalResources\n'
+                                  'WHERE    idGlobalRsrc = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idGlobalRsrc, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idGlobalRsrc));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = GlobalResourceData();
+                oEntry.initFromDbRow(aaoRow);
+                self.dCache[idGlobalRsrc] = oEntry;
+        return oEntry;
+
 
     def getAll(self, tsEffective = None):
Index: /trunk/src/VBox/ValidationKit/testmanager/core/schedgroup.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/schedgroup.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/schedgroup.py	(revision 65040)
@@ -425,4 +425,8 @@
     SchedGroup logic.
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb);
+        self.dCache = None;
 
     #
@@ -610,4 +614,38 @@
         return True;
 
+
+    def cachedLookup(self, idSchedGroup):
+        """
+        Looks up the most recent SchedGroupData object for idSchedGroup
+        via an object cache.
+
+        Returns a shared SchedGroupData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('SchedGroup');
+
+        oEntry = self.dCache.get(idSchedGroup, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     SchedGroups\n'
+                              'WHERE    idSchedGroup = %s\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , (idSchedGroup, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     SchedGroups\n'
+                                  'WHERE    idSchedGroup = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idSchedGroup, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idSchedGroup));
+
+            if self._oDb.getRowCount() == 1:
+                oEntry = SchedGroupData().initFromDbRow(self._oDb.fetchOne());
+                self.dCache[idSchedGroup] = oEntry;
+        return oEntry;
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/systemchangelog.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/systemchangelog.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/systemchangelog.py	(revision 65040)
@@ -31,6 +31,7 @@
 
 # Validation Kit imports.
-from testmanager.core.base import ModelLogicBase;
+from testmanager.core.base        import ModelLogicBase;
 from testmanager.core.useraccount import UserAccountLogic;
+from testmanager.core.systemlog   import SystemLogData;
 
 
@@ -40,8 +41,8 @@
     """
 
-    def __init__(self, tsEffective, oAuthor, sWhat, idWhat, sDesc):
+    def __init__(self, tsEffective, oAuthor, sEvent, idWhat, sDesc):
         self.tsEffective = tsEffective;
         self.oAuthor     = oAuthor;
-        self.sWhat       = sWhat;
+        self.sEvent      = sEvent;
         self.idWhat      = idWhat;
         self.sDesc       = sDesc;
@@ -55,17 +56,16 @@
     ## @name What kind of change.
     ## @{
-    ksWhat_TestBox          = 'TestBox';
-    ksWhat_TestCase         = 'TestCase';
-    ksWhat_Blacklisting     = 'Blacklisting';
-    ksWhat_Build            = 'Build';
-    ksWhat_BuildSource      = 'BuildSource';
-    ksWhat_FailureCategory  = 'FailureCategory';
-    ksWhat_FailureReason    = 'FailureReason';
-    ksWhat_GlobalRsrc       = 'GlobalRsrc';
-    ksWhat_SchedGroup       = 'SchedGroup';
-    ksWhat_SystemLog        = 'SystemLog';
-    ksWhat_TestGroup        = 'TestGroup';
-    ksWhat_User             = 'User';
-    ksWhat_TestResult       = 'TestResult';
+    ksWhat_TestBox          = 'chlog::TestBox';
+    ksWhat_TestCase         = 'chlog::TestCase';
+    ksWhat_Blacklisting     = 'chlog::Blacklisting';
+    ksWhat_Build            = 'chlog::Build';
+    ksWhat_BuildSource      = 'chlog::BuildSource';
+    ksWhat_FailureCategory  = 'chlog::FailureCategory';
+    ksWhat_FailureReason    = 'chlog::FailureReason';
+    ksWhat_GlobalRsrc       = 'chlog::GlobalRsrc';
+    ksWhat_SchedGroup       = 'chlog::SchedGroup';
+    ksWhat_TestGroup        = 'chlog::TestGroup';
+    ksWhat_User             = 'chlog::User';
+    ksWhat_TestResult       = 'chlog::TestResult';
     ## @}
 
@@ -84,9 +84,10 @@
         ksWhat_GlobalRsrc:       ( 'GlobalResources',    'idGlobalRsrc',        None, ),
         ksWhat_SchedGroup:       ( 'SchedGroupes',       'idSchedGroup',        None, ),
-        ksWhat_SystemLog:        ( 'SystemLog',          'tsCreated',           ksClue_TimestampId, ),
         ksWhat_TestGroup:        ( 'TestGroupes',        'idTestGroup',         None, ),
         ksWhat_User:             ( 'Users',              'idUser',              None, ),
         ksWhat_TestResult:       ( 'TestResults',        'idTestResult',        None, ),
     };
+    for sEvent in SystemLogData.kasEvents:
+        kdWhatToTable[sEvent] =  ( 'SystemLog',          'tsCreated',           ksClue_TimestampId, );
 
     ## @todo move to config.py?
@@ -155,8 +156,8 @@
         sQuery  = '(\n'
         sQuery += '    SELECT NULL AS uidAuthor,\n';
-        sQuery += '           tsCreated as tsEffective,\n';
-        sQuery += '           \'' + self.ksWhat_SystemLog + '\' AS sWhat,\n';
-        sQuery += '           NULL AS idWhat,\n';
-        sQuery += '           CONCAT(sEvent, \': \', sLogText) AS sDesc\n';
+        sQuery += '           tsCreated AS tsEffective,\n';
+        sQuery += '           sEvent    AS sEvent,\n';
+        sQuery += '           NULL      AS idWhat,\n';
+        sQuery += '           sLogText  AS sDesc\n';
         sQuery += '    FROM   SystemLog\n';
         sQuery += sWhereTime.replace('tsEffective', 'tsCreated');
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py	(revision 65040)
@@ -995,5 +995,5 @@
                 # Maybe it was deleted, try get the last entry.
                 self._oDb.execute('SELECT   TestBoxesWithStrings.*\n'
-                                  'FROM     TestBoxes\n'
+                                  'FROM     TestBoxesWithStrings\n'
                                   'WHERE    idTestBox = %s\n'
                                   'ORDER BY tsExpire DESC\n'
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testgroup.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testgroup.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testgroup.py	(revision 65040)
@@ -372,4 +372,8 @@
     """
 
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.dCache = None;
+
     #
     # Standard methods.
@@ -550,4 +554,42 @@
         return True;
 
+    def cachedLookup(self, idTestGroup):
+        """
+        Looks up the most recent TestGroupDataEx object for idTestGroup
+        via an object cache.
+
+        Returns a shared TestGroupDataEx object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('TestGroupDataEx');
+        oEntry = self.dCache.get(idTestGroup, None);
+        if oEntry is None:
+            fNeedTsNow = False;
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     TestGroups\n'
+                              'WHERE    idTestGroup = %s\n'
+                              '     AND tsExpire    = \'infinity\'::TIMESTAMP\n'
+                              , (idTestGroup, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     TestGroups\n'
+                                  'WHERE    idTestGroup = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idTestGroup, ));
+                fNeedTsNow = True;
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestGroup));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = TestGroupDataEx();
+                tsNow  = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
+                oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow);
+                self.dCache[idTestGroup] = oEntry;
+        return oEntry;
+
 
     #
Index: /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65040)
@@ -411,4 +411,17 @@
 }
 
+/*
+ * Generic class for div elements wrapping pre inside a table.  This prevents
+ * the <pre> from taking up way more screen space that available.
+ */
+.tdpre {
+    display:        table;
+    table-layout:   fixed;
+    width:          100%;
+}
+.tdpre pre {
+    overflow:       auto;
+}
+
 
 /*
@@ -567,4 +580,7 @@
 }
 
+.tmsyschlogattr {
+    font-size:      0.64em;
+}
 
 /*
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py	(revision 65040)
@@ -167,6 +167,6 @@
         # System Log actions.
         #
+        self._dDispatch[self.ksActionSystemChangelogList]       = self._actionSystemChangelogList;
         self._dDispatch[self.ksActionSystemLogList]             = self._actionSystemLogList;
-        self._dDispatch[self.ksActionSystemChangelogList]       = self._actionSystemChangelogList;
 
         #
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py	(revision 65040)
@@ -30,9 +30,25 @@
 
 
+from common import webutils;
+
 # Validation Kit imports.
-from testmanager.webui.wuicontentbase   import WuiListContentBase #, WuiTmLink;
-#from testmanager.core.testbox           import TestBoxData;
-#from testmanager.core.systemchangelog   import SystemChangelogLogic;
-#from testmanager.core.useraccount       import UserAccountData;
+from testmanager.webui.wuicontentbase   import WuiListContentBase, WuiHtmlKeeper, WuiAdminLink, \
+                                               WuiMainLink, WuiElementText, WuiHtmlBase;
+
+from testmanager.core.base              import AttributeChangeEntryPre;
+from testmanager.core.buildblacklist    import BuildBlacklistLogic, BuildBlacklistData;
+from testmanager.core.build             import BuildLogic, BuildData;
+from testmanager.core.buildsource       import BuildSourceLogic, BuildSourceData;
+from testmanager.core.globalresource    import GlobalResourceLogic, GlobalResourceData;
+from testmanager.core.failurecategory   import FailureCategoryLogic, FailureCategoryData;
+from testmanager.core.failurereason     import FailureReasonLogic, FailureReasonData;
+from testmanager.core.systemlog         import SystemLogData;
+from testmanager.core.systemchangelog   import SystemChangelogLogic;
+from testmanager.core.schedgroup        import SchedGroupLogic, SchedGroupData;
+from testmanager.core.testbox           import TestBoxLogic, TestBoxData;
+from testmanager.core.testcase          import TestCaseLogic, TestCaseData;
+from testmanager.core.testgroup         import TestGroupLogic, TestGroupData;
+from testmanager.core.testset           import TestSetData;
+from testmanager.core.useraccount       import UserAccountLogic, UserAccountData;
 
 
@@ -45,18 +61,357 @@
         WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Changelog',
                                     fnDPrint = fnDPrint, oDisp = oDisp);
-        self._asColumnHeaders = ['Date', 'Author', 'What', 'Description'];
-        self._asColumnAttribs = ['', '', '', ''];
+        self._asColumnHeaders = [ 'When', 'User', 'Event', 'Details' ];
+        self._asColumnAttribs = [ 'align="center"', 'align="center"', '', '' ];
+        self._oBuildBlacklistLogic  = BuildBlacklistLogic(oDisp.getDb());
+        self._oBuildLogic           = BuildLogic(oDisp.getDb());
+        self._oBuildSourceLogic     = BuildSourceLogic(oDisp.getDb());
+        self._oFailureCategoryLogic = FailureCategoryLogic(oDisp.getDb());
+        self._oFailureReasonLogic   = FailureReasonLogic(oDisp.getDb());
+        self._oGlobalResourceLogic  = GlobalResourceLogic(oDisp.getDb());
+        self._oSchedGroupLogic      = SchedGroupLogic(oDisp.getDb());
+        self._oTestBoxLogic         = TestBoxLogic(oDisp.getDb());
+        self._oTestCaseLogic        = TestCaseLogic(oDisp.getDb());
+        self._oTestGroupLogic       = TestGroupLogic(oDisp.getDb());
+        self._oUserAccountLogic     = UserAccountLogic(oDisp.getDb());
+        self._sPrevDate             = '';
         _ = cDaysBack;
 
-    def _formatListEntry(self, iEntry):
-        from testmanager.webui.wuiadmin import WuiAdmin;
-        oEntry  = self._aoEntries[iEntry];
-
-        return [
-            oEntry.tsEffective,
-            oEntry.oAuthor.sUsername if oEntry.oAuthor is not None else '',
-            '%s(%s)' % (oEntry.sWhat, oEntry.idWhat,),
-            oEntry.sDesc,
-        ];
-
-
+    #   oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+    def _createBlacklistingDetailsLink(self, idBlacklisting, tsEffective):
+        """ Creates a link to the build source details. """
+        oBlacklisting = self._oBuildBlacklistLogic.cachedLookup(idBlacklisting);
+        if oBlacklisting is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink('Blacklisting #%u' % (oBlacklisting.idBlacklisting,),
+                                WuiAdmin.ksActionBuildBlacklistDetails, tsEffective,
+                                { BuildBlacklistData.ksParam_idBlacklisting: oBlacklisting.idBlacklisting },
+                                fBracketed = False);
+        return WuiElementText('[blacklisting #%u not found]' % (idBlacklisting,));
+
+    def _createBuildDetailsLink(self, idBuild, tsEffective):
+        """ Creates a link to the build details. """
+        oBuild = self._oBuildLogic.cachedLookup(idBuild);
+        if oBuild is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink('%s %sr%u' % ( oBuild.oCat.sProduct, oBuild.sVersion, oBuild.iRevision),
+                                WuiAdmin.ksActionBuildDetails, tsEffective,
+                                { BuildData.ksParam_idBuild: oBuild.idBuild },
+                                fBracketed = False,
+                                sTitle = 'build #%u for %s, type %s'
+                                       % (oBuild.idBuild, ' & '.join(oBuild.oCat.asOsArches), oBuild.oCat.sType));
+        return WuiElementText('[build #%u not found]' % (idBuild,));
+
+    def _createBuildSourceDetailsLink(self, idBuildSrc, tsEffective):
+        """ Creates a link to the build source details. """
+        oBuildSource = self._oBuildSourceLogic.cachedLookup(idBuildSrc);
+        if oBuildSource is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oBuildSource.sName, WuiAdmin.ksActionBuildSrcDetails, tsEffective,
+                                { BuildSourceData.ksParam_idBuildSrc: oBuildSource.idBuildSrc },
+                                fBracketed = False,
+                                sTitle = 'Build source #%u' % (oBuildSource.idBuildSrc,));
+        return WuiElementText('[build source #%u not found]' % (idBuildSrc,));
+
+    def _createFailureCategoryDetailsLink(self, idFailureCategory, tsEffective):
+        """ Creates a link to the failure category details. """
+        oFailureCategory = self._oFailureCategoryLogic.cachedLookup(idFailureCategory);
+        if oFailureCategory is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oFailureCategory.sShort, WuiAdmin.ksActionFailureCategoryDetails, tsEffective,
+                                { FailureCategoryData.ksParam_idFailureCategory: oFailureCategory.idFailureCategory },
+                                fBracketed = False,
+                                sTitle = 'Failure category #%u' % (oFailureCategory.idFailureCategory,));
+        return WuiElementText('[failure category #%u not found]' % (idFailureCategory,));
+
+    def _createFailureReasonDetailsLink(self, idFailureReason, tsEffective):
+        """ Creates a link to the failure reason details. """
+        oFailureReason = self._oFailureReasonLogic.cachedLookup(idFailureReason);
+        if oFailureReason is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oFailureReason.sShort, WuiAdmin.ksActionFailureReasonDetails, tsEffective,
+                                { FailureReasonData.ksParam_idFailureReason: oFailureReason.idFailureReason },
+                                fBracketed = False,
+                                sTitle = 'Failure reason #%u, category %s'
+                                       % (oFailureReason.idFailureReason, oFailureReason.oCategory.sShort));
+        return WuiElementText('[failure reason #%u not found]' % (idFailureReason,));
+
+    def _createGlobalResourceDetailsLink(self, idGlobalRsrc, tsEffective):
+        """ Creates a link to the global resource details. """
+        oGlobalResource = self._oGlobalResourceLogic.cachedLookup(idGlobalRsrc);
+        if oGlobalResource is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oGlobalResource.sName, '@todo', tsEffective,
+                                { GlobalResourceData.ksParam_idGlobalRsrc: oGlobalResource.idGlobalRsrc },
+                                fBracketed = False,
+                                sTitle = 'Global resource #%u' % (oGlobalResource.idGlobalRsrc,));
+        return WuiElementText('[global resource #%u not found]' % (idGlobalRsrc,));
+
+    def _createSchedGroupDetailsLink(self, idSchedGroup, tsEffective):
+        """ Creates a link to the scheduling group details. """
+        oSchedGroup = self._oSchedGroupLogic.cachedLookup(idSchedGroup);
+        if oSchedGroup is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oSchedGroup.sName, WuiAdmin.ksActionSchedGroupDetails, tsEffective,
+                                { SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup },
+                                fBracketed = False,
+                                sTitle = 'Scheduling group #%u' % (oSchedGroup.idSchedGroup,));
+        return WuiElementText('[scheduling group #%u not found]' % (idSchedGroup,));
+
+    def _createTestBoxDetailsLink(self, idTestBox, tsEffective):
+        """ Creates a link to the testbox details. """
+        oTestBox = self._oTestBoxLogic.cachedLookup(idTestBox);
+        if oTestBox is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oTestBox.sName, WuiAdmin.ksActionTestBoxDetails, tsEffective,
+                                { TestBoxData.ksParam_idTestBox: oTestBox.idTestBox },
+                                fBracketed = False, sTitle = 'Testbox #%u' % (oTestBox.idTestBox,));
+        return WuiElementText('[testbox #%u not found]' % (idTestBox,));
+
+    def _createTestCaseDetailsLink(self, idTestCase, tsEffective):
+        """ Creates a link to the test case details. """
+        oTestCase = self._oTestCaseLogic.cachedLookup(idTestCase);
+        if oTestCase is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oTestCase.sName, WuiAdmin.ksActionTestCaseDetails, tsEffective,
+                                { TestCaseData.ksParam_idTestCase: oTestCase.idTestCase },
+                                fBracketed = False, sTitle = 'Test case #%u' % (oTestCase.idTestCase,));
+        return WuiElementText('[test case #%u not found]' % (idTestCase,));
+
+    def _createTestGroupDetailsLink(self, idTestGroup, tsEffective):
+        """ Creates a link to the test group details. """
+        oTestGroup = self._oTestGroupLogic.cachedLookup(idTestGroup);
+        if oTestGroup is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oTestGroup.sName, WuiAdmin.ksActionTestGroupDetails, tsEffective,
+                                { TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup },
+                                fBracketed = False, sTitle = 'Test group #%u' % (oTestGroup.idTestGroup,));
+        return WuiElementText('[test group #%u not found]' % (idTestGroup,));
+
+    def _createTestSetResultsDetailsLink(self, idTestSet, tsEffective):
+        """ Creates a link to the test set results. """
+        _ = tsEffective;
+        from testmanager.webui.wuimain import WuiMain;
+        return WuiMainLink('test set #%u' % idTestSet, WuiMain.ksActionTestSetDetails,
+                           { TestSetData.ksParam_idTestSet: idTestSet }, fBracketed = False);
+
+    def _createTestSetDetailsLinkByResult(self, idTestResult, tsEffective):
+        """ Creates a link to the test set results. """
+        _ = tsEffective;
+        from testmanager.webui.wuimain import WuiMain;
+        return WuiMainLink('test result #%u' % idTestResult, WuiMain.ksActionTestSetDetailsFromResult,
+                           { TestSetData.ksParam_idTestResult: idTestResult }, fBracketed = False);
+
+    def _createUserAccountDetailsLink(self, uid, tsEffective):
+        """ Creates a link to the user account details. """
+        oUser = self._oUserAccountLogic.cachedLookup(uid);
+        if oUser is not None:
+            from testmanager.webui.wuiadmin import WuiAdmin;
+            return WuiAdminLink(oUser.sUsername, '@todo', tsEffective, { UserAccountData.ksParam_uid: oUser.uid },
+                                fBracketed = False, sTitle = '%s (#%u)' % (oUser.sFullName, oUser.uid));
+        return WuiElementText('[user #%u not found]' % (uid,));
+
+    def _formatDescGeneric(self, sDesc, oEntry):
+        """
+        Generically format system log the description.
+        """
+        oRet = WuiHtmlKeeper();
+        asWords = sDesc.split();
+        for sWord in asWords:
+            offEqual = sWord.find('=');
+            if offEqual > 0:
+                sKey = sWord[:offEqual];
+                try:    idValue = int(sWord[offEqual+1:].rstrip('.,'));
+                except: pass;
+                else:
+                    if sKey == 'idTestSet':
+                        oRet.append(self._createTestSetResultsDetailsLink(idValue, oEntry.tsEffective));
+                        continue;
+                    if sKey == 'idTestBox':
+                        oRet.append(self._createTestBoxDetailsLink(idValue, oEntry.tsEffective));
+                        continue;
+                    if sKey == 'idSchedGroup':
+                        oRet.append(self._createSchedGroupDetailsLink(idValue, oEntry.tsEffective));
+                        continue;
+
+            oRet.append(WuiElementText(sWord));
+        return oRet;
+
+    def _formatListEntryHtml(self, iEntry):
+        """
+        Overridden parent method.
+        """
+        oEntry = self._aoEntries[iEntry];
+
+        #
+        # Format the timestamp.
+        #
+        sDate = self.formatTsShort(oEntry.tsEffective);
+        if sDate[:10] == self._sPrevDate:
+            sDate = sDate[11:]
+        else:
+            self._sPrevDate = sDate[:10];
+            sDate = '<b>' + sDate[:10] + '</b><br>' + sDate[11:];
+
+        #
+        # System log events.
+        # pylint: disable=redefined-variable-type
+        #
+        aoChanges = None;
+        if   oEntry.sEvent == SystemLogData.ksEvent_CmdNacked:
+            sEvent = 'Command not acknowleged';
+            oDetails = oEntry.sDesc;
+
+        elif oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown:
+            sEvent = 'Unknown testbox';
+            oDetails = oEntry.sDesc;
+
+        elif oEntry.sEvent == SystemLogData.ksEvent_TestSetAbandoned:
+            sEvent = 'Abandoned test set';
+            oDetails = self._formatDescGeneric(oEntry.sDesc, oEntry);
+
+        elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown:
+            sEvent = 'Unknown user account';
+            oDetails = oEntry.sDesc;
+
+        elif oEntry.sEvent == SystemLogData.ksEvent_XmlResultMalformed:
+            sEvent = 'Malformed XML result';
+            oDetails = oEntry.sDesc;
+
+        elif oEntry.sEvent == SystemLogData.ksEvent_SchedQueueRecreate:
+            sEvent = 'Recreating scheduling queue';
+            asWords = oEntry.sDesc.split();
+            if len(asWords) > 3 and asWords[0] == 'User' and asWords[1][0] == '#':
+                try:    idAuthor = int(asWords[1][1:]);
+                except: pass;
+                else:
+                    oEntry.oAuthor = self._oUserAccountLogic.cachedLookup(idAuthor);
+                    if oEntry.oAuthor is not None:
+                        i = 2;
+                        if asWords[i] == 'recreated':   i += 1;
+                        oEntry.sDesc = ' '.join(asWords[i:]);
+            oDetails = self._formatDescGeneric(oEntry.sDesc.replace('sched queue #', 'for scheduling group idSchedGroup='),
+                                               oEntry);
+        #
+        # System changelog events.
+        #
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Blacklisting:
+            sEvent = 'Modified blacklisting';
+            oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Build:
+            sEvent = 'Modified build';
+            oDetails = self._createBuildDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_BuildSource:
+            sEvent = 'Modified build source';
+            oDetails = self._createBuildSourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_GlobalRsrc:
+            sEvent = 'Modified global resource';
+            oDetails = self._createGlobalResourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureCategory:
+            sEvent = 'Modified failure category';
+            oDetails = self._createFailureCategoryDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+            (aoChanges, _) = self._oFailureCategoryLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureReason:
+            sEvent = 'Modified failure reason';
+            oDetails = self._createFailureReasonDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+            (aoChanges, _) = self._oFailureReasonLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_SchedGroup:
+            sEvent = 'Modified scheduling group';
+            oDetails = self._createSchedGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestBox:
+            sEvent = 'Modified testbox';
+            oDetails = self._createTestBoxDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+            (aoChanges, _) = self._oTestBoxLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestCase:
+            sEvent = 'Modified test case';
+            oDetails = self._createTestCaseDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+            (aoChanges, _) = self._oTestCaseLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestGroup:
+            sEvent = 'Modified test group';
+            oDetails = self._createTestGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestResult:
+            sEvent = 'Modified test failure reason';
+            oDetails = self._createTestSetDetailsLinkByResult(oEntry.idWhat, oEntry.tsEffective);
+
+        elif oEntry.sEvent == SystemChangelogLogic.ksWhat_User:
+            sEvent = 'Modified user account';
+            oDetails = self._createUserAccountDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+        else:
+            sEvent   = '%s(%s)' % (oEntry.sEvent, oEntry.idWhat,);
+            oDetails = '!Unknown event!' + (oEntry.sDesc if oEntry.sDesc else '');
+
+        #
+        # Do the formatting.
+        #
+        sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
+
+        if aoChanges is not None and len(aoChanges) > 0:
+            oChangeEntry    = aoChanges[0];
+            cAttribsChanged = len(oChangeEntry.aoChanges) + 1;
+        else:
+            oChangeEntry    = None;
+            cAttribsChanged = 1;
+
+        sHtml = u'  <tr class="%s">\n' \
+                u'    <td rowspan="%d">%s</td>\n' \
+                u'    <td rowspan="%d">%s</td>\n' \
+                u'    <td rowspan="%d">%s</td>\n' \
+                u'    <td colspan="3">%s</td>\n' \
+                u'  </tr>\n' \
+              % ( sRowClass,
+                 cAttribsChanged, sDate,
+                 cAttribsChanged, webutils.escapeElem(oEntry.oAuthor.sUsername if oEntry.oAuthor is not None else ''),
+                 cAttribsChanged, webutils.escapeElem(sEvent),
+                 oDetails.toHtml() if isinstance(oDetails, WuiHtmlBase) else oDetails,
+                 );
+
+        if oChangeEntry is not None:
+            for j, oChange in enumerate(oChangeEntry.aoChanges):
+                if isinstance(oChange, AttributeChangeEntryPre):
+                    sHtml += '        <tr class="%s%s tmsyschlogattr"><td>%s</td>'\
+                             '<td><div class="tdpre"><pre>%s</pre></div></td>' \
+                             '<td><div class="tdpre"><pre>%s</pre></div></td></tr>\n' \
+                           % ( sRowClass, 'odd' if j & 1 else 'even',
+                               webutils.escapeElem(oChange.sAttr),
+                               webutils.escapeElem(oChange.sOldText),
+                               webutils.escapeElem(oChange.sNewText), );
+                else:
+                    sHtml += '        <tr class="%s%s tmsyschlogattr"><td>%s</td><td>%s</td><td>%s</td></tr>\n' \
+                           % ( sRowClass, 'odd' if j & 1 else 'even',
+                               webutils.escapeElem(oChange.sAttr),
+                               webutils.escapeElem(oChange.sOldText),
+                               webutils.escapeElem(oChange.sNewText), );
+        sHtml += u'  </tr>\n'
+        return sHtml;
+
+
+    def _generateTableHeaders(self):
+        """
+        Overridden parent method.
+        """
+
+        sHtml = u'<thead class="tmheader">\n' \
+                u' <tr>\n' \
+                u'  <th rowspan="2">When</th>\n' \
+                u'  <th rowspan="2">Who</th>\n' \
+                u'  <th rowspan="2">Event</th>\n' \
+                u'  <th colspan="3">Details</th>\n' \
+                u' </tr>\n' \
+                u' <tr>\n' \
+                u'  <th>Attribute</th>\n' \
+                u'  <th>Old Value</th>\n' \
+                u'  <th>New Value</th>\n' \
+                u' </tr>\n' \
+                u'</thead>\n';
+        return sHtml;
+
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 65039)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 65040)
@@ -111,4 +111,5 @@
         return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName));
 
+
 class WuiTmLink(WuiLinkBase): # pylint: disable=R0903
     """ Local link to the test manager. """
@@ -132,4 +133,36 @@
 
 
+class WuiAdminLink(WuiTmLink): # pylint: disable=R0903
+    """ Local link to the test manager's admin portion. """
+
+    def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None,
+                 sFragmentId = None, fBracketed = True):
+        from testmanager.webui.wuiadmin import WuiAdmin;
+        if dParams is None or len(dParams) == 0:
+            dParams = dict();
+        else:
+            dParams = dict(dParams);
+        if sAction is not None:
+            dParams[WuiAdmin.ksParamAction] = sAction;
+        if tsEffectiveDate is not None:
+            dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate;
+        WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+                           sFragmentId = sFragmentId, fBracketed = fBracketed);
+
+class WuiMainLink(WuiTmLink): # pylint: disable=R0903
+    """ Local link to the test manager's main portion. """
+
+    def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None,
+                 sFragmentId = None, fBracketed = True):
+        from testmanager.webui.wuimain import WuiMain;
+        if dParams is None or len(dParams) == 0:
+            dParams = dict();
+        else:
+            dParams = dict(dParams);
+        if sAction is not None:
+            dParams[WuiMain.ksParamAction] = sAction;
+        WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+                           sFragmentId = sFragmentId, fBracketed = fBracketed);
+
 class WuiSvnLink(WuiLinkBase): # pylint: disable=R0903
     """
@@ -214,4 +247,10 @@
                                 % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),));
 
+class WuiElementText(WuiRawHtml): # pylint: disable=R0903
+    """
+    Outputs the given element text.
+    """
+    def __init__(self, sText):
+        WuiRawHtml.__init__(self, webutils.escapeElem(sText));
 
 
@@ -254,5 +293,6 @@
             self._fnDPrint(sText);
 
-    def formatTsShort(self, oTs):
+    @staticmethod
+    def formatTsShort(oTs):
         """
         Formats a timestamp (db rep) into a short form.
@@ -399,5 +439,6 @@
         return [];
 
-    def _calcChangeLogEntryLinks(self, aoEntries, iEntry):
+    @staticmethod
+    def _calcChangeLogEntryLinks(aoEntries, iEntry):
         """
         Returns an array of links to go with the change log entry.
@@ -410,5 +451,6 @@
         return [];
 
-    def _guessChangeLogEntryDescription(self, aoEntries, iEntry):
+    @staticmethod
+    def _guessChangeLogEntryDescription(aoEntries, iEntry):
         """
         Guesses the action + author that caused the change log entry.
@@ -435,5 +477,6 @@
         return 'Modified by %s.' % (sAuthor,);
 
-    def _formatChangeLogEntry(self, aoEntries, iEntry):
+    @staticmethod
+    def formatChangeLogEntry(aoEntries, iEntry):
         """
         Formats one change log entry into one or more HTML table rows.
@@ -453,8 +496,8 @@
                    '    </tr>\n' \
                  % ( sRowClass,
-                     len(oEntry.aoChanges) + 1, webutils.escapeElem(self.formatTsShort(oEntry.tsEffective)),
-                     len(oEntry.aoChanges) + 1, webutils.escapeElem(self.formatTsShort(oEntry.tsExpire)),
-                     self._guessChangeLogEntryDescription(aoEntries, iEntry),
-                     ' '.join(oLink.toHtml() for oLink in self._calcChangeLogEntryLinks(aoEntries, iEntry)),);
+                     len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)),
+                     len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)),
+                     WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry),
+                     ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),);
 
         # Additional rows for each changed attribute.
@@ -574,6 +617,6 @@
                     '    <tbody>\n';
 
-        for iEntry in range(len(aoEntries)):
-            sContent += self._formatChangeLogEntry(aoEntries, iEntry);
+        for iEntry, _ in enumerate(aoEntries):
+            sContent += self.formatChangeLogEntry(aoEntries, iEntry);
 
         sContent += '    <tbody>\n' \
@@ -894,4 +937,20 @@
         return sNavigation;
 
+    def _generateTableHeaders(self):
+        """
+        Generate table headers.
+        Returns raw html string.
+        Overridable.
+        """
+
+        sHtml  = '  <thead class="tmheader"><tr>';
+        for oHeader in self._asColumnHeaders:
+            if isinstance(oHeader, WuiHtmlBase):
+                sHtml += '<th>' + oHeader.toHtml() + '</th>';
+            else:
+                sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
+        sHtml += '</tr><thead>\n';
+        return sHtml
+
     def _generateTable(self):
         """
@@ -910,11 +969,5 @@
             self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
 
-        sPageBody += '  <thead class="tmheader"><tr>';
-        for oHeader in self._asColumnHeaders:
-            if isinstance(oHeader, WuiHtmlBase):
-                sPageBody += '<th>' + oHeader.toHtml() + '</th>';
-            else:
-                sPageBody += '<th>' + webutils.escapeElem(oHeader) + '</th>';
-        sPageBody += '</tr><thead>\n';
+        sPageBody += self._generateTableHeaders();
 
         #
