Skip to content

Commit 0155957

Browse files
authored
[Tracing] Add event history table in the testcase detail page (#4970)
This PR introduces an event history table on the testcase detail App Engine webpage. This table includes all events associated with a specific testcase, a download icon for task's logs related to an event, and a redirection icon linking the user to the GCP logs webpage (pre-filtered to show the task's logs). Key changes: - Added `testcase-event-history.html` containing the styling and logic necessary to display the event history on the testcase detail webpage. - Added the new `testcase-detail/task-log` endpoint. This design ensures that log downloading occurs only upon a user's click on the download icon. Related to b/394060581.
1 parent f4c6516 commit 0155957

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed

src/appengine/handlers/testcase_detail/show.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ def get_testcase_detail(testcase):
559559
_parse_suspected_cls(metadata.get('predator_result')),
560560
'testcase':
561561
testcase,
562+
'testcase_event_history':
563+
testcase_status_events.get_testcase_event_history(testcase_id),
562564
'testcase_status_info':
563565
testcase_status_events.get_testcase_status_info(testcase_id),
564566
'timestamp':
@@ -630,3 +632,21 @@ def post(self):
630632
"""Serve the testcase detail JSON."""
631633
testcase_id = flask.request.get('testcaseId')
632634
return self.render_json(get_testcase_detail_by_id(testcase_id))
635+
636+
class TaskLogHandler(base_handler.Handler):
637+
"""Handler for downloading a task's log."""
638+
639+
@handler.get(handler.TEXT)
640+
def get(self):
641+
"""Serve the task log."""
642+
task_id = flask.request.args.get('task_id')
643+
task_name = flask.request.args.get('task_name')
644+
testcase_id = flask.request.args.get('testcase_id')
645+
log_content = testcase_status_events.get_task_log(testcase_id, task_id,
646+
task_name)
647+
648+
response = flask.make_response(log_content)
649+
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
650+
response.headers['Content-Disposition'] = (
651+
f'attachment; filename="task_{task_id}_log.txt"')
652+
return response

