From 0f1699fdd926399e81bae40cdc350fc1aee4c237 Mon Sep 17 00:00:00 2001
From: Sebastien Robin <seb@nexedi.com>
Date: Thu, 6 Apr 2017 16:05:14 +0000
Subject: [PATCH] erp5_test_result: add a javascript graph on performance tests

---
 .../Test%20Result/view_performance_graph.xml  | 100 +++++++++++
 .../TestResultModule_getTestPerfResultList.py |  46 +++--
 ...TestResultModule_getTestPerfResultList.xml |   2 +-
 ...PerformanceGraphGadgetConfigurationDict.py |  27 +++
 ...erformanceGraphGadgetConfigurationDict.xml |  62 +++++++
 .../TestResult_viewPerformanceGraph.xml       | 152 ++++++++++++++++
 .../performance_graph_gadget.xml              | 145 +++++++++++++++
 .../erp5_test_result/test_result_js.xml       |  26 +++
 ...p5_test_result_performance_graph.html.html |  36 ++++
 ...rp5_test_result_performance_graph.html.xml |  28 +++
 ...t_erp5_test_result_performance_graph.js.js | 168 ++++++++++++++++++
 ..._erp5_test_result_performance_graph.js.xml |  32 ++++
 .../bt/template_action_path_list              |   1 +
 13 files changed, 806 insertions(+), 19 deletions(-)
 create mode 100644 bt5/erp5_test_result/ActionTemplateItem/portal_types/Test%20Result/view_performance_graph.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.py
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph/performance_graph_gadget.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.html
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.xml
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.js
 create mode 100644 bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.xml

diff --git a/bt5/erp5_test_result/ActionTemplateItem/portal_types/Test%20Result/view_performance_graph.xml b/bt5/erp5_test_result/ActionTemplateItem/portal_types/Test%20Result/view_performance_graph.xml
new file mode 100644
index 0000000000..dbf8fb5379
--- /dev/null
+++ b/bt5/erp5_test_result/ActionTemplateItem/portal_types/Test%20Result/view_performance_graph.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>action</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>categories</string> </key>
+            <value>
+              <tuple>
+                <string>action_type/object_view</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>category</string> </key>
+            <value> <string>object_view</string> </value>
+        </item>
+        <item>
+            <key> <string>condition</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>icon</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>view_performance_graph</string> </value>
+        </item>
+        <item>
+            <key> <string>permissions</string> </key>
+            <value>
+              <tuple>
+                <string>View</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Action Information</string> </value>
+        </item>
+        <item>
+            <key> <string>priority</string> </key>
+            <value> <float>3.0</float> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string>Performance Graph</string> </value>
+        </item>
+        <item>
+            <key> <string>visible</string> </key>
+            <value> <int>1</int> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="Expression" module="Products.CMFCore.Expression"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>text</string> </key>
+            <value> <string>string:${object_url}/TestResult_viewPerformanceGraph</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="Expression" module="Products.CMFCore.Expression"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>text</string> </key>
+            <value> <string>python: \'PERF\' in (here.getTitle() or \'\')</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.py b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.py
index 9fd16589dc..a25459a0c3 100644
--- a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.py
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.py
@@ -1,7 +1,9 @@
-from Products.ERP5Type.Document import newTempBase
-from Products.ZSQLCatalog.SQLCatalog import ComplexQuery, SimpleQuery
+#from Products.ERP5Type.Document import newTempBase
+#from Products.ZSQLCatalog.SQLCatalog import ComplexQuery, SimpleQuery
 from DateTime import DateTime
 
+
+"""
 rev_query_list = []
 if isinstance(kw.get('from_date'), DateTime):
   rev_query_list.append(SimpleQuery(creation_date=kw['from_date'],
@@ -9,23 +11,25 @@ if isinstance(kw.get('from_date'), DateTime):
 if isinstance(kw.get('at_date'), DateTime):
   rev_query_list.append(SimpleQuery(creation_date=kw['at_date'],
                                     comparison_operator='<='))
-
+"""
 test_result_list = []
 revision = None
 new_test_result_list = []
