/* * RHQ Management Platform * Copyright (C) 2005-2010 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.coregui.client.dashboard.portlets.groups; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import com.smartgwt.client.types.Alignment; import com.smartgwt.client.types.ContentsType; import com.smartgwt.client.types.Overflow; import com.smartgwt.client.types.VerticalAlignment; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.HTMLFlow; import com.smartgwt.client.widgets.Window; import com.smartgwt.client.widgets.events.CloseClickEvent; import com.smartgwt.client.widgets.events.CloseClickHandler; import com.smartgwt.client.widgets.form.DynamicForm; import com.smartgwt.client.widgets.form.events.SubmitValuesEvent; import com.smartgwt.client.widgets.form.events.SubmitValuesHandler; import com.smartgwt.client.widgets.form.fields.CanvasItem; import com.smartgwt.client.widgets.form.fields.LinkItem; import com.smartgwt.client.widgets.form.fields.StaticTextItem; import com.smartgwt.client.widgets.form.fields.events.ClickEvent; import com.smartgwt.client.widgets.form.fields.events.ClickHandler; import com.smartgwt.client.widgets.layout.VLayout; import org.rhq.core.domain.common.EntityContext; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.criteria.MeasurementScheduleCriteria; import org.rhq.core.domain.criteria.ResourceGroupCriteria; import org.rhq.core.domain.dashboard.DashboardPortlet; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementSchedule; import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite; import org.rhq.core.domain.resource.group.GroupCategory; import org.rhq.core.domain.resource.group.composite.ResourceGroupComposite; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.coregui.client.LinkManager; import org.rhq.coregui.client.components.measurement.CustomConfigMeasurementRangeEditor; import org.rhq.coregui.client.dashboard.AutoRefreshPortlet; import org.rhq.coregui.client.dashboard.AutoRefreshUtil; import org.rhq.coregui.client.dashboard.CustomSettingsPortlet; import org.rhq.coregui.client.dashboard.Portlet; import org.rhq.coregui.client.dashboard.PortletViewFactory; import org.rhq.coregui.client.dashboard.PortletWindow; import org.rhq.coregui.client.dashboard.portlets.PortletConfigurationEditorComponent; import org.rhq.coregui.client.dashboard.portlets.PortletConfigurationEditorComponent.Constant; import org.rhq.coregui.client.gwt.GWTServiceLookup; import org.rhq.coregui.client.inventory.common.detail.summary.AbstractActivityView; import org.rhq.coregui.client.inventory.common.graph.CustomDateRangeState; import org.rhq.coregui.client.inventory.groups.detail.monitoring.table.CompositeGroupD3GraphListView; import org.rhq.coregui.client.inventory.groups.detail.monitoring.table.CompositeGroupD3MultiLineGraph; import org.rhq.coregui.client.util.BrowserUtility; import org.rhq.coregui.client.util.Log; import org.rhq.coregui.client.util.async.Command; import org.rhq.coregui.client.util.async.CountDownLatch; import org.rhq.coregui.client.util.enhanced.EnhancedVLayout; /** * This portlet allows the end user to customize the metric display * * @author Simeon Pinder */ public class GroupMetricsPortlet extends EnhancedVLayout implements CustomSettingsPortlet, AutoRefreshPortlet { public static final String CHART_TITLE = MSG.common_title_metric_chart(); private EntityContext context; protected Canvas recentMeasurementsContent = new Canvas(); protected boolean currentlyLoading = false; // A non-displayed, persisted identifier for the portlet public static final String KEY = "GroupMetrics"; // A default displayed, persisted name for the portlet public static final String NAME = MSG.view_portlet_defaultName_group_metrics(); public static final String ID = "id"; // set on initial configuration, the window for this portlet view. protected PortletWindow portletWindow; //instance ui widgets protected Timer refreshTimer; private volatile List<MeasurementSchedule> enabledSchedules = null; private volatile boolean renderChart = false; // final version needed to pass to anon classes // so we can call refresh in anon callback handler final protected GroupMetricsPortlet refreshablePortlet; //defines the list of configuration elements to load/persist for this portlet protected static List<String> CONFIG_INCLUDE = new ArrayList<String>(); static { CONFIG_INCLUDE.add(Constant.METRIC_RANGE); CONFIG_INCLUDE.add(Constant.METRIC_RANGE_BEGIN_END_FLAG); CONFIG_INCLUDE.add(Constant.METRIC_RANGE_ENABLE); CONFIG_INCLUDE.add(Constant.METRIC_RANGE_LASTN); CONFIG_INCLUDE.add(Constant.METRIC_RANGE_UNIT); } public GroupMetricsPortlet(EntityContext context) { super(); this.context = context; this.refreshablePortlet = this; } @Override protected void onInit() { setRefreshing(true); initializeUi(); loadData(); } /**Defines layout for the portlet page. */ protected void initializeUi() { setPadding(5); setMembersMargin(5); addMember(recentMeasurementsContent); } /** Responsible for initialization and lazy configuration of the portlet values */ public void configure(PortletWindow portletWindow, DashboardPortlet storedPortlet) { //populate portlet configuration details if (null == this.portletWindow && null != portletWindow) { this.portletWindow = portletWindow; } if ((null == storedPortlet) || (null == storedPortlet.getConfiguration())) { return; } Configuration portletConfig = storedPortlet.getConfiguration(); //lazy init any elements not yet configured. for (String key : PortletConfigurationEditorComponent.CONFIG_PROPERTY_INITIALIZATION.keySet()) { if ((portletConfig.getSimple(key) == null) && CONFIG_INCLUDE.contains(key)) { portletConfig.put(new PropertySimple(key, PortletConfigurationEditorComponent.CONFIG_PROPERTY_INITIALIZATION.get(key))); } } } public Canvas getHelpCanvas() { return new HTMLFlow(MSG.view_portlet_help_metrics()); } public static final class Factory implements PortletViewFactory { public static final PortletViewFactory INSTANCE = new Factory(); public final Portlet getInstance(EntityContext context) { if (EntityContext.Type.ResourceGroup != context.getType()) { throw new IllegalArgumentException("Context [" + context + "] not supported by portlet"); } return new GroupMetricsPortlet(context); } } protected void loadData() { currentlyLoading = true; getRecentMetrics(); } /** Builds custom config UI, using shared widgets */ @Override public DynamicForm getCustomSettingsForm() { //root form. DynamicForm customSettings = new DynamicForm(); //embed range editor in it own container EnhancedVLayout page = new EnhancedVLayout(); final DashboardPortlet storedPortlet = this.portletWindow.getStoredPortlet(); final Configuration portletConfig = storedPortlet.getConfiguration(); final CustomConfigMeasurementRangeEditor measurementRangeEditor = PortletConfigurationEditorComponent .getMeasurementRangeEditor(portletConfig); //submit handler customSettings.addSubmitValuesHandler(new SubmitValuesHandler() { @Override public void onSubmitValues(SubmitValuesEvent event) { //retrieve range editor values Configuration updatedConfig = AbstractActivityView.saveMeasurementRangeEditorSettings( measurementRangeEditor, portletConfig); //persist storedPortlet.setConfiguration(updatedConfig); configure(portletWindow, storedPortlet); refresh(); } }); page.addMember(measurementRangeEditor); customSettings.addChild(page); return customSettings; } /** Update the range of dates in each refresh cycle of the Portlet, according to the user time range * preference, if the user sets a custom date range, this function doesn't update the dates. */ private void updateTimeRangeToNow() { CustomDateRangeState customDateRS = CustomDateRangeState.getInstance(); if (!customDateRS.isCustomDateRangeActive()) { Date now = new Date(); long timeRange = CustomDateRangeState.getInstance().getTimeRange(); Date newStartDate = new Date(now.getTime() - timeRange); // Update the date range without refreshing the CoreGui customDateRS.saveDateRange(newStartDate.getTime(), now.getTime(), false); } } /** Fetches recent metric information and updates the DynamicForm instance with i)sparkline information, * ii) link to recent metric graph for more details and iii) last metric value formatted to show significant * digits. */ protected void getRecentMetrics() { renderChart = true; //display container final VLayout column = new VLayout(); column.setHeight(10);//pack final CountDownLatch latch = CountDownLatch.create(2, new Command() { @Override public void execute() { if (enabledSchedules.isEmpty() || !renderChart) { DynamicForm row = getEmptyDataForm(); column.addMember(row); return; } //build id mapping for measurementDefinition instances Ex. Free Memory -> MeasurementDefinition[100071] final HashMap<String, MeasurementDefinition> measurementDefMap = new HashMap<String, MeasurementDefinition>(); for (MeasurementSchedule schedule : enabledSchedules) { measurementDefMap.put(schedule.getDefinition().getDisplayName(), schedule.getDefinition()); } Set<String> displayNamesSet = measurementDefMap.keySet(); //bundle definition ids for async call. int[] definitionArrayIds = new int[displayNamesSet.size()]; final String[] displayOrder = new String[displayNamesSet.size()]; displayNamesSet.toArray(displayOrder); //sort the charting data ex. Free Memory, Free Swap Space,..System Load Arrays.sort(displayOrder); //organize definitionArrayIds for ordered request on server. int index = 0; for (String definitionToDisplay : displayOrder) { definitionArrayIds[index++] = measurementDefMap.get(definitionToDisplay).getId(); } updateTimeRangeToNow(); fetchEnabledMetrics(enabledSchedules, definitionArrayIds, displayOrder, measurementDefMap, column); } }); //fetch only enabled schedules fetchEnabledSchedules(latch); //fetch the resource type fetchResourceType(latch, column); //cleanup for (Canvas child : recentMeasurementsContent.getChildren()) { child.destroy(); } recentMeasurementsContent.addChild(column); recentMeasurementsContent.markForRedraw(); } private void fetchEnabledSchedules(final CountDownLatch latch) { MeasurementScheduleCriteria criteria = new MeasurementScheduleCriteria(); criteria.addFilterEnabled(true); criteria.fetchDefinition(true); criteria.setPageControl(PageControl.getUnlimitedInstance()); addFilterKey(criteria); GWTServiceLookup.getMeasurementDataService().findMeasurementSchedulesByCriteria(criteria, new AsyncCallback<PageList<MeasurementSchedule>>() { @Override public void onSuccess(PageList<MeasurementSchedule> result) { enabledSchedules = result; latch.countDown(); } @Override public void onFailure(Throwable caught) { latch.countDown(); } }); } protected void fetchResourceType(final CountDownLatch latch, final VLayout layout) { //locate resourceGroupRef ResourceGroupCriteria criteria = new ResourceGroupCriteria(); criteria.addFilterId(context.getGroupId()); // for autoclusters and autogroups we need to add more criteria if (context.isAutoCluster()) { criteria.addFilterVisible(false); } else if (context.isAutoGroup()) { criteria.addFilterVisible(false); criteria.addFilterPrivate(true); } criteria.fetchConfigurationUpdates(false); criteria.fetchExplicitResources(false); criteria.fetchGroupDefinition(false); criteria.fetchOperationHistories(false); //locate the resource group GWTServiceLookup.getResourceGroupService().findResourceGroupCompositesByCriteria(criteria, new AsyncCallback<PageList<ResourceGroupComposite>>() { @Override public void onFailure(Throwable caught) { Log.debug("Error retrieving resource group composite for group [" + context.getGroupId() + "]:" + caught.getMessage()); setRefreshing(false); latch.countDown(); } @Override public void onSuccess(PageList<ResourceGroupComposite> results) { if (results.isEmpty() || results.get(0).getResourceGroup().getGroupCategory() != GroupCategory.COMPATIBLE) { renderChart = false; } latch.countDown(); } }); } @Override public void startRefreshCycle() { refreshTimer = AutoRefreshUtil.startRefreshCycleWithPageRefreshInterval(this, this, refreshTimer); //call out to 3rd party javascript lib BrowserUtility.graphSparkLines(); recentMeasurementsContent.markForRedraw(); } @Override protected void onDestroy() { AutoRefreshUtil.onDestroy(refreshTimer); super.onDestroy(); } @Override public boolean isRefreshing() { return this.currentlyLoading; } @Override public void refresh() { if (!isRefreshing()) { loadData(); } } protected void setRefreshing(boolean currentlyRefreshing) { this.currentlyLoading = currentlyRefreshing; } public static class ChartViewWindow extends Window { public ChartViewWindow(String title, String windowTitle, final GroupMetricsPortlet portlet) { super(); if ((windowTitle != null) && (!windowTitle.trim().isEmpty())) { setTitle(windowTitle + ": " + title); } else { setTitle(CHART_TITLE + ": " + title); } setShowMinimizeButton(false); setShowMaximizeButton(false); setShowCloseButton(true); setIsModal(true); setShowModalMask(true); setWidth(950); setHeight(550); setShowResizer(true); setCanDragResize(true); centerInPage(); addCloseClickHandler(new CloseClickHandler() { @Override public void onCloseClick(CloseClickEvent event) { try { ChartViewWindow.this.destroy(); portlet.refresh(); } catch (Throwable e) { Log.warn("Cannot destroy chart display window.", e); } } }); } } protected void fetchEnabledMetrics(List<MeasurementSchedule> schedules, int[] definitionArrayIds, final String[] displayOrder, final Map<String, MeasurementDefinition> measurementDefMap, final VLayout layout) { GWTServiceLookup.getMeasurementDataService().findDataForCompatibleGroup(context.getGroupId(), definitionArrayIds, CustomDateRangeState.getInstance().getStartTime(), CustomDateRangeState.getInstance().getEndTime(), 60, new AsyncCallback<List<List<MeasurementDataNumericHighLowComposite>>>() { @Override public void onFailure(Throwable caught) { Log.debug("Error retrieving recent metrics charting data for group [" + context.getGroupId() + "]:" + caught.getMessage()); setRefreshing(false); } @Override public void onSuccess(List<List<MeasurementDataNumericHighLowComposite>> results) { renderData(results, displayOrder, measurementDefMap, layout); } }); } protected void renderData(List<List<MeasurementDataNumericHighLowComposite>> results, String[] displayOrder, Map<String, MeasurementDefinition> measurementDefMap, VLayout layout) { if (!results.isEmpty() && !measurementDefMap.isEmpty()) { boolean someChartedData = false; layout.setWidth100(); //iterate over the retrieved charting data for (int index = 0; index < displayOrder.length; index++) { //retrieve the correct measurement definition final MeasurementDefinition md = measurementDefMap.get(displayOrder[index]); //load the data results for the given metric definition List<MeasurementDataNumericHighLowComposite> data = results.get(index); //locate last and minimum values. double lastValue = -1; double minValue = Double.MAX_VALUE; //collapse the data into comma delimited list for consumption by third party javascript library(jquery.sparkline) String commaDelimitedList = ""; for (MeasurementDataNumericHighLowComposite d : data) { if ((!Double.isNaN(d.getValue())) && (!String.valueOf(d.getValue()).contains("NaN"))) { commaDelimitedList += d.getValue() + ","; if (d.getValue() < minValue) { minValue = d.getValue(); } lastValue = d.getValue(); } } if (commaDelimitedList.lastIndexOf(",") >= 0) { DynamicForm row = new DynamicForm(); row.setNumCols(3); row.setColWidths(65, "*", 100); row.setWidth100(); row.setAutoHeight(); row.setOverflow(Overflow.VISIBLE); HTMLFlow graph = new HTMLFlow(); String contents = "<span id='sparkline_" + index + "' class='dynamicsparkline' width='0' " + "values='" + commaDelimitedList.substring(0, commaDelimitedList.lastIndexOf(",")) + "'>...</span>"; graph.setContents(contents); graph.setContentsType(ContentsType.PAGE); //disable scrollbars on span graph.setScrollbarSize(0); CanvasItem graphContainer = new CanvasItem(); graphContainer.setShowTitle(false); graphContainer.setHeight(16); graphContainer.setWidth(60); graphContainer.setCanvas(graph); final String title = md.getDisplayName(); LinkItem link = new LinkItem(); link.setLinkTitle(title); link.setShowTitle(false); link.setClipValue(false); link.setWrap(true); if (!BrowserUtility.isBrowserPreIE9()) { link.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { showPopupWithChart(title, md); } }); } else { link.disable(); } //Value String convertedValue = AbstractActivityView.convertLastValueForDisplay(lastValue, md); StaticTextItem value = AbstractActivityView.newTextItem(convertedValue); value.setVAlign(VerticalAlignment.TOP); value.setAlign(Alignment.RIGHT); value.setWidth("100%"); row.setItems(graphContainer, link, value); //if graph content returned if ((!md.getName().trim().contains("Trait.")) && (lastValue != -1)) { layout.addMember(row); someChartedData = true; } } } if (!someChartedData) {// when there are results but no chartable entries. DynamicForm row = getEmptyDataForm(); layout.addMember(row); } else { //insert see more link DynamicForm row = new DynamicForm(); String link = getSeeMoreLink(); AbstractActivityView.addSeeMoreLink(row, link, layout); } //call out to 3rd party javascript lib new Timer() { @Override public void run() { BrowserUtility.graphSparkLines(); } }.schedule(200); } else { DynamicForm row = getEmptyDataForm(); layout.addMember(row); } setRefreshing(false); } protected void showPopupWithChart(final String title, final MeasurementDefinition md) { ChartViewWindow window = new ChartViewWindow(title, "", refreshablePortlet); CompositeGroupD3GraphListView graph = new CompositeGroupD3MultiLineGraph(context, md.getId()); window.addItem(graph); graph.populateData(); window.show(); } protected DynamicForm getEmptyDataForm() { return AbstractActivityView.createEmptyDisplayRow(AbstractActivityView.RECENT_MEASUREMENTS_GROUP_NONE); } protected String getSeeMoreLink() { return LinkManager.getGroupMonitoringGraphsLink(context); } protected MeasurementScheduleCriteria addFilterKey(MeasurementScheduleCriteria criteria) { criteria.addFilterResourceGroupId(context.getGroupId()); return criteria; } }