Index: tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/static/forms.js |
diff --git a/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/static/forms.js b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/static/forms.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3c59252234f58e7a7806bd97eed06820a9ff6752 |
--- /dev/null |
+++ b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/static/forms.js |
@@ -0,0 +1,685 @@ |
+// Copyright 2010 Google Inc. |
+// |
+// Licensed under the Apache License, Version 2.0 (the "License"); |
+// you may not use this file except in compliance with the License. |
+// You may obtain a copy of the License at |
+// |
+// http://www.apache.org/licenses/LICENSE-2.0 |
+// |
+// Unless required by applicable law or agreed to in writing, software |
+// distributed under the License is distributed on an "AS IS" BASIS, |
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+// See the License for the specific language governing permissions and |
+// limitations under the License. |
+// |
+ |
+/** |
+ * @fileoverview Render form appropriate for RPC method. |
+ * @author rafek@google.com (Rafe Kaplan) |
+ */ |
+ |
+ |
+var FORM_VISIBILITY = { |
+ SHOW_FORM: 'Show Form', |
+ HIDE_FORM: 'Hide Form' |
+}; |
+ |
+ |
+var LABEL = { |
+ OPTIONAL: 'OPTIONAL', |
+ REQUIRED: 'REQUIRED', |
+ REPEATED: 'REPEATED' |
+}; |
+ |
+ |
+var objectId = 0; |
+ |
+ |
+/** |
+ * Variants defined in protorpc/messages.py. |
+ */ |
+var VARIANT = { |
+ DOUBLE: 'DOUBLE', |
+ FLOAT: 'FLOAT', |
+ INT64: 'INT64', |
+ UINT64: 'UINT64', |
+ INT32: 'INT32', |
+ BOOL: 'BOOL', |
+ STRING: 'STRING', |
+ MESSAGE: 'MESSAGE', |
+ BYTES: 'BYTES', |
+ UINT32: 'UINT32', |
+ ENUM: 'ENUM', |
+ SINT32: 'SINT32', |
+ SINT64: 'SINT64' |
+}; |
+ |
+ |
+/** |
+ * Data structure used to represent a form to data element. |
+ * @param {Object} field Field descriptor that form element represents. |
+ * @param {Object} container Element that contains field. |
+ * @return {FormElement} New object representing a form element. Element |
+ * starts enabled. |
+ * @constructor |
+ */ |
+function FormElement(field, container) { |
+ this.field = field; |
+ this.container = container; |
+ this.enabled = true; |
+} |
+ |
+ |
+/** |
+ * Display error message in error panel. |
+ * @param {string} message Message to display in panel. |
+ */ |
+function error(message) { |
+ $('<div>').appendTo($('#error-messages')).text(message); |
+} |
+ |
+ |
+/** |
+ * Display request errors in error panel. |
+ * @param {object} XMLHttpRequest object. |
+ */ |
+function handleRequestError(response) { |
+ var contentType = response.getResponseHeader('content-type'); |
+ if (contentType == 'application/json') { |
+ var response_error = $.parseJSON(response.responseText); |
+ var error_message = response_error.error_message; |
+ if (error.state == 'APPLICATION_ERROR' && error.error_name) { |
+ error_message = error_message + ' (' + error.error_name + ')'; |
+ } |
+ } else { |
+ error_message = '' + response.status + ': ' + response.statusText; |
+ } |
+ |
+ error(error_message); |
+} |
+ |
+ |
+/** |
+ * Send JSON RPC to remote method. |
+ * @param {string} path Path of service on originating server to send request. |
+ * @param {string} method Name of method to invoke. |
+ * @param {Object} request Message to send as request. |
+ * @param {function} on_success Function to call upon successful request. |
+ */ |
+function sendRequest(path, method, request, onSuccess) { |
+ $.ajax({url: path + '.' + method, |
+ type: 'POST', |
+ contentType: 'application/json', |
+ data: $.toJSON(request), |
+ dataType: 'json', |
+ success: onSuccess, |
+ error: handleRequestError |
+ }); |
+} |
+ |
+ |
+/** |
+ * Create callback that enables and disables field element when associated |
+ * checkbox is clicked. |
+ * @param {Element} checkbox Checkbox that will be clicked. |
+ * @param {FormElement} form Form element that will be toggled for editing. |
+ * @param {Object} disableMessage HTML element to display in place of element. |
+ * @return Callback that is invoked every time checkbox is clicked. |
+ */ |
+function toggleInput(checkbox, form, disableMessage) { |
+ return function() { |
+ var checked = checkbox.checked; |
+ if (checked) { |
+ buildIndividualForm(form); |
+ form.enabled = true; |
+ disableMessage.hide(); |
+ } else { |
+ form.display.empty(); |
+ form.enabled = false; |
+ disableMessage.show(); |
+ } |
+ }; |
+} |
+ |
+ |
+/** |
+ * Build an enum field. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildEnumField(form) { |
+ form.descriptor = enumDescriptors[form.field.type_name]; |
+ form.input = $('<select>'). |
+ appendTo(form.display); |
+ |
+ $('<option>'). |
+ appendTo(form.input).attr('value', ''). |
+ text('Select enum'); |
+ $.each(form.descriptor.values, function(index, enumValue) { |
+ option = $('<option>'); |
+ option. |
+ appendTo(form.input). |
+ attr('value', enumValue.name). |
+ text(enumValue.name); |
+ if (enumValue.number == form.field.default_value) { |
+ option.attr('selected', 1); |
+ } |
+ }); |
+} |
+ |
+ |
+/** |
+ * Build nested message field. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildMessageField(form) { |
+ form.table = $('<table border="1">').appendTo(form.display); |
+ buildMessageForm(form, messageDescriptors[form.field.type_name]); |
+} |
+ |
+ |
+/** |
+ * Build boolean field. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildBooleanField(form) { |
+ form.input = $('<input type="checkbox">'); |
+ form.input[0].checked = Boolean(form.field.default_value); |
+} |
+ |
+ |
+/** |
+ * Build text field. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildTextField(form) { |
+ form.input = $('<input type="text">'); |
+ form.input. |
+ attr('value', form.field.default_value || ''); |
+} |
+ |
+ |
+/** |
+ * Build individual input element. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildIndividualForm(form) { |
+ form.required = form.label == LABEL.REQUIRED; |
+ |
+ if (form.field.variant == VARIANT.ENUM) { |
+ buildEnumField(form); |
+ } else if (form.field.variant == VARIANT.MESSAGE) { |
+ buildMessageField(form); |
+ } else if (form.field.variant == VARIANT.BOOL) { |
+ buildBooleanField(form); |
+ } else { |
+ buildTextField(form); |
+ } |
+ |
+ form.display.append(form.input); |
+ |
+ // TODO: Handle base64 encoding for BYTES field. |
+ if (form.field.variant == VARIANT.BYTES) { |
+ $("<i>use base64 encoding</i>").appendTo(form.display); |
+ } |
+} |
+ |
+ |
+/** |
+ * Add repeated field. This function is called when an item is added |
+ * @param {FormElement} form Repeated form element to create item for. |
+ */ |
+function addRepeatedFieldItem(form) { |
+ var row = $('<tr>').appendTo(form.display); |
+ subForm = new FormElement(form.field, row); |
+ form.fields.push(subForm); |
+ buildFieldForm(subForm, false); |
+} |
+ |
+ |
+/** |
+ * Build repeated field. Contains a button that can be used for adding new |
+ * items. |
+ * @param {FormElement} form Form to build element for. |
+ */ |
+function buildRepeatedForm(form) { |
+ form.fields = []; |
+ form.display = $('<table border="1" width="100%">'). |
+ appendTo(form.container); |
+ var header_row = $('<tr>').appendTo(form.display); |
+ var header = $('<td colspan="3">').appendTo(header_row); |
+ var add_button = $('<button>').text('+').appendTo(header); |
+ |
+ add_button.click(function() { |
+ addRepeatedFieldItem(form); |
+ }); |
+} |
+ |
+ |
+/** |
+ * Build a form field. Populates form content with values required by |
+ * all fields. |
+ * @param {FormElement} form Repeated form element to create item for. |
+ * @param allowRepeated {Boolean} Allow display of repeated field. If set to |
+ * to true, will treat repeated fields as individual items of a repeated |
+ * field and render it as an individual field. |
+ */ |
+function buildFieldForm(form, allowRepeated) { |
+ // All form fields are added to a row of a table. |
+ var inputData = $('<td>'); |
+ |
+ // Set name. |
+ if (allowRepeated) { |
+ var nameData = $('<td>'); |
+ nameData.text(form.field.name + ':'); |
+ form.container.append(nameData); |
+ } |
+ |
+ // Set input. |
+ form.repeated = form.field.label == LABEL.REPEATED; |
+ if (allowRepeated && form.repeated) { |
+ inputData.attr('colspan', '2'); |
+ buildRepeatedForm(form); |
+ } else { |
+ if (!allowRepeated) { |
+ inputData.attr('colspan', '2'); |
+ } |
+ |
+ form.display = $('<div>'); |
+ |
+ var controlData = $('<td>'); |
+ if (form.field.label != LABEL.REQUIRED && allowRepeated) { |
+ form.enabled = false; |
+ var checkbox_id = 'checkbox-' + objectId; |
+ objectId++; |
+ $('<label for="' + checkbox_id + '">Enabled</label>').appendTo(controlData); |
+ var checkbox = $('<input id="' + checkbox_id + '" type="checkbox">').appendTo(controlData); |
+ var disableMessage = $('<div>').appendTo(inputData); |
+ checkbox.change(toggleInput(checkbox[0], form, disableMessage)); |
+ } else { |
+ buildIndividualForm(form); |
+ } |
+ |
+ if (form.repeated) { |
+ // TODO: Implement deletion of repeated items. Needs to delete |
+ // from DOM and also delete from form model. |
+ } |
+ |
+ form.container.append(controlData); |
+ } |
+ |
+ inputData.append(form.display); |
+ form.container.append(inputData); |
+} |
+ |
+ |
+/** |
+ * Top level function for building an entire message form. Called once at form |
+ * creation and may be called again for nested message fields. Constructs a |
+ * a table and builds a row for each sub-field. |
+ * @params {FormElement} form Form to build message form for. |
+ */ |
+function buildMessageForm(form, messageType) { |
+ form.fields = []; |
+ form.descriptor = messageType; |
+ if (messageType.fields) { |
+ $.each(messageType.fields, function(index, field) { |
+ var row = $('<tr>').appendTo(form.table); |
+ var fieldForm = new FormElement(field, row); |
+ fieldForm.parent = form; |
+ buildFieldForm(fieldForm, true); |
+ form.fields.push(fieldForm); |
+ }); |
+ } |
+} |
+ |
+ |
+/** |
+ * HTML Escape a string |
+ */ |
+function htmlEscape(value) { |
+ if (typeof(value) == "string") { |
+ return value |
+ .replace(/&/g, '&') |
+ .replace(/>/g, '>') |
+ .replace(/</g, '<') |
+ .replace(/"/g, '"') |
+ .replace(/'/g, ''') |
+ .replace(/ /g, ' '); |
+ } else { |
+ return value; |
+ } |
+} |
+ |
+ |
+/** |
+ * JSON formatted in HTML for display to users. This method recursively calls |
+ * itself to render sub-JSON objects. |
+ * @param {Object} value JSON object to format for display. |
+ * @param {Integer} indent Indentation level for object being displayed. |
+ * @return {string} Formatted JSON object. |
+ */ |
+function formatJSON(value, indent) { |
+ var indentation = ''; |
+ for (var index = 0; index < indent; ++index) { |
+ indentation = indentation + ' '; |
+ } |
+ var type = typeof(value); |
+ |
+ var result = ''; |
+ |
+ if (type == 'object') { |
+ if (value.constructor === Array) { |
+ result += '[<br>'; |
+ $.each(value, function(index, item) { |
+ result += indentation + formatJSON(item, indent + 1) + ',<br>'; |
+ }); |
+ result += indentation + ']'; |
+ } else { |
+ result += '{<br>'; |
+ $.each(value, function(name, item) { |
+ result += (indentation + htmlEscape(name) + ': ' + |
+ formatJSON(item, indent + 1) + ',<br>'); |
+ }); |
+ result += indentation + '}'; |
+ } |
+ } else { |
+ result += htmlEscape(value); |
+ } |
+ |
+ return result; |
+} |
+ |
+ |
+/** |
+ * Construct array from repeated form element. |
+ * @param {FormElement} form Form element to build array from. |
+ * @return {Array} Array of repeated elements read from input form. |
+ */ |
+function fromRepeatedForm(form) { |
+ var values = []; |
+ $.each(form.fields, function(index, subForm) { |
+ values.push(fromIndividualForm(subForm)); |
+ }); |
+ return values; |
+} |
+ |
+ |
+/** |
+ * Construct value from individual form element. |
+ * @param {FormElement} form Form element to get value from. |
+ * @return {string, Float, Integer, Boolean, object} Value extracted from |
+ * individual field. The type depends on the field variant. |
+ */ |
+function fromIndividualForm(form) { |
+ switch(form.field.variant) { |
+ case VARIANT.MESSAGE: |
+ return fromMessageForm(form); |
+ break; |
+ |
+ case VARIANT.DOUBLE: |
+ case VARIANT.FLOAT: |
+ return parseFloat(form.input.val()); |
+ |
+ case VARIANT.BOOL: |
+ return form.input[0].checked; |
+ break; |
+ |
+ case VARIANT.ENUM: |
+ case VARIANT.STRING: |
+ case VARIANT.BYTES: |
+ return form.input.val(); |
+ |
+ default: |
+ break; |
+ } |
+ return parseInt(form.input.val(), 10); |
+} |
+ |
+ |
+/** |
+ * Extract entire message from a complete form. |
+ * @param {FormElement} form Form to extract message from. |
+ * @return {Object} Fully populated message object ready to transmit |
+ * as JSON message. |
+ */ |
+function fromMessageForm(form) { |
+ var message = {}; |
+ $.each(form.fields, function(index, subForm) { |
+ if (subForm.enabled) { |
+ var subMessage = undefined; |
+ if (subForm.field.label == LABEL.REPEATED) { |
+ subMessage = fromRepeatedForm(subForm); |
+ } else { |
+ subMessage = fromIndividualForm(subForm); |
+ } |
+ |
+ message[subForm.field.name] = subMessage; |
+ } |
+ }); |
+ |
+ return message; |
+} |
+ |
+ |
+/** |
+ * Send form as an RPC. Extracts message from root form and transmits to |
+ * originating ProtoRPC server. Response is formatted as JSON and displayed |
+ * to user. |
+ */ |
+function sendForm() { |
+ $('#error-messages').empty(); |
+ $('#form-response').empty(); |
+ message = fromMessageForm(root_form); |
+ if (message === null) { |
+ return; |
+ } |
+ |
+ sendRequest(servicePath, methodName, message, function(response) { |
+ $('#form-response').html(formatJSON(response, 0)); |
+ hideForm(); |
+ }); |
+} |
+ |
+ |
+/** |
+ * Reset form to original state. Deletes existing form and rebuilds a new |
+ * one from scratch. |
+ */ |
+function resetForm() { |
+ var panel = $('#form-panel'); |
+ var serviceType = serviceMap[servicePath]; |
+ var service = serviceDescriptors[serviceType]; |
+ |
+ panel.empty(); |
+ |
+ function formGenerationError(message) { |
+ error(message); |
+ panel.html('<div class="error-message">' + |
+ 'There was an error generating the service form' + |
+ '</div>'); |
+ } |
+ |
+ // Find method. |
+ var requestTypeName = null; |
+ $.each(service.methods, function(index, method) { |
+ if (method.name == methodName) { |
+ requestTypeName = method.request_type; |
+ } |
+ }); |
+ |
+ if (!requestTypeName) { |
+ formGenerationError('No such method definition for: ' + methodName); |
+ return; |
+ } |
+ |
+ requestType = messageDescriptors[requestTypeName]; |
+ if (!requestType) { |
+ formGenerationError('No such message-type: ' + requestTypeName); |
+ return; |
+ } |
+ |
+ var root = $('<table border="1">'). |
+ appendTo(panel); |
+ |
+ root_form = new FormElement(null, null); |
+ root_form.table = root; |
+ buildMessageForm(root_form, requestType); |
+ $('<button>').appendTo(panel).text('Send Request').click(sendForm); |
+ $('<button>').appendTo(panel).text('Reset').click(resetForm); |
+} |
+ |
+ |
+/** |
+ * Hide main RPC form from user. The information in the form is preserved. |
+ * Called after RPC to server is completed. |
+ */ |
+function hideForm() { |
+ var expander = $('#form-expander'); |
+ var formPanel = $('#form-panel'); |
+ formPanel.hide(); |
+ expander.text(FORM_VISIBILITY.SHOW_FORM); |
+} |
+ |
+ |
+/** |
+ * Toggle the display of the main RPC form. Called when form expander button |
+ * is clicked. |
+ */ |
+function toggleForm() { |
+ var expander = $('#form-expander'); |
+ var formPanel = $('#form-panel'); |
+ if (expander.text() == FORM_VISIBILITY.HIDE_FORM) { |
+ hideForm(); |
+ } else { |
+ formPanel.show(); |
+ expander.text(FORM_VISIBILITY.HIDE_FORM); |
+ } |
+} |
+ |
+ |
+/** |
+ * Create form. Called after all service information and file sets have been |
+ * loaded. |
+ */ |
+function createForm() { |
+ $('#form-expander').click(toggleForm); |
+ resetForm(); |
+} |
+ |
+ |
+/** |
+ * Display available services and their methods. |
+ */ |
+function showMethods() { |
+ var methodSelector = $('#method-selector'); |
+ if (serviceMap) { |
+ $.each(serviceMap, function(serviceName) { |
+ var descriptor = serviceDescriptors[serviceMap[serviceName]]; |
+ methodSelector.append(descriptor.name); |
+ var block = $('<blockquote>').appendTo(methodSelector); |
+ $.each(descriptor.methods, function(index, method) { |
+ var url = (formPath + '?path=' + serviceName + |
+ '&method=' + method.name); |
+ var label = serviceName + '.' + method.name; |
+ $('<a>').attr('href', url).text(label).appendTo(block); |
+ $('<br>').appendTo(block); |
+ }); |
+ }); |
+ } |
+} |
+ |
+ |
+/** |
+ * Populate map of fully qualified message names to descriptors. This method |
+ * is called recursively to populate message definitions nested within other |
+ * message definitions. |
+ * @param {Object} messages Array of message descriptors as returned from the |
+ * RegistryService.get_file_set call. |
+ * @param {string} container messages may be an Array of messages nested within |
+ * either a FileDescriptor or a MessageDescriptor. The container is the |
+ * fully qualified name of the file descriptor or message descriptor so |
+ * that the fully qualified name of the messages in the list may be |
+ * constructed. |
+ */ |
+function populateMessages(messages, container) { |
+ if (messages) { |
+ $.each(messages, function(messageIndex, message) { |
+ var messageName = container + '.' + message.name; |
+ messageDescriptors[messageName] = message; |
+ |
+ if (message.message_types) { |
+ populateMessages(message.message_types, messageName); |
+ } |
+ |
+ if (message.enum_types) { |
+ $.each(message.enum_types, function(enumIndex, enumerated) { |
+ var enumName = messageName + '.' + enumerated.name; |
+ enumDescriptors[enumName] = enumerated; |
+ }); |
+ } |
+ }); |
+ } |
+} |
+ |
+ |
+/** |
+ * Populates all descriptors from a FileSet descriptor. Each of the three |
+ * descriptor collections (service, message and enum) map the fully qualified |
+ * name of a definition to it's descriptor. |
+ */ |
+function populateDescriptors(file_set) { |
+ serviceDescriptors = {}; |
+ messageDescriptors = {}; |
+ enumDescriptors = {}; |
+ $.each(file_set.files, function(index, file) { |
+ if (file.service_types) { |
+ $.each(file.service_types, function(serviceIndex, service) { |
+ var serviceName = file['package'] + '.' + service.name; |
+ serviceDescriptors[serviceName] = service; |
+ }); |
+ } |
+ |
+ populateMessages(file.message_types, file['package']); |
+ }); |
+} |
+ |
+ |
+/** |
+ * Load all file sets from ProtoRPC registry service. |
+ * @param {function} when_done Called after all file sets are loaded. |
+ */ |
+function loadFileSets(when_done) { |
+ var paths = []; |
+ $.each(serviceMap, function(serviceName) { |
+ paths.push(serviceName); |
+ }); |
+ |
+ sendRequest( |
+ registryPath, |
+ 'get_file_set', |
+ {'names': paths}, |
+ function(response) { |
+ populateDescriptors(response.file_set, when_done); |
+ when_done(); |
+ }); |
+} |
+ |
+ |
+/** |
+ * Load all services from ProtoRPC registry service. When services are |
+ * loaded, will then load all file_sets from the server. |
+ * @param {function} when_done Called after all file sets are loaded. |
+ */ |
+function loadServices(when_done) { |
+ sendRequest( |
+ registryPath, |
+ 'services', |
+ {}, |
+ function(response) { |
+ serviceMap = {}; |
+ $.each(response.services, function(index, service) { |
+ serviceMap[service.name] = service.definition; |
+ }); |
+ loadFileSets(when_done); |
+ }); |
+} |