Changeset 11053
- Timestamp:
- 06/27/14 16:39:43 (10 years ago)
- Location:
- branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3
- Files:
-
- 1 deleted
- 14 edited
Legend:
- Unmodified
- Added
- Removed
-
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/App_Code/ChartHelper.cshtml
r11036 r11053 160 160 $.ajax({ 161 161 async: false, url: "@(new HtmlString(url))" + GetRequest, datatype: "json", success: function(result) { 162 if( result.length==0) {162 if(@(userName) == null) { 163 163 $("#@container").append( 164 '<section class="chartContainer"><h1 class="title">' + @userName + ' has no tasks for the specified filters!</h1></section>' 164 '<section class="chartContainer">' + 165 '<h1 class="title">Please select a user!</h1>' + 166 '</section>' 165 167 ) 166 168 } 167 var waitTime; 168 var transferTime; 169 var runTime; 170 var seriesDescriptions = ["Time waiting","Time transferring","Time calculating"]; 171 172 //Checks if multipage display, if it is then trims results to 173 //the results for the page to be displayed 174 @ChartHelper.NumberPages("result",limit,container,functionName,pageNumber) 175 176 //Globally accesible, for use when resizing, eliminates extra DB request 177 window["numberTasks"] = result.length; 178 179 //Set display of all errors to none, errors matching the tasks will be reset below 180 $(".errorContainer, .errorTitle, .errorTask, .errorMessage").css('display','none'); 181 182 //For each result create a seperate collapsable section with a chart and info label 183 for(var i = 0; i < result.length; i++){ 184 $("#" + "@container").append( 185 '<section class="chartContainer"><h1 class="title" id="' + result[i].Key + '">Task ' + result[i].Key + 186 '</h1><button class="collapse" onclick="collapseSection(this)">+</button><div id="@(destinationTag)' + i + 187 '"></div><label id="@(destinationTag)' + i + 'Info"></label></section>' 169 else if(result.length==0) { 170 $("#@container").append( 171 '<section class="chartContainer">' + 172 '<h1 class="title">' + @userName + ' has no tasks for the specified filters!</h1>' + 173 '</section>' 188 174 ) 189 //Re-enables the error display if any of the tasks on the page have Ids matching 190 //those of errors 191 $(".errorTask").each(function() { 192 if($(this).html()==result[i].Key) { 193 $("#" + result[i].Key).css("color","red"); 194 $("#" + result[i].Key).append(" - ERROR"); 195 $(".errorContainer, .errorTitle, .underline").css('display','inline-block'); 196 $(this).css('display','inline-block'); 197 $(this).next().css('display','inline-block'); 198 } 199 }); 200 waitTime = [result[i].Value[0]]; 201 transferTime = [result[i].Value[1]]; 202 runTime = [result[i].Value[2]]; 203 window["@(destinationTag)Plot" + i] = $.jqplot("@destinationTag" + i, [waitTime,transferTime,runTime], { 204 seriesDefaults:{ 205 renderer:$.jqplot.BarRenderer, 206 shadowAngle: 135, 207 pointLabels: {show: true, formatString: '%.3f'} 208 }, 209 series:[ 210 {label:'Waiting'}, 211 {label:'Transferring'}, 212 {label:'Calculating'} 213 ], 214 legend: { 215 show: true, 216 location: 'e', 217 placement: 'outside' 218 }, 219 axes: { 220 xaxis: { 221 renderer: $.jqplot.CategoryAxisRenderer, 222 showLabel: false, 223 pad: 0 175 } 176 else { 177 var waitTime; 178 var transferTime; 179 var runTime; 180 var seriesDescriptions = ["Time waiting","Time transferring","Time calculating"]; 181 182 //Checks if multipage display, if it is then trims results to 183 //the results for the page to be displayed 184 @ChartHelper.NumberPages("result",limit,container,functionName,pageNumber) 185 186 //Globally accesible, for use when resizing, eliminates extra DB request 187 window["numberTasks"] = result.length; 188 189 //Set display of all errors to none, errors matching the tasks will be reset below 190 $("#@(container)").find(".errorContainer, .errorTitle, .errorTask, .errorMessage").css('display','none'); 191 192 //For each result create a seperate collapsable section with a chart and info label 193 for(var i = 0; i < result.length; i++){ 194 $("#" + "@container").append( 195 '<section class="chartContainer">' + 196 '<h1 class="title" id="@(container)' + result[i].Key + '">Task ' + result[i].Key + '</h1>' + 197 '<button class="collapse" onclick="CollapseSection(this)">+</button>' + 198 '<div id="@(destinationTag)Plot' + i + '"></div>' + 199 '<label id="@(destinationTag)PlotInfo' + i + '"></label>' + 200 '</section>' 201 ) 202 //Re-enables the error display if any of the tasks on the page have Ids matching 203 //those of errors 204 $(".errorTask").each(function() { 205 if($(this).html()==result[i].Key) { 206 $("#@(container)" + result[i].Key).css("color","red"); 207 $("#@(container)" + result[i].Key).append(" - ERROR"); 208 $(".errorContainer, .errorTitle, .underline").css('display','inline-block'); 209 $(this).css('display','inline-block'); 210 $(this).next().css('display','inline-block'); 211 } 212 }); 213 waitTime = [result[i].Value[0]]; 214 transferTime = [result[i].Value[1]]; 215 runTime = [result[i].Value[2]]; 216 window["@(destinationTag)Plot" + i] = $.jqplot("@(destinationTag)Plot" + i, [waitTime,transferTime,runTime], { 217 seriesDefaults:{ 218 renderer:$.jqplot.BarRenderer, 219 shadowAngle: 135, 220 pointLabels: {show: true, formatString: '%.3f'} 224 221 }, 225 yaxis: { 226 pad: 0 222 series:[ 223 {label:'Waiting'}, 224 {label:'Transferring'}, 225 {label:'Calculating'} 226 ], 227 legend: { 228 show: true, 229 location: 'e', 230 placement: 'outside' 231 }, 232 axes: { 233 xaxis: { 234 renderer: $.jqplot.CategoryAxisRenderer, 235 showLabel: false, 236 pad: 0 237 }, 238 yaxis: { 239 pad: 0 240 } 241 }, 242 cursor: { 243 showTooltip: false 227 244 } 228 }, 229 cursor: { 230 showTooltip: false 231 } 232 }); 233 /* Bind a datalistener to each chart and display details of clicked 234 upon data in the label below the chart */ 235 $("#" + "@(destinationTag)" + i).bind('jqplotDataClick', function (ev, seriesIndex, pointIndex, data) { 236 $("#" + $(this).attr('id') + "Info").html(seriesDescriptions[seriesIndex] + ': ' + data[1]); 237 }); 238 239 collapsedByDefault(document.getElementById("@(destinationTag)" + i)); 245 }); 246 /* Bind a datalistener to each chart and display details of clicked 247 upon data in the label below the chart */ 248 $("#" + "@(destinationTag)Plot" + i).bind('jqplotDataClick', function (ev, seriesIndex, pointIndex, data) { 249 $(this).siblings("label").html(seriesDescriptions[seriesIndex] + ": " + data[1]); 250 }); 251 252 CollapsedByDefault(document.getElementById("@(container)" + result[i].Key)); 253 } 240 254 } 241 255 }}); … … 248 262 //Resize event, resets bar width for task charts and replots them 249 263 $(window).resize(function() { 250 for(var i = 0; i < numberTasks; i++) { 251 $.each(window[ "@(destinationTag)Plot" + i].series, function(index, series) { 252 series.barWidth = undefined; 253 }); 254 window["@(destinationTag)Plot" + i].replot({ resetAxes: true }); 264 if(typeof numberTasks !== 'undefined') { 265 for(var i = 0; i < numberTasks; i++) { 266 $.each(window[ "@(destinationTag)Plot" + i].series, function(index, series) { 267 series.barWidth = undefined; 268 }); 269 window["@(destinationTag)Plot" + i].replot({ resetAxes: true }); 270 } 255 271 } 256 272 }); … … 269 285 //Used to return a string containing the names of the series 270 286 //to be used in plot creation 271 function getSeries(numberData,dataName){287 function GetSeries(numberData,dataName){ 272 288 var result = "["; 273 289 for(i=0; i < numberData; i++) { … … 348 364 //Declares the jqPlot variable, evals the string of series returned 349 365 //from getSeries which allows for any number of series 350 window[ "@(destinationTag)Plot"] = $.jqplot('@destinationTag', eval(getSeries(@(dataName)CurrentValue.length,"@(dataName)Data")), @(destinationTag)PlotOptions); 351 </text> 352 } 353 354 @helper UpdateStreamChart(string dataName, string destinationTag, string url, string fixedY = null) 355 { 366 window[ "@(destinationTag)Plot"] = $.jqplot('@destinationTag', eval(GetSeries(@(dataName)CurrentValue.length,"@(dataName)Data")), @(destinationTag)PlotOptions); 367 </text> 368 } 369 370 @helper UpdateStreamChart(string dataName, string destinationTag, string url, string fixedY = null) { 356 371 <text> 357 372 //If the data is beyond chartSize use shift to trim one off the end … … 390 405 //Re-assigns the jqPlot variable, evals the string of series returned 391 406 //from getSeries which allows for any number of series 392 @(destinationTag)Plot = $.jqplot('@destinationTag', eval(getSeries(@(dataName)CurrentValue.length,"@(dataName)Data")), @(destinationTag)PlotOptions); 393 </text> 394 } 395 396 @helper SlaveInfoChart(string destinationTag, string url, string limit, string startDate, string endDate, string userName, string functionName, string pageNumber=null) 397 { 407 @(destinationTag)Plot = $.jqplot('@destinationTag', eval(GetSeries(@(dataName)CurrentValue.length,"@(dataName)Data")), @(destinationTag)PlotOptions); 408 </text> 409 } 410 411 @helper SlaveInfoChart(string destinationTag, string url, string limit, string startDate, string endDate, string userName, string functionName, string pageNumber=null, string slaveId=null) { 398 412 <text> 399 413 var GetRequest = "?limit=" + @(limit); … … 413 427 @:} 414 428 } 429 @if (slaveId != null) { 430 @:if(@(slaveId)!=null) { 431 @:GetRequest += "&slaveId=" + @slaveId; 432 @:} 433 } 415 434 $.ajax({ 416 435 async: false, url: "@(new HtmlString(url))" + GetRequest, datatype: "json", success: function(result) { 417 $("#@(destinationTag)").html(""); 418 var time = new Date(); 419 420 //Checks if multipage display, if it is then trims results to 421 //the results for the page to be displayed 422 @ChartHelper.NumberPages("result",limit,destinationTag,functionName,pageNumber) 423 424 for(i = 0; i < result.length; i++) { 425 var coreSeries = []; 426 coreSeries[0] = []; 427 coreSeries[1] = []; 428 var memorySeries = []; 429 memorySeries[0] = []; 430 memorySeries[1] = []; 431 var cpuSeries = []; 432 cpuSeries[0] = []; 433 $("#@(destinationTag)").append('<section class="chartContainer"><h1 class="title" id="' + result[i][0].SlaveID + '">Slave ' 434 + result[i][0].SlaveID + '</h1><button class="collapse" onclick="collapseSection(this)">+</button>' + 435 '<div id="TotalUsedCores' + result[i][0].SlaveID + '"></div><div id="TotalUsedMemory' + result[i][0].SlaveID + '"></div>' + 436 '<div id="CPUUtilization' + result[i][0].SlaveID + '"></div></section>'); 436 437 //Set chart names for use in creation below, must be set identically in 438 //ResizeSlaves 439 var slaveChartNames = ["TotalUsedCores","TotalUsedMemory","CPUUtilization"]; 440 441 if(result.length == 0) { 442 $('#' + "@(destinationTag)").append( 443 '<section class="chartContainer">' + 444 '<h1 class="title">No slave information for the specified filters!</h1>' + 445 '</section>' 446 ) 447 } 448 else { 449 $('#' + "@(destinationTag)").html(""); 450 var time = new Date(); 451 452 //Checks if multipage display, if it is then trims results to 453 //the results for the page to be displayed 454 @ChartHelper.NumberPages("result",limit,destinationTag,functionName,pageNumber) 455 456 //Globally accesible, for use when resizing, eliminates extra DB request 457 window["numberSlaves"] = result.length; 458 459 for(i = 0; i < result.length; i++) { 460 var coreSeries = []; 461 coreSeries[0] = []; 462 coreSeries[1] = []; 463 var memorySeries = []; 464 memorySeries[0] = []; 465 memorySeries[1] = []; 466 var cpuSeries = []; 467 cpuSeries[0] = []; 468 $('#' + "@(destinationTag)").append( 469 '<section class="chartContainer">' + 470 '<h1 class="title" id="' + result[i][0].SlaveID + '">Slave ' + result[i][0].SlaveID + '</h1>' + 471 '<button class="collapse" onclick="CollapseSection(this)">+</button>' + 472 '<div id="' + slaveChartNames[0] + i + '"></div>' + 473 '<div id="' + slaveChartNames[1] + i + '"></div>' + 474 '<div id="' + slaveChartNames[2] + i + '"></div>' + 475 '</section>'); 437 476 for(j = 0; j < result[i].length; j++) { 438 477 time.setTime(result[i][j].Time.replace(/\D/g,'')); … … 443 482 cpuSeries[0].push([time.toUTCString(),result[i][j].CPUUtilization]); 444 483 } 445 if(result[i].length > 1) { 446 @ChartHelper.LineChartGivenSeries("TotalUsedCores","result[i][0].SlaveID","coreSeries","Total/Used Cores") 447 @ChartHelper.LineChartGivenSeries("TotalUsedMemory","result[i][0].SlaveID","memorySeries","Total/Used Memory") 448 @ChartHelper.LineChartGivenSeries("CPUUtilization","result[i][0].SlaveID","cpuSeries","CPU Utilization",0,100,"%.1f%%") 449 } 450 else { 451 @ChartHelper.BarChartGivenSeries("TotalUsedCores","result[i][0].SlaveID","coreSeries","Total/Used Cores") 452 @ChartHelper.BarChartGivenSeries("TotalUsedMemory","result[i][0].SlaveID","memorySeries","Total/Used Memory") 453 @ChartHelper.BarChartGivenSeries("CPUUtilization","result[i][0].SlaveID","cpuSeries","CPU Utilization",0,100,"%.1f%%") 454 } 455 collapsedByDefault(document.getElementById("UsedTotalCores" + result[i][0].SlaveID)); 456 collapsedByDefault(document.getElementById("UsedTotalMemory" + result[i][0].SlaveID)); 457 collapsedByDefault(document.getElementById("CPUUtilization" + result[i][0].SlaveID)); 484 if(result[i].length > 1) { 485 @ChartHelper.LineChartGivenSeries("slaveChartNames[0]","i","coreSeries","Total/Used Cores") 486 @ChartHelper.LineChartGivenSeries("slaveChartNames[1]","i","memorySeries","Total/Used Memory",0) 487 @ChartHelper.LineChartGivenSeries("slaveChartNames[2]","i","cpuSeries","CPU Utilization",0,100,"%.1f%%") 488 } 489 else { 490 @ChartHelper.BarChartGivenSeries("slaveChartNames[0]","i","coreSeries","Total/Used Cores") 491 @ChartHelper.BarChartGivenSeries("slaveChartNames[1]","i","memorySeries","Total/Used Memory",0) 492 @ChartHelper.BarChartGivenSeries("slaveChartNames[2]","i","cpuSeries","CPU Utilization",0,100,"%.1f%%") 493 } 494 for(k = 0; k < slaveChartNames.length; k++) { 495 CollapsedByDefault(document.getElementById(slaveChartNames[k] + i)); 496 } 497 } 458 498 } 459 499 }}); … … 461 501 } 462 502 463 @helper LineChartGivenSeries(string destinationTag, string chartId, string series, string title, double? minY = null, double? maxY = null, string axisYFormat = null) 464 { 465 <text> 466 window["@(destinationTag)" + @(chartId) + "Plot"] = $.jqplot("@(destinationTag)" + @(chartId), @series, { 503 @helper ResizeSlaves() { 504 <text> 505 //Resize event, resets bar width for slave charts and replots them 506 $(window).resize(function() { 507 if(typeof numberSlaves !== 'undefined') { 508 509 //Set chart names for use in creation below, must be set identically in 510 //ResizeSlaves 511 var slaveChartNames = ["TotalUsedCores","TotalUsedMemory","CPUUtilization"]; 512 513 for(i = 0; i < numberSlaves; i++) { 514 for(j = 0; j < slaveChartNames.length; j++) { 515 $.each(window[slaveChartNames[j] + "Plot" + i].series, function(index, series) { 516 series.barWidth = undefined; 517 }); 518 window[slaveChartNames[j] + "Plot" + i].replot({ resetAxes: true }); 519 } 520 } 521 } 522 }); 523 </text> 524 } 525 526 @helper LineChartGivenSeries(string destinationTag, string chartId, string series, string title, double? minY = null, double? maxY = null, string axisYFormat = null) { 527 <text> 528 window[@(destinationTag) + "Plot" + @(chartId)] = $.jqplot(@(destinationTag) + @(chartId), @series, { 467 529 title: "@title", 468 530 axes: { … … 499 561 } 500 562 }); 501 502 $(window).resize(function() { 503 window["@(destinationTag)" + @(chartId) + "Plot"].replot({ resetAxes: true }); 504 }); 505 </text> 506 } 507 508 @helper BarChartGivenSeries(string destinationTag, string chartId, string series, string title, double? minY = null, double? maxY = null, string axisYFormat = null) 509 { 510 <text> 511 window["@(destinationTag)" + @(chartId) + "Plot"] = $.jqplot("@(destinationTag)" + @(chartId), @series, { 563 </text> 564 } 565 566 @helper BarChartGivenSeries(string destinationTag, string chartId, string series, string title, double? minY = null, double? maxY = null, string axisYFormat = null) { 567 <text> 568 window[@(destinationTag) + "Plot" + @(chartId)] = $.jqplot(@(destinationTag) + @(chartId), @series, { 512 569 title: "@title", 513 570 seriesDefaults:{ … … 549 606 } 550 607 }); 551 552 $(window).resize(function() { 553 $.each(window["@(destinationTag)" + @(chartId) + "Plot"].series, function(index, series) { 554 series.barWidth = undefined; 555 }); 556 window["@(destinationTag)" + @(chartId) + "Plot"].replot({ resetAxes: true }); 557 }); 558 559 </text> 560 } 608 </text> 609 } -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/App_Code/ExceptionHelper.cshtml
r11030 r11053 18 18 *@ 19 19 20 @helper UserExceptions(string destinationTag, string url, string userName, string limit, string startDate = null, string endDate = null, string jobId = null, string taskState = null) { 21 if(userName!="" || userName!=null) { 22 <text> 23 var GetRequest = "?userName=" + @(userName) + "&limit=" + @(limit); 24 @if(startDate!=null) { 25 @:if(@(startDate)!=null) { 26 @:GetRequest += "&start=" + @startDate; 27 @:} 20 @helper UserExceptions(string destinationTag, string url, string limit, string userName = null, string startDate = null, string endDate = null, string jobId = null, string taskState = null) 21 { 22 <text> 23 var GetRequest = "?limit=" + @(limit); 24 @if (userName != null) { 25 @:if(@(userName)!=null) { 26 @:GetRequest += "&userName=" + @userName; 27 @:} 28 } 29 @if(startDate!=null) { 30 @:if(@(startDate)!=null) { 31 @:GetRequest += "&start=" + @startDate; 32 @:} 33 } 34 @if(endDate!=null) { 35 @:if(@(endDate)!=null) { 36 @:GetRequest += "&end=" + @endDate; 37 @:} 38 } 39 @if(jobId!=null) { 40 @:if(@(jobId)!=null) { 41 @:GetRequest += "&jobId=" + @jobId; 42 @:} 43 } 44 @if(taskState!=null) { 45 @:if(@(taskState)==null) { 46 @:GetRequest += "&taskState=" + @taskState; 47 @:} 48 } 49 $.ajax({async: false, url: "@(new HtmlString(url))" + GetRequest, datatype: "json", success: function(result) { 50 $("#" + "@destinationTag").html(""); 51 if(result.Key.length > 0) { 52 var ErrorHTML = '<section class="errorContainer">' + 53 '<p class="errorTitle">Some tasks were found in error state!</p>' + 54 '<label class="errorTask underline">Task</label>' + 55 '<label class="errorMessage underline">Error</label>'; 56 for (var i = 0; i < result.Key.length; i++) { 57 ErrorHTML += '<a class="errorTask" onclick="ScrollTo(this)">' + result.Key[i] + '</a>' + 58 '<label class="errorMessage">' + result.Value[i] + '</label>'; 59 } 60 ErrorHTML += '</section>'; 61 $("#" + "@destinationTag").append(ErrorHTML); 62 } 28 63 } 29 @if(endDate!=null) { 30 @:if(@(endDate)!=null) { 31 @:GetRequest += "&end=" + @endDate; 32 @:} 33 } 34 @if(jobId!=null) { 35 @:if(@(jobId)!=null) { 36 @:GetRequest += "&jobId=" + @jobId; 37 @:} 38 } 39 @if(taskState!=null) { 40 @:if(@(taskState)==null) { 41 @:GetRequest += "&taskState=" + @taskState; 42 @:} 43 } 44 $.ajax({async: false, url: "@(new HtmlString(url))" + GetRequest, datatype: "json", success: function(result) { 45 $("#" + "@destinationTag").html(""); 46 if(result.Key.length > 0) { 47 var ErrorHTML = '<section class="errorContainer"><p class="errorTitle">Some tasks were found in error state!</p><label class="errorTask underline">Task</label><label class="errorMessage underline">Error</label>'; 48 for (var i = 0; i < result.Key.length; i++) { 49 ErrorHTML += '<a class="errorTask" onclick="scrollTo(this)">' + result.Key[i] + '</a><label class="errorMessage">' + result.Value[i] + '</label>'; 50 } 51 ErrorHTML += '</section>'; 52 $("#" + "@destinationTag").append(ErrorHTML); 53 } 54 } 55 }); 56 </text> 57 } 64 }); 65 </text> 58 66 } 59 67 60 68 @helper ScrollToException() { 61 69 <text> 62 function scrollTo(caller) { 63 openOnError(document.getElementById($(caller).html())); 70 function ScrollTo(caller) { 71 var taskErrorId = $(caller).parent().parent().attr('id') + $(caller).html(); 72 OpenOnError(document.getElementById(taskErrorId)); 64 73 $('html, body').animate({ 65 scrollTop: $("#" + $(caller).html()).parent().offset().top74 scrollTop: $("#" + taskErrorId).parent().offset().top 66 75 }, 2000); 67 76 } … … 91 100 </text> 92 101 } 102 103 @helper ShowErrors(string destinationTag) { 104 <text> 105 $("#@(destinationTag)").children(".errorContainer, .errorTitle, .errorTask, .errorMessage").css('display','inline-block'); 106 </text> 107 } 108 109 @helper ErrorsOnSlaves(string destinationTag, string url, string limit, string startDate = null, string endDate = null, string clientId=null) { 110 <text> 111 var GetRequest = "?limit=" + @(limit); 112 @if(startDate!=null) { 113 @:if(@(startDate)!=null) { 114 @:GetRequest += "&start=" + @startDate; 115 @:} 116 } 117 @if(endDate!=null) { 118 @:if(@(endDate)!=null) { 119 @:GetRequest += "&end=" + @endDate; 120 @:} 121 } 122 @if (clientId != null) { 123 @:if(@(clientId)!=null) { 124 @:GetRequest += "&clientId=" + @clientId; 125 @:} 126 } 127 $.ajax({ 128 async: false, url: "@(new HtmlString(url))" + GetRequest, datatype: "json", success: function(result) { 129 for(i = 0; i < result.length; i++) { 130 $("#" + "@(destinationTag)").append( 131 '<section class="chartContainer">' + 132 '<h1 class="title" id="@(destinationTag)' + result[i][0] + '">Task ' + result[i][0] + '</h1>' + 133 '<button class="collapse" onclick="CollapseSection(this)">+</button>' + 134 '<label id="' + result[i][0] + 'ErrorMessage">Error Message: ' + result[i][1] + '</label>' + 135 '<label id="' + result[i][0] + 'Client">Slave: <a onclick="ShowSlaveInfo(this)">' + result[i][2] + '</a></label>' + 136 '<label id="' + result[i][0] + 'User">User Name: ' + result[i][3] + '</label>' + 137 '<label id="' + result[i][0] + 'Date">Date/Time: ' + result[i][4] + '</label>' + 138 '</section>' 139 ); 140 CollapsedByDefault(document.getElementById("@(destinationTag)" + result[i][0])); 141 } 142 } 143 }); 144 </text> 145 } 146 147 @helper ShowSlaveInfo(string url, string limit, string startDate=null, string endDate=null) { 148 <text> 149 //Opens a sub-container with slave information 150 function ShowSlaveInfo(caller) { 151 @*@ChartHelper.SlaveInfoChart("",url,limit,startDate,endDate,"null","NotApplicable","null","$(caller).html()")*@ 152 } 153 </text> 154 } -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Content/Site.css
r11036 r11053 605 605 margin-top: 2%; 606 606 } 607 608 /* About Page Styles */ 609 .aboutMain { 610 width: 96%; 611 margin: 1% 0% 1% 2%; 612 } 613 614 .aboutMain h2 { 615 font-size: 1.4em; 616 } 617 618 .aboutMain p { 619 font-size: 1.2em; 620 } 621 622 .aboutMain p:first-letter { 623 font-size: 1.4em; 624 } 625 626 #ExceptionContainer .chartContainer label { 627 margin: 1% 0 1% 1%; 628 width: 98%; 629 font-size: 1.4em; 630 color: #6B6B6B; 631 display: inline-block; 632 } -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Controllers/ChartDataController.cs
r11036 r11053 192 192 List<KeyValuePair<string, List<double>>> results = new List<KeyValuePair<string, List<double>>>(); 193 193 194 List<string> id = new List<string>(); 195 List<double> wait = new List<double>(); 196 List<double> transfer = new List<double>(); 197 List<double> run = new List<double>(); 198 List<double> times = new List<double>(); 199 data.ForEach(i => id.Add(i.TaskID.ToString())); 200 data.ForEach(w => wait.Add(w.TotalWaiting)); 201 data.ForEach(t => transfer.Add(t.TotalTransfer)); 202 data.ForEach(r => run.Add(r.TotalRuntime)); 203 204 for (int i = 0; i < id.Count; i++) { 205 results.Add(new KeyValuePair<string, List<double>>(id[i],new List<double>{wait[i],transfer[i],run[i]})); 194 for (int i = 0; i < data.Count; i++) { 195 results.Add( 196 new KeyValuePair<string, List<double>>( 197 data[i].TaskID.ToString(),new List<double>{data[i].TotalWaiting,data[i].TotalTransfer,data[i].TotalRuntime} 198 ) 199 ); 206 200 } 207 201 … … 210 204 } 211 205 212 public JsonResult SlaveInfo(string limit, DateTime? start = null, DateTime? end = null, string userName = null )206 public JsonResult SlaveInfo(string limit, DateTime? start = null, DateTime? end = null, string userName = null, string slaveId=null) 213 207 { 214 208 using (var db = new HiveDataContext(Settings.Default.HeuristicLab_Hive_LinqConnectionString)) … … 220 214 where (!start.HasValue || slaves.Time >= start) && 221 215 (!end.HasValue || slaves.Time < end) && 222 (string.IsNullOrEmpty(userName) || users.Name == userName) 216 (string.IsNullOrEmpty(userName) || users.Name == userName) && 217 (string.IsNullOrEmpty(slaveId) || slaves.ClientId.ToString() == slaveId) 223 218 select new 224 219 { -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Controllers/ExceptionDataController.cs
r11030 r11053 9 9 public class ExceptionDataController : Controller { 10 10 11 public JsonResult TaskExceptions(string userName, string limit, DateTime? start = null, DateTime? end = null, string jobId = null, string taskState = null) { 11 public JsonResult TaskExceptions(string limit, string userName, DateTime? start = null, DateTime? end = null, string jobId = null, string taskState = null) 12 { 12 13 using (var db = new HiveDataContext(Settings.Default.HeuristicLab_Hive_LinqConnectionString)) { 13 14 TaskState state = GetTaskState(taskState); 14 15 var data = 15 (from errors in db.StateLogs 16 join tasks in db.FactTasks 17 on errors.TaskId equals tasks.TaskId 16 (from tasks in db.FactTasks 18 17 join jobs in db.DimJobs 19 18 on tasks.JobId equals jobs.JobId 20 where jobs.UserName == userName &&21 errors.Exception != null &&22 !errors.Exception.Equals(string.Empty) &&19 where tasks.Exception != null && 20 !tasks.Exception.Equals(string.Empty) && 21 (string.IsNullOrEmpty(userName) || jobs.UserName == userName) && 23 22 (!start.HasValue || tasks.StartTime >= start) && 24 23 (!end.HasValue || tasks.EndTime < end) && … … 27 26 select new 28 27 { 29 TaskID = errors.TaskId,30 ErrorMessage = errors.Exception,28 TaskID = tasks.TaskId, 29 ErrorMessage = tasks.Exception, 31 30 StartDate = tasks.StartTime 32 31 }).OrderByDescending(s => s.StartDate).Take(Convert.ToInt32(limit)).ToList(); … … 38 37 39 38 return Json(new KeyValuePair<List<string>, List<string>>(Task, Error), JsonRequestBehavior.AllowGet); 39 } 40 } 41 42 public JsonResult SlaveExceptions(string limit, DateTime? start = null, DateTime? end = null, string clientId = null) { 43 using (var db = new HiveDataContext(Settings.Default.HeuristicLab_Hive_LinqConnectionString)) { 44 var data = 45 (from tasks in db.FactTasks 46 join jobs in db.DimJobs 47 on tasks.JobId equals jobs.JobId 48 where tasks.Exception != null && 49 !tasks.Exception.Equals(string.Empty) && 50 (!start.HasValue || tasks.StartTime >= start) && 51 (!end.HasValue || tasks.EndTime < end) && 52 (string.IsNullOrEmpty(clientId) || tasks.LastClientId.ToString() == clientId) 53 select new { 54 TaskID = tasks.TaskId, 55 ErrorMessage = tasks.Exception, 56 ClientID = tasks.LastClientId, 57 UserName = jobs.UserName, 58 StartDate = tasks.StartTime 59 }).OrderByDescending(s => s.StartDate).Take(Convert.ToInt32(limit)).ToList(); 60 61 List<List<string>> results = new List<List<string>>(); 62 63 for (int i = 0; i < data.Count; i++) { 64 results.Add( 65 new List<string> { data[i].TaskID.ToString(), data[i].ErrorMessage, data[i].ClientID.ToString(), data[i].UserName, 66 data[i].StartDate.ToString("dd/MM/yyyy HH:mm")} 67 ); 68 } 69 return Json(results, JsonRequestBehavior.AllowGet); 40 70 } 41 71 } -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Controllers/HomeController.cs
r9646 r11053 59 59 } 60 60 61 public ActionResult Contact() {62 ViewBag.Message = "Your contact page.";63 64 return View();65 }66 67 61 public ActionResult Error() { 68 62 throw new Exception(); -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Controllers/LoginRequiredController.cs
r11030 r11053 41 41 select task.TaskState).Distinct(); 42 42 ViewBag.TaskStates = new SelectList(TaskStateList); 43 var SlaveList = (from slave in db.FactClientInfos 44 select slave.ClientId.ToString()).Distinct(); 45 ViewBag.Slaves = new SelectList(SlaveList); 43 46 } 44 47 -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/HeuristicLab.Services.Hive.Statistics-3.3.csproj
r11030 r11053 292 292 <Content Include="Views\Account\Login.cshtml" /> 293 293 <Content Include="Views\Home\About.cshtml" /> 294 <Content Include="Views\Home\Contact.cshtml" />295 294 <Content Include="Views\Home\Index.cshtml" /> 296 295 <Content Include="Views\Shared\Error.cshtml" /> -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Scripts/CollapsingSection.js
r11030 r11053 15 15 was impossible. Therefore this function is hardcoded as an HTML onclick event 16 16 for the dynamically created buttons of these sections */ 17 function collapseSection(caller) {17 function CollapseSection(caller) { 18 18 var jqCaller = $(caller); 19 19 if (jqCaller.html() == "-") { … … 29 29 /* Passed the interior div this fucntion is used to create an automatically collapsed 30 30 section */ 31 function collapsedByDefault(caller) {31 function CollapsedByDefault(caller) { 32 32 $(caller).parent().children("canvas, div, fieldset, label").css("display","none"); 33 33 } 34 34 35 35 /* Used when scrolling to a task by error, opens the container before task is scrolled to */ 36 function openOnError(caller) {36 function OpenOnError(caller) { 37 37 var jqCaller = $(caller); 38 38 jqCaller.parent().children("canvas, div, fieldset, label").fadeIn(); -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Views/Home/About.cshtml
r9604 r11053 1 @{ 2 ViewBag.Title = "About"; 3 } 1 @using HeuristicLab.Services.Hive.Statistics.Helper 2 @{ViewBag.Title = "About";} 4 3 5 <hgroup class="title"> 6 <h1>@ViewBag.Title.</h1> 7 <h2>@ViewBag.Message</h2> 8 </hgroup> 4 <h1>@ViewBag.Title</h1> 9 5 10 <article> 11 <p> 12 Use this area to provide additional information. 13 </p> 14 15 <p> 16 Use this area to provide additional information. 17 </p> 18 19 <p> 20 Use this area to provide additional information. 21 </p> 6 <article class="aboutMain"> 7 <h2>Welcome to the HeuristicLab Hive status monitor.</h2> 8 <p>From this monitor you can view the status of your hive in different ways. 9 </p> 10 <p>The @Html.MenuItem("Home", "Index", "Home") page offers a general overview of both current and historic hive status. 11 </p> 12 <p>The @Html.MenuItem("User", "UserTask", "LoginRequired") page, which requires logging in as a user, gives an overview of tasks 13 belonging to the user, the Task Information tab, as well as the resources utilized by the user, the Task Overview tab. 14 </p> 15 <p>The @Html.MenuItem("Admin", "Admin", "LoginRequired") page requires logging in as a user with administrative priveleges and 16 offers many different options. The User Overview tab allows the administrator to view tasks belonging to any user. The Task 17 Overview tab offers a view of the resources utilized by any user. The Slave Overview tab provides a view of resource usage by on 18 individual slaves. The Exception Overview tab offers a view of exceptions which occured and the slave on which they occured. 19 </p> 20 <p>HeuristicLab development site: <a href="http://dev.heuristiclab.com/trac/hl/core">HeuristicLab Development</a> 21 </p> 22 <p>Support is available here: <a href="http://dev.heuristiclab.com/trac/hl/core/wiki/UsersSupport">HeuristicLab Support</a> 23 </p> 22 24 </article> 23 24 <aside>25 <h3>Aside Title</h3>26 <p>27 Use this area to provide additional information.28 </p>29 <ul>30 <li>@Html.ActionLink("Home", "Index", "Home")</li>31 <li>@Html.ActionLink("About", "About", "Home")</li>32 <li>@Html.ActionLink("Contact", "Contact", "Home")</li>33 </ul>34 </aside> -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Views/Home/Index.cshtml
r11020 r11053 6 6 7 7 <h1>Current Status</h1> 8 9 @*10 Original code11 <section>12 <table>13 <tr>14 <td>Overall Available Cores</td>15 <td>@Model.OverallCurrentlyAvailableCores</td>16 </tr>17 <tr>18 <td>Availabe Cores (real)</td>19 <td>@Model.CurrentlyAvailableCores</td>20 </tr>21 <tr>22 <td>Used Cores</td>23 <td>@Model.CurrentlyUsedCores</td>24 </tr>25 <tr>26 <td>System-Wide Waiting Tasks</td>27 <td>@Model.CurrentlyJobsWaiting</td>28 </tr>29 <tr>30 <td>Overall Avg. CPU Utilization</td>31 <td>@Model.OverallCpuUtilization</td>32 </tr>33 <tr>34 <td>Available Avg. CPU Utilization</td>35 <td>@Model.AvailableCpuUtilization</td>36 </tr>37 </table>38 </section>*@39 40 8 <section class="chartContainer"> 41 9 <h1 class="title">Current Hive Status</h1> 42 10 <button class="collapse">-</button> 43 @* <label id="lblCurrentCPUUtilization" class="smoothieLabel">Current CPU Utilization</label>44 <div class="smoothieHolder">45 <canvas id="CurrentCPUUtilization" class="smoothieChart"></canvas>46 </div>47 <label id="lblCurrentTotalUsedCores" class="smoothieLabel">Current Total and Used Cores</label>48 <div class="smoothieHolder">49 <canvas id="CurrentTotalUsedCores" class="smoothieChart"></canvas>50 </div>51 <label id="lblCurrentTotalUsedMemory" class="smoothieLabel">Current Total and Used Memory</label>52 <div class="smoothieHolder">53 <canvas id="CurrentTotalUsedMemory" class="smoothieChart"></canvas>54 </div>*@55 11 <div id="CurrentCPUUtilization"></div> 56 12 <div id="CurrentTotalUsedCores"></div> … … 118 74 $(".date").datepicker({ 119 75 dateFormat: "yy-mm-dd", 120 onSelect: function () { refreshCharts(); }76 onSelect: function () { RefreshCharts(); } 121 77 }); 122 78 123 79 $("#Refresh").click(function () { 124 refreshCharts();80 RefreshCharts(); 125 81 }); 126 127 //resizeSmoothie();128 //createSmoothie();129 82 }); 130 83 131 function refreshCharts() {84 function RefreshCharts() { 132 85 var startDate = $('#Start').val(); 133 86 var endDate = $('#End').val(); … … 138 91 } 139 92 140 @* function createSmoothie() {141 //Create new SmoothieChart(s)142 @ChartHelper.CreateSmoothieChart("CurrentCPUUtilization","rgb(179, 179, 179)","rgb(242, 242, 242)","rgb(0, 22, 84)")143 @ChartHelper.CreateSmoothieChart("CurrentTotalUsedCores","rgb(179, 179, 179)","rgb(242, 242, 242)","rgb(0, 22, 84)")144 @ChartHelper.CreateSmoothieChart("CurrentTotalUsedMemory","rgb(179, 179, 179)","rgb(242, 242, 242)","rgb(0, 22, 84)")145 146 //Second argument is delay in chart display, used to make drawing smooth,147 //Should be matched to interval time below148 @ChartHelper.SetSmoothieCanvas("CurrentCPUUtilization","5000")149 @ChartHelper.SetSmoothieCanvas("CurrentTotalUsedCores","5000")150 @ChartHelper.SetSmoothieCanvas("CurrentTotalUsedMemory","5000")151 152 //Set interval to run ajax and get new values153 @ChartHelper.AssignTimeSeries("averageUsage", "CurrentCPUUtilization", "rgb(0, 255, 0)", "rgba(0, 255, 0, 0.4)")154 @ChartHelper.AssignTimeSeries("overallCore", "CurrentTotalUsedCores", "rgb(0, 255, 0)", "rgba(0, 255, 0, 0.4)")155 @ChartHelper.AssignTimeSeries("usedCore", "CurrentTotalUsedCores", "rgb(242, 182, 70)", "rgba(255, 199, 94, 0.4)")156 @ChartHelper.AssignTimeSeries("overallMemory", "CurrentTotalUsedMemory", "rgb(0, 255, 0)", "rgba(0, 255, 0, 0.4)")157 @ChartHelper.AssignTimeSeries("usedMemory", "CurrentTotalUsedMemory", "rgb(242, 182, 70)", "rgba(255, 199, 94, 0.4)")158 159 setInterval(function () { getChartUpdate() }, 5000);160 161 //Hit by interval, jQuery ajax request to get new data from db162 function getChartUpdate() {163 @ChartHelper.UpdateChartData("CurrentCPUUtilization", Url.Action("CurrentCpuUtilization","ChartData"))164 @ChartHelper.UpdateChartData("CurrentTotalUsedCores", Url.Action("CurrentCores","ChartData"))165 @ChartHelper.UpdateChartData("CurrentTotalUsedMemory", Url.Action("CurrentMemory","ChartData"))166 }167 }*@168 169 93 $(document).ready(function () { 170 94 @ChartHelper.SetStreamingProperties(1000,20,10) … … 174 98 @ChartHelper.CreateStreamChart("CurrentMemory", "CurrentTotalUsedMemory",Url.Action("CurrentMemory","ChartData"),"Current Total vs. Used Memory") 175 99 176 function doUpdate() {100 function DoUpdate() { 177 101 @ChartHelper.UpdateStreamChart("CurrentCPU","CurrentCPUUtilization", Url.Action("CurrentCpuUtilization","ChartData"),"FixedY") 178 102 @ChartHelper.UpdateStreamChart("CurrentCores","CurrentTotalUsedCores", Url.Action("CurrentCores","ChartData")) 179 103 @ChartHelper.UpdateStreamChart("CurrentMemory","CurrentTotalUsedMemory", Url.Action("CurrentMemory","ChartData")) 180 setTimeout( doUpdate, refreshRate);104 setTimeout(DoUpdate, refreshRate); 181 105 } 182 106 183 doUpdate();107 DoUpdate(); 184 108 185 109 }); -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Views/LoginRequired/Admin.cshtml
r11036 r11053 63 63 <button id="UserApply">Apply</button> 64 64 </fieldset> 65 <button id="ScrollTop">^</button>66 65 <section id="TasksContainer" class="tabDataContainer"></section> 67 66 </section> … … 113 112 <section id="SlavesContainer" class="tabDataContainer"></section> 114 113 </section> 115 // Slaveoverview114 //Exception overview 116 115 <section id="ExceptionOverviewTab" class="tabSection"> 117 116 <fieldset class="filterChoices"> … … 119 118 <label>Date</label> 120 119 <input type="checkbox" name="filterChoice" value="ExceptionDate" checked="checked"> 121 <label> Users</label>122 <input type="checkbox" name="filterChoice" value="Exception User">120 <label>Slave</label> 121 <input type="checkbox" name="filterChoice" value="ExceptionSlave"> 123 122 <label>Limit</label> 124 123 <select id="ExceptionLimit"> … … 138 137 @Html.TextBox("ExceptionEnd", (DateTime.Now + new TimeSpan(1, 0, 0, 0)).ToString("yyyy-MM-dd"), new { @class = "date" }) 139 138 </fieldset> 140 <fieldset id="FilterExceptionUser" class="filterContainer"> 141 <legend>User</legend> 142 <select id="ExceptionUserList"> 143 </select> 144 </fieldset> 145 <button id="ExcpetionApply">Apply</button> 139 <fieldset id="FilterExceptionSlave" class="filterContainer"> 140 <legend>Slave</legend> 141 @Html.DropDownList("Slaves") 142 </fieldset> 143 <button id="ExceptionApply">Apply</button> 146 144 </fieldset> 147 145 <section id="ExceptionContainer" class="tabDataContainer"></section> 148 146 </section> 147 <button id="ScrollTop">^</button> 149 148 } 150 149 … … 171 170 var endDate; 172 171 var selectedUser; 172 var selectedSlave; 173 173 var limit; 174 174 var pageNumber; 175 175 var userId; 176 var numberTasks = 0;177 176 178 177 $(document).ready(function () { … … 218 217 }); 219 218 219 $(".tabButton").click(function () { 220 var sender = $(this).attr('id'); 221 $(".tabButton").css({ 'border-bottom': '1px solid #8297F5', 'background': 'none' }); 222 $(this).css({ 'border-bottom': '1px solid #E0E6FF', 'background-color': '#E0E6FF' }); 223 $(".tabSection").css('display', 'none'); 224 $("#" + sender.slice(0, -6) + "Tab").css('display', 'block'); 225 }) 226 227 //User Overview 220 228 function RefreshUser() { 221 229 selectedUser = $("#UserList").val(); … … 235 243 taskState = $('#TaskState').val(); 236 244 } 237 @ExceptionHelper.UserExceptions("TasksContainer", Url.Action("TaskExceptions", "ExceptionData"), " selectedUser", "limit", "startDate", "endDate", "jobId", "taskState")245 @ExceptionHelper.UserExceptions("TasksContainer", Url.Action("TaskExceptions", "ExceptionData"), "limit", "selectedUser", "startDate", "endDate", "jobId", "taskState") 238 246 @ChartHelper.TasksForUser("TasksContainer", "Task", Url.Action("UserTask", "ChartData"), "RefreshUser", "selectedUser", "limit", "startDate", "endDate", "jobId", "taskState", "pageNumber") 239 247 pageNumber = null; … … 243 251 @ExceptionHelper.ScrollToException() 244 252 253 //Task Overview 245 254 $("#TaskUserList").change(function () { 246 255 RefreshTask(); … … 254 263 } 255 264 256 $("#SlaveList").change(function () { 257 RefreshSlave(); 258 }); 259 265 //Slave Overview 260 266 function RefreshSlave() { 261 267 selectedUser = null; … … 270 276 selectedUser = $('#SlaveUserList').val(); 271 277 } 272 @ChartHelper.SlaveInfoChart("SlavesContainer", Url.Action("SlaveInfo","ChartData"),"limit","startDate","endDate","selectedUser","RefreshSlave","pageNumber")278 @ChartHelper.SlaveInfoChart("SlavesContainer", Url.Action("SlaveInfo", "ChartData"), "limit", "startDate", "endDate", "selectedUser", "RefreshSlave", "pageNumber") 273 279 pageNumber = null; 274 280 } 275 281 276 $(".tabButton").click(function () { 277 var sender = $(this).attr('id'); 278 $(".tabButton").css({'border-bottom' : '1px solid #8297F5', 'background' : 'none' }); 279 $(this).css({ 'border-bottom': '1px solid #E0E6FF', 'background-color': '#E0E6FF' }); 280 $(".tabSection").css('display', 'none'); 281 $("#" + sender.slice(0, -6) + "Tab").css('display', 'block'); 282 }) 282 @ChartHelper.ResizeSlaves() 283 284 //Exceptions Overview 285 $("#ExceptionSlaveList").change(function () { 286 RefreshException(); 287 }); 288 289 function RefreshException() { 290 selectedSlave = null; 291 limit = $("#ExceptionLimit").val(); 292 startDate = null; 293 endDate = null; 294 if ($("[value='ExceptionDate']").is(":checked")) { 295 startDate = $('#ExceptionStart').val(); 296 endDate = $('#ExceptionEnd').val(); 297 } 298 if ($("[value='ExceptionSlave']").is(':checked')) { 299 selectedSlave = $('#Slaves').val(); 300 } 301 @ExceptionHelper.UserExceptions("ExceptionContainer", Url.Action("TaskExceptions", "ExceptionData"), "limit", null, "startDate", "endDate", null, null) 302 @ExceptionHelper.ShowErrors("ExceptionContainer") 303 @ExceptionHelper.ErrorsOnSlaves("ExceptionContainer",Url.Action("SlaveExceptions", "ExceptionData"),"limit","startDate", "endDate","selectedSlave") 304 } 305 @*@ExceptionHelper.ShowSlaveInfo(Url.Action("SlaveInfo", "ChartData"),"limit","startDate","endDate")*@ 283 306 </script> 284 307 } -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Views/LoginRequired/UserTask.cshtml
r11036 r11053 129 129 taskState = $('#TaskState').val(); 130 130 } 131 @ExceptionHelper.UserExceptions("TasksContainer",Url.Action("TaskExceptions", "ExceptionData")," userName","limit","startDate","endDate","jobId","taskState")131 @ExceptionHelper.UserExceptions("TasksContainer",Url.Action("TaskExceptions", "ExceptionData"),"limit","userName","startDate","endDate","jobId","taskState") 132 132 @ChartHelper.TasksForUser("TasksContainer","Task",Url.Action("UserTask", "ChartData"),"TaskInformation","userName","limit","startDate","endDate","jobId","taskState","pageNumber") 133 133 pageNumber = null; -
branches/HiveStatistics/sources/HeuristicLab.Services.Hive.Statistics/3.3/Views/Shared/_Layout.cshtml
r11020 r11053 25 25 <li>@Html.MenuItem("Admin", "Admin", "LoginRequired")</li> 26 26 <li>@Html.MenuItem("About", "About", "Home")</li> 27 <li>@Html.MenuItem("Contact", "Contact", "Home")</li>28 27 </ul> 29 28 </nav>
Note: See TracChangeset
for help on using the changeset viewer.