-context.log("rev_query_list", rev_query_list)
-if rev_query_list:
-  result = context.searchFolder(title='PERF-ERP5-MASTER', simulation_state='stopped',
-    revision=ComplexQuery(logical_operator='AND', *rev_query_list),
-    sort_on=(('delivery.start_date', 'ASC'),),src__=1)
-  context.log("result", result)
-  for test in context.searchFolder(title='PERF-ERP5-MASTER', simulation_state='stopped',
-    revision=ComplexQuery(logical_operator='AND', *rev_query_list),
+
+portal = context.getPortalObject()
+if query:
+  for test in portal.test_result_module.searchFolder(title='PERF-ERP5-MASTER', simulation_state='stopped',
+    full_text=query,
     sort_on=(('delivery.start_date', 'ASC'),)):
     test = test.getObject()
     if revision != test.getReference():
       revision = test.getReference()
-      test_result = {'rev': str(revision)}
+      revision_list = []
+      for revision_part in revision.split(','):
+        repository, commit_hash = revision_part.split('-')
+        revision_list.append('%s-%s' % (repository, commit_hash[0:8]))
+      revision = ",".join(revision_list)
+      test_result = {'revision': str(revision)}
       test_result_list.append(test_result)
     for prop in 'all_tests', 'failures', 'errors':
       test_result[prop] = test_result.get(prop, 0) + test.getProperty(prop, 0)
@@ -35,17 +39,23 @@ if rev_query_list:
       for k, v in line.items():
         timing_dict.setdefault(k, []).append(v)
 
-  normalize = kw.get('normalize')
+  normalize = kw.get('normalize', 1)
   base_result = {}
 
   for test_result in test_result_list:
     if test_result['errors'] < test_result['all_tests']:
-      new_test_result = newTempBase(context, '')
+      new_test_result = {}
       for k, v in test_result.pop('timing_dict').items():
         if v:
           v = sum(v) / len(v)
-          test_result[k] = v / base_result.setdefault(k, normalize and v or 1)
-      new_test_result.edit(**test_result)
+          # too much value is not productive
+          if k in ('all_tests', 'errors', 'failures') or k.find('_') > 0 and k.split('_')[1] in ('200', '300', '400', '500', '600', '700', '800', '900'):
+            continue
+          new_test_result[k] = v / base_result.setdefault(k, normalize and v or 1)
+      new_test_result['revision'] = test_result['revision']
+      new_test_result.update(**new_test_result)
+      new_test_result['_links'] = {'self': {}} # required by jio.allDocs API
       new_test_result_list.append(new_test_result)
-
-return new_test_result_list
+import json
+context.log("new_test_result_list", new_test_result_list)
+return json.dumps({'_embedded': {'contents':new_test_result_list}})
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.xml
index 4b84dbc9e9..044f1c17a3 100644
--- a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.xml
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResultModule_getTestPerfResultList.xml
@@ -50,7 +50,7 @@
         </item>
         <item>
             <key> <string>_params</string> </key>
-            <value> <string>*args, **kw</string> </value>
+            <value> <string>test_title=None, from_date=None, to_date=None, query=None, title=None, **kw</string> </value>
         </item>
         <item>
             <key> <string>id</string> </key>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.py b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.py
new file mode 100644
index 0000000000..828ccfe74b
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.py
@@ -0,0 +1,27 @@
+from base64 import urlsafe_b64encode
+import json
+
+list_method_template = ""
+
+# list_method_template is a hack until better API is found.
+# it allows to make jio.all_docs calling a python script
+# http://10.0.80.187:2200/erp5/web_site_module/renderjs_runner/hateoas/ERP5Document_getHateoas
+portal = context.getPortalObject()
+custom_search_template_no_editable = "%(root_url)s/%(script_id)s?mode=search" + \
+                     "&relative_url=%(relative_url)s" \
+                     "&list_method=%(list_method)s" \
+                     "&default_param_json=%(default_param_json)s" \
+                     "&test_title=%(test_title)s" \
+                     "{&query,select_list*,limit*,sort_on*,local_roles*}"
+
+list_method_template = custom_search_template_no_editable % {
+        "root_url": portal.absolute_url(),
+        "script_id": "TestResultModule_getTestPerfResultList",
+        "relative_url": context.getRelativeUrl().replace("/", "%2F"),
+        "list_method": "TestResultModule_getTestPerfResultList",
+        "default_param_json": urlsafe_b64encode(json.dumps({'test_title': context.getTitle()})),
+        "test_title": context.getTitle()
+      }
+
+
+return {'list_method_template': list_method_template}
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.xml
new file mode 100644
index 0000000000..48798900aa
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_getPerformanceGraphGadgetConfigurationDict.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>Script_magic</string> </key>
+            <value> <int>3</int> </value>
+        </item>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_container</string> </key>
+                                <value> <string>container</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_context</string> </key>
+                                <value> <string>context</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_m_self</string> </key>
+                                <value> <string>script</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>TestResult_getPerformanceGraphGadgetConfigurationDict</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph.xml
new file mode 100644
index 0000000000..f60c425f61
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ERP5 Form" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary/>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>_objects</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>action</string> </key>
+            <value> <string>Base_edit</string> </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>edit_order</string> </key>
+            <value>
+              <list/>
+            </value>
+        </item>
+        <item>
+            <key> <string>encoding</string> </key>
+            <value> <string>UTF-8</string> </value>
+        </item>
+        <item>
+            <key> <string>enctype</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>group_list</string> </key>
+            <value>
+              <list>
+                <string>left</string>
+                <string>right</string>
+                <string>center</string>
+                <string>bottom</string>
+                <string>hidden</string>
+              </list>
+            </value>
+        </item>
+        <item>
+            <key> <string>groups</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>bottom</string> </key>
+                    <value>
+                      <list>
+                        <string>performance_graph_gadget</string>
+                      </list>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>center</string> </key>
+                    <value>
+                      <list/>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>hidden</string> </key>
+                    <value>
+                      <list/>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>left</string> </key>
+                    <value>
+                      <list/>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>right</string> </key>
+                    <value>
+                      <list/>
+                    </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>TestResult_viewPerformanceGraph</string> </value>
+        </item>
+        <item>
+            <key> <string>method</string> </key>
+            <value> <string>POST</string> </value>
+        </item>
+        <item>
+            <key> <string>name</string> </key>
+            <value> <string>TestResult_view</string> </value>
+        </item>
+        <item>
+            <key> <string>pt</string> </key>
+            <value> <string>form_view</string> </value>
+        </item>
+        <item>
+            <key> <string>row_length</string> </key>
+            <value> <int>4</int> </value>
+        </item>
+        <item>
+            <key> <string>stored_encoding</string> </key>
+            <value> <string>UTF-8</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string>Performance Graph</string> </value>
+        </item>
+        <item>
+            <key> <string>unicode_mode</string> </key>
+            <value> <int>0</int> </value>
+        </item>
+        <item>
+            <key> <string>update_action</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>update_action_title</string> </key>
+            <value> <string></string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph/performance_graph_gadget.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph/performance_graph_gadget.xml
new file mode 100644
index 0000000000..0a20b7d6de
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/TestResult_viewPerformanceGraph/performance_graph_gadget.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>delegated_list</string> </key>
+            <value>
+              <list>
+                <string>default</string>
+                <string>editable</string>
+                <string>gadget_url</string>
+                <string>title</string>
+              </list>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>performance_graph_gadget</string> </value>
+        </item>
+        <item>
+            <key> <string>message_values</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>external_validator_failed</string> </key>
+                    <value> <string>The input failed the external validator.</string> </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+        <item>
+            <key> <string>overrides</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>field_id</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>form_id</string> </key>
+                    <value> <string></string> </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+        <item>
+            <key> <string>tales</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>default</string> </key>
+                    <value>
+                      <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>editable</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>field_id</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>form_id</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>gadget_url</string> </key>
+                    <value>
+                      <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+                    </value>
+                </item>
+                <item>
+                    <key> <string>title</string> </key>
+                    <value> <string></string> </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+        <item>
+            <key> <string>values</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>default</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>editable</string> </key>
+                    <value> <int>0</int> </value>
+                </item>
+                <item>
+                    <key> <string>field_id</string> </key>
+                    <value> <string>my_gadget_field</string> </value>
+                </item>
+                <item>
+                    <key> <string>form_id</string> </key>
+                    <value> <string>Base_viewFieldLibrary</string> </value>
+                </item>
+                <item>
+                    <key> <string>gadget_url</string> </key>
+                    <value> <string></string> </value>
+                </item>
+                <item>
+                    <key> <string>title</string> </key>
+                    <value> <string>Performance Graph</string> </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="TALESMethod" module="Products.Formulator.TALESField"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_text</string> </key>
+            <value> <string>python: here.TestResult_getPerformanceGraphGadgetConfigurationDict()</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="TALESMethod" module="Products.Formulator.TALESField"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_text</string> </key>
+            <value> <string>python:field.restrictedTraverse(\'test_result_js/gadget_erp5_test_result_performance_graph.html\').absolute_url()</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js.xml
new file mode 100644
index 0000000000..ceeb8527df
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Folder" module="OFS.Folder"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_objects</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>test_result_js</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string></string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.html b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.html
new file mode 100644
index 0000000000..a2814f3453
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+  <head>
+   <!--
+     data-i18n=No records
+     data-i18n=Records
+   -->
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    <meta name="viewport" content="width=device-width, user-scalable=no" />
+    <title>ERP5 Test Result Performance</title>
+
+    <!-- renderjs -->
+    <script src="../rsvp.js"></script>
+    <script src="../renderjs.js"></script>
+
+    <!-- custom script -->
+    <script src="gadget_erp5_test_result_performance_graph.js" type="text/javascript"></script>
+
+  <body>
+
+            <div class="ui-field-contain" >
+               <label data-i18n="From Date:">From Date:</label>
+               <input type="date" name="from_date" title="From Date">
+            </div>
+
+            <div class="ui-field-contain" >
+               <label data-i18n="At Date:">At Date:</label>
+               <input type="date" name="at_date" title="At Date">
+            </div>
+
+
+    <div class="document-content">
+
+    </div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.xml
new file mode 100644
index 0000000000..93e91bbb48
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.html.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="File" module="OFS.Image"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>__name__</string> </key>
+            <value> <string>gadget_erp5_test_result_performance_graph.html</string> </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>text/html</string> </value>
+        </item>
+        <item>
+            <key> <string>precondition</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string></string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.js b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.js
new file mode 100644
index 0000000000..9a202c76de
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.js
@@ -0,0 +1,168 @@
+/*jslint indent: 2, nomen: true */
+/*global window, rJS, RSVP, console, loopEventListener */
+(function (window, rJS, RSVP, loopEventListener) {
+  "use strict";
+
+  var WIDGET_GRAPH_URL = "../gadget_officejs_widget_graph_chart.html";
+
+  function getDateAsString(date) {
+    var date_string = "" + date.getFullYear() + "-",
+        month = date.getMonth() + 1,
+        day = date.getDate();
+    if (month < 10) {
+      date_string = date_string + "0";
+    }
+    date_string = date_string  + month + "-";
+    if (day < 10) {
+      date_string = date_string + "0";
+    }
+    date_string = date_string  + day;
+    return date_string;
+  }
+
+  function getCheckDateAndRefreshGraphFunction(gadget) {
+    return function checkDateAndRefreshGraph() {
+      var from_date, at_date, i;
+      from_date = gadget.property_dict.element.querySelector('[name="from_date"]').value;
+      at_date = gadget.property_dict.element.querySelector('[name="at_date"]').value;
+      if (at_date !== "" && from_date !== "") {
+        console.log("will need to get data");
+        return gadget.jio_allDocs({
+          "list_method_template": gadget.property_dict.option_dict.list_method_template,
+          "query": '( delivery.start_date: >= "' + from_date + '" AND delivery.start_date: < "' + at_date + '" )',
+          "limit": [],
+          "select_list": [],
+          "sort_on": []
+        })
+        .push(function (data_list) {
+          console.log('data_list', data_list);
+          var graph_data_list = [],
+              revision, line_list = data_list.data.rows, value, i, j,
+              property_list = [];
+          if (line_list.length !== 0) {
+            Object.keys(line_list[0].value).forEach(function (property) {
+              if (property !== "revision") {
+                property_list.push(property);
+              }
+            });
+            if (property_list.length !== 0) {
+              property_list.forEach(function (property) {
+                var i, data = {};
+                console.log("property", property);
+                data.type = "line";
+                data.title = property;
+                data.value_dict = {0: [], 1: []};
+                for (i = 0 ; i < line_list.length; i = i + 1) {
+                  data.value_dict[0].push(line_list[i].value.revision);
+                  data.value_dict[1].push(line_list[i].value[property]);
+                }
+                graph_data_list.push(data);
+              });
+            }
+          }
+          gadget.property_dict.graph_data_dict.data = graph_data_list;
+          return gadget.property_dict.graph_widget.updateConfiguration(
+            gadget.property_dict.graph_data_dict);
+        });
+      }
+    };
+  }
+
+  rJS(window)
+    .ready(function (gadget) {
+      gadget.property_dict = {};
+      return gadget.getElement()
+        .push(function (element) {
+          gadget.property_dict.element = element;
+          gadget.property_dict.deferred = RSVP.defer();
+        });
+    })
+
+    //////////////////////////////////////////////
+    // acquired method
+    //////////////////////////////////////////////
+    .declareAcquiredMethod("jio_allDocs", "jio_allDocs")
+
+    //////////////////////////////////////////////
+    // initialize the gadget content
+    //////////////////////////////////////////////
+    .declareMethod("render", function (option_dict) {
+      console.log("gadget_erp5_test_result_performance_graph, render, options", option_dict);
+      var gadget = this;
+      gadget.property_dict.option_dict = option_dict.value;
+      return new RSVP.Queue()
+        .push(function () {
+          return gadget.property_dict.deferred.resolve();
+        });
+    })
+    /////////////////////////////////////////
+    // Render text content gadget
+    /////////////////////////////////////////
+    .declareService(function () {
+      var gadget = this,
+        graph_gadget = null;
+
+      return new RSVP.Queue()
+        .push(function () {
+          return gadget.property_dict.deferred.promise;
+        })
+        .push(function () {
+          return RSVP.all([
+            gadget.declareGadget(
+              WIDGET_GRAPH_URL,
+              {
+                scope: "graph",
+                element: gadget.property_dict.element.querySelector(".document-content")
+              })
+            ]);
+        })
+
+        .push(function (result) {
+          graph_gadget = result[0];
+          gadget.property_dict.graph_widget = graph_gadget;
+          gadget.property_dict.graph_data_dict = {data: [{
+            value_dict: {0: [],
+                         1: []},
+            type: "line"
+            }],
+            layout: {axis_dict : {0: {"title": "commit"},
+                              1: {"title": "performance"}
+                             },
+                     title: "Test Result Performance"}
+          };
+          return graph_gadget.render(
+            gadget.property_dict.graph_data_dict
+            );
+        })
+        .push(function () {
+          var now = new Date(), last_month, tomorrow;
+          last_month = new Date(now.valueOf() - 86400 * 1000 * 30); // - 30 days
+          tomorrow = new Date(now.valueOf() + 86400 * 1000 * 1); // + 1 day
+          gadget.property_dict.element.querySelector('[name="from_date"]').value = getDateAsString(last_month);
+          gadget.property_dict.element.querySelector('[name="at_date"]').value = getDateAsString(tomorrow);
+        })
+        .push(function () {
+          getCheckDateAndRefreshGraphFunction(gadget)();
+        });
+    })
+    .declareService(function () {
+      var gadget = this;
+      return loopEventListener(
+        gadget.property_dict.element.querySelector('[name="from_date"]'),
+        'change',
+        false,
+        function () {
+          getCheckDateAndRefreshGraphFunction(gadget)();
+        });
+    })
+    .declareService(function () {
+      var gadget = this;
+      return loopEventListener(
+        gadget.property_dict.element.querySelector('[name="at_date"]'),
+        'change',
+        false,
+        function () {
+          getCheckDateAndRefreshGraphFunction(gadget)();
+        });
+    });
+}(window, rJS, RSVP, loopEventListener));
diff --git a/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.xml b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.xml
new file mode 100644
index 0000000000..c610a31b9a
--- /dev/null
+++ b/bt5/erp5_test_result/SkinTemplateItem/portal_skins/erp5_test_result/test_result_js/gadget_erp5_test_result_performance_graph.js.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="File" module="OFS.Image"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_Cacheable__manager_id</string> </key>
+            <value> <string>http_cache</string> </value>
+        </item>
+        <item>
+            <key> <string>__name__</string> </key>
+            <value> <string>gadget_erp5_test_result_performance_graph.js</string> </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>application/javascript</string> </value>
+        </item>
+        <item>
+            <key> <string>precondition</string> </key>
+            <value> <string></string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string></string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_test_result/bt/template_action_path_list b/bt5/erp5_test_result/bt/template_action_path_list
index 7ffdd6a40e..c16f659192 100644
--- a/bt5/erp5_test_result/bt/template_action_path_list
+++ b/bt5/erp5_test_result/bt/template_action_path_list
@@ -28,6 +28,7 @@ Test Result | jump_viewvc_revision
 Test Result | view
 Test Result | view_graph
 Test Result | view_node_list
+Test Result | view_performance_graph
 Test Suite Module | view
 Test Suite Repository | view
 Test Suite | view
\ No newline at end of file
-- 
2.30.9