src/appengine/private/components/testcase-detail/testcase-detail.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<link rel="import" href="find-similar-issues-panel.html">
2929
<link rel="import" href="redo-dialog.html">
3030
<link rel="import" href="refresh-button.html">
31+
<link rel="import" href="testcase-event-history.html">
3132
<link rel="import" href="testcase-status-events.html">
3233
<link rel="import" href="testcase-variants.html">
3334
<link rel="import" href="set-security-dialog.html">
@@ -653,6 +654,9 @@
653654
<div class="title">Metadata</div>
654655
<div class="body padding pre-line">[[info.footer]]</div>
655656
</div>
657+
658+
<testcase-event-history info="[[info]]"></testcase-event-history>
659+
656660
</div>
657661
</div>
658662
</template>
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<!--
2+
Copyright 2025 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
-->
13+
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
14+
<link rel="import" href="../../bower_components/polymer/polymer.html">
15+
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
16+
<link rel="import" href="../common/if-else/if-else.html">
17+
18+
<dom-module id="testcase-event-history">
19+
<link rel="import" href="../../stylesheets/main.css" type="css">
20+
<template>
21+
<style>
22+
:host {
23+
display: block;
24+
}
25+
26+
:host table.revision {
27+
width: 100%;
28+
table-layout: fixed;
29+
}
30+
31+
:host table.revision tr.title td {
32+
width: 33%;
33+
font-size: 14px;
34+
padding: 4px 6px;
35+
background-color: #373b50;
36+
color: #fff;
37+
border-left: 1px dotted #ccc;
38+
border-top: 1px dotted #ccc;
39+
text-transform: uppercase;
40+
font-weight: normal;
41+
}
42+
43+
:host table.revision tr.title td:first-child {
44+
border-left: 0px;
45+
}
46+
47+
:host table.revision tr.body td {
48+
padding: 4px 6px;
49+
font-size: 14px;
50+
vertical-align: top;
51+
border-bottom: 1px dotted #777;
52+
border-left: 1px dotted #777;
53+
white-space: pre-wrap;
54+
overflow-wrap: break-word;
55+
}
56+
57+
:host table.revision tr.body td iron-icon {
58+
width: 22px;
59+
height: 22px;
60+
}
61+
62+
:host table.revision tr.body td a {
63+
text-decoration: none;
64+
display: inline-flex;
65+
align-items: center;
66+
justify-content: center;
67+
color: #3a80b0;
68+
}
69+
70+
:host table.revision tr.body td.event-cell {
71+
font-family: 'Source Code Pro', monospace;
72+
font-size: 13px;
73+
white-space: pre-wrap;
74+
overflow-wrap: break-word;
75+
}
76+
77+
:host table.revision tr.expand-control td {
78+
border-top: 1px dotted #777;
79+
text-align: center;
80+
}
81+
82+
:host paper-button {
83+
font-size: 12px;
84+
padding: 0px;
85+
display: block;
86+
text-align: center;
87+
}
88+
89+
:host paper-button iron-icon {
90+
--iron-icon-width: 16px;
91+
--iron-icon-height: 16px;
92+
}
93+
94+
</style>
95+
<div id="event-history-section" class="section">
96+
<div class="title" title="Testcase event history.">Event History</div>
97+
<template is="dom-if" if="[[info.testcase_event_history.length]]">
98+
<table cellpadding="0" cellspacing="0" class="revision">
99+
<tbody><tr class="title">
100+
<td style="width: 80.00%;">Event</td>
101+
<td style="width: 10.00%;">Download Task Logs</td>
102+
<td style="width: 10.00%;">GCP Task Logs</td>
103+
</tr>
104+
<template is="dom-if" if="[[!shouldPreview]]">
105+
<template is="dom-repeat" items="[[_reversedHistory]]" as="event">
106+
<tr class="body">
107+
<td class="event-cell" inner-h-t-m-l="[[_formatEvent(event)]]"></td>
108+
<td>
109+
<template is="dom-if" if="[[event.gcp_log_url]]">
110+
<a href$="/testcase-detail/task-log?testcase_id=[[info.testcase.id]]&amp;task_id=[[event.task_id]]&amp;task_name=[[event.task_name]]" title="Download task logs">
111+
<iron-icon icon="icons:cloud-download"></iron-icon>
112+
</a>
113+
</template>
114+
</td>
115+
<td>
116+
<template is="dom-if" if="[[event.gcp_log_url]]">
117+
<a href$="[[event.gcp_log_url]]" target="_blank" title="View task logs in GCP">
118+
<iron-icon icon="icons:open-in-new"></iron-icon>
119+
</a>
120+
</template>
121+
</td>
122+
</tr>
123+
</template>
124+
</template>
125+
<template is="dom-if" if="[[shouldPreview]]">
126+
<template is="dom-repeat" items="[[previewItems]]" as="event">
127+
<tr class="body">
128+
<td class="event-cell" inner-h-t-m-l="[[_formatEvent(event)]]"></td>
129+
<td>
130+
<template is="dom-if" if="[[event.gcp_log_url]]">
131+
<a href$="/testcase-detail/task-log?testcase_id=[[info.testcase.id]]&amp;task_id=[[event.task_id]]&amp;task_name=[[event.task_name]]" title="Download task logs">
132+
<iron-icon icon="icons:cloud-download"></iron-icon>
133+
</a>
134+
</template>
135+
</td>
136+
<td>
137+
<template is="dom-if" if="[[event.gcp_log_url]]">
138+
<a href$="[[event.gcp_log_url]]" target="_blank" title="View task logs in GCP">
139+
<iron-icon icon="icons:open-in-new"></iron-icon>
140+
</a>
141+
</template>
142+
</td>
143+
</tr>
144+
</template>
145+
</template> <template is="dom-if" if="[[canExpand(info.testcase_event_history)]]">
146+
<tr class="expand-control">
147+
<td colspan="3">
148+
<paper-button on-tap="toggle">
149+
<if-else condition="[[shouldPreview]]">
150+
<iron-icon slot="t" icon="icons:expand-more"></iron-icon>
151+
<iron-icon slot="f" icon="icons:expand-less"></iron-icon>
152+
</if-else>
153+
</paper-button>
154+
</td>
155+
</tr>
156+
</template>
157+
</tbody></table>
158+
</template>
159+
<template is="dom-if" if="[[!info.testcase_event_history.length]]">
160+
<div class="body padding">
161+
No event history found.
162+
</div>
163+
</template>
164+
</div>
165+
</template>
166+
<script>
167+
Polymer({
168+
is: 'testcase-event-history',
169+
properties: {
170+
info: Object,
171+
shouldPreview: {
172+
type: Boolean,
173+
value: true,
174+
},
175+
previewItems: {
176+
type: Array,
177+
computed: '_computePreviewItems(_reversedHistory)'
178+
},
179+
_reversedHistory: {
180+
type: Array,
181+
computed: '_computeReversedHistory(info.testcase_event_history)'
182+
}
183+
},
184+
_computeReversedHistory(history) {
185+
return history ? history.slice().reverse() : [];
186+
},
187+
_computePreviewItems(reversedHistory) {
188+
return reversedHistory ? reversedHistory.slice(0, 3) : [];
189+
},
190+
canExpand(history) {
191+
return history && history.length > 3;
192+
},
193+
toggle() {
194+
this.shouldPreview = !this.shouldPreview;
195+
},
196+
_formatEvent(event) {
197+
if (!event) {
198+
return '';
199+
}
200+
const eventCopy = Object.assign({}, event);
201+
delete eventCopy.gcp_log_url;
202+
const boldKeys = ['event_type', 'task_name', 'timestamp', 'task_id'];
203+
const keys = Object.keys(eventCopy).sort();
204+
let result = '{';
205+
result += keys.map(key => {
206+
const value = JSON.stringify(eventCopy[key]);
207+
if (boldKeys.includes(key)) {
208+
return `<b>"${key}"</b>: <b>${value}</b>`;
209+
}
210+
return `"${key}": ${value}`;
211+
}).join(', ');
212+
result += '}';
213+
return result;
214+
},
215+
});
216+
</script>
217+
</dom-module>

src/appengine/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def register_routes(flask_app, routes):
185185
('/testcase-detail/remove-duplicate', remove_duplicate.Handler),
186186
('/testcase-detail/remove-issue', remove_issue.Handler),
187187
('/testcase-detail/remove-group', remove_group.Handler),
188+
('/testcase-detail/task-log', show_testcase.TaskLogHandler),
188189
('/testcase-detail/testcase-variants', testcase_variants.Handler),
189190
('/testcase-detail/update-from-trunk', update_from_trunk.Handler),
190191
('/testcase-detail/update-issue', update_issue.Handler),

0 commit comments

Comments
 (0)