aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2018-11-22 14:27:37 +0100
committerDmitry Vyukov <dvyukov@google.com>2018-11-22 14:36:32 +0100
commit13ab4beeefd2c49666ce771753fbb3a28c9d2f2c (patch)
tree95591888c2e60e548c30e111bdce6e0d2e6a3963 /pkg
parent582e1f0d1d51b9237d2dedfcb4c1540b849da8c2 (diff)
syz-manager: modernize web UI
1. Use dashboard style. 2. Allow sorting of tables. 3. Show old crashes in grey. 4. Use tables instead of text output for more pages. 5. Show corpus inputs on a separate page to allow copy-pasting. 6. Use standard JS sorting instead of custom bubble sort (much faster). 7. Fix off-by one in table sorting. Fixes #694
Diffstat (limited to 'pkg')
-rw-r--r--pkg/html/generated.go221
-rw-r--r--pkg/html/html.go110
2 files changed, 331 insertions, 0 deletions
diff --git a/pkg/html/generated.go b/pkg/html/generated.go
new file mode 100644
index 000000000..7c5157aab
--- /dev/null
+++ b/pkg/html/generated.go
@@ -0,0 +1,221 @@
+package html
+
+const style = `
+#topbar {
+ padding: 5px 10px;
+ background: #E0EBF5;
+}
+
+#topbar a {
+ color: #375EAB;
+ text-decoration: none;
+}
+
+h1, h2, h3, h4 {
+ margin: 0;
+ padding: 0;
+ color: #375EAB;
+ font-weight: bold;
+}
+
+table {
+ border: 1px solid #ccc;
+ margin: 20px 5px;
+ border-collapse: collapse;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+table caption {
+ font-weight: bold;
+}
+
+table td, table th {
+ vertical-align: top;
+ padding: 2px 8px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.position_table {
+ border: 0px;
+ margin: 0px;
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.position_table td, .position_table tr {
+ vertical-align: center;
+ padding: 0px;
+}
+
+.position_table .search {
+ text-align: right;
+}
+
+.list_table td, .list_table th {
+ border-left: 1px solid #ccc;
+}
+
+.list_table th {
+ background: #F4F4F4;
+}
+
+.list_table tr:nth-child(2n+1) {
+ background: #F4F4F4;
+}
+
+.list_table tr:hover {
+ background: #ffff99;
+}
+
+.list_table .namespace {
+ width: 100pt;
+ max-width: 100pt;
+}
+
+.list_table .title {
+ width: 350pt;
+ max-width: 350pt;
+}
+
+.list_table .tag {
+ font-family: monospace;
+ font-size: 8pt;
+ width: 40pt;
+ max-width: 40pt;
+}
+
+.list_table .opts {
+ width: 40pt;
+ max-width: 40pt;
+}
+
+.list_table .status {
+ width: 250pt;
+ max-width: 250pt;
+}
+
+.list_table .patched {
+ width: 60pt;
+ max-width: 60pt;
+ text-align: center;
+}
+
+.list_table .kernel {
+ width: 60pt;
+ max-width: 60pt;
+}
+
+.list_table .maintainers {
+ width: 150pt;
+ max-width: 150pt;
+}
+
+.list_table .result {
+ width: 60pt;
+ max-width: 60pt;
+}
+
+.list_table .stat {
+ width: 50pt;
+ max-width: 50pt;
+ font-family: monospace;
+ text-align: right;
+}
+
+.list_table .stat_name {
+ width: 150pt;
+ max-width: 150pt;
+ font-family: monospace;
+}
+
+.list_table .stat_value {
+ width: 100pt;
+ max-width: 100pt;
+ font-family: monospace;
+}
+
+.bad {
+ color: #f00;
+ font-weight: bold;
+}
+
+.inactive {
+ color: #888;
+}
+
+.plain {
+ text-decoration: none;
+}
+
+textarea {
+ width:100%;
+ font-family: monospace;
+}
+`
+const js = `
+// Copyright 2018 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+function sortTable(item, colName, conv, desc = false) {
+ table = item.parentNode.parentNode.parentNode;
+ rows = table.rows;
+ col = findColumnByName(rows[0].getElementsByTagName("th"), colName);
+ values = [];
+ for (i = 1; i < rows.length; i++)
+ values.push([conv(rows[i].getElementsByTagName("td")[col].textContent), rows[i]]);
+ if (desc)
+ desc = !isSorted(values.slice().reverse())
+ else
+ desc = isSorted(values);
+ values.sort(function(a, b) {
+ if (a[0] == b[0]) return 0;
+ if (desc && a[0] > b[0] || !desc && a[0] < b[0]) return -1;
+ return 1;
+ });
+ for (i = 0; i < values.length; i++)
+ table.appendChild(values[i][1]);
+ return false;
+}
+
+function findColumnByName(headers, colName) {
+ for (i = 0; i < headers.length; i++) {
+ if (headers[i].textContent == colName)
+ return i;
+ }
+ return 0;
+}
+
+function isSorted(values) {
+ for (i = 0; i < values.length - 1; i++) {
+ if (values[i][0] > values[i + 1][0])
+ return false;
+ }
+ return true;
+}
+
+function textSort(v) { return v.toLowerCase(); }
+function numSort(v) { return -parseInt(v); }
+function floatSort(v) { return -parseFloat(v); }
+function reproSort(v) { return v == "C" ? 0 : v == "syz" ? 1 : 2; }
+function patchedSort(v) { return v == "" ? -1 : parseInt(v); }
+
+function timeSort(v) {
+ if (v == "now")
+ return 0;
+ m = v.indexOf('m');
+ h = v.indexOf('h');
+ d = v.indexOf('d');
+ if (m > 0 && h < 0)
+ return parseInt(v);
+ if (h > 0 && m > 0)
+ return parseInt(v) * 60 + parseInt(v.substring(h + 1));
+ if (d > 0 && h > 0)
+ return parseInt(v) * 60 * 24 + parseInt(v.substring(d + 1)) * 60;
+ if (d > 0)
+ return parseInt(v) * 60 * 24;
+ return 1000000000;
+}
+`
diff --git a/pkg/html/html.go b/pkg/html/html.go
new file mode 100644
index 000000000..c7f85da88
--- /dev/null
+++ b/pkg/html/html.go
@@ -0,0 +1,110 @@
+// Copyright 2018 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+//go:generate bash -c "echo '// AUTOGENERATED FILE' > generated.go"
+//go:generate bash -c "echo 'package html' > generated.go"
+//go:generate bash -c "echo 'const style = `' >> generated.go"
+//go:generate bash -c "cat ../../dashboard/app/static/style.css >> generated.go"
+//go:generate bash -c "echo '`' >> generated.go"
+//go:generate bash -c "echo 'const js = `' >> generated.go"
+//go:generate bash -c "cat ../../dashboard/app/static/common.js >> generated.go"
+//go:generate bash -c "echo '`' >> generated.go"
+
+package html
+
+import (
+ "fmt"
+ "html/template"
+ "strings"
+ "time"
+
+ "github.com/google/syzkaller/dashboard/dashapi"
+)
+
+func CreatePage(page string) *template.Template {
+ const headTempl = `<style type="text/css" media="screen">%v</style><script>%v</script>`
+ page = strings.Replace(page, "{{HEAD}}", fmt.Sprintf(headTempl, style, js), 1)
+ return template.Must(template.New("").Funcs(funcs).Parse(page))
+}
+
+func CreateGlob(glob string) *template.Template {
+ return template.Must(template.New("").Funcs(funcs).ParseGlob(glob))
+}
+
+var funcs = template.FuncMap{
+ "formatTime": FormatTime,
+ "formatClock": formatClock,
+ "formatDuration": formatDuration,
+ "formatLateness": formatLateness,
+ "formatReproLevel": formatReproLevel,
+ "formatStat": formatStat,
+ "formatShortHash": formatShortHash,
+}
+
+func FormatTime(t time.Time) string {
+ if t.IsZero() {
+ return ""
+ }
+ return t.Format("2006/01/02 15:04")
+}
+
+func formatClock(t time.Time) string {
+ if t.IsZero() {
+ return ""
+ }
+ return t.Format("15:04")
+}
+
+func formatDuration(d time.Duration) string {
+ if d == 0 {
+ return ""
+ }
+ days := int(d / (24 * time.Hour))
+ hours := int(d / time.Hour % 24)
+ mins := int(d / time.Minute % 60)
+ if days >= 10 {
+ return fmt.Sprintf("%vd", days)
+ } else if days != 0 {
+ return fmt.Sprintf("%vd%02vh", days, hours)
+ } else if hours != 0 {
+ return fmt.Sprintf("%vh%02vm", hours, mins)
+ }
+ return fmt.Sprintf("%vm", mins)
+}
+
+func formatLateness(now, t time.Time) string {
+ if t.IsZero() {
+ return "never"
+ }
+ d := now.Sub(t)
+ if d < 5*time.Minute {
+ return "now"
+ }
+ return formatDuration(d)
+}
+
+func formatReproLevel(l dashapi.ReproLevel) string {
+ switch l {
+ case dashapi.ReproLevelSyz:
+ return "syz"
+ case dashapi.ReproLevelC:
+ return "C"
+ default:
+ return ""
+ }
+}
+
+func formatStat(v int64) string {
+ if v == 0 {
+ return ""
+ }
+ return fmt.Sprint(v)
+}
+
+func formatShortHash(v string) string {
+ const hashLen = 8
+ if len(v) <= hashLen {
+ return v
+ }
+ return v[:hashLen]
+}