Web Inspector: resource tree elements should provide "Download File" context menu...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceTimelineDataGridNode.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.ResourceTimelineDataGridNode = class ResourceTimelineDataGridNode extends WebInspector.TimelineDataGridNode
27 {
28     constructor(resourceTimelineRecord, includesGraph, graphDataSource)
29     {
30         super(includesGraph, graphDataSource);
31
32         this._resource = resourceTimelineRecord.resource;
33         this._record = resourceTimelineRecord;
34
35         this._resource.addEventListener(WebInspector.Resource.Event.LoadingDidFinish, this._needsRefresh, this);
36         this._resource.addEventListener(WebInspector.Resource.Event.LoadingDidFail, this._needsRefresh, this);
37         this._resource.addEventListener(WebInspector.Resource.Event.URLDidChange, this._needsRefresh, this);
38
39         if (includesGraph)
40             this._record.addEventListener(WebInspector.TimelineRecord.Event.Updated, this._timelineRecordUpdated, this);
41         else {
42             this._resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._needsRefresh, this);
43             this._resource.addEventListener(WebInspector.Resource.Event.SizeDidChange, this._needsRefresh, this);
44             this._resource.addEventListener(WebInspector.Resource.Event.TransferSizeDidChange, this._needsRefresh, this);
45         }
46     }
47
48     // Public
49
50     get records()
51     {
52         return [this._record];
53     }
54
55     get resource()
56     {
57         return this._resource;
58     }
59
60     get data()
61     {
62         if (this._cachedData)
63             return this._cachedData;
64
65         var resource = this._resource;
66         var data = {};
67
68         if (!this._includesGraph) {
69             var zeroTime = this.graphDataSource ? this.graphDataSource.zeroTime : 0;
70
71             data.domain = WebInspector.displayNameForHost(resource.urlComponents.host);
72             data.scheme = resource.urlComponents.scheme ? resource.urlComponents.scheme.toUpperCase() : "";
73             data.method = resource.requestMethod;
74             data.type = resource.type;
75             data.statusCode = resource.statusCode;
76             data.cached = resource.cached;
77             data.size = resource.size;
78             data.transferSize = resource.transferSize;
79             data.requestSent = resource.requestSentTimestamp - zeroTime;
80             data.duration = resource.receiveDuration;
81             data.latency = resource.latency;
82         }
83
84         data.graph = this._record.startTime;
85
86         this._cachedData = data;
87         return data;
88     }
89
90     createCellContent(columnIdentifier, cell)
91     {
92         var resource = this._resource;
93
94         if (resource.failed || resource.canceled || resource.statusCode >= 400)
95             cell.classList.add("error");
96
97         var value = this.data[columnIdentifier];
98
99         switch (columnIdentifier) {
100         case "name":
101             cell.classList.add(...this.iconClassNames());
102             cell.title = resource.displayURL;
103             this._updateStatus(cell);
104             return this._createNameCellDocumentFragment();
105
106         case "type":
107             return WebInspector.Resource.displayNameForType(value);
108
109         case "statusCode":
110             cell.title = resource.statusText || "";
111             return value || emDash;
112
113         case "cached":
114             return value ? WebInspector.UIString("Yes") : WebInspector.UIString("No");
115
116         case "domain":
117             return value || emDash;
118
119         case "size":
120         case "transferSize":
121             return isNaN(value) ? emDash : Number.bytesToString(value, true);
122
123         case "requestSent":
124         case "latency":
125         case "duration":
126             return isNaN(value) ? emDash : Number.secondsToString(value, true);
127         }
128
129         return super.createCellContent(columnIdentifier, cell);
130     }
131
132     refresh()
133     {
134         if (this._scheduledRefreshIdentifier) {
135             cancelAnimationFrame(this._scheduledRefreshIdentifier);
136             this._scheduledRefreshIdentifier = undefined;
137         }
138
139         this._cachedData = null;
140
141         super.refresh();
142     }
143
144     iconClassNames()
145     {
146         return [WebInspector.ResourceTreeElement.ResourceIconStyleClassName, this.resource.type];
147     }
148
149     appendContextMenuItems(contextMenu)
150     {
151         contextMenu.appendItem(WebInspector.UIString("Save File"), () => {
152             this._resource.requestContent().then(() => {
153                 WebInspector.saveDataToFile({
154                     url: this._resource.url,
155                     content: this._resource.content
156                 });
157             });
158         });
159
160         if (this._resource.urlComponents.scheme !== "data")
161             contextMenu.appendItem(WebInspector.UIString("Copy as cURL"), () => { this._resource.generateCURLCommand(); });
162     }
163
164     // Protected
165
166     filterableDataForColumn(columnIdentifier)
167     {
168         if (columnIdentifier === "name")
169             return this._resource.url;
170         return super.filterableDataForColumn(columnIdentifier);
171     }
172
173     // Private
174
175     _createNameCellDocumentFragment()
176     {
177         let fragment = document.createDocumentFragment();
178         let mainTitle = this.displayName();
179         fragment.append(mainTitle);
180
181         // Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
182         let frame = this._resource.parentFrame;
183         let isMainResource = this._resource.isMainResource();
184         let parentResourceHost;
185         if (frame && isMainResource) {
186             // When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
187             parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
188         } else if (frame) {
189             // When the resource is a normal sub-resource, get the host from the current frame's main resource.
190             parentResourceHost = frame.mainResource.urlComponents.host;
191         }
192
193         if (parentResourceHost !== this._resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
194             let subtitle = WebInspector.displayNameForHost(this._resource.urlComponents.host);
195             if (mainTitle !== subtitle) {
196                 let subtitleElement = document.createElement("span");
197                 subtitleElement.classList.add("subtitle");
198                 subtitleElement.textContent = subtitle;
199                 fragment.append(subtitleElement);
200             }
201         }
202
203         return fragment;
204     }
205
206     _needsRefresh()
207     {
208         if (this.dataGrid instanceof WebInspector.TimelineDataGrid) {
209             this.dataGrid.dataGridNodeNeedsRefresh(this);
210             return;
211         }
212
213         if (this._scheduledRefreshIdentifier)
214             return;
215
216         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
217     }
218
219     _timelineRecordUpdated(event)
220     {
221         if (this.isRecordVisible(this._record))
222             this.needsGraphRefresh();
223     }
224
225     _dataGridNodeGoToArrowClicked()
226     {
227         WebInspector.showSourceCode(this._resource);
228     }
229
230     _updateStatus(cell)
231     {
232         if (this._resource.failed)
233             cell.classList.add("error");
234         else {
235             cell.classList.remove("error");
236
237             if (this._resource.finished)
238                 this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
239         }
240
241         if (this._spinner)
242             this._spinner.element.remove();
243
244         if (this._resource.finished || this._resource.failed)
245             return;
246
247         if (!this._spinner)
248             this._spinner = new WebInspector.IndeterminateProgressSpinner;
249
250         let contentElement = cell.firstChild;
251         contentElement.appendChild(this._spinner.element);
252     }
253 };