Package tsd.client

Source Code of tsd.client.QueryUi$AdjustYRangeCheckOnClick

// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package tsd.client;

/*
* DISCLAIMER
* This my first GWT code ever, so it's most likely horribly wrong as I've had
* virtually no exposure to the technology except through the tutorial. --tsuna
*/

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.ErrorEvent;
import com.google.gwt.event.dom.client.ErrorHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.logical.shared.BeforeSelectionEvent;
import com.google.gwt.event.logical.shared.BeforeSelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.HistoryListener;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.DecoratedTabPanel;
import com.google.gwt.user.client.ui.DecoratorPanel;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
* Root class for the 'query UI'.
* Manages the entire UI, forms to query the TSDB and other misc panels.
*/
public class QueryUi implements EntryPoint, HistoryListener {
  // Some URLs we use to fetch data from the TSD.
  private static final String AGGREGATORS_URL = "/aggregators";
  private static final String LOGS_URL = "/logs?json";
  private static final String STATS_URL = "/stats?json";
  private static final String VERSION_URL = "/version?json";

  private static final DateTimeFormat FULLDATE =
    DateTimeFormat.getFormat("yyyy/MM/dd-HH:mm:ss");

  private final Label current_error = new Label();

  private final DateTimeBox start_datebox = new DateTimeBox();
  private final DateTimeBox end_datebox = new DateTimeBox();
  private final CheckBox autoreload = new CheckBox("Autoreload");
  private final ValidatedTextBox autoreoload_interval = new ValidatedTextBox();
  private Timer autoreoload_timer;

  private final ValidatedTextBox yrange = new ValidatedTextBox();
  private final ValidatedTextBox y2range = new ValidatedTextBox();
  private final CheckBox ylog = new CheckBox();
  private final CheckBox y2log = new CheckBox();
  private final TextBox ylabel = new TextBox();
  private final TextBox y2label = new TextBox();
  private final ValidatedTextBox yformat = new ValidatedTextBox();
  private final ValidatedTextBox y2format = new ValidatedTextBox();
  private final ValidatedTextBox wxh = new ValidatedTextBox();

  private String keypos = ""// Position of the key on the graph.
  private final CheckBox horizontalkey = new CheckBox("Horizontal layout");
  private final CheckBox keybox = new CheckBox("Box");
  private final CheckBox nokey = new CheckBox("No key (overrides others)");

  // Styling options.
  private final CheckBox smooth = new CheckBox();

  /**
   * Handles every change to the query form and gets a new graph.
   * Whenever the user changes one of the parameters of the graph, we want
   * to automatically get a new graph.
   */
  private final EventsHandler refreshgraph = new EventsHandler() {
    protected <H extends EventHandler> void onEvent(final DomEvent<H> event) {
      refreshGraph();
    }
  };

  final MetricForm.MetricChangeHandler metric_change_handler =
    new MetricForm.MetricChangeHandler() {
      public void onMetricChange(final MetricForm metric) {
        final int index = metrics.getWidgetIndex(metric);
        metrics.getTabBar().setTabText(index, getTabTitle(metric));
      }
      private String getTabTitle(final MetricForm metric) {
        final String metrictext = metric.getMetric();
        final int last_period = metrictext.lastIndexOf('.');
        if (last_period < 0) {
          return metrictext;
        }
        return metrictext.substring(last_period + 1);
      }
    };

  final EventsHandler updatey2range = new EventsHandler() {
      protected <H extends EventHandler> void onEvent(final DomEvent<H> event) {
        for (final Widget metric : metrics) {
          if (!(metric instanceof MetricForm)) {
            continue;
          }
          if (((MetricForm) metric).x1y2().getValue()) {
            y2range.setEnabled(true);
            y2log.setEnabled(true);
            y2label.setEnabled(true);
            y2format.setEnabled(true);
            return;
          }
        }
        y2range.setEnabled(false);
        y2log.setEnabled(false);
        y2label.setEnabled(false);
        y2format.setEnabled(false);
      }
    };

  /** List of known aggregation functions.  Fetched once from the server. */
  private final ArrayList<String> aggregators = new ArrayList<String>();

  private final DecoratedTabPanel metrics = new DecoratedTabPanel();

  /** Panel to place generated graphs and a box for zoom highlighting. */
  private final AbsolutePanel graphbox = new AbsolutePanel();
  private final Image graph = new Image();
  private final ZoomBox zoom_box = new ZoomBox();
  private final Label graphstatus = new Label();
  /** Remember the last URI requested to avoid requesting twice the same. */
  private String lastgraphuri;

  /**
   * We only send one request at a time, how many have we not sent yet?.
   * Note that we don't buffer pending requests.  When there are multiple
   * ones pending, we will only execute the last one and discard the other
   * intermediate ones, since the user is no longer interested in them.
   */
  private int pending_requests = 0;
  /** How many graph requests we make.  */
  private int nrequests = 0;

  // Other misc panels.
  private final FlexTable logs = new FlexTable();
  private final FlexTable stats_table = new FlexTable();
  private final HTML build_data = new HTML("Loading...");

  /**
   * This is the entry point method.
   */
  public void onModuleLoad() {
    asyncGetJson(AGGREGATORS_URL, new GotJsonCallback() {
      public void got(final JSONValue json) {
        // Do we need more manual type checking?  Not sure what will happen
        // in the browser if something other than an array is returned.
        final JSONArray aggs = json.isArray();
        for (int i = 0; i < aggs.size(); i++) {
          aggregators.add(aggs.get(i).isString().stringValue());
        }
        ((MetricForm) metrics.getWidget(0)).setAggregators(aggregators);
        refreshFromQueryString();
        refreshGraph();
      }
    });

    // All UI elements need to regenerate the graph when changed.
    {
      final ValueChangeHandler<Date> vch = new ValueChangeHandler<Date>() {
        public void onValueChange(final ValueChangeEvent<Date> event) {
          refreshGraph();
        }
      };
      TextBox tb = start_datebox.getTextBox();
      tb.addBlurHandler(refreshgraph);
      tb.addKeyPressHandler(refreshgraph);
      start_datebox.addValueChangeHandler(vch);
      tb = end_datebox.getTextBox();
      tb.addBlurHandler(refreshgraph);
      tb.addKeyPressHandler(refreshgraph);
      end_datebox.addValueChangeHandler(vch);
    }
    autoreoload_interval.addBlurHandler(refreshgraph);
    autoreoload_interval.addKeyPressHandler(refreshgraph);
    yrange.addBlurHandler(refreshgraph);
    yrange.addKeyPressHandler(refreshgraph);
    y2range.addBlurHandler(refreshgraph);
    y2range.addKeyPressHandler(refreshgraph);
    ylog.addClickHandler(new AdjustYRangeCheckOnClick(ylog, yrange));
    y2log.addClickHandler(new AdjustYRangeCheckOnClick(y2log, y2range));
    ylog.addClickHandler(refreshgraph);
    y2log.addClickHandler(refreshgraph);
    ylabel.addBlurHandler(refreshgraph);
    ylabel.addKeyPressHandler(refreshgraph);
    y2label.addBlurHandler(refreshgraph);
    y2label.addKeyPressHandler(refreshgraph);
    yformat.addBlurHandler(refreshgraph);
    yformat.addKeyPressHandler(refreshgraph);
    y2format.addBlurHandler(refreshgraph);
    y2format.addKeyPressHandler(refreshgraph);
    wxh.addBlurHandler(refreshgraph);
    wxh.addKeyPressHandler(refreshgraph);
    horizontalkey.addClickHandler(refreshgraph);
    keybox.addClickHandler(refreshgraph);
    nokey.addClickHandler(refreshgraph);
    smooth.addClickHandler(refreshgraph);

    yrange.setValidationRegexp("^("                            // Nothing or
                               + "|\\[([-+.0-9eE]+|\\*)?"      // "[start
                               + ":([-+.0-9eE]+|\\*)?\\])$")//   :end]"
    yrange.setVisibleLength(5);
    yrange.setMaxLength(44)// MAX=2^26=20 chars: "[-$MAX:$MAX]"
    yrange.setText("[0:]");

    y2range.setValidationRegexp("^("                            // Nothing or
                                + "|\\[([-+.0-9eE]+|\\*)?"      // "[start
                                + ":([-+.0-9eE]+|\\*)?\\])$")//   :end]"
    y2range.setVisibleLength(5);
    y2range.setMaxLength(44)// MAX=2^26=20 chars: "[-$MAX:$MAX]"
    y2range.setText("[0:]");
    y2range.setEnabled(false);
    y2log.setEnabled(false);

    ylabel.setVisibleLength(10);
    ylabel.setMaxLength(50)// Arbitrary limit.
    y2label.setVisibleLength(10);
    y2label.setMaxLength(50)// Arbitrary limit.
    y2label.setEnabled(false);

    yformat.setValidationRegexp("^(|.*%..*)$")// Nothing or at least one %?
    yformat.setVisibleLength(10);
    yformat.setMaxLength(16)// Arbitrary limit.
    y2format.setValidationRegexp("^(|.*%..*)$")// Nothing or at least one %?
    y2format.setVisibleLength(10);
    y2format.setMaxLength(16)// Arbitrary limit.
    y2format.setEnabled(false);

    wxh.setValidationRegexp("^[1-9][0-9]{2,}x[1-9][0-9]{2,}$")// 100x100
    wxh.setVisibleLength(9);
    wxh.setMaxLength(11)// 99999x99999
    wxh.setText((Window.getClientWidth() - 20) + "x"
                + (Window.getClientHeight() * 4 / 5));

    final FlexTable table = new FlexTable();
    table.setText(0, 0, "From");
    {
      final HorizontalPanel hbox = new HorizontalPanel();
      hbox.add(new InlineLabel("To"));
      final Anchor now = new Anchor("(now)");
      now.addClickHandler(new ClickHandler() {
        public void onClick(final ClickEvent event) {
          end_datebox.setValue(new Date());
          refreshGraph();
        }
      });
      hbox.add(now);
      hbox.add(autoreload);
      hbox.setWidth("100%");
      table.setWidget(0, 1, hbox);
    }
    autoreload.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
      @Override
      public void onValueChange(final ValueChangeEvent<Boolean> event) {
        if (autoreload.getValue()) {
          final HorizontalPanel hbox = new HorizontalPanel();
          hbox.setWidth("100%");
          hbox.add(new InlineLabel("Every:"));
          hbox.add(autoreoload_interval);
          hbox.add(new InlineLabel("seconds"));
          table.setWidget(1, 1, hbox);
          if (autoreoload_interval.getValue().isEmpty()) {
            autoreoload_interval.setValue("15");
          }
          autoreoload_interval.setFocus(true);
          lastgraphuri = ""// Force refreshGraph.
          refreshGraph();     // Trigger the 1st auto-reload
        } else {
          table.setWidget(1, 1, end_datebox);
        }
      }
    });
    autoreoload_interval.setValidationRegexp("^([5-9]|[1-9][0-9]+)$")// >=5s
    autoreoload_interval.setMaxLength(4);
    autoreoload_interval.setVisibleLength(8);

    table.setWidget(1, 0, start_datebox);
    table.setWidget(1, 1, end_datebox);
    {
      final HorizontalPanel hbox = new HorizontalPanel();
      hbox.add(new InlineLabel("WxH:"));
      hbox.add(wxh);
      table.setWidget(0, 3, hbox);
    }
    {
      addMetricForm("metric 1", 0);
      metrics.selectTab(0);
      metrics.add(new InlineLabel("Loading..."), "+");
      metrics.addBeforeSelectionHandler(new BeforeSelectionHandler<Integer>() {
        public void onBeforeSelection(final BeforeSelectionEvent<Integer> event) {
          final int item = event.getItem();
          final int nitems = metrics.getWidgetCount();
          if (item == nitems - 1) {  // Last item: the "+" was clicked.
            event.cancel();
            final MetricForm metric = addMetricForm("metric " + nitems, item);
            metrics.selectTab(item);
            metric.setFocus(true);
          }
        }
      });
      table.setWidget(2, 0, metrics);
    }
    table.getFlexCellFormatter().setColSpan(2, 0, 2);
    table.getFlexCellFormatter().setRowSpan(1, 3, 2);
    final DecoratedTabPanel optpanel = new DecoratedTabPanel();
    optpanel.add(makeAxesPanel(), "Axes");
    optpanel.add(makeKeyPanel(), "Key");
    optpanel.add(makeStylePanel(), "Style");
    optpanel.selectTab(0);
    table.setWidget(1, 3, optpanel);

    final DecoratorPanel decorator = new DecoratorPanel();
    decorator.setWidget(table);
    final VerticalPanel graphpanel = new VerticalPanel();
    graphpanel.add(decorator);
    {
      final VerticalPanel graphvbox = new VerticalPanel();
      graphvbox.add(graphstatus);

      graph.setVisible(false);

      // Put the graph image element and the zoombox elements inside the absolute panel
      graphbox.add(graph, 0, 0);
      zoom_box.setVisible(false);
      graphbox.add(zoom_box, 0, 0);
      graph.addMouseOverHandler(new MouseOverHandler() {
        public void onMouseOver(final MouseOverEvent event) {
          final Style style = graphbox.getElement().getStyle();
          style.setCursor(Cursor.CROSSHAIR);
        }
      });
      graph.addMouseOutHandler(new MouseOutHandler() {
        public void onMouseOut(final MouseOutEvent event) {
          final Style style = graphbox.getElement().getStyle();
          style.setCursor(Cursor.AUTO);
        }
      });

      graphvbox.add(graphbox);
      graph.addErrorHandler(new ErrorHandler() {
        public void onError(final ErrorEvent event) {
          graphstatus.setText("Oops, failed to load the graph.");
        }
      });
      graph.addLoadHandler(new LoadHandler() {
        public void onLoad(final LoadEvent event) {
          graphbox.setWidth(graph.getWidth() + "px");
          graphbox.setHeight(graph.getHeight() + "px");
        }
      });

      graphpanel.add(graphvbox);
    }
    final DecoratedTabPanel mainpanel = new DecoratedTabPanel();
    mainpanel.setWidth("100%");
    mainpanel.add(graphpanel, "Graph");
    mainpanel.add(stats_table, "Stats");
    mainpanel.add(logs, "Logs");
    mainpanel.add(build_data, "Version");
    mainpanel.selectTab(0);
    mainpanel.addBeforeSelectionHandler(new BeforeSelectionHandler<Integer>() {
      public void onBeforeSelection(final BeforeSelectionEvent<Integer> event) {
        clearError();
        final int item = event.getItem();
        switch (item) {
          case 1: refreshStats(); return;
          case 2: refreshLogs(); return;
          case 3: refreshVersion(); return;
        }
      }
    });
    final VerticalPanel root = new VerticalPanel();
    root.setWidth("100%");
    root.add(current_error);
    current_error.setVisible(false);
    current_error.addStyleName("dateBoxFormatError");
    root.add(mainpanel);
    RootPanel.get("queryuimain").add(root);
    // Must be done at the end, once all the widgets are attached.
    ensureSameWidgetSize(optpanel);

    History.addHistoryListener(this);
  }

  @Override
  public void onHistoryChanged(String historyToken) {
    refreshFromQueryString();
    refreshGraph();
  }

  /** Additional styling options.  */
  private Grid makeStylePanel() {
    final Grid grid = new Grid(5, 3);
    grid.setText(0, 1, "Smooth");
    grid.setWidget(0, 2, smooth);
    return grid;
  }

  /**
   * Builds the panel containing customizations for the axes of the graph.
   */
  private Grid makeAxesPanel() {
    final Grid grid = new Grid(5, 3);
    grid.setText(0, 1, "Y");
    grid.setText(0, 2, "Y2");
    setTextAlignCenter(grid.getRowFormatter().getElement(0));
    grid.setText(1, 0, "Label");
    grid.setWidget(1, 1, ylabel);
    grid.setWidget(1, 2, y2label);
    grid.setText(2, 0, "Format");
    grid.setWidget(2, 1, yformat);
    grid.setWidget(2, 2, y2format);
    grid.setText(3, 0, "Range");
    grid.setWidget(3, 1, yrange);
    grid.setWidget(3, 2, y2range);
    grid.setText(4, 0, "Log scale");
    grid.setWidget(4, 1, ylog);
    grid.setWidget(4, 2, y2log);
    setTextAlignCenter(grid.getCellFormatter().getElement(4, 1));
    setTextAlignCenter(grid.getCellFormatter().getElement(4, 2));
    return grid;
  }

  private MetricForm addMetricForm(final String label, final int item) {
    final MetricForm metric = new MetricForm(refreshgraph);
    metric.x1y2().addClickHandler(updatey2range);
    metric.setMetricChangeHandler(metric_change_handler);
    metric.setAggregators(aggregators);
    metrics.insert(metric, label, item);
    return metric;
  }

  private final HashMap<String, RadioButton> keypos_map =
    new HashMap<String, RadioButton>(17);

  /**
   * Small helper to build a radio button used to change the position of the
   * key of the graph.
   */
  private RadioButton addKeyRadioButton(final Grid grid,
                                        final int row, final int col,
                                        final String pos) {
    final RadioButton rb = new RadioButton("keypos");
    rb.addClickHandler(new ClickHandler() {
      public void onClick(final ClickEvent event) {
        keypos = pos;
      }
    });
    rb.addClickHandler(refreshgraph);
    grid.setWidget(row, col, rb);
    keypos_map.put(pos, rb);
    return rb;
  }

  /**
   * Builds the panel containing the customizations for the key of the graph.
   */
  private Widget makeKeyPanel() {
    final Grid grid = new Grid(5, 5);
    addKeyRadioButton(grid, 0, 0, "out left top");
    addKeyRadioButton(grid, 0, 2, "out center top");
    addKeyRadioButton(grid, 0, 4, "out right top");
    addKeyRadioButton(grid, 1, 1, "top left");
    addKeyRadioButton(grid, 1, 2, "top center");
    addKeyRadioButton(grid, 1, 3, "top right").setValue(true);
    addKeyRadioButton(grid, 2, 0, "out center left");
    addKeyRadioButton(grid, 2, 1, "center left");
    addKeyRadioButton(grid, 2, 2, "center");
    addKeyRadioButton(grid, 2, 3, "center right");
    addKeyRadioButton(grid, 2, 4, "out center right");
    addKeyRadioButton(grid, 3, 1, "bottom left");
    addKeyRadioButton(grid, 3, 2, "bottom center");
    addKeyRadioButton(grid, 3, 3, "bottom right");
    addKeyRadioButton(grid, 4, 0, "out bottom left");
    addKeyRadioButton(grid, 4, 2, "out bottom center");
    addKeyRadioButton(grid, 4, 4, "out bottom right");
    final Grid.CellFormatter cf = grid.getCellFormatter();
    cf.getElement(1, 1).getStyle().setProperty("borderLeft", "1px solid #000");
    cf.getElement(1, 1).getStyle().setProperty("borderTop", "1px solid #000");
    cf.getElement(1, 2).getStyle().setProperty("borderTop", "1px solid #000");
    cf.getElement(1, 3).getStyle().setProperty("borderTop", "1px solid #000");
    cf.getElement(1, 3).getStyle().setProperty("borderRight", "1px solid #000");
    cf.getElement(2, 1).getStyle().setProperty("borderLeft", "1px solid #000");
    cf.getElement(2, 3).getStyle().setProperty("borderRight", "1px solid #000");
    cf.getElement(3, 1).getStyle().setProperty("borderLeft", "1px solid #000");
    cf.getElement(3, 1).getStyle().setProperty("borderBottom", "1px solid #000");
    cf.getElement(3, 2).getStyle().setProperty("borderBottom", "1px solid #000");
    cf.getElement(3, 3).getStyle().setProperty("borderBottom", "1px solid #000");
    cf.getElement(3, 3).getStyle().setProperty("borderRight", "1px solid #000");
    final VerticalPanel vbox = new VerticalPanel();
    vbox.add(new InlineLabel("Key location:"));
    vbox.add(grid);
    vbox.add(horizontalkey);
    keybox.setValue(true);
    vbox.add(keybox);
    vbox.add(nokey);
    return vbox;
  }

  private void refreshStats() {
    asyncGetJson(STATS_URL, new GotJsonCallback() {
      public void got(final JSONValue json) {
        final JSONArray stats = json.isArray();
        final int nstats = stats.size();
        for (int i = 0; i < nstats; i++) {
          final String stat = stats.get(i).isString().stringValue();
          String part = stat.substring(0, stat.indexOf(' '));
          stats_table.setText(i, 0, part)// metric
          int pos = part.length() + 1;
          part = stat.substring(pos, stat.indexOf(' ', pos));
          stats_table.setText(i, 1, part)// timestamp
          pos += part.length() + 1;
          part = stat.substring(pos, stat.indexOf(' ', pos));
          stats_table.setText(i, 2, part)// value
          pos += part.length() + 1;
          stats_table.setText(i, 3, stat.substring(pos))// tags
        }
      }
    });
  }

  private void refreshVersion() {
    asyncGetJson(VERSION_URL, new GotJsonCallback() {
      public void got(final JSONValue json) {
        final JSONObject bd = json.isObject();
        final JSONString shortrev = bd.get("short_revision").isString();
        final JSONString status = bd.get("repo_status").isString();
        final JSONString stamp = bd.get("timestamp").isString();
        final JSONString user = bd.get("user").isString();
        final JSONString host = bd.get("host").isString();
        final JSONString repo = bd.get("repo").isString();
        final JSONString version = bd.get("version").isString();
        build_data.setHTML(
          "OpenTSDB version [" + version.stringValue() + "] built from revision "
          + shortrev.stringValue()
          + " in a " + status.stringValue() + " state<br/>"
          + "Built on " + new Date((Long.parseLong(stamp.stringValue()) * 1000))
          + " by " + user.stringValue() + '@' + host.stringValue()
          + ':' + repo.stringValue());
      }
    });
  }

  private void refreshLogs() {
    asyncGetJson(LOGS_URL, new GotJsonCallback() {
      public void got(final JSONValue json) {
        final JSONArray logmsgs = json.isArray();
        final int nmsgs = logmsgs.size();
        final FlexTable.FlexCellFormatter fcf = logs.getFlexCellFormatter();
        final FlexTable.RowFormatter rf = logs.getRowFormatter();
        for (int i = 0; i < nmsgs; i++) {
          final String msg = logmsgs.get(i).isString().stringValue();
          String part = msg.substring(0, msg.indexOf('\t'));
          logs.setText(i * 2, 0,
                       new Date(Integer.valueOf(part) * 1000L).toString());
          logs.setText(i * 2 + 1, 0, "")// So we can change the style ahead.
          int pos = part.length() + 1;
          part = msg.substring(pos, msg.indexOf('\t', pos));
          if ("WARN".equals(part)) {
            rf.getElement(i * 2).getStyle().setBackgroundColor("#FCC");
            rf.getElement(i * 2 + 1).getStyle().setBackgroundColor("#FCC");
          } else if ("ERROR".equals(part)) {
            rf.getElement(i * 2).getStyle().setBackgroundColor("#F99");
            rf.getElement(i * 2 + 1).getStyle().setBackgroundColor("#F99");
          } else {
            rf.getElement(i * 2).getStyle().clearBackgroundColor();
            rf.getElement(i * 2 + 1).getStyle().clearBackgroundColor();
            if ((i % 2) == 0) {
              rf.addStyleName(i * 2, "subg");
              rf.addStyleName(i * 2 + 1, "subg");
            }
          }
          pos += part.length() + 1;
          logs.setText(i * 2, 1, part); // level
          part = msg.substring(pos, msg.indexOf('\t', pos));
          pos += part.length() + 1;
          logs.setText(i * 2, 2, part); // thread
          part = msg.substring(pos, msg.indexOf('\t', pos));
          pos += part.length() + 1;
          if (part.startsWith("net.opentsdb.")) {
            part = part.substring(13);
          } else if (part.startsWith("org.hbase.")) {
            part = part.substring(10);
          }
          logs.setText(i * 2, 3, part); // logger
          logs.setText(i * 2 + 1, 0, msg.substring(pos)); // message
          fcf.setColSpan(i * 2 + 1, 0, 4);
          rf.addStyleName(i * 2, "fwf");
          rf.addStyleName(i * 2 + 1, "fwf");
        }
      }
    });
  }

  private void addLabels(final StringBuilder url) {
    final String ylabel = this.ylabel.getText();
    if (!ylabel.isEmpty()) {
      url.append("&ylabel=").append(ylabel);
    }
    if (y2label.isEnabled()) {
      final String y2label = this.y2label.getText();
      if (!y2label.isEmpty()) {
        url.append("&y2label=").append(y2label);
      }
    }
  }

  private void addFormats(final StringBuilder url) {
    final String yformat = this.yformat.getText();
    if (!yformat.isEmpty()) {
      url.append("&yformat=").append(yformat);
    }
    if (y2format.isEnabled()) {
      final String y2format = this.y2format.getText();
      if (!y2format.isEmpty()) {
        url.append("&y2format=").append(y2format);
      }
    }
  }

  private void addYRanges(final StringBuilder url) {
    final String yrange = this.yrange.getText();
    if (!yrange.isEmpty()) {
      url.append("&yrange=").append(yrange);
    }
    if (y2range.isEnabled()) {
      final String y2range = this.y2range.getText();
      if (!y2range.isEmpty()) {
        url.append("&y2range=").append(y2range);
      }
    }
  }

  private void addLogscales(final StringBuilder url) {
    if (ylog.getValue()) {
      url.append("&ylog");
    }
    if (y2log.isEnabled() && y2log.getValue()) {
      url.append("&y2log");
    }
  }

  /**
   * Maybe sets the text of a {@link TextBox} from a query string parameter.
   * @param qs A parsed query string.
   * @param key Name of the query string parameter.
   * If this parameter wasn't passed, the {@link TextBox} will be emptied.
   * @param tb The {@link TextBox} to change.
   */
  private static void maybeSetTextbox(final QueryString qs,
                                      final String key,
                                      final TextBox tb) {
    final ArrayList<String> values = qs.get(key);
    if (values == null) {
      tb.setText("");
      return;
    }
    tb.setText(values.get(0));
  }

  /**
   * Sets the text of a {@link TextBox} from a query string parameter.
   * @param qs A parsed query string.
   * @param key Name of the query string parameter.
   * @param tb The {@link TextBox} to change.
   */
  private static void setTextbox(final QueryString qs,
                                 final String key,
                                 final TextBox tb) {
    final ArrayList<String> values = qs.get(key);
    if (values != null) {
      tb.setText(values.get(0));
    }
  }

  private static QueryString getQueryString(final String qs) {
    return qs.isEmpty() ? new QueryString() : QueryString.decode(qs);
  }

  private void refreshFromQueryString() {
    final QueryString qs = getQueryString(History.getToken());

    maybeSetTextbox(qs, "start", start_datebox.getTextBox());
    maybeSetTextbox(qs, "end", end_datebox.getTextBox());
    setTextbox(qs, "wxh", wxh);
    autoreload.setValue(qs.containsKey("autoreload"), true);
    maybeSetTextbox(qs, "autoreload", autoreoload_interval);

    final ArrayList<String> newmetrics = qs.get("m");
    if (newmetrics == null) {  // Clear all metric forms.
      final int toremove = metrics.getWidgetCount() - 1;
      addMetricForm("metric 1", 0);
      metrics.selectTab(0);
      for (int i = 0; i < toremove; i++) {
        metrics.remove(1);
      }
      return;
    }
    final int n = newmetrics.size()// We want this many metrics.
    ArrayList<String> options = qs.get("o");
    if (options == null) {
      options = new ArrayList<String>(n);
    }
    for (int i = options.size(); i < n; i++) {  // Make both arrays equal size.
      options.add("")// Add missing o's.
    }

    for (int i = 0; i < newmetrics.size(); ++i) {
      if (i == metrics.getWidgetCount() - 1) {
        addMetricForm("", i);
      }

      final MetricForm metric = (MetricForm) metrics.getWidget(i);
      metric.updateFromQueryString(newmetrics.get(i), options.get(i));
    }
    // Remove extra metric forms.
    final int m = metrics.getWidgetCount() - 1; // We have this many metrics.
    int showing = metrics.getTabBar().getSelectedTab()// Currently selected.
    for (int i = m - 1; i >= n; i--) {
      if (showing == i) {  // If we're about to remove the currently selected,
        metrics.selectTab(--showing)// fix focus to not wind up nowhere.
      }
      metrics.remove(i);
    }
    updatey2range.onEvent(null);

    maybeSetTextbox(qs, "ylabel", ylabel);
    maybeSetTextbox(qs, "y2label", y2label);
    maybeSetTextbox(qs, "yformat", yformat);
    maybeSetTextbox(qs, "y2format", y2format);
    maybeSetTextbox(qs, "yrange", yrange);
    maybeSetTextbox(qs, "y2range", y2range);
    ylog.setValue(qs.containsKey("ylog"));
    y2log.setValue(qs.containsKey("y2log"));

    if (qs.containsKey("key")) {
      final String key = qs.getFirst("key");
      keybox.setValue(key.contains(" box"));
      horizontalkey.setValue(key.contains(" horiz"));
      keypos = key.replaceAll(" (box|horiz\\w*)", "");
      keypos_map.get(keypos).setChecked(true);
    } else {
      keybox.setValue(false);
      horizontalkey.setValue(false);
      keypos_map.get("top right").setChecked(true);
      keypos = "";
    }
    nokey.setValue(qs.containsKey("nokey"));
    smooth.setValue(qs.containsKey("smooth"));
  }

  private void refreshGraph() {
    final Date start = start_datebox.getValue();
    if (start == null) {
      graphstatus.setText("Please specify a start time.");
      return;
    }
    final Date end = end_datebox.getValue();
    if (end != null && !autoreload.getValue()) {
      if (end.getTime() <= start.getTime()) {
        end_datebox.addStyleName("dateBoxFormatError");
        graphstatus.setText("End time must be after start time!");
        return;
      }
    }
    final StringBuilder url = new StringBuilder();
    url.append("/q?start=");
    final String start_text = start_datebox.getTextBox().getText();
    if (start_text.endsWith(" ago") || start_text.endsWith("-ago")) {
      url.append(start_text);
    } else {
      url.append(FULLDATE.format(start));
    }
    if (end != null && !autoreload.getValue()) {
      url.append("&end=");
      final String end_text = end_datebox.getTextBox().getText();
      if (end_text.endsWith(" ago") || end_text.endsWith("-ago")) {
        url.append(end_text);
      } else {
        url.append(FULLDATE.format(end));
      }
    } else {
      // If there's no end-time, the graph may change while the URL remains
      // the same.  No browser seems to re-fetch an image once it's been
      // fetched, even if we destroy the DOM object and re-created it with the
      // same src attribute.  This has nothing to do with caching headers sent
      // by the server.  The browsers simply won't retrieve the same URL again
      // through JavaScript manipulations, period.  So as a workaround, we add
      // a special parameter that the server will delete from the query.
      url.append("&ignore=" + nrequests++);
    }
    if (!addAllMetrics(url)) {
      return;
    }
    addLabels(url);
    addFormats(url);
    addYRanges(url);
    addLogscales(url);
    if (nokey.getValue()) {
      url.append("&nokey");
    } else if (!keypos.isEmpty() || horizontalkey.getValue()) {
      url.append("&key=");
      if (!keypos.isEmpty()) {
        url.append(keypos);
      }
      if (horizontalkey.getValue()) {
        url.append(" horiz");
      }
      if (keybox.getValue()) {
        url.append(" box");
      }
    }
    url.append("&wxh=").append(wxh.getText());
    if (smooth.getValue()) {
      url.append("&smooth=csplines");
    }
    final String unencodedUri = url.toString();
    final String uri = URL.encode(unencodedUri);
    if (uri.equals(lastgraphuri)) {
      return// Don't re-request the same graph.
    } else if (pending_requests++ > 0) {
      return;
    }
    lastgraphuri = uri;
    graphstatus.setText("Loading graph...");
    asyncGetJson(uri + "&json", new GotJsonCallback() {
      public void got(final JSONValue json) {
        if (autoreoload_timer != null) {
          autoreoload_timer.cancel();
          autoreoload_timer = null;
        }
        final JSONObject result = json.isObject();
        final JSONValue err = result.get("err");
        String msg = "";
        if (err != null) {
          displayError("An error occurred while generating the graph: "
                       + err.isString().stringValue());
          graphstatus.setText("Please correct the error above.");
        } else {
          clearError();

          String history = unencodedUri.substring(3)      // Remove "/q?".
            .replaceFirst("ignore=[^&]*&", "")// Unnecessary cruft.
          if (autoreload.getValue()) {
            history += "&autoreload=" + autoreoload_interval.getText();
          }
          if (!history.equals(History.getToken())) {
            History.newItem(history, false);
          }

          final JSONValue nplotted = result.get("plotted");
          final JSONValue cachehit = result.get("cachehit");
          if (cachehit != null) {
            msg += "Cache hit (" + cachehit.isString().stringValue() + "). ";
          }
          if (nplotted != null && nplotted.isNumber().doubleValue() > 0) {
            graph.setUrl(uri + "&png");
            graph.setVisible(true);

            msg += result.get("points").isNumber() + " points retrieved, "
              + nplotted + " points plotted";
          } else {
            graph.setVisible(false);
            msg += "Your query didn't return anything";
          }
          final JSONValue timing = result.get("timing");
          if (timing != null) {
            msg += " in " + timing + "ms.";
          } else {
            msg += '.';
          }
        }
        final JSONValue info = result.get("info");
        if (info != null) {
          if (!msg.isEmpty()) {
            msg += ' ';
          }
          msg += info.isString().stringValue();
        }
        graphstatus.setText(msg);
        if (result.get("etags") != null) {
          final JSONArray etags = result.get("etags").isArray();
          final int netags = etags.size();
          for (int i = 0; i < netags; i++) {
            if (i >= metrics.getWidgetCount()) {
              break;
            }
            final Widget widget = metrics.getWidget(i);
            if (!(widget instanceof MetricForm)) {
              break;
            }
            final MetricForm metric = (MetricForm) widget;
            final JSONArray tags = etags.get(i).isArray();
            final int ntags = tags.size();
            for (int j = 0; j < ntags; j++) {
              metric.autoSuggestTag(tags.get(j).isString().stringValue());
            }
          }
        }
        if (autoreload.getValue()) {
          final int reload_in = Integer.parseInt(autoreoload_interval.getValue());
          if (reload_in >= 5) {
            autoreoload_timer = new Timer() {
              public void run() {
                // Verify that we still want auto reload and that the graph
                // hasn't been updated in the mean time.
                if (autoreload.getValue() && lastgraphuri == uri) {
                  // Force refreshGraph to believe that we want a new graph.
                  lastgraphuri = "";
                  refreshGraph();
                }
              }
            };
            autoreoload_timer.schedule(reload_in * 1000);
          }
        }
        if (--pending_requests > 0) {
          pending_requests = 0;
          refreshGraph();
        }
      }
    });
  }

  private boolean addAllMetrics(final StringBuilder url) {
    boolean found_metric = false;
    for (final Widget widget : metrics) {
      if (!(widget instanceof MetricForm)) {
        continue;
      }
      final MetricForm metric = (MetricForm) widget;
      found_metric |= metric.buildQueryString(url);
    }
    if (!found_metric) {
      graphstatus.setText("Please specify a metric.");
    }
    return found_metric;
  }

  private void asyncGetJson(final String url, final GotJsonCallback callback) {
    final RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
    try {
      builder.sendRequest(null, new RequestCallback() {
        public void onError(final Request request, final Throwable e) {
          displayError("Failed to get " + url + ": " + e.getMessage());
          // Since we don't call the callback we've been given, reset this
          // bit of state as we're not going to retry anything right now.
          pending_requests = 0;
        }

        public void onResponseReceived(final Request request,
                                       final Response response) {
          final int code = response.getStatusCode();
          if (code == Response.SC_OK) {
            clearError();
            callback.got(JSONParser.parse(response.getText()));
            return;
          } else if (code >= Response.SC_BAD_REQUEST) {  // 400+ => Oops.
            // Since we don't call the callback we've been given, reset this
            // bit of state as we're not going to retry anything right now.
            pending_requests = 0;
            String err = response.getText();
            // If the response looks like a JSON object, it probably contains
            // an error message.
            if (!err.isEmpty() && err.charAt(0) == '{') {
              final JSONValue json = JSONParser.parse(err);
              final JSONObject result = json == null ? null : json.isObject();
              final JSONValue jerr = result == null ? null : result.get("err");
              final JSONString serr = jerr == null ? null : jerr.isString();
              err = serr.stringValue();
              // If the error message has multiple lines (which is common if
              // it contains a stack trace), show only the first line and
              // hide the rest in a panel users can expand.
              final int newline = err.indexOf('\n', 1);
              final String msg = "Request failed: " + response.getStatusText();
              if (newline < 0) {
                displayError(msg + ": " + err);
              } else {
                displayError(msg);
                final DisclosurePanel dp =
                  new DisclosurePanel(err.substring(0, newline));
                RootPanel.get("queryuimain").add(dp)// Attach the widget.
                final InlineLabel content =
                  new InlineLabel(err.substring(newline, err.length()));
                content.addStyleName("fwf")// For readable stack traces.
                dp.setContent(content);
                current_error.getElement().appendChild(dp.getElement());
              }
            } else {
              displayError("Request failed while getting " + url + ": "
                           + response.getStatusText());
              // Since we don't call the callback we've been given, reset this
              // bit of state as we're not going to retry anything right now.
              pending_requests = 0;
            }
            graphstatus.setText("");
          }
        }
      });
    } catch (RequestException e) {
      displayError("Failed to get " + url + ": " + e.getMessage());
    }
  }

  private void displayError(final String errmsg) {
    current_error.setText(errmsg);
    current_error.setVisible(true);
  }

  private void clearError() {
    current_error.setVisible(false);
  }

  static void setTextAlignCenter(final Element element) {
    element.getStyle().setProperty("textAlign", "center");
  }

  /** Zoom box and associated event handlers.  */
  private final class ZoomBox extends HTML
    implements MouseUpHandler, MouseMoveHandler, MouseDownHandler {

    /** "Fudge factor" to account for the axes present on the image. */
    private static final int OFFSET_WITH_AXIS = 45;
    private static final int OFFSET_WITHOUT_AXIS = 15;

    private boolean zoom_selection_active = false;
    /** Rectangle of the selection.  */
    private int start_x;
    private int end_x;
    private int start_y;
    private int end_y;

    private HandlerRegistration graph_move_handler;
    private HandlerRegistration box_move_handler;

    ZoomBox() {
      // Set ourselves up as the event handler for all mouse-draggable events.
      graph.addMouseDownHandler(this);
      graph.addMouseUpHandler(this);

      // Also add the handlers on the actual zoom highlight box (this is in
      // case the cursor gets on the zoombox, so that it keeps responding
      // correctly).
      super.addMouseUpHandler(this);

      final Style style = super.getElement().getStyle();
      style.setProperty("background", "red");
      style.setProperty("filter", "alpha(opacity=50)");
      style.setProperty("opacity", "0.4");
      // Needed to make this object focusable.
      super.getElement().setAttribute("tabindex", "-1");
    }

    @Override
    public void onMouseDown(final MouseDownEvent event) {
      event.preventDefault();

      // Check if the zoom selection is active, if so, it's possible that the
      // mouse left the browser mid-selection and got stuck enabled even
      // though the mouse isn't still pressed. If that's the case, do a similar
      // operation to the onMouseUp event.
      if (zoom_selection_active) {
        endSelection(event);
        return;
      }

      final Element image = graph.getElement();
      zoom_selection_active = true;
      start_x = event.getRelativeX(image);
      start_y = event.getRelativeY(image);
      end_x = 0;
      end_y = 0;

      graphbox.setWidgetPosition(this, start_x, start_y);
      super.setWidth("0px");
      super.setHeight("0px");
      super.setVisible(true);
      // Workaround to steal the focus from whatever had it previously,
      // which may cause the graph to reload as a side effect.
      super.getElement().focus();

      graph_move_handler = graph.addMouseMoveHandler(this);
      box_move_handler = super.addMouseMoveHandler(this);
    }

    @Override
    public void onMouseMove(final MouseMoveEvent event) {
      event.preventDefault();

      final int x = event.getRelativeX(graph.getElement());
      final int y = event.getRelativeY(graph.getElement());
      int left;
      int top;
      int width;
      int height;

      // Figure out the top, left, height, and width of the box based
      // on current cursor location.
      if (x < start_x) {
        left = x;
        width = start_x - x;
      } else {
        left = start_x;
        width = x - start_x;
      }
      if (y < start_y) {
        top = y;
        height = start_y - y;
      } else {
        top = start_y;
        height = y - start_y;
      }

      // Resize / move the box as needed based on cursor location.
      super.setVisible(false);
      graphbox.setWidgetPosition(this, left, top);
      super.setWidth(width + "px");
      super.setHeight(height + "px");
      super.setVisible(true);
    }

    @Override
    public void onMouseUp(final MouseUpEvent event) {
      if (zoom_selection_active) {
        endSelection(event);
      }
    }

    /**
     * Perform operations for when a user completes their selection.
     * This involves removing the highlight box and kicking off the
     * zoom in operation.
     * @param event The event that triggered the end of the selection.
     */
    private <H extends EventHandler> void endSelection(final MouseEvent<H> event) {
      zoom_selection_active = false;

      // Stop tracking cursor movements to improve performance.
      graph_move_handler.removeHandler();
      graph_move_handler = null;
      box_move_handler.removeHandler();
      box_move_handler = null;

      final Element image = graph.getElement();
      end_x = event.getRelativeX(image);
      end_y = event.getRelativeY(image);

      // Hide the zoom box
      super.setVisible(false);
      super.setWidth("0px");
      super.setHeight("0px");

      // Calculate the true start/end points of the zoom area selected by
      // mouse. If the mouse was dragged left on the graph before being
      // let up, then start_x is the right-most edge of the zoomable area.
      // If the mouse was dragged right on the graph before being let up,
      // then start_x is the left-most edge of the zoomable area.
      if (start_x < end_x) {
        start_x = start_x - OFFSET_WITH_AXIS;
        end_x = end_x - OFFSET_WITH_AXIS;
      } else {
        final int saved_start = start_x;
        start_x = end_x - OFFSET_WITH_AXIS;
        end_x = saved_start - OFFSET_WITH_AXIS;
      }
      int actual_width = graph.getWidth() - OFFSET_WITH_AXIS;
      if (y2range.isEnabled()) {  // If we have a second Y axis.
        actual_width -= OFFSET_WITH_AXIS;
      } else {
        actual_width -= OFFSET_WITHOUT_AXIS;
      }

      // Prevent division by zero if image is pathologically small.
      // or: Prevent changing anything if the distance the cursor traveled was
      // too small (as happens during a simple click or unintentional click).
      if (actual_width < 1 || end_x - start_x <= 5) {
        return;
      }

      // Total span of time represented between the start and end times.
      final long duration;
      final long start = start_datebox.getValue().getTime();
      {
        final long end;
        final Date end_date = end_datebox.getValue();
        if (end_date != null) {
          end = end_date.getTime();
        } else {
          end = new Date().getTime();
        }
        duration = end - start;
      }

      // Get the start and end positions of the mouse drag operation on the
      // image as a percentage of the image size.
      final long start_change = start_x * duration / actual_width;
      final long end_change = end_x * duration / actual_width;

      start_datebox.setValue(new Date(start + start_change));
      end_datebox.setValue(new Date(start + end_change));
      refreshGraph();
    }

  };

  private final class AdjustYRangeCheckOnClick implements ClickHandler {

    private final CheckBox box;
    private final ValidatedTextBox range;

    public AdjustYRangeCheckOnClick(final CheckBox box,
                                    final ValidatedTextBox range) {
      this.box = box;
      this.range = range;
    }

    public void onClick(final ClickEvent event) {
      if (box.isEnabled() && box.getValue()
          && "[0:]".equals(range.getValue())) {
        range.setValue("[1:]");
      } else if (box.isEnabled() && !box.getValue()
                 && "[1:]".equals(range.getValue())) {
        range.setValue("[0:]");
      }
    }

  };

  /**
   * Ensures all the widgets in the given panel have the same size.
   * Otherwise by default the panel will automatically resize itself to the
   * contents of the currently active panel's widget, which is annoying
   * because it makes a number of things move around in the UI.
   * @param panel The panel containing the widgets to resize.
   */
  private static void ensureSameWidgetSize(final DecoratedTabPanel panel) {
    if (!panel.isAttached()) {
      throw new IllegalArgumentException("panel not attached: " + panel);
    }
    int maxw = 0;
    int maxh = 0;
    for (final Widget widget : panel) {
      final int w = widget.getOffsetWidth();
      final int h = widget.getOffsetHeight();
      if (w > maxw) {
        maxw = w;
      }
      if (h > maxh) {
        maxh = h;
      }
    }
    if (maxw == 0 || maxh == 0) {
      throw new IllegalArgumentException("maxw=" + maxw + " maxh=" + maxh);
    }
    for (final Widget widget : panel) {
      setOffsetWidth(widget, maxw);
      setOffsetHeight(widget, maxh);
    }
  }

  /**
   * Properly sets the total width of a widget.
   * This takes into account decorations such as border, margin, and padding.
   */
  private static void setOffsetWidth(final Widget widget, int width) {
    widget.setWidth(width + "px");
    final int offset = widget.getOffsetWidth();
    if (offset > 0) {
      width -= offset - width;
      if (width > 0) {
        widget.setWidth(width + "px");
      }
    }
  }

  /**
   * Properly sets the total height of a widget.
   * This takes into account decorations such as border, margin, and padding.
   */
  private static void setOffsetHeight(final Widget widget, int height) {
    widget.setHeight(height + "px");
    final int offset = widget.getOffsetHeight();
    if (offset > 0) {
      height -= offset - height;
      if (height > 0) {
        widget.setHeight(height + "px");
      }
    }
  }

}
TOP

Related Classes of tsd.client.QueryUi$AdjustYRangeCheckOnClick

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.