Pushed by: admin License: TA9O64YS7EPT (Professional) Timestamp: 2026-02-21T22:16:26.040137masterdev
@ -0,0 +1,7 @@
|
||||
# Metricator for Nmon
|
||||
|
||||
Copyright 2017-2018 Octamis limited - Copyright 2017-2018 Guilhem Marchand
|
||||
|
||||
All rights reserved.
|
||||
|
||||
https://www.octamis.com/services/metricator
|
||||
@ -0,0 +1,2 @@
|
||||
[logging]
|
||||
loglevel =
|
||||
@ -0,0 +1,2 @@
|
||||
2.0.2
|
||||
2.0.2
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"dependencies": null,
|
||||
"incompatibleApps": null,
|
||||
"info": {
|
||||
"author": [
|
||||
{
|
||||
"company": "Octamis",
|
||||
"email": "support@octamis.com",
|
||||
"name": "Octamis"
|
||||
}
|
||||
],
|
||||
"classification": {
|
||||
"categories": [
|
||||
"Unix",
|
||||
"Linux",
|
||||
"Performance",
|
||||
"Monitoring",
|
||||
"Capacity planning",
|
||||
"System administration",
|
||||
"Benchmarking"
|
||||
],
|
||||
"developmentStatus": "Production/Stable",
|
||||
"intendedAudience": "IT Professionals"
|
||||
},
|
||||
"commonInformationModels": null,
|
||||
"description": "Metricator for Nmon provides rich and efficient monitoring and capacity planning for Linux, IBM AIX and Oracle Solaris",
|
||||
"id": {
|
||||
"group": null,
|
||||
"name": "metricator-for-nmon",
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"license": {
|
||||
"name": "Octamis",
|
||||
"text": "./license.txt",
|
||||
"uri": ""
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"name": null,
|
||||
"text": null,
|
||||
"uri": null
|
||||
},
|
||||
"releaseDate": "6 September 2021",
|
||||
"releaseNotes": {
|
||||
"name": "version 2.0.1, please consult online documentation",
|
||||
"text": "./README.md",
|
||||
"uri": ""
|
||||
},
|
||||
"title": "Metricator for Nmon"
|
||||
},
|
||||
"inputGroups": null,
|
||||
"platformRequirements": null,
|
||||
"schemaVersion": "2.0.0",
|
||||
"supportedDeployments": [
|
||||
"_standalone",
|
||||
"_distributed",
|
||||
"_search_head_clustering"
|
||||
],
|
||||
"targetWorkloads": [
|
||||
"_search_heads",
|
||||
"_indexers"
|
||||
],
|
||||
"tasks": null
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
.html h1 {
|
||||
color: royalblue;
|
||||
}
|
||||
|
||||
.html h2 {
|
||||
color: #29547f;
|
||||
}
|
||||
|
||||
/* margin left required for Firefox */
|
||||
|
||||
.list li {
|
||||
/*margin-left: 4px; */
|
||||
margin-left: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: inline-block;
|
||||
margin-right: 50px;
|
||||
}
|
||||
@ -0,0 +1,374 @@
|
||||
/* Logo subtitle */
|
||||
|
||||
.logosubtitle h2 {
|
||||
font-size: 16px;
|
||||
margin: 10px 0;
|
||||
font-weight: normal;
|
||||
color: darkslategrey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* links color */
|
||||
a {
|
||||
color: #adbacd;
|
||||
}
|
||||
|
||||
.mainbutton_container {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.mainbutton_container_nomargin {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mainbutton {
|
||||
display: inline-block;
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.mainbutton_highmargin {
|
||||
display: inline-block;
|
||||
margin-left: 90px;
|
||||
margin-right: 90px;
|
||||
}
|
||||
|
||||
/* main category icons and titles */
|
||||
|
||||
.imgheader {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.imgheader img {
|
||||
float: left;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.imgheader h1 {
|
||||
color: lightslategrey;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
top: 9px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.imgheader h2 {
|
||||
position: relative;
|
||||
top: 18px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.imgminiheader img {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
/* Hide the view title as an App Home Page */
|
||||
/* Now disabled
|
||||
|
||||
.dashboard-header h2 {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/* margin left required for Firefox */
|
||||
|
||||
.ui_list li {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.custom h1 {
|
||||
color: lightslategrey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.customleft h1 {
|
||||
color: lightslategrey;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.customright h1 {
|
||||
color: lightslategrey;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.custom img {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Home Button */
|
||||
|
||||
.round-button {
|
||||
width: 3%;
|
||||
height: 0;
|
||||
padding-bottom: 3%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #f5f5f5;
|
||||
overflow: hidden;
|
||||
background: #464646;
|
||||
box-shadow: 0 0 3px gray;
|
||||
}
|
||||
.round-button:hover {
|
||||
background: #262626;
|
||||
}
|
||||
.round-button img {
|
||||
display: block;
|
||||
width: 76%;
|
||||
padding: 12%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.cat_title {
|
||||
color: #5379af;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bootstrap_title {
|
||||
color: #5379af;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
position: relative;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/* Home page button links, inspired from http://www.w3schools.com */
|
||||
|
||||
a.tryitbtn,
|
||||
a.tryitbtn:link,
|
||||
a.tryitbtn:visited,
|
||||
a.showbtn,
|
||||
a.showbtn:link,
|
||||
a.showbtn:visited {
|
||||
display: inline-block;
|
||||
color: #469496;
|
||||
background-color: #1f1f1f;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 4px;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
/* margin-left: 5px; */
|
||||
margin-right: 10px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #aaaaaa;
|
||||
border: 1px solid #469496;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
a.tryitbtn:hover,
|
||||
a.tryitbtn:active,
|
||||
a.tryitbtn:hover,
|
||||
a.tryitbtn:active {
|
||||
background-color: #469496;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
a.tryitbtnxl,
|
||||
a.tryitbtnxl:link,
|
||||
a.tryitbtnxl:visited,
|
||||
a.showbtnxl,
|
||||
a.showbtnxl:link,
|
||||
a.showbtnxl:visited {
|
||||
display: inline-block;
|
||||
color: #469496;
|
||||
background-color: #1f1f1f;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 4px;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
/* margin-left: 5px; */
|
||||
margin-right: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #aaaaaa;
|
||||
border: 1px solid #469496;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
a.tryitbtnxl:hover,
|
||||
a.tryitbtnxl:active,
|
||||
a.tryitbtnxl:hover,
|
||||
a.tryitbtnxl:active {
|
||||
background-color: #469496;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
a.tryitbtnxxl,
|
||||
a.tryitbtnxxl:link,
|
||||
a.tryitbtnxxl:visited,
|
||||
a.showbtnxxl,
|
||||
a.showbtnxxl:link,
|
||||
a.showbtnxxl:visited {
|
||||
display: inline-block;
|
||||
color: #469496;
|
||||
background-color: #1f1f1f;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 4px;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
/* margin-left: 5px; */
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #aaaaaa;
|
||||
border: 1px solid #469496;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
a.tryitbtnxxl:hover,
|
||||
a.tryitbtnxxl:active,
|
||||
a.tryitbtnxxl:hover,
|
||||
a.tryitbtnxxl:active {
|
||||
background-color: #469496;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
a.tryitbtn-alt,
|
||||
a.tryitbtn-alt:link,
|
||||
a.tryitbtn-alt:visited,
|
||||
a.tryitbtn-alt,
|
||||
a.tryitbtn-alt:link,
|
||||
a.tryitbtn-alt:visited {
|
||||
display: inline-block;
|
||||
color: #c171b1;
|
||||
background-color: #1f1f1f;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 4px;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
/* margin-left: 5px; */
|
||||
margin-right: 10px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #aaaaaa;
|
||||
border: 1px solid #c171b1;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
a.tryitbtn-alt:hover,
|
||||
a.tryitbtn-alt:active,
|
||||
a.tryitbtn-alt:hover,
|
||||
a.tryitbtn-alt:active {
|
||||
background-color: #c171b1;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
a.rt {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a.rt {
|
||||
-webkit-transition: all 1s ease; /* Safari and Chrome */
|
||||
-moz-transition: all 1s ease; /* Firefox */
|
||||
-ms-transition: all 1s ease; /* IE 9 */
|
||||
-o-transition: all 1s ease; /* Opera */
|
||||
transition: all 1s ease;
|
||||
}
|
||||
|
||||
a.rt:hover img {
|
||||
-webkit-transform: scale(1.1); /* Safari and Chrome */
|
||||
-moz-transform: scale(1.1); /* Firefox */
|
||||
-ms-transform: scale(1.1); /* IE 9 */
|
||||
-o-transform: scale(1.1); /* Opera */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Fix single label trouble with Splunk 6.3.0 */
|
||||
|
||||
.before-label {
|
||||
font-size: medium !important;
|
||||
}
|
||||
|
||||
.after-label {
|
||||
font-size: medium !important;
|
||||
}
|
||||
|
||||
.single-result-unit {
|
||||
font-size: medium !important;
|
||||
}
|
||||
|
||||
/* Prevents modal window from generating troubles within Splunk interfaces */
|
||||
.modal {
|
||||
display: none;
|
||||
}
|
||||
/* Custom Modals widths */
|
||||
|
||||
.modal[class^="custom-modal"] {
|
||||
left: 50%;
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
.custom-modal-30 {
|
||||
width: 30%;
|
||||
margin-left: -15%;
|
||||
}
|
||||
|
||||
.custom-modal-50 {
|
||||
width: 50%;
|
||||
margin-left: -25%;
|
||||
}
|
||||
|
||||
.custom-modal-60 {
|
||||
width: 60%;
|
||||
margin-left: -30%;
|
||||
}
|
||||
|
||||
.custom-modal-70 {
|
||||
width: 70%;
|
||||
margin-left: -35%;
|
||||
}
|
||||
|
||||
.custom-modal-80 {
|
||||
width: 80%;
|
||||
margin-left: -40%;
|
||||
}
|
||||
|
||||
.custom-modal-96 {
|
||||
width: 96%;
|
||||
margin-left: -48%;
|
||||
}
|
||||
|
||||
/* Panels title bar customization */
|
||||
.dashboard-row .dashboard-panel h2.panel-title {
|
||||
/* text-align: center; */
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background-color: #444444;
|
||||
padding: 5px 5px 5px 5px;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
(function() {
|
||||
require([
|
||||
"underscore",
|
||||
"jquery",
|
||||
"splunkjs/mvc",
|
||||
"appUtils",
|
||||
"splunkjs/ready!",
|
||||
"splunkjs/mvc/simplexml/ready!",
|
||||
], function(_, $, mvc, appUtils) {
|
||||
|
||||
/////////////////////////////////////////
|
||||
/// Start Main Code Here
|
||||
/////////////////////////////////////////
|
||||
|
||||
var ref = appUtils.getTokenModels();
|
||||
var defaultTokenModel = ref[0];
|
||||
var submittedTokenModel = ref[1];
|
||||
|
||||
appUtils.checkEmptyTokenFocus("host1", appUtils.getToken("host1"));
|
||||
appUtils.checkEmptyTokenFocus("host2", appUtils.getToken("host2"));
|
||||
appUtils.checkEmptyTokenFocus("metric_name", appUtils.getToken("metric_name"));
|
||||
|
||||
defaultTokenModel.on("change:host1", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("host1", value);
|
||||
if (typeof value !== 'undefined' && value.toString().trim() === "") {
|
||||
appUtils.setToken("form.host1", undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
defaultTokenModel.on("change:host2", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("host2", value);
|
||||
if (typeof value !== 'undefined' && value.toString().trim() === "") {
|
||||
appUtils.setToken("form.host2", undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
defaultTokenModel.on("change:metric_name", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("metric_name", value);
|
||||
if (typeof value !== 'undefined' && value.toString().trim() === "") {
|
||||
appUtils.setToken("form.metric_name", undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
appUtils.submitTokens();
|
||||
});
|
||||
}).call(this);
|
||||
@ -0,0 +1,38 @@
|
||||
(function() {
|
||||
require([
|
||||
"underscore",
|
||||
"jquery",
|
||||
"splunkjs/mvc",
|
||||
"appUtils",
|
||||
"splunkjs/ready!",
|
||||
"splunkjs/mvc/simplexml/ready!",
|
||||
], function(_, $, mvc, appUtils) {
|
||||
|
||||
/////////////////////////////////////////
|
||||
/// Start Main Code Here
|
||||
/////////////////////////////////////////
|
||||
|
||||
var ref = appUtils.getTokenModels();
|
||||
var defaultTokenModel = ref[0];
|
||||
var submittedTokenModel = ref[1];
|
||||
|
||||
appUtils.checkEmptyTokenFocus("metric_name", appUtils.getToken("metric_name"));
|
||||
appUtils.checkEmptyTokenFocus("host", appUtils.getToken("host"));
|
||||
|
||||
defaultTokenModel.on("change:metric_name", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("metric_name", value);
|
||||
if (typeof value !== 'undefined' && value.toString().trim() === "") {
|
||||
appUtils.setToken("form.metric_name", undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
defaultTokenModel.on("change:host", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("host", value);
|
||||
if (typeof value !== 'undefined' && value.toString().trim() === "") {
|
||||
appUtils.setToken("form.host", undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
appUtils.submitTokens();
|
||||
});
|
||||
}).call(this);
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@ -0,0 +1,74 @@
|
||||
require(['splunkjs/mvc/simplexml/ready!'], function(){
|
||||
require(['splunkjs/ready!', 'splunkjs/mvc'], function(mvc){
|
||||
|
||||
/*
|
||||
--------------------------------------------------------------
|
||||
Multi depends buttons - Written by François Toulouse, thanks !
|
||||
--------------------------------------------------------------
|
||||
|
||||
Usage: Add an html bootstrap button
|
||||
|
||||
<button class="btn" data-token-name="foo" data-token-value="1">Activate foo token</button>
|
||||
<button class="btn" data-token-name="bar" data-token-value="1">Activate bar token</button>
|
||||
|
||||
|
||||
*/
|
||||
|
||||
var defaultTokenModel = mvc.Components.getInstance('default', {create: true});
|
||||
var submittedTokenModel = mvc.Components.getInstance('submitted', {create: true});
|
||||
|
||||
function setToken(name, value) {
|
||||
defaultTokenModel.set(name, value);
|
||||
submittedTokenModel.set(name, value);
|
||||
}
|
||||
|
||||
function getToken(name) {
|
||||
var ret = null;
|
||||
|
||||
if(defaultTokenModel.get(name) != undefined){
|
||||
ret = defaultTokenModel.get(name);
|
||||
}
|
||||
else if(submittedTokenModel.get(name) != undefined){
|
||||
ret = submittedTokenModel.get(name);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function unsetToken(name) {
|
||||
defaultTokenModel.unset(name);
|
||||
submittedTokenModel.unset(name);
|
||||
}
|
||||
|
||||
// For each button with the class "custom-sub-nav"
|
||||
$('.custom-sub-nav').each(function(){
|
||||
var $btn_group = $(this);
|
||||
|
||||
/* for each button in this nav:
|
||||
- Cliking on the button: create the token "data-token-name" with attribute value "data-token-value"
|
||||
- Button has been clicked already and the user click on it again: removes the token "data-token-name"
|
||||
*/
|
||||
$btn_group.find('button').on('click', function(){
|
||||
var $btn = $(this);
|
||||
var btn_current_label = $btn.html();
|
||||
var btn_alt_label = $btn.attr('data-alt-label');
|
||||
var tk_name = $btn.attr('data-token-name');
|
||||
var tk_value = $btn.attr('data-token-value');
|
||||
|
||||
if( getToken(tk_name) == null){
|
||||
setToken(tk_name, tk_value);
|
||||
$btn.addClass('active');
|
||||
}
|
||||
else{
|
||||
unsetToken(tk_name);
|
||||
$btn.removeClass('active');
|
||||
}
|
||||
|
||||
// Manage button label
|
||||
$btn.html(btn_alt_label);
|
||||
$btn.attr('data-alt-label', btn_current_label);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
|
||||
/* blue color for current rendering */
|
||||
|
||||
td.data-bar-cell {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
td.data-bar-cell .data-bar-wrapper .data-bar {
|
||||
height: 16px;
|
||||
min-width: 1px;
|
||||
background-color: #5479AF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-bar-over { color: #FFFFFF; }
|
||||
.data-bar-under { color: #000000; }
|
||||
|
||||
.data-bar-wrapper {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* red color for laerting */
|
||||
|
||||
td.red-data-bar-cell {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
td.red-data-bar-cell .red-data-bar-wrapper .red-data-bar {
|
||||
height: 16px;
|
||||
min-width: 1px;
|
||||
background-color: #CD5C5C;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.red-data-bar-over { color: #FFFFFF; }
|
||||
.red-data-bar-under { color: #000000; }
|
||||
|
||||
.red-data-bar-wrapper {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
require([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'splunkjs/mvc',
|
||||
'views/shared/results_table/renderers/BaseCellRenderer',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function($, _, mvc, BaseCellRenderer) {
|
||||
|
||||
// blue rendering for current
|
||||
|
||||
var DataBarCellRenderer = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'current_used_percent');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="data-bar-under"
|
||||
if(cell.value > 15){ pColor="data-bar-over" }
|
||||
$td.addClass('data-bar-cell').html(_.template('<div class="data-bar-wrapper"><div class="data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_inventory').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
// red rendering for alerting
|
||||
|
||||
var DataBarCellRenderer2 = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'max_used_percent_alert');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="red-data-bar-under"
|
||||
if(cell.value > 15){ pColor="red-data-bar-over" }
|
||||
$td.addClass('red-data-bar-cell').html(_.template('<div class="red-data-bar-wrapper"><div class="red-data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_content').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer2());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
|
||||
/* red color for alerting */
|
||||
|
||||
td.red-data-bar-cell {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
td.red-data-bar-cell .red-data-bar-wrapper .red-data-bar {
|
||||
height: 16px;
|
||||
min-width: 1px;
|
||||
background-color: #CD5C5C;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.red-data-bar-over { color: #FFFFFF; }
|
||||
.red-data-bar-under { color: #000000; }
|
||||
|
||||
.red-data-bar-wrapper {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
require([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'splunkjs/mvc',
|
||||
'views/shared/results_table/renderers/BaseCellRenderer',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function($, _, mvc, BaseCellRenderer) {
|
||||
|
||||
// red rendering for alerting
|
||||
|
||||
var DataBarCellRenderer1 = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'max_cpu_percent');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="red-data-bar-under"
|
||||
if(cell.value > 15){ pColor="red-data-bar-over" }
|
||||
$td.addClass('red-data-bar-cell').html(_.template('<div class="red-data-bar-wrapper"><div class="red-data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_content').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer1());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
var DataBarCellRenderer2 = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'max_phy_percent');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="red-data-bar-under"
|
||||
if(cell.value > 15){ pColor="red-data-bar-over" }
|
||||
$td.addClass('red-data-bar-cell').html(_.template('<div class="red-data-bar-wrapper"><div class="red-data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_content').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer2());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
var DataBarCellRenderer3 = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'max_vir_percent');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="red-data-bar-under"
|
||||
if(cell.value > 15){ pColor="red-data-bar-over" }
|
||||
$td.addClass('red-data-bar-cell').html(_.template('<div class="red-data-bar-wrapper"><div class="red-data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_content').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer3());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
require([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'splunkjs/mvc',
|
||||
'views/shared/results_table/renderers/BaseCellRenderer',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function($, _, mvc, BaseCellRenderer) {
|
||||
|
||||
// red rendering for alerting
|
||||
|
||||
var DataBarCellRenderer1 = BaseCellRenderer.extend({
|
||||
canRender: function(cell) {
|
||||
return (cell.field === 'max_fs_percent');
|
||||
},
|
||||
render: function($td, cell) {
|
||||
var pColor="red-data-bar-under"
|
||||
if(cell.value > 15){ pColor="red-data-bar-over" }
|
||||
$td.addClass('red-data-bar-cell').html(_.template('<div class="red-data-bar-wrapper"><div class="red-data-bar <%- pColor %>" style="width:<%- percent %>%"> <%- ppp %>%</div></div>', {
|
||||
percent: Math.min(Math.max(parseFloat(cell.value), 0), 100),
|
||||
ppp: parseFloat(cell.value).toFixed(2),
|
||||
pColor: pColor
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
mvc.Components.get('element_table_show_lookup_content').getVisualization(function(tableView) {
|
||||
tableView.table.addCellRenderer(new DataBarCellRenderer1());
|
||||
tableView.table.render();
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
require.config({
|
||||
paths: {
|
||||
"app": "../app"
|
||||
}
|
||||
});
|
||||
|
||||
require(['splunkjs/mvc/simplexml/ready!'], function(){
|
||||
require(['splunkjs/ready!'], function(){
|
||||
// The splunkjs/ready loader script will automatically instantiate all elements
|
||||
// declared in the dashboard's HTML.
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "bubblechart",
|
||||
"version": "1.0.0",
|
||||
"main": "bubblechart.js",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"d3": "3.3.x"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
.splunk-toolkit-bubble-chart {
|
||||
font-family: arial;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.splunk-toolkit-bubble-chart svg {
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
}
|
||||
.splunk-toolkit-bubble-chart g {
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
.bubble-chart-tooltip {
|
||||
position: absolute;
|
||||
background-color: #424242;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
padding: 7px;
|
||||
font-size: 1.0em;
|
||||
color: white;
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
.node:hover{
|
||||
opacity: .7;
|
||||
}
|
||||
@ -0,0 +1,236 @@
|
||||
// Bubble Chart
|
||||
// this displays information as different 'bubbles,' their unique values represented with
|
||||
// the size of the bubble.
|
||||
// supports drilldown clicks
|
||||
|
||||
// available settings:
|
||||
// - nameField: the field to use as the label on each bubble
|
||||
// - valueField: the field to use as the value of each bubble (also dictates size)
|
||||
// - categoryField: the field to use for grouping similar data (usually the same field as nameField)
|
||||
|
||||
// ---expected data format---
|
||||
// a splunk search like this: source=foo | stats count by artist_name, track_name
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var _ = require('underscore');
|
||||
var d3 = require("../d3/d3");
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
|
||||
require("css!./bubblechart.css");
|
||||
|
||||
var BubbleChart = SimpleSplunkView.extend({
|
||||
|
||||
className: "splunk-toolkit-bubble-chart",
|
||||
|
||||
options: {
|
||||
managerid: null,
|
||||
data: "preview",
|
||||
nameField: null,
|
||||
valueField: 'count',
|
||||
categoryField: null
|
||||
},
|
||||
|
||||
output_mode: "json",
|
||||
|
||||
initialize: function() {
|
||||
_.extend(this.options, {
|
||||
formatName: _.identity,
|
||||
formatTitle: function(d) {
|
||||
return (d.source.name + ' -> ' + d.target.name +
|
||||
': ' + d.value);
|
||||
}
|
||||
});
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this.settings.enablePush("value");
|
||||
|
||||
// in the case that any options are changed, it will dynamically update
|
||||
// without having to refresh. copy the following line for whichever field
|
||||
// you'd like dynamic updating on
|
||||
this.settings.on("change:valueField", this.render, this);
|
||||
this.settings.on("change:nameField", this.render, this);
|
||||
this.settings.on("change:categoryField", this.render, this);
|
||||
|
||||
// Set up resize callback. The first argument is a this
|
||||
// pointer which gets passed into the callback event
|
||||
$(window).resize(this, _.debounce(this._handleResize, 20));
|
||||
},
|
||||
|
||||
_handleResize: function(e){
|
||||
|
||||
// e.data is the this pointer passed to the callback.
|
||||
// here it refers to this object and we call render()
|
||||
e.data.render();
|
||||
},
|
||||
|
||||
createView: function() {
|
||||
|
||||
// Here we wet up the initial view layout
|
||||
var margin = {top: 0, right: 0, bottom: 0, left: 0};
|
||||
var availableWidth = parseInt(this.settings.get("width") || this.$el.width());
|
||||
var availableHeight = parseInt(this.settings.get("height") || this.$el.height());
|
||||
|
||||
this.$el.html("");
|
||||
|
||||
var svg = d3.select(this.el)
|
||||
.append("svg")
|
||||
.attr("width", availableWidth)
|
||||
.attr("height", availableHeight)
|
||||
.attr("pointer-events", "all");
|
||||
|
||||
var tooltip = d3.select(this.el).append("div")
|
||||
.attr("class", "bubble-chart-tooltip");
|
||||
|
||||
// The returned object gets passed to updateView as viz
|
||||
return { container: this.$el, svg: svg, margin: margin, tooltip: tooltip};
|
||||
},
|
||||
|
||||
// making the data look how we want it to for updateView to do its job
|
||||
formatData: function(data) {
|
||||
// getting settings
|
||||
var nameField = this.settings.get('nameField');
|
||||
var valueField = this.settings.get('valueField');
|
||||
var categoryField = this.settings.get('categoryField');
|
||||
var collection = data;
|
||||
var bubblechart = { 'name': nameField+"s", 'children': [ ] }; // how we want it to look
|
||||
|
||||
// making the children formatted array
|
||||
for (var i=0; i < collection.length; i++) {
|
||||
var Idx = -1;
|
||||
$.each(bubblechart.children, function(idx, el) {
|
||||
if (el.name == collection[i][categoryField]) {
|
||||
Idx = idx;
|
||||
}
|
||||
});
|
||||
if (Idx == -1) {
|
||||
bubblechart.children.push({ 'name': collection[i][categoryField], children: [ ] });
|
||||
Idx = bubblechart.children.length - 1;
|
||||
}
|
||||
|
||||
bubblechart.children[Idx].children.push({ 'name': collection[i][nameField], 'size': collection[i][valueField] || 1 });
|
||||
}
|
||||
return bubblechart; // this is passed into updateView as 'data'
|
||||
},
|
||||
|
||||
updateView: function(viz, data) {
|
||||
var that = this;
|
||||
|
||||
// Clear svg
|
||||
var svg = $(viz.svg[0]);
|
||||
svg.empty();
|
||||
|
||||
var tooltip = viz.tooltip;
|
||||
|
||||
// Add the graph group as a child of the main svg
|
||||
var graph = viz.svg
|
||||
.append("g")
|
||||
.attr("class", "bubble")
|
||||
.attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")");
|
||||
|
||||
// Set format and color
|
||||
var format = d3.format(",d");
|
||||
var color = d3.scale.category20c();
|
||||
|
||||
// We have two phases in layout. We tell the
|
||||
// d3 lout how much room it has, then set
|
||||
// the sizes of it's containers to match
|
||||
// the size it returns.
|
||||
var containerHeight = this.$el.height();
|
||||
var containerWidth = this.$el.width();
|
||||
var diameter = Math.min(containerWidth, containerHeight);
|
||||
|
||||
// Tell the layout to layout
|
||||
var bubble = d3.layout.pack()
|
||||
.sort(null)
|
||||
.size([diameter, diameter])
|
||||
.padding(1.5);
|
||||
|
||||
// Set containers' sizes to match actual layout
|
||||
var width = bubble.size()[0];
|
||||
var height = bubble.size()[1];
|
||||
graph.attr("width", width)
|
||||
.attr("height", height);
|
||||
svg.height(height);
|
||||
svg.width(width);
|
||||
|
||||
var node = graph.selectAll(".node")
|
||||
.data(bubble.nodes(classes(data))
|
||||
.filter(function(d) { return !d.children; }))
|
||||
.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
||||
|
||||
// NOTE: this is taken out because we have a custom tooltip.
|
||||
// It may need to be put back for accessibility
|
||||
// node.append("title")
|
||||
// .text(function(d) { return d.className + ": " + format(d.value); });
|
||||
|
||||
node.append("circle")
|
||||
.attr("r", function(d) { return d.r; })
|
||||
.style("fill", function(d) { return color(d.packageName); });
|
||||
|
||||
node.append("text")
|
||||
.attr("dy", ".3em")
|
||||
.style("text-anchor", "middle")
|
||||
// ensure the text is truncated if the bubble is tiny
|
||||
.text(function(d) { return (d.className + " " + format(d.value)).substring(0, d.r / 3); });
|
||||
|
||||
// Re-flatten the child array
|
||||
function classes(data) {
|
||||
var classes = [];
|
||||
function recurse(name, node) {
|
||||
if (node.children)
|
||||
node.children.forEach(function(child) {
|
||||
recurse(node.name, child);
|
||||
});
|
||||
else
|
||||
classes.push({packageName: name || "", className: node.name || "", value: node.size});
|
||||
}
|
||||
|
||||
recurse(null, data);
|
||||
return {children: classes};
|
||||
}
|
||||
|
||||
// Tooltips
|
||||
function doMouseEnter(d){
|
||||
var text;
|
||||
if(d.className === undefined || d.className === ""){
|
||||
text = "Event: " + d.value;
|
||||
} else {
|
||||
text = d.className+": " + d.value;
|
||||
}
|
||||
tooltip
|
||||
.text(text)
|
||||
.style("opacity", function(){
|
||||
if(d.value !== undefined) { return 1; }
|
||||
return 0;
|
||||
})
|
||||
.style("left", (d3.mouse(that.el)[0]) + "px")
|
||||
.style("top", (d3.mouse(that.el)[1]) + "px");
|
||||
}
|
||||
|
||||
// More tooltips
|
||||
function doMouseOut(d){
|
||||
tooltip.style("opacity", 1e-6);
|
||||
}
|
||||
|
||||
node.on("mouseover", doMouseEnter);
|
||||
node.on("mouseout", doMouseOut);
|
||||
|
||||
// Drilldown clickings. edit this in order to change the search token that
|
||||
// is set to 'value' (a token in bubbles django), this will change the drilldown
|
||||
// search.
|
||||
node.on('click', function(e) {
|
||||
var clickEvent = {
|
||||
name: e.className,
|
||||
category: e.packageName,
|
||||
value: e.value
|
||||
};
|
||||
that.settings.set("value", e.className);
|
||||
that.trigger("click", clickEvent);
|
||||
});
|
||||
}
|
||||
});
|
||||
return BubbleChart;
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "calendarheatmap",
|
||||
"version": "1.0.0",
|
||||
"main": "calendarheatmap.js",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"d3": "3.3.x"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
.splunk-toolkit-cal-heatmap {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container hr {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-series-title {
|
||||
margin-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-buttons {
|
||||
margin-bottom: 5px;
|
||||
float: right;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/* Cal-HeatMap CSS */
|
||||
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .graph
|
||||
{
|
||||
clear: both;
|
||||
display: block;
|
||||
font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .graph-label
|
||||
{
|
||||
fill: #999;
|
||||
font-size: 10px
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .graph, .splunk-toolkit-cal-heatmap .heatmap-container .graph-legend rect {
|
||||
shape-rendering: crispedges
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .graph-rect
|
||||
{
|
||||
fill: #ededed;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .graph rect:hover
|
||||
{
|
||||
stroke: #000;
|
||||
stroke-width: 1px
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .subdomain-text {
|
||||
font-size: 8px;
|
||||
fill: #999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .hover_cursor:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .qi {
|
||||
background-color: #999;
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q0
|
||||
{
|
||||
background-color: #fff;
|
||||
fill: #fff;
|
||||
stroke: #ededed
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q1
|
||||
{
|
||||
background-color: #89dae2 !important;
|
||||
fill: #89dae2 !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q2
|
||||
{
|
||||
background-color: #9ccedb !important;
|
||||
fill: #699cc0 !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q3
|
||||
{
|
||||
background-color: #6bb5cf !important;
|
||||
fill: #45669d !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q4
|
||||
{
|
||||
background-color: #396379 !important;
|
||||
fill: #396379 !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .q5
|
||||
{
|
||||
background-color: #273b64 !important;
|
||||
fill: #273b64 !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container rect.highlight
|
||||
{
|
||||
stroke:#444;
|
||||
stroke-width:1;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container text.highlight
|
||||
{
|
||||
fill: #444;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container rect.now
|
||||
{
|
||||
stroke: white !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container text.now
|
||||
{
|
||||
fill: white !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.splunk-toolkit-cal-heatmap .heatmap-container .domain-background {
|
||||
fill: none;
|
||||
shape-rendering: crispedges;
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
|
||||
|
||||
// calheat!
|
||||
// shows a cool looking heatmap based on different time signatures
|
||||
// requires a timechart search. it dynamically guesses how to set up the
|
||||
// way to show the time, but you can define any settings you want in the html
|
||||
// docs: http://kamisama.github.io/cal-heatmap
|
||||
|
||||
// ---settings---
|
||||
|
||||
// domain: (hour, day, week, month, year)
|
||||
// subDomain: (min, x_min, hour, x_hour, day, x_day, week, x_week, month, x_month)
|
||||
// -- x_ variants are used to rotate the reading order to left to right, then top to bottom.
|
||||
// start: set to 'current' for current time or 'earliest' for your earliest data point
|
||||
|
||||
// TODO:
|
||||
// add a setting for each option at http://kamisama.github.io/cal-heatmap/#options
|
||||
// rather than using the JS method in the HTML like i'm doing now.
|
||||
|
||||
|
||||
|
||||
// the data is expected in this format after formatData (epoch time: event count):
|
||||
// {
|
||||
// "timestamps":[
|
||||
// {
|
||||
// "1378225500":"8",
|
||||
// "1378225560":"8",
|
||||
// "1378225620":"8",
|
||||
// },
|
||||
// {
|
||||
// "1378230300":"4",
|
||||
// "1378230360":"4",
|
||||
// "1378230660":"2"
|
||||
// },
|
||||
// {
|
||||
// "1378225500":"7",
|
||||
// "1378225560":"7",
|
||||
// },
|
||||
// {
|
||||
// "1378225500":"6",
|
||||
// "1378225560":"6",
|
||||
// "1378225620":"7",
|
||||
// },
|
||||
// {
|
||||
// "1378225500":"41",
|
||||
// "1378225560":"41",
|
||||
// },
|
||||
// {
|
||||
// "1378225500":"22",
|
||||
// "1378225560":"22",
|
||||
// }
|
||||
// ],
|
||||
|
||||
// -- we add this part onto the actual data --
|
||||
|
||||
// "start":"2013-09-03T16:25:00.000Z",
|
||||
// "domain":"hour",
|
||||
// "subDomain":"min"
|
||||
// }
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var _ = require('underscore');
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
var d3 = require("../d3/d3");
|
||||
var CalHeatMap = require("./contrib/cal-heatmap");
|
||||
|
||||
require("css!./calendarheatmap.css");
|
||||
|
||||
var CalendarHeatMap = SimpleSplunkView.extend({
|
||||
moduleId: module.id,
|
||||
|
||||
className: "splunk-toolkit-cal-heatmap",
|
||||
|
||||
heatmapOptionNames: [
|
||||
'cellRadius', 'domainMargin', 'maxDate', 'dataType',
|
||||
'considerMissingDataAsZero', 'verticalOrientation',
|
||||
'domainDynamicDimension', 'label', 'legendCellSize',
|
||||
'legendCellPadding', 'legendMargin', 'legendVerticalPosition',
|
||||
'legendHorizontalPosition', 'domainLabelFormat',
|
||||
'subDomainDateFormat', 'subDomainTextFormat', 'nextSelector',
|
||||
'previousSelector', 'itemNamespace', 'onMaxDomainReached',
|
||||
'onMinDomainReached', 'width', 'height'],
|
||||
|
||||
options: {
|
||||
managerid: "search1", // your MANAGER ID
|
||||
data: "preview", // Results type
|
||||
domain: 'hour', // the largest unit it will differentiate by in squares
|
||||
subDomain: 'min', // the smaller unit the calheat goes off of
|
||||
uID: null,
|
||||
range: 4
|
||||
},
|
||||
|
||||
validDomains: {
|
||||
'min': ['hour'],
|
||||
'hour': ['day', 'week'],
|
||||
'day': ['week', 'month', 'year'],
|
||||
'week': ['month', 'year'],
|
||||
'month': ['year']
|
||||
},
|
||||
|
||||
output_mode: "json_rows",
|
||||
|
||||
initialize: function() {
|
||||
var that = this;
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
this.settings.enablePush("value");
|
||||
// whenever domain or subDomain are changed, we will re-render.
|
||||
this.settings.on("change:domain", this.onDomainChange, this);
|
||||
this.settings.on("change:subDomain", this.onDomainChange, this);
|
||||
this.settings.on("change", this._onSettingsChange, this);
|
||||
var uniqueID=Math.floor(Math.random()*1000001);
|
||||
this.settings.set("uID", uniqueID);
|
||||
},
|
||||
|
||||
onDomainChange: function() {
|
||||
var dom = this.settings.get('domain');
|
||||
var sd = this.settings.get('subDomain');
|
||||
|
||||
// Knock off the prefix cause it doesnt matter here
|
||||
var sdShort = sd.replace("x_", "");
|
||||
|
||||
// If the current domain is valid for this subdomain
|
||||
if (_.contains(this.validDomains[sdShort], dom)){
|
||||
this.render();
|
||||
}
|
||||
else{
|
||||
console.log(sd + " is and invalid subDomain for " + dom);
|
||||
}
|
||||
},
|
||||
|
||||
_onSettingsChange: function(changed) {
|
||||
// Route heatmap visualization changes to the renderer
|
||||
if ((_.intersection(_.keys(changed.changed), this.heatmapOptionNames)).length > 0) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
createView: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
// making the data look how we want it to for updateView to do its job
|
||||
// in this case, it looks like this:
|
||||
// {timestamp1: count, timestamp2: count, ... }
|
||||
formatData: function(data) {
|
||||
var rawFields = this.resultsModel.data().fields;
|
||||
var domain = this.settings.get('domain');
|
||||
var subDomain = this.settings.get('subDomain');
|
||||
|
||||
var filteredFields = _.filter(rawFields, function(d){ return d[0] !== "_"; });
|
||||
var objects = _.map(data, function(row) {
|
||||
return _.object(rawFields, row);
|
||||
});
|
||||
|
||||
var series = [];
|
||||
for(var i = 0; i < filteredFields.length; i++) {
|
||||
series.push({ name: filteredFields[i], timestamps: {}, min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY });
|
||||
}
|
||||
|
||||
_.each(objects, function(object) {
|
||||
// Get the timestamp for this object
|
||||
var time = new Date(object['_time']);
|
||||
var timeValue = time.valueOf() / 1000;
|
||||
|
||||
// For each actual value, store it in the timestamp object
|
||||
_.each(filteredFields, function(field, i) {
|
||||
var value = object[field];
|
||||
series[i].timestamps[timeValue] = parseInt(value, 10) || 0;
|
||||
series[i].min = Math.min(series[i].min, value);
|
||||
series[i].max = Math.max(series[i].max, value);
|
||||
});
|
||||
});
|
||||
|
||||
_.each(series, function(serie) {
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
series: series,
|
||||
domain: domain,
|
||||
subDomain: subDomain,
|
||||
start: new Date(objects[0]['_time']),
|
||||
min: new Date(objects[0]['_time']),
|
||||
max: new Date(objects[objects.length - 1]['_time'])
|
||||
};
|
||||
},
|
||||
|
||||
updateView: function(viz, data) {
|
||||
var that = this;
|
||||
// Options that can be set externally after instantiation
|
||||
// that affect the display. Ensure that any "empty" values
|
||||
// are set to null (use default). Some controls hand back
|
||||
// empty strings, which result in nothing being shown.
|
||||
// Not what is wanted.
|
||||
|
||||
var vizOptions = _.chain(this.settings.toJSON())
|
||||
.pairs()
|
||||
.filter(function(kv) { return _.contains(that.heatmapOptionNames, kv[0]); })
|
||||
.filter(function(kv) { return ! (_.isNull(kv[1]) || _.isUndefined(kv[1])) || (kv[1] !== ""); })
|
||||
.object()
|
||||
.value();
|
||||
|
||||
this.$el.html('');
|
||||
_.each(data.series, function(series, idx) {
|
||||
var scale = d3.scale.quantile()
|
||||
.domain([series.min, series.max])
|
||||
.range([0,1,2,3,4]);
|
||||
var legend = _.map(scale.quantiles(), function(x) { return Math.round(x); });
|
||||
|
||||
var title = series.name;
|
||||
|
||||
var $el = $("<div class='heatmap-container'/>").appendTo(that.el);
|
||||
var $title = $("<h4 class='heatmap-series-title'>Heatmap for: " + series.name + "</h4>").appendTo($el);
|
||||
var $buttons = $("<div class='heatmap-buttons'/>").appendTo($el);
|
||||
var $prev = $("<a class='heatmap-prev btn-pill icon-triangle-left'></a>").appendTo($buttons);
|
||||
var $next = $("<a class='heatmap-next btn-pill icon-triangle-right'></a>").appendTo($buttons);
|
||||
var options = _.extend({
|
||||
itemSelector: $el[0],
|
||||
previousSelector: $prev[0],
|
||||
nextSelector: $next[0],
|
||||
data: series.timestamps,
|
||||
domain: data.domain,
|
||||
subDomain: data.subDomain,
|
||||
start: data.start,
|
||||
range: 4,
|
||||
cellSize: 20,
|
||||
cellPadding: 3,
|
||||
domainGutter: 10,
|
||||
highlight: ['now', new Date()],
|
||||
legend: legend,
|
||||
legendMargin: [0, 0, 20, 0],
|
||||
legendCellSize: 14,
|
||||
minDate: data.min,
|
||||
maxDate: data.max,
|
||||
onMinDomainReached: function(hit) {
|
||||
$prev.attr("disabled", hit ? "disabled" : false);
|
||||
},
|
||||
onMaxDomainReached: function(hit) {
|
||||
$next.attr("disabled", hit ? "disabled" : false);
|
||||
},
|
||||
onClick: function(date, value, title) {
|
||||
that.trigger('click', {
|
||||
date: date,
|
||||
value: value,
|
||||
series: series.name
|
||||
});
|
||||
that.settings.set('value', date.valueOf());
|
||||
}
|
||||
}, vizOptions);
|
||||
|
||||
var cal = new CalHeatMap();
|
||||
cal.init(options); // create the calendar using either default or user defined options */
|
||||
|
||||
if (idx < data.series.length - 1) {
|
||||
$("<hr/>").appendTo($el);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return CalendarHeatMap;
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2012 Tyler Kellen, contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -0,0 +1,110 @@
|
||||
/* Cal-HeatMap CSS */
|
||||
|
||||
|
||||
.graph
|
||||
{
|
||||
clear: both;
|
||||
display: block;
|
||||
font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.graph-label
|
||||
{
|
||||
fill: #999;
|
||||
font-size: 10px
|
||||
}
|
||||
|
||||
.graph, .graph-legend rect {
|
||||
shape-rendering: crispedges
|
||||
}
|
||||
|
||||
.graph-rect
|
||||
{
|
||||
fill: #ededed;
|
||||
}
|
||||
|
||||
.graph rect:hover
|
||||
{
|
||||
stroke: #000;
|
||||
stroke-width: 1px
|
||||
}
|
||||
|
||||
.subdomain-text {
|
||||
font-size: 8px;
|
||||
fill: #999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hover_cursor:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qi {
|
||||
background-color: #999;
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.q0
|
||||
{
|
||||
background-color: #fff;
|
||||
fill: #fff;
|
||||
stroke: #ededed
|
||||
}
|
||||
|
||||
.q1
|
||||
{
|
||||
background-color: #dae289;
|
||||
fill: #dae289
|
||||
}
|
||||
|
||||
.q2
|
||||
{
|
||||
background-color: #cedb9c;
|
||||
fill: #9cc069
|
||||
}
|
||||
|
||||
.q3
|
||||
{
|
||||
background-color: #b5cf6b;
|
||||
fill: #669d45
|
||||
}
|
||||
|
||||
.q4
|
||||
{
|
||||
background-color: #637939;
|
||||
fill: #637939
|
||||
}
|
||||
|
||||
.q5
|
||||
{
|
||||
background-color: #3b6427;
|
||||
fill: #3b6427
|
||||
}
|
||||
|
||||
rect.highlight
|
||||
{
|
||||
stroke:#444;
|
||||
stroke-width:1;
|
||||
}
|
||||
|
||||
text.highlight
|
||||
{
|
||||
fill: #444;
|
||||
}
|
||||
|
||||
rect.now
|
||||
{
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
text.now
|
||||
{
|
||||
fill: red;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.domain-background {
|
||||
fill: none;
|
||||
shape-rendering: crispedges;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
Copyright (c) 2013, Michael Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* The name Michael Bostock may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "d3",
|
||||
"version": "3.3.5",
|
||||
"main": "d3.js",
|
||||
"ignore": [],
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
.node circle {
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.node text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
path.link {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
@ -0,0 +1,361 @@
|
||||
// Cluster Dendrogram D3.js code taken and modified from http://bl.ocks.org/mbostock/4063570 by Mike Bostock
|
||||
|
||||
define(function (require, exports, module) {
|
||||
var d3 = require("../d3/d3.layout");
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
var _ = require("underscore");
|
||||
require("css!./dendrogram.css");
|
||||
|
||||
var Dendrogram = SimpleSplunkView.extend({
|
||||
className: "splunk-toolkit-dendrogram",
|
||||
options: {
|
||||
managerid: null,
|
||||
data: "preview",
|
||||
root_label: "root_label not set",
|
||||
height: "auto",
|
||||
node_outline_color: "#509DDD",
|
||||
node_close_color: "#e7969c",
|
||||
node_open_color: "#ffffff",
|
||||
label_size_color: "#509DDD",
|
||||
label_count_color: "#1f77b4",
|
||||
has_size: true,
|
||||
initial_open_level: 1,
|
||||
margin_left: 100,
|
||||
margin_right: 400,
|
||||
},
|
||||
output_mode: "json_rows",
|
||||
initialize: function () {
|
||||
_(this.options).extend({
|
||||
height_px: 500,
|
||||
width_px: 2000,
|
||||
});
|
||||
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this.settings.on("change:order", this.render, this);
|
||||
|
||||
$(window).resize(this, _.debounce(this._handleResize, 20));
|
||||
},
|
||||
_handleResize: function (e) {
|
||||
// e.data is the this pointer passed to the callback.
|
||||
// here it refers to this object and we call render()
|
||||
e.data.render();
|
||||
},
|
||||
createView: function () {
|
||||
return true;
|
||||
},
|
||||
// Making the data look how we want it to for updateView to do its job
|
||||
formatData: function (data) {
|
||||
var height = this.settings.get("height");
|
||||
var height_px = this.settings.get("height_px");
|
||||
var width = this.settings.get("width");
|
||||
var width_px = this.settings.get("width_px");
|
||||
var root_label = this.settings.get("root_label");
|
||||
var has_size = this.settings.get("has_size");
|
||||
|
||||
this.settings.set(
|
||||
"height_px",
|
||||
height === "auto" ? Math.max(data.length * 30, height_px) : height
|
||||
);
|
||||
|
||||
data = _(data).map(function (row) {
|
||||
return _(row).map(function (item, i) {
|
||||
// Convert the string value to number
|
||||
return has_size && i + 1 === row.length ? parseFloat(item) : item;
|
||||
});
|
||||
});
|
||||
|
||||
var get_sum = function (list) {
|
||||
return _(list)
|
||||
.pluck(list[0].length - 1)
|
||||
.reduce(function (memo, num) {
|
||||
return memo + num;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var nest = function (list) {
|
||||
var groups = _(list).groupBy(0);
|
||||
|
||||
return _(groups).map(function (value, key) {
|
||||
var children = _(value)
|
||||
.chain()
|
||||
.map(function (v) {
|
||||
return _(v).rest();
|
||||
})
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
if (has_size) {
|
||||
var sum = get_sum(children);
|
||||
var count = children.length;
|
||||
|
||||
return children.length == 1 && children[0].length === 1
|
||||
? { name: key, size: children[0][0] }
|
||||
: { name: key, sum: sum, count: count, children: nest(children) };
|
||||
} else {
|
||||
return children.length == 1 && children[0].length === 0
|
||||
? { name: key }
|
||||
: { name: key, children: nest(children) };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var formatted_data = {
|
||||
name: root_label,
|
||||
children: nest(data),
|
||||
};
|
||||
|
||||
if (has_size) {
|
||||
_(formatted_data).extend({
|
||||
sum: get_sum(data),
|
||||
count: data.length,
|
||||
});
|
||||
}
|
||||
|
||||
return formatted_data;
|
||||
},
|
||||
updateView: function (viz, data) {
|
||||
this.$el.html("");
|
||||
|
||||
//this.$el.append('<button id="open_all">Open all</button>');
|
||||
//this.$el.append('<button id="close_all">Close all</button>');
|
||||
|
||||
//$("#open_all").on("click", function() {
|
||||
// $("g.node_close").click();
|
||||
//});
|
||||
|
||||
//$("#close_all").on("click", function() {
|
||||
// $("g.node_open").click();
|
||||
//});
|
||||
|
||||
var has_size = this.settings.get("has_size");
|
||||
|
||||
var node_outline_color = this.settings.get("node_outline_color");
|
||||
var node_close_color = this.settings.get("node_close_color");
|
||||
var node_open_color = this.settings.get("node_open_color");
|
||||
var label_size_color = this.settings.get("label_size_color");
|
||||
var label_count_color = this.settings.get("label_count_color");
|
||||
|
||||
var width = this.settings.get("width_px");
|
||||
var height = this.settings.get("height_px");
|
||||
|
||||
var m = [
|
||||
20,
|
||||
this.settings.get("margin_right"),
|
||||
20,
|
||||
this.settings.get("margin_left"),
|
||||
],
|
||||
w = width - m[1] - m[3],
|
||||
h = height - m[0] - m[2],
|
||||
i = 0;
|
||||
|
||||
var tree = d3.layout.tree().size([h, w]);
|
||||
|
||||
var diagonal = d3.svg.diagonal().projection(function (d) {
|
||||
return [d.y, d.x];
|
||||
});
|
||||
|
||||
var vis = d3
|
||||
.select(this.el)
|
||||
.append("svg:svg")
|
||||
.attr("width", w + m[1] + m[3])
|
||||
.attr("height", h + m[0] + m[2])
|
||||
.append("svg:g")
|
||||
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
||||
|
||||
data.x0 = h / 2;
|
||||
data.y0 = 0;
|
||||
|
||||
function toggle_children(tree, level) {
|
||||
if (tree.children) {
|
||||
_(tree.children).each(function (child) {
|
||||
toggle_children(child, level + 1);
|
||||
});
|
||||
|
||||
if (level >= initial_open_level) {
|
||||
toggle(tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initial_open_level = this.settings.get("initial_open_level");
|
||||
|
||||
if (initial_open_level >= 0) {
|
||||
toggle_children(data, 0);
|
||||
}
|
||||
|
||||
var duration = 0;
|
||||
update(data);
|
||||
duration = d3.event && d3.event.altKey ? 5000 : 500;
|
||||
|
||||
function update(source) {
|
||||
// Compute the new tree layout.
|
||||
var nodes = tree.nodes(data).reverse();
|
||||
|
||||
// Normalize for fixed-depth.
|
||||
nodes.forEach(function (d) {
|
||||
d.y = d.depth * 180;
|
||||
});
|
||||
|
||||
// Update the nodes…
|
||||
var node = vis.selectAll("g.node").data(nodes, function (d) {
|
||||
return d.id || (d.id = ++i);
|
||||
});
|
||||
|
||||
// Enter any new nodes at the parent's previous position.
|
||||
var nodeEnter = node
|
||||
.enter()
|
||||
.append("svg:g")
|
||||
//.attr("class", "node")
|
||||
.attr("class", function (d) {
|
||||
return d._children ? "node node_close" : "node node_open";
|
||||
})
|
||||
.attr("transform", function (d) {
|
||||
return "translate(" + source.y0 + "," + source.x0 + ")";
|
||||
})
|
||||
.on("click", function (d) {
|
||||
toggle(d);
|
||||
update(d);
|
||||
});
|
||||
|
||||
nodeEnter
|
||||
.append("svg:circle")
|
||||
.attr("r", 1e-6)
|
||||
.style("fill", function (d) {
|
||||
return d._children ? node_close_color : node_open_color;
|
||||
})
|
||||
.style("cursor", function (d) {
|
||||
return d.children || d._children ? "pointer" : "default";
|
||||
})
|
||||
.style("stroke", node_outline_color);
|
||||
|
||||
nodeEnter
|
||||
.append("svg:text")
|
||||
.attr("x", function (d) {
|
||||
return d.children || d._children ? -10 : 10;
|
||||
})
|
||||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", function (d) {
|
||||
return d.children || d._children ? "end" : "start";
|
||||
})
|
||||
.style("cursor", function (d) {
|
||||
return d.children || d._children ? "pointer" : "default";
|
||||
})
|
||||
.style("fill-opacity", 1e-6)
|
||||
.html(function (d) {
|
||||
if (has_size) {
|
||||
var sum = Number(d.sum).toLocaleString("en");
|
||||
var size = Number(d.size).toLocaleString("en");
|
||||
|
||||
var long_label =
|
||||
d.name +
|
||||
' - <tspan fill="' +
|
||||
label_size_color +
|
||||
'">' +
|
||||
sum +
|
||||
'</tspan> - <tspan fill="' +
|
||||
label_count_color +
|
||||
'">' +
|
||||
d.count +
|
||||
"<tspan>";
|
||||
var short_label =
|
||||
d.name +
|
||||
' - <tspan fill="' +
|
||||
label_size_color +
|
||||
'">' +
|
||||
size +
|
||||
"<tspan>";
|
||||
|
||||
return d.children || d._children ? long_label : short_label;
|
||||
} else {
|
||||
return d.name;
|
||||
}
|
||||
});
|
||||
|
||||
// Transition nodes to their new position.
|
||||
var nodeUpdate = node
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr("transform", function (d) {
|
||||
return "translate(" + d.y + "," + d.x + ")";
|
||||
});
|
||||
|
||||
nodeUpdate
|
||||
.select("circle")
|
||||
.attr("r", 4.5)
|
||||
.style("fill", function (d) {
|
||||
return d._children ? node_close_color : node_open_color;
|
||||
});
|
||||
|
||||
nodeUpdate.select("text").style("fill-opacity", 1);
|
||||
nodeUpdate.select("text").style("fill", "white");
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
var nodeExit = node
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr("transform", function (d) {
|
||||
return "translate(" + source.y + "," + source.x + ")";
|
||||
})
|
||||
.remove();
|
||||
|
||||
nodeExit.select("circle").attr("r", 1e-6);
|
||||
|
||||
nodeExit.select("text").style("fill-opacity", 1e-6);
|
||||
|
||||
// Update the links…
|
||||
var link = vis
|
||||
.selectAll("path.link")
|
||||
.data(tree.links(nodes), function (d) {
|
||||
return d.target.id;
|
||||
});
|
||||
|
||||
// Enter any new links at the parent's previous position.
|
||||
link
|
||||
.enter()
|
||||
.insert("svg:path", "g")
|
||||
.attr("class", "link")
|
||||
.attr("d", function (d) {
|
||||
var o = { x: source.x0, y: source.y0 };
|
||||
return diagonal({ source: o, target: o });
|
||||
})
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr("d", diagonal);
|
||||
|
||||
// Transition links to their new position.
|
||||
link.transition().duration(duration).attr("d", diagonal);
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
link
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr("d", function (d) {
|
||||
var o = { x: source.x, y: source.y };
|
||||
return diagonal({ source: o, target: o });
|
||||
})
|
||||
.remove();
|
||||
|
||||
// Stash the old positions for transition.
|
||||
nodes.forEach(function (d) {
|
||||
d.x0 = d.x;
|
||||
d.y0 = d.y;
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle children.
|
||||
function toggle(d) {
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d.children = null;
|
||||
} else {
|
||||
d.children = d._children;
|
||||
d._children = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Dendrogram;
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "forcedirected",
|
||||
"version": "1.0.0",
|
||||
"main": "forcedirected.js",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"d3": "3.3.x"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
.splunk-toolkit-force-directed {
|
||||
overflow: hidden;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed circle.node {
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .link, .splunk-toolkit-force-directed #arrowEnd {
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
fill: none;
|
||||
}
|
||||
.splunk-toolkit-force-directed #arrowEnd {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed circle.node {
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed circle.nodeHighlight,
|
||||
.splunk-toolkit-force-directed circle.highlight {
|
||||
stroke-width: 2px;
|
||||
stroke: #E89595;
|
||||
}
|
||||
|
||||
.linkHighlight {
|
||||
stroke: red !important;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed circle.nodeHighlight.highlight {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
|
||||
.splunk-toolkit-force-directed line.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed #chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed #tooltipContainer {
|
||||
border: 1px solid hsl(0, 0%, 80%);
|
||||
position: absolute;
|
||||
min-width: 200px;
|
||||
min-height: 50px;
|
||||
border-radius:3px;
|
||||
z-index:100;
|
||||
background: #3A3A3A;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
top:50px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .group-swatch {
|
||||
width:20px;
|
||||
height:20px;
|
||||
float:left;
|
||||
margin:2px;
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .group-name {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .tooltipLabel {
|
||||
font-weight:bold;
|
||||
padding-right:5px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .tooltipRow {
|
||||
margin-bottom:10px;
|
||||
}
|
||||
|
||||
.splunk-toolkit-force-directed .panCursor {
|
||||
cursor: move;
|
||||
}
|
||||
@ -0,0 +1,546 @@
|
||||
// Force Directed Graphs!
|
||||
// these require an input of (at least) 3 fields in the format
|
||||
// 'stats count by field1 field2 field3'
|
||||
|
||||
// ---- settings ----
|
||||
// height, width
|
||||
// panAndZoom: the ability to zoom (true, false)
|
||||
// directional: true, false
|
||||
// valueField: what field to count by
|
||||
// charges, gravity: change the look of the graph, play around with these!
|
||||
// linkDistance: the distance between each node
|
||||
|
||||
// ---- expected data format ----
|
||||
// a splunk search like this: source=*somedata* | stats count by artist_name track_name device
|
||||
// each group is an artist/song pairing
|
||||
// {
|
||||
// "nodes":[
|
||||
// {
|
||||
// "source":"Bruno Mars",
|
||||
// "group":0
|
||||
// },
|
||||
// {
|
||||
// "source":"It Will Rain",
|
||||
// "group":0
|
||||
// },
|
||||
// {
|
||||
// "source":"Cobra Starship",
|
||||
// "group":1
|
||||
// },
|
||||
// {
|
||||
// "source":"You Make Me Feel",
|
||||
// "group":1
|
||||
// },
|
||||
// {
|
||||
// "source":"Gym Class Heroes",
|
||||
// "group":2
|
||||
// },
|
||||
// {
|
||||
// "source":"Stereo Hearts",
|
||||
// "group":2
|
||||
// },
|
||||
// ],
|
||||
// "links":[
|
||||
// {
|
||||
// "source":0,
|
||||
// "target":1,
|
||||
// "value":null
|
||||
// },
|
||||
// {
|
||||
// "source":2,
|
||||
// "target":3,
|
||||
// "value":null
|
||||
// },
|
||||
// {
|
||||
// "source":4,
|
||||
// "target":5,
|
||||
// "value":null
|
||||
// },
|
||||
// ],
|
||||
|
||||
// - we add this part -
|
||||
|
||||
// "groupNames":{
|
||||
// "iphone":49,
|
||||
// "android":53,
|
||||
// "blackberry":48,
|
||||
// "ipad":52,
|
||||
// "ipod":50
|
||||
// },
|
||||
// "groupLookup":[
|
||||
// "iphone",
|
||||
// "android",
|
||||
// "blackberry",
|
||||
// "ipad",
|
||||
// "ipod"
|
||||
// ]
|
||||
// }
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var _ = require('underscore');
|
||||
var d3 = require("../d3/d3");
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
|
||||
var ForceDirected = SimpleSplunkView.extend({
|
||||
moduleId: module.id,
|
||||
|
||||
className: "splunk-toolkit-force-directed",
|
||||
|
||||
options: {
|
||||
managerid: null,
|
||||
data: 'preview',
|
||||
panAndZoom: true,
|
||||
directional: true,
|
||||
valueField: 'count',
|
||||
charges: -500,
|
||||
gravity: 0.2,
|
||||
linkDistance: 15,
|
||||
swoop: false,
|
||||
isStatic: true
|
||||
},
|
||||
|
||||
output_mode: "json_rows",
|
||||
|
||||
initialize: function() {
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
// in the case that any options are changed, it will dynamically update
|
||||
// without having to refresh.
|
||||
this.settings.on("change:charges", this.render, this);
|
||||
this.settings.on("change:gravity", this.render, this);
|
||||
this.settings.on("change:linkDistance", this.render, this);
|
||||
this.settings.on("change:directional", this.render, this);
|
||||
this.settings.on("change:panAndZoom", this.render, this);
|
||||
this.settings.on("change:swoop", this.render, this);
|
||||
this.settings.on("change:isStatic", this.render, this);
|
||||
},
|
||||
|
||||
createView: function() {
|
||||
var margin = {top: 10, right: 10, bottom: 10, left: 10};
|
||||
var availableWidth = parseInt(this.settings.get("width") || this.$el.width(), 10);
|
||||
var availableHeight = parseInt(this.settings.get("height") || this.$el.height(), 10);
|
||||
|
||||
this.$el.html("");
|
||||
|
||||
var svg = d3.select(this.el)
|
||||
.append("svg")
|
||||
.attr("width", availableWidth)
|
||||
.attr("height", availableHeight)
|
||||
.attr("pointer-events", "all");
|
||||
|
||||
return { container: this.$el, svg: svg, margin: margin };
|
||||
},
|
||||
|
||||
// making the data look how we want it to for updateView to do its job
|
||||
formatData: function(data) {
|
||||
var nodes = {};
|
||||
var links = [];
|
||||
data.forEach(function(link) {
|
||||
var sourceName = link[0];
|
||||
var targetName = link[1];
|
||||
var groupName = link[2];
|
||||
var newLink = {};
|
||||
newLink.source = nodes[sourceName] ||
|
||||
(nodes[sourceName] = {name: sourceName, group: groupName, value: 0});
|
||||
newLink.target = nodes[targetName] ||
|
||||
(nodes[targetName] = {name: targetName, group: groupName, value: 0});
|
||||
newLink.value = +link[3];
|
||||
newLink.source.value += newLink.value;
|
||||
newLink.target.value += newLink.value;
|
||||
links.push(newLink);
|
||||
});
|
||||
|
||||
return {nodes: d3.values(nodes), links: links};
|
||||
},
|
||||
|
||||
updateView: function(viz, data) {
|
||||
var that = this;
|
||||
var containerHeight = this.$el.height();
|
||||
var containerWidth = this.$el.width();
|
||||
|
||||
// Clear svg
|
||||
var svg = $(viz.svg[0]);
|
||||
svg.empty();
|
||||
svg.height(containerHeight);
|
||||
svg.width(containerWidth);
|
||||
|
||||
// Add the graph group as a child of the main svg
|
||||
var graphWidth = containerWidth - viz.margin.left - viz.margin.right;
|
||||
var graphHeight = containerHeight - viz.margin.top - viz.margin.bottom;
|
||||
var graph = viz.svg
|
||||
.append("g")
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")");
|
||||
|
||||
// Get settings
|
||||
this.charge = this.settings.get('charges');
|
||||
this.gravity = this.settings.get('gravity');
|
||||
this.linkDistance = this.settings.get('linkDistance');
|
||||
this.zoomable = this.settings.get('panAndZoom');
|
||||
this.swoop = this.settings.get('swoop');
|
||||
this.isStatic = this.settings.get('isStatic');
|
||||
this.isDirectional = this.settings.get('directional');
|
||||
this.zoomFactor = 0.5;
|
||||
|
||||
this.groupNameLookup = data.groupLookup;
|
||||
|
||||
// Set up graph
|
||||
var r = 6;
|
||||
var height = graphHeight;
|
||||
var width = graphWidth;
|
||||
var force = d3.layout.force()
|
||||
.gravity(this.gravity)
|
||||
.charge(this.charge)
|
||||
.linkDistance(this.linkDistance)
|
||||
.size([width, height]);
|
||||
|
||||
this.color = d3.scale.category20();
|
||||
|
||||
this.tooltips = new Tooltips(graph);
|
||||
|
||||
if (this.zoomable) {
|
||||
initPanZoom.call(this, viz.svg);
|
||||
}
|
||||
|
||||
graph.style("opacity", 1e-6)
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.style("opacity", 1);
|
||||
|
||||
graph.append("svg:defs").selectAll("marker")
|
||||
.data(["arrowEnd"])
|
||||
.enter().append("svg:marker")
|
||||
.attr("id", String)
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 0)
|
||||
.attr("refY", 0)
|
||||
.attr("markerWidth", 6)
|
||||
.attr("markerHeight", 6)
|
||||
.attr("markerUnits", "userSpaceOnUse")
|
||||
.attr("orient", "auto")
|
||||
.append("svg:path")
|
||||
.attr("d", "M0,-5L10,0L0,5");
|
||||
|
||||
var link = graph.selectAll("line.link")
|
||||
.data(data.links)
|
||||
.enter().append('path')
|
||||
.attr("class", "link")
|
||||
.attr("marker-end", function(d) {
|
||||
if (that.isDirectional) {
|
||||
return "url(#" + "arrowEnd" + ")";
|
||||
}
|
||||
})
|
||||
.style("stroke-width", function(d) {
|
||||
var num = Math.max(Math.round(Math.log(d.value)), 1);
|
||||
return _.isNaN(num) ? 1 : num;
|
||||
});
|
||||
|
||||
link
|
||||
.on('click', function(d) {
|
||||
that.trigger('click:link', {
|
||||
source: d.source.name,
|
||||
sourceGroup: d.source.group,
|
||||
target: d.target.name,
|
||||
targetGroup: d.target.group,
|
||||
value: d.value
|
||||
});
|
||||
})
|
||||
.on('mouseover', function(d) {
|
||||
d3.select(this).classed('linkHighlight', true);
|
||||
openLinkTooltip(d, this);
|
||||
})
|
||||
.on('mouseout', function(d) {
|
||||
d3.select(this).classed('linkHighlight', false);
|
||||
that.tooltips.close(this);
|
||||
});
|
||||
|
||||
var node = graph.selectAll("circle.node")
|
||||
.data(data.nodes)
|
||||
.enter().append("svg:circle")
|
||||
.attr("class", "node")
|
||||
.attr("r", r - 1)
|
||||
.style("fill", function(d) {
|
||||
return that.color(d.group);
|
||||
})
|
||||
.call(force.drag);
|
||||
|
||||
node.append("title")
|
||||
.text(function(d) { return d.name; });
|
||||
|
||||
node
|
||||
.on('click', function(d) {
|
||||
that.trigger('click:node', {
|
||||
name: d.name,
|
||||
group: d.group,
|
||||
value: d.value
|
||||
});
|
||||
})
|
||||
.on('mouseover', function(d) {
|
||||
d3.select(this).classed('nodeHighlight', true);
|
||||
openNodeTooltip(d, this);
|
||||
})
|
||||
.on('mouseout', function(d) {
|
||||
d3.select(this).classed('nodeHighlight', false);
|
||||
that.tooltips.close(this);
|
||||
});
|
||||
|
||||
force.nodes(data.nodes)
|
||||
.links(data.links)
|
||||
.on("tick", function() {
|
||||
link.attr("d", function(d) {
|
||||
var diffX = d.target.x - d.source.x;
|
||||
var diffY = d.target.y - d.source.y;
|
||||
|
||||
// Length of path from center of source node to center of target node
|
||||
var pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
|
||||
|
||||
// x and y distances from center to outside edge of target node
|
||||
var offsetX = (diffX * (r * 2)) / pathLength;
|
||||
var offsetY = (diffY * (r * 2)) / pathLength;
|
||||
|
||||
if (!that.swoop) {
|
||||
pathLength = 0;
|
||||
}
|
||||
|
||||
return "M" + d.source.x + "," + d.source.y + "A" + pathLength + "," + pathLength + " 0 0,1 " + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
|
||||
});
|
||||
|
||||
node.attr("cx", function(d) {
|
||||
d.x = Math.max(r, Math.min(width - r, d.x));
|
||||
return d.x;
|
||||
})
|
||||
.attr("cy", function(d) {
|
||||
d.y = Math.max(r, Math.min(height - r, d.y));
|
||||
return d.y;
|
||||
});
|
||||
|
||||
}).start();
|
||||
|
||||
if (this.isStatic) {
|
||||
forwardAlpha(force, 0.005, 1000);
|
||||
}
|
||||
|
||||
function forwardAlpha(layout, alpha, max) {
|
||||
alpha = alpha || 0;
|
||||
max = max || 1000;
|
||||
var i = 0;
|
||||
while (layout.alpha() > alpha && i++ < max) {
|
||||
layout.tick();
|
||||
}
|
||||
}
|
||||
|
||||
// draggin'
|
||||
function initPanZoom(svg) {
|
||||
var that = this;
|
||||
svg.on('mousedown.drag', function() {
|
||||
if (that.zoomable) {
|
||||
svg.classed('panCursor', true);
|
||||
}
|
||||
// console.log('drag start');
|
||||
});
|
||||
|
||||
svg.on('mouseup.drag', function() {
|
||||
svg.classed('panCursor', false);
|
||||
// console.log('drag stop');
|
||||
});
|
||||
|
||||
svg.call(d3.behavior.zoom().on("zoom", function() {
|
||||
panZoom();
|
||||
}));
|
||||
}
|
||||
|
||||
// zoomin'
|
||||
function panZoom() {
|
||||
graph.attr("transform",
|
||||
"translate(" + d3.event.translate + ")"
|
||||
+ " scale(" + d3.event.scale + ")");
|
||||
}
|
||||
|
||||
function openNodeTooltip(d, node) {
|
||||
var groupName;
|
||||
|
||||
if (that.groupNameLookup !== undefined) {
|
||||
groupName = that.groupNameLookup[d.group];
|
||||
} else {
|
||||
groupName = d.group;
|
||||
}
|
||||
|
||||
that.tooltips.open('nodes', {
|
||||
slots: {
|
||||
val: d.name,
|
||||
group: groupName
|
||||
},
|
||||
swatch: that.color(d.group)
|
||||
}, node);
|
||||
}
|
||||
|
||||
function openLinkTooltip(d, node) {
|
||||
that.tooltips.open('links', {
|
||||
slots: {
|
||||
source: d.source.name,
|
||||
target: d.target.name
|
||||
}
|
||||
}, node);
|
||||
}
|
||||
|
||||
//TODO: This doesn't seem to be used in this file
|
||||
function getSafeVal(getobj, name) {
|
||||
var retVal;
|
||||
if (getobj.hasOwnProperty(name) && getobj[name] !== null) {
|
||||
retVal = getobj[name];
|
||||
} else {
|
||||
retVal = name;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function highlightNodes(val) {
|
||||
var self = this, groupName;
|
||||
if (val !== ' ' && val !== '') {
|
||||
graph.selectAll('circle')
|
||||
.filter(function(d, i) {
|
||||
groupName = self.groupNameLookup[d.group];
|
||||
if (d.source.indexOf(val) >= 0 || groupName.indexOf(val) >= 0) {
|
||||
d3.select(this).classed('highlight', true);
|
||||
} else {
|
||||
d3.select(this).classed('highlight', false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
graph.selectAll('circle').classed('highlight', false);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////// formerly known as tooltips.js /////////////////////////////
|
||||
|
||||
function Tooltips(svg) {
|
||||
var tooltipTimer = null,
|
||||
tooltipOpenCoords = {},
|
||||
tooltipIsOpen = false,
|
||||
tooltipContents,
|
||||
$tooltipContainer,
|
||||
isReady = false,
|
||||
layouts;
|
||||
|
||||
setup(svg, viz.container);
|
||||
|
||||
function setup(svg, $container) {
|
||||
var self = this,
|
||||
data = [0],
|
||||
$nodeVal, $nodeGroup, $nodeContainer,
|
||||
$linkSource, $linkTarget, $linkContainer;
|
||||
|
||||
$tooltipContainer = $("<div id='tooltipContainer'></div>");
|
||||
|
||||
$nodeContainer = $("<div class='nodeContainer'></div>");
|
||||
$nodeVal = $("<div class='node-value tooltipRow'><span class='tooltipLabel'>Value: </span><span class='field1-val'></span></div>");
|
||||
$nodeGroup = $("<div class='node-group tooltipRow'></div><div class='group-swatch'></div><div class='group-name'><span class='tooltipLabel'>Group: </span><span class='group-val'></span></div>");
|
||||
$nodeContainer.append($nodeVal);
|
||||
$nodeContainer.append($nodeGroup);
|
||||
$tooltipContainer.append($nodeContainer);
|
||||
|
||||
$linkContainer = $("<div class='linkContainer'></div>");
|
||||
$linkSource = $("<div class='source tooltipRow'><span class='tooltipLabel'>Source: </span><span class='source-val'></span></div>");
|
||||
$linkTarget = $("<div class='target tooltipRow'><span class='tooltipLabel'>Target: </span><span class='target-val'></span></div>");
|
||||
$linkContainer.append($linkSource);
|
||||
$linkContainer.append($linkTarget);
|
||||
$tooltipContainer.append($linkContainer);
|
||||
|
||||
$tooltipContainer.find('.group-swatch').hide();
|
||||
|
||||
$container.prepend($tooltipContainer);
|
||||
$tooltipContainer.hide();
|
||||
|
||||
layouts = {
|
||||
'nodes': {
|
||||
"container": $nodeContainer,
|
||||
"slots": {
|
||||
"val": $nodeVal.find('.field1-val'),
|
||||
"group": $nodeGroup.find('.group-val')
|
||||
},
|
||||
"swatch": $nodeContainer.find('.group-swatch')
|
||||
},
|
||||
'links': {
|
||||
"container": $linkContainer,
|
||||
"slots": {
|
||||
"source": $linkSource.find('.source-val'),
|
||||
"target": $linkTarget.find('.target-val')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
isReady = true;
|
||||
}
|
||||
|
||||
function clearTooltips() {
|
||||
if (isReady) {
|
||||
$.each(layouts, function(k, layout) {
|
||||
$.each(layout.slots, function(k, v) {
|
||||
// this isnt really neccesary because it's either hidden or shown with newly-replaced content
|
||||
v.empty();
|
||||
});
|
||||
layout.container.hide();
|
||||
if (layout.swatch !== undefined) {
|
||||
layout.swatch.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.close = function(node) {
|
||||
// return false;
|
||||
var self = this,
|
||||
dx, dy;
|
||||
|
||||
var mouseCoords = d3.mouse(node);
|
||||
|
||||
if (tooltipTimer !== null) {
|
||||
window.clearTimeout(tooltipTimer);
|
||||
}
|
||||
|
||||
dx = Math.abs(tooltipOpenCoords.x - mouseCoords[0]);
|
||||
dy = Math.abs(tooltipOpenCoords.y - mouseCoords[1]);
|
||||
|
||||
/*
|
||||
only close the tooltip when the user has moved a certain distance away
|
||||
this helps when an element is very small and the user might have
|
||||
difficulty keeping their mouse directly over it
|
||||
*/
|
||||
if (dy > 10 || dx > 10) {
|
||||
tooltipIsOpen = false;
|
||||
tooltipTimer = window.setTimeout(function() {
|
||||
$tooltipContainer.fadeOut(400);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
this.open = function(layout, data, node) {
|
||||
var mouseCoords = d3.mouse(node);
|
||||
tooltipIsOpen = true;
|
||||
tooltipOpenCoords = {
|
||||
x: mouseCoords[0] + 6 * 2,
|
||||
y: mouseCoords[1] + 6 * 3
|
||||
};
|
||||
|
||||
clearTooltips();
|
||||
$.each(data.slots, function(k, v) {
|
||||
layouts[layout]['slots'][k].append(v);
|
||||
});
|
||||
layouts[layout]['container'].show();
|
||||
if (layouts[layout]['swatch'] !== undefined) {
|
||||
layouts[layout]['swatch'].show().css('background-color', data.swatch);
|
||||
}
|
||||
|
||||
$tooltipContainer
|
||||
.css("left", tooltipOpenCoords.x)
|
||||
.css("top", tooltipOpenCoords.y);
|
||||
$tooltipContainer.fadeIn(400);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
return ForceDirected;
|
||||
});
|
||||
@ -0,0 +1,53 @@
|
||||
// This code has been imported from the following nice work: https://splunkbase.splunk.com/app/3171/
|
||||
|
||||
// Author: Ryan Thibodeaux
|
||||
//
|
||||
//
|
||||
// Options module for passing
|
||||
// parameters between modules/functions.
|
||||
|
||||
(function() {
|
||||
define([
|
||||
"module",
|
||||
], function(module) {
|
||||
|
||||
var appOptions;
|
||||
var options = {};
|
||||
var config = module.config();
|
||||
|
||||
if (typeof config !== 'undefined' && config !== null) {
|
||||
options = config.options;
|
||||
}
|
||||
|
||||
return appOptions = (function() {
|
||||
function appOptions() {} // empty constructor
|
||||
|
||||
// check if appOptions contains parameter 'name'
|
||||
appOptions.hasOption = function(name) {
|
||||
if (typeof options === 'undefined' ||
|
||||
typeof name === 'undefined' ||
|
||||
options.hasOwnProperty(name) !== true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// return value stored in parameter 'name'
|
||||
appOptions.getOptionValue = function(name) {
|
||||
return (appOptions.hasOption(name) ? options[name] : undefined);
|
||||
};
|
||||
|
||||
// set parameter 'name' to value
|
||||
appOptions.setOptionValue = function(name, value) {
|
||||
if (!appOptions.hasOption(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
options[name] = value;
|
||||
return true;
|
||||
};
|
||||
|
||||
return appOptions;
|
||||
})();
|
||||
});
|
||||
}).call(this);
|
||||
@ -0,0 +1,740 @@
|
||||
// This code has been imported from the following nice work: https://splunkbase.splunk.com/app/3171/
|
||||
|
||||
// Author: Ryan Thibodeaux
|
||||
//
|
||||
//
|
||||
// Utility functions used throughout
|
||||
// other modules and funcitons in the app.
|
||||
|
||||
|
||||
(function() {
|
||||
define([
|
||||
"jquery",
|
||||
"underscore",
|
||||
"appOptions",
|
||||
"splunkjs/mvc",
|
||||
"splunkjs/mvc/simplexml/ready!",
|
||||
], function($, _, appOptions, mvc) {
|
||||
|
||||
var appUtils;
|
||||
var footerRemovalTimerOn = 0;
|
||||
|
||||
var submittedTokenModel = mvc.Components.get('submitted');
|
||||
var defaultTokenModel = mvc.Components.get('default');
|
||||
|
||||
if (typeof mvc === 'undefined' || !submittedTokenModel || !defaultTokenModel) {
|
||||
var str = "Failed to load Splunk components. " +
|
||||
"This is probably a symptom of a bigger problem.";
|
||||
alert(str);
|
||||
console.error(str);
|
||||
}
|
||||
|
||||
return appUtils = (function() {
|
||||
|
||||
function appUtils() {} // empty constructor
|
||||
|
||||
|
||||
// Initializes app tokens and set footer removal timer
|
||||
appUtils.initiliazeApp = function(submit) {
|
||||
|
||||
// make sure appName and pageName are set, and set
|
||||
// the 'my_app' and 'my_view' tokens accordingly
|
||||
var myApp = appOptions.getOptionValue('appName');
|
||||
var myView = appOptions.getOptionValue('pageName');
|
||||
if (typeof myApp === 'undefined' || myApp.toString().trim().length < 1 ||
|
||||
typeof myView === 'undefined' || myView.toString().trim().length < 1) {
|
||||
|
||||
var comps = (location.pathname.split('?')[0]).split('/');
|
||||
var idx = comps.indexOf('app');
|
||||
myApp = comps[idx + 1];
|
||||
myView = comps[idx + 2];
|
||||
|
||||
appOptions.setOptionValue('appName', myApp);
|
||||
appOptions.setOptionValue('pageName', myView);
|
||||
}
|
||||
|
||||
appUtils.setToken('my_app', myApp);
|
||||
appUtils.setToken('my_view', myView);
|
||||
|
||||
if (!!submit) { //!!undefined is false
|
||||
appUtils.submitTokens();
|
||||
}
|
||||
|
||||
// make sure pageStartTime is set
|
||||
var startTime = appOptions.getOptionValue('pageStartTime');
|
||||
if (typeof startTime === 'undefined' || startTime <= 0) {
|
||||
startTime = new Date().valueOf();
|
||||
appOptions.setOptionValue('pageStartTime', startTime);
|
||||
}
|
||||
|
||||
// set timer to remove footer instead of relying on
|
||||
// page 'load' event (that was unreliable)
|
||||
appUtils.setFooterEditTimer(200);
|
||||
};
|
||||
|
||||
|
||||
// return token model objects
|
||||
appUtils.getTokenModels = function() {
|
||||
return ([defaultTokenModel, submittedTokenModel]);
|
||||
}
|
||||
|
||||
|
||||
// set generic wildcard tooltip on passed element name(s) where
|
||||
// inputs can be an array or a comma-delimited list
|
||||
appUtils.setWildCardTooltip = function(inputElements) {
|
||||
|
||||
if (typeof inputElements === 'undefined' || inputElements.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newArray = inputElements;
|
||||
|
||||
// test if not an Array - turn it into one if it is not
|
||||
if (Object.prototype.toString.call(inputElements) !== '[object Array]') {
|
||||
newArray = inputElements.replace(/^,+|,+$/gm, '').split(",");
|
||||
}
|
||||
|
||||
var len = newArray.length;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
appUtils.setTooltip(newArray[i], 'Use \"*\" as a wildcard');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// set the 'tip' string as the tooltip for element 'name'
|
||||
appUtils.setTooltip = function(name, tip) {
|
||||
|
||||
if (typeof name === 'undefined' || name.length < 1 || typeof tip === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
var eleID = (name[0] === '#' ? name : '#' + name);
|
||||
var e = $(eleID);
|
||||
|
||||
// element exists, so add tooltips
|
||||
if (e.length) {
|
||||
|
||||
// add tooltips to text inputs
|
||||
var textChild = e.children('div.splunk-textinput');
|
||||
if (textChild.length) {
|
||||
textChild.attr('title', tip);
|
||||
var textChildInputs = textChild.find('input');
|
||||
if (textChildInputs.length) {
|
||||
textChildInputs.attr('placeholder', tip);
|
||||
}
|
||||
}
|
||||
|
||||
// add tooltips to multiselect and dropdown inputs
|
||||
var msChild = e.children('div.splunk-choice-input');
|
||||
if (msChild.length) {
|
||||
msChild.attr('title', tip);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// remove links from Splunk footer
|
||||
appUtils.hideFooterLinks = function() {
|
||||
|
||||
footerRemovalTimerOn = 0;
|
||||
|
||||
var footer = $('#footer');
|
||||
|
||||
if (footer.length > 0) {
|
||||
links = footer.find('a');
|
||||
if (links.length > 0) {
|
||||
links.hide();
|
||||
|
||||
// hide the "Hide Filters" link on top of the
|
||||
// dashboard if it is present
|
||||
var hideLink = $('.hide-global-filters');
|
||||
if (hideLink.length > 0) {
|
||||
hideLink.hide();
|
||||
}
|
||||
|
||||
} else {
|
||||
appUtils.setFooterEditTimer();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// setup timer to remove links from Splunk footer, where
|
||||
// the footer is checked 'delayMS' milliseconds from now
|
||||
appUtils.setFooterEditTimer = function(delayMS) {
|
||||
|
||||
// set default delay for when to check for footer
|
||||
// if delayMS was not set
|
||||
if (Math.floor(delayMS) > 1) {} else {
|
||||
delayMS = 1000;
|
||||
}
|
||||
|
||||
// don't allow setting this timer after page has been loaded
|
||||
// for more than 60 seconds
|
||||
if (appUtils.getPageLoadedSecs() > 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only allow assessing the footer if the footer is on
|
||||
// the dashboard, i.e., hideFooter is not true
|
||||
if ($('#footer').length > 0) {
|
||||
if (!footerRemovalTimerOn) {
|
||||
setTimeout(appUtils.hideFooterLinks, delayMS);
|
||||
footerRemovalTimerOn = 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// number of seconds the page has been loaded
|
||||
appUtils.getPageLoadedSecs = function() {
|
||||
return ((new Date().valueOf() - appOptions.getOptionValue('pageStartTime')) / 1000);
|
||||
};
|
||||
|
||||
|
||||
// Sets token 'name' to 'value' in submittedTokenModel and
|
||||
// defaultTokenModel unless excludeDefault is set to true
|
||||
appUtils.setSubmittedToken = function(name, value, excludeDefault) {
|
||||
if (typeof name === 'undefined' || !submittedTokenModel) {
|
||||
return;
|
||||
}
|
||||
if (!excludeDefault) {
|
||||
appUtils.setDefaulToken(name, value);
|
||||
}
|
||||
submittedTokenModel.set(name, value);
|
||||
};
|
||||
|
||||
// Sets token 'name' to 'value' in defaultTokenModel.
|
||||
appUtils.setDefaulToken = function(name, value) {
|
||||
if (typeof name === 'undefined' || !defaultTokenModel) {
|
||||
return;
|
||||
}
|
||||
defaultTokenModel.set(name, value);
|
||||
};
|
||||
|
||||
// Sets token 'name' to 'value' in defaultTokenModel and
|
||||
// submit all tokens if set
|
||||
appUtils.setToken = function(name, value, submit) {
|
||||
appUtils.setDefaulToken(name, value);
|
||||
|
||||
if (!!submit) {
|
||||
appUtils.submitTokens();
|
||||
}
|
||||
};
|
||||
|
||||
// Returns value of token 'name' in defaultTokenModel and
|
||||
appUtils.getDefaultToken = function(name) {
|
||||
if (typeof name === 'undefined' || !defaultTokenModel) {
|
||||
return undefined;
|
||||
}
|
||||
return defaultTokenModel.get(name);
|
||||
};
|
||||
|
||||
// Returns value of token 'name' in submittedTokenModel and
|
||||
appUtils.getSubmittedToken = function(name) {
|
||||
if (typeof name === 'undefined' || !submittedTokenModel) {
|
||||
return undefined;
|
||||
}
|
||||
return submittedTokenModel.get(name);
|
||||
};
|
||||
|
||||
// Returns value of token 'name' in token model 'model'
|
||||
appUtils.getToken = function(name, model) {
|
||||
var tokens = (typeof model === 'undefined') ? defaultTokenModel : model;
|
||||
|
||||
if (typeof name === 'undefined' || !tokens) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return tokens.get(name);
|
||||
};
|
||||
|
||||
|
||||
// Copy defaultTokenModel values into submittedTokenModels
|
||||
appUtils.submitTokens = function() {
|
||||
if (submittedTokenModel && defaultTokenModel) {
|
||||
submittedTokenModel.set(defaultTokenModel.toJSON());
|
||||
}
|
||||
};
|
||||
|
||||
// Return boolean answer to if 'checkval' matches 'newval'
|
||||
appUtils.checkTokenValue = function(checkval, newval) {
|
||||
return (newval === checkval ? true : false);
|
||||
};
|
||||
|
||||
|
||||
// Jumps to div element eleID
|
||||
appUtils.scrollIntoView = function(eleID, setting) {
|
||||
var e = document.getElementById(eleID);
|
||||
if (!!e && e.scrollIntoView) {
|
||||
e.scrollIntoView(setting);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// return if a token is set or not, where "lax" determines
|
||||
// if the token is checked if it is an empty value as well
|
||||
appUtils.checkEmptyValue = function(value, lax) {
|
||||
if (!!lax) {
|
||||
return (typeof value === 'undefined')
|
||||
} else {
|
||||
return (typeof value === 'undefined' || value.length < 1)
|
||||
}
|
||||
};
|
||||
|
||||
// Toggles visibility of HTML elements of a dashboard
|
||||
appUtils.hideHtmlElement = function(eleID, hide) {
|
||||
if (appUtils.checkEmptyValue(eleID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var e = (eleID[0] === '#' ? eleID : '#' + eleID);
|
||||
if ($(e).length) {
|
||||
if (!!hide) {
|
||||
$(e).hide();
|
||||
} else {
|
||||
$(e).show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// loop through input elements and evaluate each one for focus
|
||||
appUtils.checkEmptyTokenFocusForDashboard = function(inputs) {
|
||||
if (typeof inputs === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
var len = inputs.length;
|
||||
var currentValue = undefined;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
currentValue = (defaultTokenModel.attributes.hasOwnProperty('form.' + inputs[i]) ? appUtils.getToken('form.' + inputs[i]) : appUtils.getToken(inputs[i]));
|
||||
appUtils.checkEmptyTokenFocus(inputs[i], currentValue);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// set border style based on state of 'value'
|
||||
appUtils.checkEmptyTokenFocus = function(name, value) {
|
||||
var id = (name[0] === '#' ? name : '#' + name);
|
||||
var p = $(id);
|
||||
if (p.length) {
|
||||
if (typeof value === 'undefined' || value.length < 1) {
|
||||
appUtils.setInputFocus(p);
|
||||
return true;
|
||||
} else {
|
||||
appUtils.clearInputFocus(p);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// set the focus effects on the based element
|
||||
appUtils.setInputFocus = function(el) {
|
||||
if (el.hasClass('input-text') || el.hasClass('splunk-textinput')) {
|
||||
el.find('input[type="text"]').css("border-color", "red").css("box-shadow", "0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px rgba(222, 79, 79, 0.6");
|
||||
} else if (el.hasClass('input-dropdown')) {
|
||||
el.find('.select2-choice').css("border-color", "red").css("box-shadow", "0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px rgba(222, 79, 79, 0.6");
|
||||
} else if (el.hasClass('input-multiselect')) {
|
||||
el.find('.select2-choices').css("border-color", "red").css("box-shadow", "0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px rgba(222, 79, 79, 0.6");
|
||||
} else {
|
||||
el.css("border-style", "double");
|
||||
}
|
||||
};
|
||||
|
||||
// clear the focus effects on the based element
|
||||
appUtils.clearInputFocus = function(el) {
|
||||
if (el.hasClass('input-text') || el.hasClass('splunk-textinput')) {
|
||||
el.find('input[type="text"]').css("border-color", "").css("box-shadow", "");
|
||||
} else if (el.hasClass('input-dropdown')) {
|
||||
el.find('.select2-choice').css("border-color", "").css("box-shadow", "");
|
||||
} else if (el.hasClass('input-multiselect')) {
|
||||
el.find('.select2-choices').css("border-color", "").css("box-shadow", "");
|
||||
} else {
|
||||
el.css("border-style", "none");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// add event listener of type to object using the assigned callback
|
||||
appUtils.addEvent = function(object, type, callback) {
|
||||
if (typeof object === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (object.addEventListener) {
|
||||
object.addEventListener(type, callback, false);
|
||||
} else if (object.attachEvent) {
|
||||
object.attachEvent("on" + type, callback);
|
||||
} else {
|
||||
object["on" + type] = callback;
|
||||
}
|
||||
};
|
||||
|
||||
// redirects to a new page in the current app where urlSegment
|
||||
// starts with the new page to go to and newTab indicates if
|
||||
// we want to open a new tab or not
|
||||
appUtils.drilldownRedirect = function(urlSegment, newTab) {
|
||||
|
||||
if (typeof urlSegment === 'undefined' || urlSegment.toString().trim().length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the new segment starts with a '/'
|
||||
var segment = urlSegment.toString().trim();
|
||||
segment = (segment[0] === '/' ? segment : '/' + segment);
|
||||
|
||||
// get strip everything in the current URL and strip it down
|
||||
// to what comes before the current page, including the last '/'
|
||||
var uri = window.location.toString();
|
||||
var currentPage = appUtils.getToken('my_view');
|
||||
var path = uri.substr(0, uri.indexOf(currentPage)).replace(/\/+$/i, '');
|
||||
|
||||
// go to new URL
|
||||
if (!!newTab) {
|
||||
window.open(path + segment, "_blank");
|
||||
} else {
|
||||
window.location = path + segment;
|
||||
}
|
||||
};
|
||||
|
||||
// generate html button in parent element
|
||||
// id: id and name to use on the html button
|
||||
// label: label/span to apply to the button
|
||||
// parent: id of parent html element in which to place the button
|
||||
// append?: should append or prepend in parent list of children
|
||||
// submit?: should it be a submit button type or not
|
||||
// vertical?: is the button used in a vertical list of items
|
||||
appUtils.generateButton = function(id, label, parent, append, submit, vertical) {
|
||||
|
||||
var btn = document.createElement('button');
|
||||
var span;
|
||||
|
||||
// apply id field
|
||||
if (typeof id !== 'undefined' && id.length > 0) {
|
||||
btn.id = id;
|
||||
btn.name = id;
|
||||
}
|
||||
|
||||
// apply label
|
||||
if (typeof label !== 'undefined' && label.length > 0) {
|
||||
span = document.createElement('span');
|
||||
span.innerHTML = label;
|
||||
btn.appendChild(span);
|
||||
}
|
||||
|
||||
// assign styling and insert if parent is set
|
||||
if (typeof parent !== 'undefined' && parent.length > 0) {
|
||||
var parentID = (parent[0] === '#' ? parent : '#' + parent);
|
||||
var p = $(parentID);
|
||||
|
||||
if (p.length) {
|
||||
|
||||
// set button in its place of the parent
|
||||
var t = p.find('.fieldset');
|
||||
if (t.length) {
|
||||
t = $(t[0]);
|
||||
if (!!append) {
|
||||
t.append(btn);
|
||||
} else {
|
||||
t.prepend(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set button type classes and CSS
|
||||
if (!!submit) {
|
||||
btn.className = 'btn btn-primary';
|
||||
} else {
|
||||
btn.className = 'btn-info btn-app-info';
|
||||
}
|
||||
|
||||
// set button CSS based on it being in a
|
||||
// vertical stack of items or not
|
||||
if (!!vertical) {
|
||||
btn.style.verticalAlign = 'middle';
|
||||
btn.style.margin = "5px 10px 5px 0px";
|
||||
} else {
|
||||
btn.style.verticalAlign = 'top';
|
||||
btn.style.marginTop = "21px";
|
||||
btn.style.marginRight = " 10px";
|
||||
}
|
||||
|
||||
return $(btn);
|
||||
};
|
||||
|
||||
// strip value of dangerous characters in Splunk and trim the result
|
||||
appUtils.cleanTxtString = function(value) {
|
||||
if (typeof value === 'undefined' || value.length < 1) {
|
||||
return "";
|
||||
}
|
||||
return value.replace(/%|\||\=|\[|\]|\(|\)/g, "").trim();
|
||||
};
|
||||
|
||||
// clean raw input text elements
|
||||
// value: current value to clean
|
||||
// defaultVal: default value to return if cleaned version is empty/undefined
|
||||
// post: function to use to clean passed value
|
||||
appUtils.cleanTextInputElement = function(value, defaultVal, post) {
|
||||
|
||||
var cleanedValue = defaultVal;
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
|
||||
if (!!post) {
|
||||
cleanedValue = post(value.toString());
|
||||
} else {
|
||||
cleanedValue = appUtils.cleanTxtString(value.toString());
|
||||
}
|
||||
|
||||
if (cleanedValue.length < 1) {
|
||||
cleanedValue = defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedValue;
|
||||
};
|
||||
|
||||
|
||||
// Forces the strict ordering of the values in the token of a checkbox
|
||||
// group identified by the argument "name". The ordering is determined
|
||||
// by the order of the individual checkboxes in the group. The current
|
||||
// values of the token is passed via the "value" argument.
|
||||
appUtils.enforceCheckboxOrdering = function(name, value) {
|
||||
var cb = mvc.Components.getInstance(name);
|
||||
if (typeof cb !== 'undefined') {
|
||||
var preferred_values_order = [];
|
||||
var new_field_list = [];
|
||||
var matched = [];
|
||||
var choices = cb.options.choices;
|
||||
|
||||
// set list of preferred order based on value ordering in checkbox
|
||||
for (var i = 0; i < choices.length; i++) {
|
||||
preferred_values_order.push(choices[i]['value']);
|
||||
}
|
||||
|
||||
// values that do match entries in ordered_preference
|
||||
matched = value.filter(function(x) { return preferred_values_order.indexOf(x) >= 0 });
|
||||
|
||||
// loop through preferred_values_order and add them if they are present in matched
|
||||
for (var j = 0; j < preferred_values_order.length; j++) {
|
||||
if (matched.indexOf(preferred_values_order[j]) >= 0) {
|
||||
new_field_list.push(preferred_values_order[j]);
|
||||
}
|
||||
}
|
||||
|
||||
appUtils.setToken("form." + name, new_field_list);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Setup the modal search tool button and event listeners
|
||||
// for the passed instance in "modalObject"
|
||||
appUtils.setupModalSearchTool = function(modalObject) {
|
||||
|
||||
if (typeof modalObject !== 'undefined') {
|
||||
|
||||
// Create a button on the top fieldset that will open the modal window
|
||||
var modalButton = appUtils.generateButton('btn_modal_open', 'Open Search Tool');
|
||||
modalButton.click(function() {
|
||||
appUtils.setToken('dd_modal_search_time.earliest', appUtils.getToken('earliest'));
|
||||
appUtils.setToken('dd_modal_search_time.latest', appUtils.getToken('latest'));
|
||||
modalObject.show();
|
||||
});
|
||||
|
||||
// add modal button to end of top fieldset after the last input / submit button
|
||||
var topFieldset = $('.dashboard-body').find('.fieldset').first();
|
||||
if (topFieldset.length > 0) {
|
||||
var topFieldsetChildren = topFieldset.children();
|
||||
if (topFieldsetChildren.length > 0) {
|
||||
var i = topFieldsetChildren.length - 1;
|
||||
for(i; i >= 0 ; i--) {
|
||||
var lastChild = $(topFieldsetChildren[i]);
|
||||
if (!lastChild.hasClass('form-submit') && !lastChild.hasClass('input')) {
|
||||
// continue, go to next previous child
|
||||
} else {
|
||||
lastChild.after(modalButton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i < 0) {
|
||||
topFieldset.append(modalButton);
|
||||
}
|
||||
} else {
|
||||
topFieldset.append(modalButton);
|
||||
}
|
||||
}
|
||||
|
||||
defaultTokenModel.on("change:dd_modal_search_value", function(model, value, options) {
|
||||
appUtils.checkEmptyTokenFocus("dd_modal_search_value", value);
|
||||
});
|
||||
|
||||
submittedTokenModel.on("change:dd_modal_search_value", function(model, value, options) {
|
||||
appUtils.setToken("dd_modal_search_value_internal", appUtils.parseModalSearchTerm("dd_modal_search_value", value), true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Parse and clean the search input string from modal
|
||||
// window search tool.
|
||||
// Returns the cleaned value.
|
||||
// name: name of the token used for the search input text box
|
||||
// value: value obtained from the text box
|
||||
appUtils.parseModalSearchTerm = function(name, value) {
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return undefined;
|
||||
} else if (value.toString().trim() === "") {
|
||||
appUtils.setToken(name, undefined, true);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var valueCleaned = value.toString().replace(/\'|\"|\||%|\[|\]|\(|\)|\=/g, '');
|
||||
|
||||
if (valueCleaned !== value) {
|
||||
alert('Search string contained disallowed characters (\'\"%|[]()=). They have been stripped in the applied search value.');
|
||||
}
|
||||
|
||||
value = valueCleaned.trim();
|
||||
|
||||
if (value === "") {
|
||||
alert("Applied search string is empty. Please enter a valid search string.");
|
||||
appUtils.setToken(name, undefined, true);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
// Parse and clean selected host. Valid inputs values
|
||||
// will be separated into a domain and user token.
|
||||
// Returns boolean value indicating if the tokens have changed.
|
||||
// name: name of the token used for the input text box
|
||||
// value: value obtained from the text box
|
||||
appUtils.parseDashboardHostTokens = function(name, value) {
|
||||
|
||||
var newHostValue = undefined;
|
||||
var currentHostValue = appUtils.getSubmittedToken('dd_target_host_internal');
|
||||
var submit = false;
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
|
||||
// trim whitespace
|
||||
var cleanedValue = value.trim();
|
||||
|
||||
// reset initial value if nothing is left after cleaning,
|
||||
// but don't call submit
|
||||
if (cleanedValue.length < 1) {
|
||||
appUtils.setToken(name, undefined);
|
||||
} else {
|
||||
|
||||
// alert the host if there are disallowed characters
|
||||
if (typeof cleanedValue !== 'undefined') {
|
||||
var cleanedUser = cleanedValue.replace(/\*|\'|\"|\||%|\[|\]|\(|\)|\=/g, '');
|
||||
if (cleanedUser !== cleanedValue) {
|
||||
alert("The passed Host value contained disallowed characters (*\'\"%|[]()=). They have been stripped from the applied search.");
|
||||
}
|
||||
newHostValue = cleanedUser.trim();
|
||||
}
|
||||
|
||||
// set user value to undefined if an empty string
|
||||
if (typeof newHostValue === 'undefined' || newHostValue.length < 1) {
|
||||
alert("The passed Host value is incomplete. Use the Search Tool to choose a Host.");
|
||||
newHostValue = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set host token value if different than the current value
|
||||
if (newHostValue !== currentHostValue) {
|
||||
appUtils.setToken('dd_target_host_internal', newHostValue);
|
||||
submit = true;
|
||||
}
|
||||
|
||||
return submit;
|
||||
};
|
||||
|
||||
// Parse and clean selected user. Valid inputs values
|
||||
// will be separated into a domain and user token.
|
||||
// Returns boolean value indicating if the tokens have changed.
|
||||
// name: name of the token used for the input text box
|
||||
// value: value obtained from the text box
|
||||
appUtils.parseDashboardUserTokens = function(name, value) {
|
||||
|
||||
var newUserValue = undefined;
|
||||
var currentUserValue = appUtils.getSubmittedToken('dd_target_user_internal');
|
||||
var newDomainValue = undefined;
|
||||
var currentDomainValue = appUtils.getSubmittedToken('dd_target_domain_internal');
|
||||
var submit = false;
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
|
||||
// trim whitespace
|
||||
var cleanedValue = value.trim();
|
||||
|
||||
// reset initial value if nothing is left after cleaning,
|
||||
// but don't call submit
|
||||
if (cleanedValue.length < 1) {
|
||||
appUtils.setToken(name, undefined);
|
||||
} else {
|
||||
|
||||
// inspect the data for validity
|
||||
var regex = /([^\x5c]*)(?:\x5c+)?([^\x5c]*)?/g;
|
||||
var match = regex.exec(cleanedValue);
|
||||
var dom = match.length > 1 ? match[1] : undefined;
|
||||
var user = match.length > 2 ? match[2] : undefined;
|
||||
|
||||
// alert the user if there are disallowed characters
|
||||
if (typeof user !== 'undefined') {
|
||||
var cleanedUser = user.replace(/\*|\'|\"|\||%|\[|\]|\(|\)|\=/g, '');
|
||||
if (cleanedUser !== user) {
|
||||
alert("The passed User value contained disallowed characters (*\'\"%|[]()=). They have been stripped from the applied search.");
|
||||
}
|
||||
newUserValue = cleanedUser.trim();
|
||||
}
|
||||
|
||||
// alert the user if there are disallowed characters
|
||||
if (typeof dom !== 'undefined') {
|
||||
var cleanedDomain = dom.replace(/\*|\'|\"|\||%|\[|\]|\(|\)|\=/g, '');
|
||||
if (cleanedDomain !== dom) {
|
||||
alert("The passed Domain value contained disallowed characters (*\'\"%|[]()=). They have been stripped from the applied search.");
|
||||
}
|
||||
newDomainValue = cleanedDomain.trim();
|
||||
}
|
||||
|
||||
// set user value to undefined if an empty string
|
||||
if (typeof newUserValue === 'undefined' || newUserValue.length < 1) {
|
||||
alert("The passed User value is incomplete. Use the Search Tool to choose a User.");
|
||||
newUserValue = undefined;
|
||||
}
|
||||
|
||||
// set domain value to undefined if an empty string
|
||||
if (typeof newDomainValue === 'undefined' || newDomainValue.length < 1) {
|
||||
alert("The passed Domain value is incomplete. Use the Search Tool to choose a Domain.");
|
||||
newDomainValue = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set user token value if different than the current value
|
||||
if (newUserValue !== currentUserValue) {
|
||||
appUtils.setToken('dd_target_user_internal', newUserValue);
|
||||
submit = true;
|
||||
}
|
||||
|
||||
// set domain token value if different than the current value
|
||||
if (newDomainValue !== currentDomainValue) {
|
||||
appUtils.setToken('dd_target_domain_internal', newDomainValue);
|
||||
submit = true;
|
||||
}
|
||||
return submit;
|
||||
};
|
||||
|
||||
return appUtils;
|
||||
})();
|
||||
});
|
||||
}).call(this);
|
||||
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017, Ryan Thibodeaux
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Ryan Thibodeaux. All Rights Reserved
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
/* top-level entity */
|
||||
.modal-text-msg {
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
width: 450px;
|
||||
margin-left: -225px;
|
||||
z-index: 200000;
|
||||
}
|
||||
|
||||
.modal-text-msg-unactivated {
|
||||
display: none !important;
|
||||
}
|
||||
.modal-text-msg-activated {}
|
||||
|
||||
.modal-text-msg > .modal-body {
|
||||
padding: 15px 20px 10px 20px;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
/* background */
|
||||
.modal-text-msg-backdrop {
|
||||
opacity: 0.5;
|
||||
background-color: black;
|
||||
z-index: 100000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-text-msg-backdrop-clear {
|
||||
opacity: 0.0 !important;
|
||||
}
|
||||
|
||||
/* header items */
|
||||
.modal-text-msg > .modal-header {
|
||||
padding: 7px 0px 7px 20px
|
||||
}
|
||||
|
||||
.modal-text-msg > .modal-header {
|
||||
background: #90b5ea;
|
||||
-webkit-border-top-left-radius: 5px;
|
||||
-moz-border-radius-topleft: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
-webkit-border-top-right-radius: 5px;
|
||||
-moz-border-radius-topright: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
.modal-text-msg > .info {
|
||||
background: #90b5ea;
|
||||
}
|
||||
.modal-text-msg > .debug {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.modal-text-msg > .warn {
|
||||
background: #dd9754;
|
||||
}
|
||||
.modal-text-msg > .error {
|
||||
background: indianred;
|
||||
}
|
||||
.modal-text-msg > .modal-header > .modal-title {
|
||||
line-height: 22px;
|
||||
overflow-wrap: break-word;
|
||||
padding-right: 30px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Text content elements */
|
||||
.modal-text-msg p {
|
||||
font-size:14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* footer items */
|
||||
.modal-text-msg > .modal-footer {
|
||||
padding: 5px 10px 5px 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0 0 5px 5px;
|
||||
-webkit-border-bottom-left-radius: 5px;
|
||||
-moz-border-radius-bottomleft: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
-webkit-border-bottom-right-radius: 5px;
|
||||
-moz-border-radius-bottomright: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-text-msg-close:before {
|
||||
color: #000000;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-text-msg-close:hover:before {
|
||||
color: #000000;
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @fileoverview Class definition for Modal Text Message
|
||||
* @author Ryan Thibodeaux
|
||||
* @version 1.0.1
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2017, Ryan Thibodeaux. All Rights Reserved
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
require("css!/static/app/metricator-for-nmon/components/modaltextmsg/modaltextmsg.css");
|
||||
|
||||
// escapes any HTML passed into string
|
||||
function escapeHTML(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ModalTextMsg object.
|
||||
* @class
|
||||
* @classdesc Modal Text Message class that displays messages as a modal box.
|
||||
* @param {Object} options
|
||||
* @param {String} options.type Type of text message to show
|
||||
* @param {String} options.title Title for the text message
|
||||
* @param {String} options.message Message content string
|
||||
* @param {String} options.id HTML ID to use
|
||||
*/
|
||||
var ModalTextMsg = Backbone.View.extend({
|
||||
|
||||
className: 'ModalTextMsg',
|
||||
content: undefined,
|
||||
|
||||
defaults: {
|
||||
title: "", // title to show on top of the modal window
|
||||
type: "info", // the type of modal message [info, debug, warn, error]
|
||||
message: "", // the message to display in the modal window
|
||||
id: "ModalTextMsgID", // the html ID to use for the modal text message
|
||||
},
|
||||
|
||||
// initialize ModalTextMsg object
|
||||
initialize: function(options) {
|
||||
this.options = options;
|
||||
this.options.title = (typeof this.options.title === 'undefined' ? this.defaults.title : escapeHTML(this.options.title).trim());
|
||||
this.options.type = (typeof this.options.type === 'undefined' ? this.defaults.type : escapeHTML(this.options.type).trim().toLowerCase());
|
||||
this.options.message = (typeof this.options.message === 'undefined' ? this.defaults.message : escapeHTML(this.options.message).trim());
|
||||
this.options.id = (typeof this.options.id === 'undefined' ? this.defaults.id : escapeHTML(this.options.id).trim());
|
||||
this.template = _.template(this.template);
|
||||
|
||||
// enforce the type to be of a specific value
|
||||
switch(this.options.type) {
|
||||
case "error":
|
||||
this.options.title = (this.options.title === "" ? "Error" : this.options.title);
|
||||
break;
|
||||
case "warn":
|
||||
this.options.title = (this.options.title === "" ? "Warning" : this.options.title);
|
||||
break;
|
||||
case "debug":
|
||||
this.options.title = (this.options.title === "" ? "Debug" : this.options.title);
|
||||
break;
|
||||
case "info":
|
||||
default:
|
||||
this.options.type = "info";
|
||||
this.options.title = (this.options.title === "" ? "Info" : this.options.title);
|
||||
break;
|
||||
}
|
||||
|
||||
// setup content div by breaking up message into
|
||||
// paragraphs for every <br/> tag found
|
||||
var c = document.createElement('div');
|
||||
c.id = "modal-text-msg-content";
|
||||
var msgParts = this.options.message.split("<br/>");
|
||||
msgParts.forEach( function(str) {
|
||||
var para = document.createElement("p");
|
||||
var t = document.createTextNode(str.trim());
|
||||
para.appendChild(t);
|
||||
c.appendChild(para);
|
||||
});
|
||||
this.content = c;
|
||||
|
||||
// render the content and add it to the HTML body but don't show it
|
||||
(this.render()).$el.addClass('modal-text-msg-unactivated');
|
||||
$(document.body).append(this.el);
|
||||
},
|
||||
|
||||
// click listeners
|
||||
events: {
|
||||
'click .modal-text-msg-close' : 'close',
|
||||
'click .modal-text-msg-backdrop' : 'close'
|
||||
},
|
||||
|
||||
// render the content based on the template
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
id : this.options.id,
|
||||
title : this.options.title,
|
||||
type : this.options.type
|
||||
}));
|
||||
this.$el.find(".modal-body").append(this.content);
|
||||
return this;
|
||||
},
|
||||
|
||||
// show the modal text message window
|
||||
show: function() {
|
||||
if (this.$el.hasClass('modal-text-msg-unactivated')) {
|
||||
this.$el.removeClass('modal-text-msg-unactivated').addClass('modal-text-msg-activated');
|
||||
}
|
||||
this.updateVisibility();
|
||||
return this;
|
||||
},
|
||||
|
||||
// close the modal window and destroy the content
|
||||
close: function() {
|
||||
this.unbind();
|
||||
this.remove();
|
||||
this.updateVisibility();
|
||||
return this;
|
||||
},
|
||||
|
||||
// update visibility of modal windows that
|
||||
// have been activated
|
||||
updateVisibility: function() {
|
||||
// make the last window visible and all others invisible
|
||||
var modals = $(".modal-text-msg-activated");
|
||||
if (modals.length > 0) {
|
||||
modals.each(function(i,m) {
|
||||
if (i == modals.length - 1) {
|
||||
$(m).show()
|
||||
} else {
|
||||
$(m).hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// html template
|
||||
template: '<div id="<%- id %>" class="modal modal-text-msg" role="dialog">' +
|
||||
'<div class="modal-header <%- type %>">' +
|
||||
'<div class="modal-title"><%- title %></div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body"></div>' +
|
||||
'<div class="modal-footer"><button class="close modal-text-msg-close"/></div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-backdrop modal-text-msg-backdrop"></div>'
|
||||
});
|
||||
return ModalTextMsg;
|
||||
});
|
||||
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Controlling logic for the Modal Text Message feature
|
||||
* @author Ryan Thibodeaux
|
||||
* @version 1.0.1
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2017, Ryan Thibodeaux. All Rights Reserved
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
require([
|
||||
"jquery",
|
||||
"ModalTextMsg",
|
||||
"splunkjs/mvc",
|
||||
"splunkjs/ready!",
|
||||
"splunkjs/mvc/simplexml/ready!"
|
||||
], function($, ModalTextMsg, mvc) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// get token models and setup modifier functions
|
||||
var defaultTokenModel = mvc.Components.get('default');
|
||||
var submittedTokenModel = mvc.Components.get('submitted');
|
||||
var urlTokenModel = mvc.Components.get('url');
|
||||
|
||||
function setToken(name, value, submit) {
|
||||
defaultTokenModel.set(name, value);
|
||||
if (!!submit) {
|
||||
submitTokens();
|
||||
}
|
||||
}
|
||||
function submitTokens() {
|
||||
if (submittedTokenModel && defaultTokenModel) {
|
||||
submittedTokenModel.set(defaultTokenModel.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// parse passed modal text message title
|
||||
function parseMessageTitle(token, title) {
|
||||
var value = title;
|
||||
if (typeof value !== 'undefined') {
|
||||
if (Object.prototype.toString.call(title) === '[object Array]') {
|
||||
value = title[0];
|
||||
}
|
||||
value = value.trim()
|
||||
if (value.length > 0) {
|
||||
next_title = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse passed modal text message content and trigger the display
|
||||
function parseMessageToken(token, msg) {
|
||||
var value = msg;
|
||||
if (typeof value !== 'undefined') {
|
||||
if (Object.prototype.toString.call(msg) === '[object Array]') {
|
||||
value = msg[0];
|
||||
}
|
||||
value = value.trim();
|
||||
if (value.length > 0) {
|
||||
next_msg = value;
|
||||
next_msg_type = token.split("_").pop();
|
||||
triggerMsgDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// show the modal text message
|
||||
function triggerMsgDisplay() {
|
||||
var k = new ModalTextMsg({
|
||||
title : next_title,
|
||||
type : next_msg_type,
|
||||
message : next_msg
|
||||
});
|
||||
k.show();
|
||||
|
||||
next_title = undefined;
|
||||
next_msg = undefined;
|
||||
next_msg_type = undefined;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/// Start Main Code Here
|
||||
/////////////////////////////////////////
|
||||
|
||||
// array of message tokens in increasing order of priority
|
||||
const MESSAGE_TOKENS = ["modal_msg_title", "modal_msg_debug", "modal_msg_info", "modal_msg_warn", "modal_msg_error"];
|
||||
const MESSAGE_URL_TOKENS = ["modal_msg_url_title", "modal_msg_url_debug", "modal_msg_url_info", "modal_msg_url_warn", "modal_msg_url_error"];
|
||||
var next_title = undefined;
|
||||
var next_msg = undefined;
|
||||
var next_msg_type = undefined;
|
||||
|
||||
// parse and display messages passed via URL tokens
|
||||
var urlTokensSet = urlTokenModel.keys();
|
||||
for (var i = 0; i < MESSAGE_URL_TOKENS.length; i++) {
|
||||
if (urlTokensSet.indexOf(MESSAGE_URL_TOKENS[i]) >= 0) {
|
||||
if (MESSAGE_URL_TOKENS[i] === "modal_msg_url_title") {
|
||||
parseMessageTitle(MESSAGE_URL_TOKENS[i], urlTokenModel.get(MESSAGE_URL_TOKENS[i]));
|
||||
} else {
|
||||
parseMessageToken(MESSAGE_URL_TOKENS[i], urlTokenModel.get(MESSAGE_URL_TOKENS[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listen for changes to the message type tokens
|
||||
MESSAGE_TOKENS.forEach(function(str) {
|
||||
submittedTokenModel.on("change:" + str, function(model, value, options) {
|
||||
if (str === "modal_msg_title") {
|
||||
parseMessageTitle(str, value);
|
||||
} else {
|
||||
parseMessageToken(str, value);
|
||||
}
|
||||
if (typeof value !== 'undefined') {
|
||||
setToken(str, undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
// see if the value was already set at page load via quick changes that
|
||||
// may not be registered in the code block above since it isn't loaded
|
||||
// quickly enough in 6.5+
|
||||
var currentValue = submittedTokenModel.get(str);
|
||||
var urlValue = urlTokenModel.get(str);
|
||||
if (typeof currentValue !== "undefined" && currentValue.length > 0) {
|
||||
if (typeof urlValue === "undefined" || urlValue.length < 1) {
|
||||
setToken(str, currentValue + " ", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},function(err) {
|
||||
// error callback
|
||||
// the error has a list of modules that failed
|
||||
var failedId = err.requireModules && err.requireModules[0];
|
||||
console.error("Error when loading dependencies in Modal Text Message wrapper: ", err);
|
||||
});
|
||||
}).call(this);
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "parallelcoords",
|
||||
"version": "1.0.0",
|
||||
"main": "parallelcoords.js",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"d3": "3.3.x"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
Copyright (c) 2012, Kai Chang
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* The name Kai Chang may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,34 @@
|
||||
.parcoords > svg, .parcoords > canvas {
|
||||
font: 14px sans-serif;
|
||||
position: absolute;
|
||||
}
|
||||
.parcoords > canvas {
|
||||
pointer-events: none;
|
||||
}
|
||||
.parcoords rect.background {
|
||||
fill: transparent;
|
||||
}
|
||||
.parcoords rect.background:hover {
|
||||
fill: rgba(120,120,120,0.2);
|
||||
}
|
||||
.parcoords .resize rect {
|
||||
fill: rgba(0,0,0,0.1);
|
||||
}
|
||||
.parcoords rect.extent {
|
||||
fill: rgba(255,255,255,0.25);
|
||||
stroke: rgba(0,0,0,0.6);
|
||||
}
|
||||
.parcoords .axis line, .parcoords .axis path {
|
||||
fill: none;
|
||||
stroke: #222;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
.parcoords canvas {
|
||||
opacity: 1;
|
||||
-moz-transition: opacity 0.3s;
|
||||
-webkit-transition: opacity 0.3s;
|
||||
-o-transition: opacity 0.3s;
|
||||
}
|
||||
.parcoords canvas.faded {
|
||||
opacity: 0.25;
|
||||
}
|
||||
@ -0,0 +1,598 @@
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var d3 = require("../../d3/d3");
|
||||
require("css!./d3-parcoords.css");
|
||||
|
||||
/// BEGIN LIBRARY CODE
|
||||
//
|
||||
d3.parcoords = function(config) {
|
||||
var __ = {
|
||||
data: [],
|
||||
dimensions: [],
|
||||
dimensionTitles: {},
|
||||
types: {},
|
||||
brushed: false,
|
||||
mode: "default",
|
||||
rate: 20,
|
||||
width: 600,
|
||||
height: 300,
|
||||
margin: { top: 24, right: 0, bottom: 12, left: 0 },
|
||||
color: "#069",
|
||||
composite: "source-over",
|
||||
alpha: 0.7
|
||||
};
|
||||
|
||||
extend(__, config);
|
||||
var pc = function(selection) {
|
||||
selection = pc.selection = d3.select(selection);
|
||||
|
||||
__.width = selection[0][0].clientWidth;
|
||||
__.height = selection[0][0].clientHeight;
|
||||
|
||||
// canvas data layers
|
||||
["shadows", "marks", "foreground", "highlight"].forEach(function(layer) {
|
||||
canvas[layer] = selection
|
||||
.append("canvas")
|
||||
.attr("class", layer)[0][0];
|
||||
ctx[layer] = canvas[layer].getContext("2d");
|
||||
});
|
||||
|
||||
// svg tick and brush layers
|
||||
pc.svg = selection
|
||||
.append("svg")
|
||||
.attr("width", __.width)
|
||||
.attr("height", __.height)
|
||||
.append("svg:g")
|
||||
.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
|
||||
|
||||
return pc;
|
||||
};
|
||||
var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush"].concat(d3.keys(__))),
|
||||
w = function() { return __.width - __.margin.right - __.margin.left; },
|
||||
h = function() { return __.height - __.margin.top - __.margin.bottom },
|
||||
flags = {
|
||||
brushable: false,
|
||||
reorderable: false,
|
||||
axes: false,
|
||||
interactive: false,
|
||||
shadows: false,
|
||||
debug: false
|
||||
},
|
||||
xscale = d3.scale.ordinal(),
|
||||
yscale = {},
|
||||
dragging = {},
|
||||
line = d3.svg.line(),
|
||||
axis = d3.svg.axis().orient("left").ticks(5),
|
||||
g, // groups for axes, brushes
|
||||
ctx = {},
|
||||
canvas = {};
|
||||
|
||||
// side effects for setters
|
||||
var side_effects = d3.dispatch.apply(this,d3.keys(__))
|
||||
.on("composite", function(d) { ctx.foreground.globalCompositeOperation = d.value; })
|
||||
.on("alpha", function(d) { ctx.foreground.globalAlpha = d.value; })
|
||||
.on("width", function(d) { pc.resize(); })
|
||||
.on("height", function(d) { pc.resize(); })
|
||||
.on("margin", function(d) { pc.resize(); })
|
||||
.on("rate", function(d) { rqueue.rate(d.value); })
|
||||
.on("data", function(d) {
|
||||
if (flags.shadows) paths(__.data, ctx.shadows);
|
||||
})
|
||||
.on("dimensions", function(d) {
|
||||
xscale.domain(__.dimensions);
|
||||
if (flags.interactive) pc.render().updateAxes();
|
||||
});
|
||||
|
||||
// expose the state of the chart
|
||||
pc.state = __;
|
||||
pc.flags = flags;
|
||||
|
||||
// create getter/setters
|
||||
getset(pc, __, events);
|
||||
|
||||
// expose events
|
||||
d3.rebind(pc, events, "on");
|
||||
|
||||
// tick formatting
|
||||
d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
|
||||
|
||||
// getter/setter with event firing
|
||||
function getset(obj,state,events) {
|
||||
d3.keys(state).forEach(function(key) {
|
||||
obj[key] = function(x) {
|
||||
if (!arguments.length) return state[key];
|
||||
var old = state[key];
|
||||
state[key] = x;
|
||||
side_effects[key].call(pc,{"value": x, "previous": old});
|
||||
events[key].call(pc,{"value": x, "previous": old});
|
||||
return obj;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function extend(target, source) {
|
||||
for (key in source) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
pc.autoscale = function() {
|
||||
// yscale
|
||||
var defaultScales = {
|
||||
"date": function(k) {
|
||||
return d3.time.scale()
|
||||
.domain(d3.extent(__.data, function(d) {
|
||||
return d[k] ? d[k].getTime() : null;
|
||||
}))
|
||||
.range([h()+1, 1])
|
||||
},
|
||||
"number": function(k) {
|
||||
return d3.scale.linear()
|
||||
.domain(d3.extent(__.data, function(d) { return +d[k]; }))
|
||||
.range([h()+1, 1])
|
||||
},
|
||||
"string": function(k) {
|
||||
return d3.scale.ordinal()
|
||||
.domain(__.data.map(function(p) { return p[k]; }))
|
||||
.rangePoints([h()+1, 1])
|
||||
}
|
||||
};
|
||||
|
||||
__.dimensions.forEach(function(k) {
|
||||
yscale[k] = defaultScales[__.types[k]](k);
|
||||
});
|
||||
|
||||
// hack to remove ordinal dimensions with many values
|
||||
pc.dimensions(pc.dimensions().filter(function(p,i) {
|
||||
var uniques = yscale[p].domain().length;
|
||||
if (__.types[p] == "string" && (uniques > 60 || uniques < 2)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
|
||||
// xscale
|
||||
xscale.rangePoints([0, w()], 1);
|
||||
|
||||
// canvas sizes
|
||||
pc.selection.selectAll("canvas")
|
||||
.style("margin-top", __.margin.top + "px")
|
||||
.style("margin-left", __.margin.left + "px")
|
||||
.attr("width", w()+2)
|
||||
.attr("height", h()+2)
|
||||
|
||||
// default styles, needs to be set when canvas width changes
|
||||
ctx.foreground.strokeStyle = __.color;
|
||||
ctx.foreground.lineWidth = 1.4;
|
||||
ctx.foreground.globalCompositeOperation = __.composite;
|
||||
ctx.foreground.globalAlpha = __.alpha;
|
||||
ctx.highlight.lineWidth = 3;
|
||||
ctx.shadows.strokeStyle = "#dadada";
|
||||
|
||||
return this;
|
||||
};
|
||||
pc.detectDimensions = function() {
|
||||
pc.types(pc.detectDimensionTypes(__.data));
|
||||
pc.dimensions(d3.keys(pc.types()));
|
||||
return this;
|
||||
};
|
||||
|
||||
// a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
|
||||
pc.toType = function(v) {
|
||||
return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
|
||||
};
|
||||
|
||||
// try to coerce to number before returning type
|
||||
pc.toTypeCoerceNumbers = function(v) {
|
||||
if ((parseFloat(v) == v) && (v != null)) return "number";
|
||||
return pc.toType(v);
|
||||
};
|
||||
|
||||
// attempt to determine types of each dimension based on first row of data
|
||||
pc.detectDimensionTypes = function(data) {
|
||||
var types = {}
|
||||
d3.keys(data[0])
|
||||
.forEach(function(col) {
|
||||
types[col] = pc.toTypeCoerceNumbers(data[0][col]);
|
||||
});
|
||||
return types;
|
||||
};
|
||||
pc.render = function() {
|
||||
// try to autodetect dimensions and create scales
|
||||
if (!__.dimensions.length) pc.detectDimensions();
|
||||
if (!(__.dimensions[0] in yscale)) pc.autoscale();
|
||||
|
||||
pc.render[__.mode]();
|
||||
|
||||
events.render.call(this);
|
||||
return this;
|
||||
};
|
||||
|
||||
pc.render.default = function() {
|
||||
pc.clear('foreground');
|
||||
if (__.brushed) {
|
||||
__.brushed.forEach(path_foreground);
|
||||
} else {
|
||||
__.data.forEach(path_foreground);
|
||||
}
|
||||
};
|
||||
|
||||
var rqueue = d3.renderQueue(path_foreground)
|
||||
.rate(50)
|
||||
.clear(function() { pc.clear('foreground'); });
|
||||
|
||||
pc.render.queue = function() {
|
||||
if (__.brushed) {
|
||||
rqueue(__.brushed);
|
||||
} else {
|
||||
rqueue(__.data);
|
||||
}
|
||||
};
|
||||
pc.shadows = function() {
|
||||
flags.shadows = true;
|
||||
if (__.data.length > 0) paths(__.data, ctx.shadows);
|
||||
return this;
|
||||
};
|
||||
|
||||
// draw little dots on the axis line where data intersects
|
||||
pc.axisDots = function() {
|
||||
var ctx = pc.ctx.marks;
|
||||
ctx.globalAlpha = d3.min([1/Math.pow(data.length, 1/2), 1]);
|
||||
__.data.forEach(function(d) {
|
||||
__.dimensions.map(function(p,i) {
|
||||
ctx.fillRect(position(p)-0.75,yscale[p](d[p])-0.75,1.5,1.5);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
// draw single polyline
|
||||
function color_path(d, ctx) {
|
||||
ctx.strokeStyle = d3.functor(__.color)(d);
|
||||
ctx.beginPath();
|
||||
__.dimensions.map(function(p,i) {
|
||||
if (i == 0) {
|
||||
ctx.moveTo(position(p),yscale[p](d[p]));
|
||||
} else {
|
||||
ctx.lineTo(position(p),yscale[p](d[p]));
|
||||
}
|
||||
});
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
// draw many polylines of the same color
|
||||
function paths(data, ctx) {
|
||||
ctx.clearRect(-1,-1,w()+2,h()+2);
|
||||
ctx.beginPath();
|
||||
data.forEach(function(d) {
|
||||
__.dimensions.map(function(p,i) {
|
||||
if (i == 0) {
|
||||
ctx.moveTo(position(p),yscale[p](d[p]));
|
||||
} else {
|
||||
ctx.lineTo(position(p),yscale[p](d[p]));
|
||||
}
|
||||
});
|
||||
});
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
function path_foreground(d) {
|
||||
return color_path(d, ctx.foreground);
|
||||
};
|
||||
|
||||
function path_highlight(d) {
|
||||
return color_path(d, ctx.highlight);
|
||||
};
|
||||
pc.clear = function(layer) {
|
||||
ctx[layer].clearRect(0,0,w()+2,h()+2);
|
||||
return this;
|
||||
};
|
||||
pc.createAxes = function() {
|
||||
if (g) pc.removeAxes();
|
||||
|
||||
// Add a group element for each dimension.
|
||||
g = pc.svg.selectAll(".dimension")
|
||||
.data(__.dimensions, function(d) { return d; })
|
||||
.enter().append("svg:g")
|
||||
.attr("class", "dimension")
|
||||
.attr("transform", function(d) { return "translate(" + xscale(d) + ")"; })
|
||||
|
||||
// Add an axis and title.
|
||||
g.append("svg:g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(0,0)")
|
||||
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
|
||||
.append("svg:text")
|
||||
.attr({
|
||||
"text-anchor": "middle",
|
||||
"y": 0,
|
||||
"transform": "translate(0,-12)",
|
||||
"x": 0,
|
||||
"class": "label"
|
||||
})
|
||||
.text(function(d) {
|
||||
return d in __.dimensionTitles ? __.dimensionTitles[d] : d; // dimension display names
|
||||
})
|
||||
|
||||
flags.axes= true;
|
||||
return this;
|
||||
};
|
||||
|
||||
pc.removeAxes = function() {
|
||||
g.remove();
|
||||
return this;
|
||||
};
|
||||
|
||||
pc.updateAxes = function() {
|
||||
var g_data = pc.svg.selectAll(".dimension")
|
||||
.data(__.dimensions, function(d) { return d; })
|
||||
|
||||
g_data.enter().append("svg:g")
|
||||
.attr("class", "dimension")
|
||||
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
|
||||
.style("opacity", 0)
|
||||
.append("svg:g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(0,0)")
|
||||
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
|
||||
.append("svg:text")
|
||||
.attr({
|
||||
"text-anchor": "middle",
|
||||
"y": 0,
|
||||
"transform": "translate(0,-12)",
|
||||
"x": 0,
|
||||
"class": "label"
|
||||
})
|
||||
.text(String);
|
||||
|
||||
g_data.exit().remove();
|
||||
|
||||
g = pc.svg.selectAll(".dimension");
|
||||
|
||||
g.transition().duration(1100)
|
||||
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
|
||||
.style("opacity", 1)
|
||||
if (flags.shadows) paths(__.data, ctx.shadows);
|
||||
return this;
|
||||
};
|
||||
|
||||
pc.brushable = function() {
|
||||
if (!g) pc.createAxes();
|
||||
|
||||
// Add and store a brush for each axis.
|
||||
g.append("svg:g")
|
||||
.attr("class", "brush")
|
||||
.each(function(d) {
|
||||
d3.select(this).call(
|
||||
yscale[d].brush = d3.svg.brush()
|
||||
.y(yscale[d])
|
||||
.on("brushstart", function() {
|
||||
d3.event.sourceEvent.stopPropagation();
|
||||
})
|
||||
.on("brush", pc.brush)
|
||||
);
|
||||
})
|
||||
.selectAll("rect")
|
||||
.style("visibility", null)
|
||||
.attr("x", -15)
|
||||
.attr("width", 30)
|
||||
flags.brushable = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Jason Davies, http://bl.ocks.org/1341281
|
||||
pc.reorderable = function() {
|
||||
if (!g) pc.createAxes();
|
||||
|
||||
g.style("cursor", "move")
|
||||
.call(d3.behavior.drag()
|
||||
.on("dragstart", function(d) {
|
||||
dragging[d] = this.__origin__ = xscale(d);
|
||||
})
|
||||
.on("drag", function(d) {
|
||||
dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
|
||||
__.dimensions.sort(function(a, b) { return position(a) - position(b); });
|
||||
xscale.domain(__.dimensions);
|
||||
pc.render();
|
||||
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
|
||||
})
|
||||
.on("dragend", function(d) {
|
||||
delete this.__origin__;
|
||||
delete dragging[d];
|
||||
d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
|
||||
pc.render();
|
||||
}));
|
||||
flags.reorderable = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// pairs of adjacent dimensions
|
||||
pc.adjacent_pairs = function(arr) {
|
||||
var ret = [];
|
||||
for (var i = 0; i < arr.length-1; i++) {
|
||||
ret.push([arr[i],arr[i+1]]);
|
||||
};
|
||||
return ret;
|
||||
};
|
||||
pc.interactive = function() {
|
||||
flags.interactive = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Get data within brushes
|
||||
pc.brush = function() {
|
||||
__.brushed = selected();
|
||||
events.brush.call(pc,__.brushed);
|
||||
pc.render();
|
||||
};
|
||||
|
||||
// expose a few objects
|
||||
pc.xscale = xscale;
|
||||
pc.yscale = yscale;
|
||||
pc.ctx = ctx;
|
||||
pc.canvas = canvas;
|
||||
pc.g = function() { return g; };
|
||||
|
||||
pc.brushReset = function(dimension) {
|
||||
if (g) {
|
||||
g.selectAll('.brush')
|
||||
.each(function(d) {
|
||||
d3.select(this).call(
|
||||
yscale[d].brush.clear()
|
||||
);
|
||||
})
|
||||
pc.brush();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// rescale for height, width and margins
|
||||
// TODO currently assumes chart is brushable, and destroys old brushes
|
||||
pc.resize = function() {
|
||||
// selection size
|
||||
pc.selection.select("svg")
|
||||
.attr("width", __.width)
|
||||
.attr("height", __.height)
|
||||
pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
|
||||
|
||||
// scales
|
||||
pc.autoscale();
|
||||
|
||||
// axes, destroys old brushes. the current brush state should pass through in the future
|
||||
if (g) pc.createAxes().brushable();
|
||||
|
||||
events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
|
||||
return this;
|
||||
};
|
||||
|
||||
// highlight an array of data
|
||||
pc.highlight = function(data) {
|
||||
pc.clear("highlight");
|
||||
d3.select(canvas.foreground).classed("faded", true);
|
||||
data.forEach(path_highlight);
|
||||
events.highlight.call(this,data);
|
||||
return this;
|
||||
};
|
||||
|
||||
// clear highlighting
|
||||
pc.unhighlight = function(data) {
|
||||
pc.clear("highlight");
|
||||
d3.select(canvas.foreground).classed("faded", false);
|
||||
return this;
|
||||
};
|
||||
|
||||
// calculate 2d intersection of line a->b with line c->d
|
||||
// points are objects with x and y properties
|
||||
pc.intersection = function(a, b, c, d) {
|
||||
return {
|
||||
x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
|
||||
y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
|
||||
};
|
||||
};
|
||||
|
||||
function is_brushed(p) {
|
||||
return !yscale[p].brush.empty();
|
||||
};
|
||||
|
||||
// data within extents
|
||||
function selected() {
|
||||
var actives = __.dimensions.filter(is_brushed),
|
||||
extents = actives.map(function(p) { return yscale[p].brush.extent(); });
|
||||
|
||||
// test if within range
|
||||
var within = {
|
||||
"date": function(d,p,dimension) {
|
||||
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
|
||||
},
|
||||
"number": function(d,p,dimension) {
|
||||
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
|
||||
},
|
||||
"string": function(d,p,dimension) {
|
||||
return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
|
||||
}
|
||||
};
|
||||
|
||||
return __.data
|
||||
.filter(function(d) {
|
||||
return actives.every(function(p, dimension) {
|
||||
return within[__.types[p]](d,p,dimension);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function position(d) {
|
||||
var v = dragging[d];
|
||||
return v == null ? xscale(d) : v;
|
||||
}
|
||||
pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
|
||||
|
||||
pc.version = "0.2.2";
|
||||
|
||||
return pc;
|
||||
};
|
||||
|
||||
d3.renderQueue = (function(func) {
|
||||
var _queue = [], // data to be rendered
|
||||
_rate = 10, // number of calls per frame
|
||||
_clear = function() {}, // clearing function
|
||||
_i = 0; // current iteration
|
||||
|
||||
var rq = function(data) {
|
||||
if (data) rq.data(data);
|
||||
rq.invalidate();
|
||||
_clear();
|
||||
rq.render();
|
||||
};
|
||||
|
||||
rq.render = function() {
|
||||
_i = 0;
|
||||
var valid = true;
|
||||
rq.invalidate = function() { valid = false; };
|
||||
|
||||
function doFrame() {
|
||||
if (!valid) return true;
|
||||
if (_i > _queue.length) return true;
|
||||
var chunk = _queue.slice(_i,_i+_rate);
|
||||
_i += _rate;
|
||||
chunk.map(func);
|
||||
}
|
||||
|
||||
d3.timer(doFrame);
|
||||
};
|
||||
|
||||
rq.data = function(data) {
|
||||
rq.invalidate();
|
||||
_queue = data.slice(0);
|
||||
return rq;
|
||||
};
|
||||
|
||||
rq.rate = function(value) {
|
||||
if (!arguments.length) return _rate;
|
||||
_rate = value;
|
||||
return rq;
|
||||
};
|
||||
|
||||
rq.remaining = function() {
|
||||
return _queue.length - _i;
|
||||
};
|
||||
|
||||
// clear the canvas
|
||||
rq.clear = function(func) {
|
||||
if (!arguments.length) {
|
||||
_clear();
|
||||
return rq;
|
||||
}
|
||||
_clear = func;
|
||||
return rq;
|
||||
};
|
||||
|
||||
rq.invalidate = function() {};
|
||||
|
||||
return rq;
|
||||
});
|
||||
|
||||
/// END LIBRARY CODE
|
||||
|
||||
return d3.parcoords;
|
||||
|
||||
});
|
||||
@ -0,0 +1,117 @@
|
||||
// parallel coords!
|
||||
// a visualisation technique for multidimensional categorical data
|
||||
// you can drag the vertical axis for each section to filter things (try it out for yourself)
|
||||
|
||||
// --- settings ---
|
||||
// none for the time being.
|
||||
// TODO: add settings to choose which data goes where
|
||||
|
||||
// --- expected data format ---
|
||||
// a splunk search like this: index=_internal sourcetype=splunkd_access | table method status
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var _ = require('underscore');
|
||||
var d3 = require("../d3/d3");
|
||||
var parcoords = require("./contrib/d3-parcoords");
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
|
||||
var ParCoords = SimpleSplunkView.extend({
|
||||
|
||||
className: "splunk-toolkit-parcoords",
|
||||
|
||||
options: {
|
||||
managerid: null, // your MANAGER ID
|
||||
data: "preview", // Results type
|
||||
},
|
||||
|
||||
output_mode: "json_rows",
|
||||
|
||||
initialize: function() {
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this.settings.enablePush("value");
|
||||
|
||||
// Set up resize callback. The first argument is a this
|
||||
// pointer which gets passed into the callback event
|
||||
$(window).resize(this, _.debounce(this._handleResize, 20));
|
||||
},
|
||||
|
||||
_handleResize: function(e){
|
||||
|
||||
// e.data is the this pointer passed to the callback.
|
||||
// here it refers to this object and we call render()
|
||||
e.data.render();
|
||||
},
|
||||
|
||||
createView: function() {
|
||||
this.$el.html(''); // clearing all prior junk from the view (eg. 'waiting for data...')
|
||||
return true;
|
||||
},
|
||||
|
||||
// making the data look how we want it to for updateView to do its job
|
||||
formatData: function(data) {
|
||||
|
||||
// Decide what fields we want
|
||||
// TODO: this should be specifialbe
|
||||
var fields = _.filter(this.resultsModel.data().fields, function(d){return d[0] !== "_" });
|
||||
var objects = _.map(data, function(row) {
|
||||
var obj = {};
|
||||
_.each(fields, function(field, idx) {
|
||||
if (row[idx] !== null) {
|
||||
obj[field] = row[idx];
|
||||
}
|
||||
else {
|
||||
obj[field] = "";
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
data = {
|
||||
'results': objects,
|
||||
'fields': fields
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
updateView: function(viz, data) {
|
||||
var that = this;
|
||||
var availableHeight = parseInt(this.settings.get("height") || this.$el.height());
|
||||
|
||||
this.$el.html('');
|
||||
var fields = data.fields;
|
||||
viz = $("<div id='"+this.id+"_parallelcoords' class='parcoords'>").appendTo(this.el)
|
||||
.css("height", availableHeight)
|
||||
var colorgen = d3.scale.category20();
|
||||
var colors = {};
|
||||
_(data.results).chain()
|
||||
.pluck(fields[0])
|
||||
.uniq()
|
||||
.each(function(d,i) {
|
||||
colors[d] = colorgen(i);
|
||||
});
|
||||
|
||||
var color = function(d) {return colors[d[fields[0]]]; };
|
||||
|
||||
var pc_progressive = d3.parcoords()('#' + this.id + '_parallelcoords')
|
||||
.data(data.results)
|
||||
.color(color)
|
||||
.alpha(0.4)
|
||||
.margin({ top: 24, left: 150, bottom: 12, right: 0 })
|
||||
.mode("queue")
|
||||
.render()
|
||||
.brushable() // enable brushing
|
||||
.interactive() // command line mode
|
||||
.on("brush", function(selected) {
|
||||
that.trigger("select", {selected: selected});
|
||||
});
|
||||
|
||||
pc_progressive.svg.selectAll("text")
|
||||
.style("font", "10px sans-serif");
|
||||
}
|
||||
});
|
||||
return ParCoords;
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "sankey",
|
||||
"version": "1.0.0",
|
||||
"main": "sankey.js",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"d3": "3.3.x"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
Copyright (c) 2013, Michael Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* The name Michael Bostock may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,585 @@
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var d3 = require('../../d3/d3');
|
||||
|
||||
/// BEGIN LIBRARY CODE
|
||||
|
||||
// A modified d3 sankey plugin
|
||||
// This is taken verbatim from:
|
||||
// https://github.com/kunalb/d3-plugins/blob/sankey/sankey/sankey.js
|
||||
// Referenced from pull request:
|
||||
// https://github.com/d3/d3-plugins/pull/39
|
||||
|
||||
d3.sankey = function() {
|
||||
var sankey = {},
|
||||
nodeWidth = 24,
|
||||
nodePadding = 8,
|
||||
size = [1, 1],
|
||||
nodes = [],
|
||||
links = [],
|
||||
components = [];
|
||||
|
||||
sankey.nodeWidth = function(_) {
|
||||
if (!arguments.length) return nodeWidth;
|
||||
nodeWidth = +_;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.nodePadding = function(_) {
|
||||
if (!arguments.length) return nodePadding;
|
||||
nodePadding = +_;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.nodes = function(_) {
|
||||
if (!arguments.length) return nodes;
|
||||
nodes = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.links = function(_) {
|
||||
if (!arguments.length) return links;
|
||||
links = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.size = function(_) {
|
||||
if (!arguments.length) return size;
|
||||
size = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.layout = function(iterations) {
|
||||
computeNodeLinks();
|
||||
computeNodeValues();
|
||||
|
||||
computeNodeStructure();
|
||||
computeNodeBreadths();
|
||||
|
||||
computeNodeDepths(iterations);
|
||||
computeLinkDepths();
|
||||
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.relayout = function() {
|
||||
computeLinkDepths();
|
||||
return sankey;
|
||||
};
|
||||
|
||||
// A more involved path generator that requires 3 elements to render --
|
||||
// It draws a starting element, intermediate and end element that are useful
|
||||
// while drawing reverse links to get an appropriate fill.
|
||||
//
|
||||
// Each link is now an area and not a basic spline and no longer guarantees
|
||||
// fixed width throughout.
|
||||
//
|
||||
// Sample usage:
|
||||
//
|
||||
// linkNodes = this._svg.append("g").selectAll(".link")
|
||||
// .data(this.links)
|
||||
// .enter().append("g")
|
||||
// .attr("fill", "none")
|
||||
// .attr("class", ".link")
|
||||
// .sort(function(a, b) { return b.dy - a.dy; });
|
||||
//
|
||||
// linkNodePieces = [];
|
||||
// for (var i = 0; i < 3; i++) {
|
||||
// linkNodePieces[i] = linkNodes.append("path")
|
||||
// .attr("class", ".linkPiece")
|
||||
// .attr("d", path(i))
|
||||
// .attr("fill", ...)
|
||||
// }
|
||||
sankey.reversibleLink = function() {
|
||||
var curvature = .5;
|
||||
|
||||
// Used when source is behind target, the first and last paths are simple
|
||||
// lines at the start and end node while the second path is the spline
|
||||
function forwardLink(part, d) {
|
||||
var x0 = d.source.x + d.source.dx,
|
||||
x1 = d.target.x,
|
||||
xi = d3.interpolateNumber(x0, x1),
|
||||
x2 = xi(curvature),
|
||||
x3 = xi(1 - curvature),
|
||||
y0 = d.source.y + d.sy,
|
||||
y1 = d.target.y + d.ty,
|
||||
y2 = d.source.y + d.sy + d.dy,
|
||||
y3 = d.target.y + d.ty + d.dy;
|
||||
|
||||
switch (part) {
|
||||
case 0:
|
||||
return "M" + x0 + "," + y0 + "L" + x0 + "," + (y0 + d.dy);
|
||||
|
||||
case 1:
|
||||
return "M" + x0 + "," + y0
|
||||
+ "C" + x2 + "," + y0 + " " + x3 + "," + y1 + " " + x1 + "," + y1
|
||||
+ "L" + x1 + "," + y3
|
||||
+ "C" + x3 + "," + y3 + " " + x2 + "," + y2 + " " + x0 + "," + y2
|
||||
+ "Z";
|
||||
|
||||
case 2:
|
||||
return "M" + x1 + "," + y1 + "L" + x1 + "," + (y1 + d.dy);
|
||||
}
|
||||
}
|
||||
|
||||
// Used for self loops and when the source is actually in front of the
|
||||
// target; the first element is a turning path from the source to the
|
||||
// destination, the second element connects the two twists and the last
|
||||
// twists into the target element.
|
||||
//
|
||||
//
|
||||
// /--Target
|
||||
// \----------------------\
|
||||
// Source--/
|
||||
//
|
||||
function backwardLink(part, d) {
|
||||
var curveExtension = 30;
|
||||
var curveDepth = 15;
|
||||
|
||||
function getDir(d) {
|
||||
return d.source.y + d.sy > d.target.y + d.ty ? -1 : 1;
|
||||
}
|
||||
|
||||
function p(x, y) {
|
||||
return x + "," + y + " ";
|
||||
}
|
||||
|
||||
var dt = getDir(d) * curveDepth,
|
||||
x0 = d.source.x + d.source.dx,
|
||||
y0 = d.source.y + d.sy,
|
||||
x1 = d.target.x,
|
||||
y1 = d.target.y + d.ty;
|
||||
|
||||
switch (part) {
|
||||
case 0:
|
||||
return "M" + p(x0, y0) +
|
||||
"C" + p(x0, y0) +
|
||||
p(x0 + curveExtension, y0) +
|
||||
p(x0 + curveExtension, y0 + dt) +
|
||||
"L" + p(x0 + curveExtension, y0 + dt + d.dy) +
|
||||
"C" + p(x0 + curveExtension, y0 + d.dy) +
|
||||
p(x0, y0 + d.dy) +
|
||||
p(x0, y0 + d.dy) +
|
||||
"Z";
|
||||
case 1:
|
||||
return "M" + p(x0 + curveExtension, y0 + dt) +
|
||||
"C" + p(x0 + curveExtension, y0 + 3 * dt) +
|
||||
p(x1 - curveExtension, y1 - 3 * dt) +
|
||||
p(x1 - curveExtension, y1 - dt) +
|
||||
"L" + p(x1 - curveExtension, y1 - dt + d.dy) +
|
||||
"C" + p(x1 - curveExtension, y1 - 3 * dt + d.dy) +
|
||||
p(x0 + curveExtension, y0 + 3 * dt + d.dy) +
|
||||
p(x0 + curveExtension, y0 + dt + d.dy) +
|
||||
"Z";
|
||||
|
||||
case 2:
|
||||
return "M" + p(x1 - curveExtension, y1 - dt) +
|
||||
"C" + p(x1 - curveExtension, y1) +
|
||||
p(x1, y1) +
|
||||
p(x1, y1) +
|
||||
"L" + p(x1, y1 + d.dy) +
|
||||
"C" + p(x1, y1 + d.dy) +
|
||||
p(x1 - curveExtension, y1 + d.dy) +
|
||||
p(x1 - curveExtension, y1 + d.dy - dt) +
|
||||
"Z";
|
||||
}
|
||||
}
|
||||
|
||||
return function(part) {
|
||||
return function(d) {
|
||||
if (d.source.x < d.target.x) {
|
||||
return forwardLink(part, d);
|
||||
} else {
|
||||
return backwardLink(part, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The standard link path using a constant width spline that needs a
|
||||
// single path element.
|
||||
sankey.link = function() {
|
||||
var curvature = .5;
|
||||
|
||||
function link(d) {
|
||||
var x0 = d.source.x + d.source.dx,
|
||||
x1 = d.target.x,
|
||||
xi = d3.interpolateNumber(x0, x1),
|
||||
x2 = xi(curvature),
|
||||
x3 = xi(1 - curvature),
|
||||
y0 = d.source.y + d.sy + d.dy / 2,
|
||||
y1 = d.target.y + d.ty + d.dy / 2;
|
||||
return "M" + x0 + "," + y0
|
||||
+ "C" + x2 + "," + y0
|
||||
+ " " + x3 + "," + y1
|
||||
+ " " + x1 + "," + y1;
|
||||
}
|
||||
|
||||
link.curvature = function(_) {
|
||||
if (!arguments.length) return curvature;
|
||||
curvature = +_;
|
||||
return link;
|
||||
};
|
||||
|
||||
return link;
|
||||
};
|
||||
|
||||
// Populate the sourceLinks and targetLinks for each node.
|
||||
// Also, if the source and target are not objects, assume they are indices.
|
||||
function computeNodeLinks() {
|
||||
nodes.forEach(function(node) {
|
||||
node.sourceLinks = [];
|
||||
node.targetLinks = [];
|
||||
});
|
||||
|
||||
links.forEach(function(link) {
|
||||
var source = link.source,
|
||||
target = link.target;
|
||||
if (typeof source === "number") source = link.source = nodes[link.source];
|
||||
if (typeof target === "number") target = link.target = nodes[link.target];
|
||||
source.sourceLinks.push(link);
|
||||
target.targetLinks.push(link);
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the value (size) of each node by summing the associated links.
|
||||
function computeNodeValues() {
|
||||
nodes.forEach(function(node) {
|
||||
node.value = Math.max(
|
||||
d3.sum(node.sourceLinks, value),
|
||||
d3.sum(node.targetLinks, value)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Take the list of nodes and create a DAG of supervertices, each consisting
|
||||
// of a strongly connected component of the graph
|
||||
//
|
||||
// Based off:
|
||||
// http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
|
||||
function computeNodeStructure() {
|
||||
var nodeStack = [],
|
||||
index = 0;
|
||||
|
||||
nodes.forEach(function(node) {
|
||||
if (!node.index) {
|
||||
connect(node);
|
||||
}
|
||||
});
|
||||
|
||||
function connect(node) {
|
||||
node.index = index++;
|
||||
node.lowIndex = node.index;
|
||||
node.onStack = true;
|
||||
nodeStack.push(node);
|
||||
|
||||
if (node.sourceLinks) {
|
||||
node.sourceLinks.forEach(function(sourceLink){
|
||||
var target = sourceLink.target;
|
||||
if (!target.hasOwnProperty('index')) {
|
||||
connect(target);
|
||||
node.lowIndex = Math.min(node.lowIndex, target.lowIndex);
|
||||
} else if (target.onStack) {
|
||||
node.lowIndex = Math.min(node.lowIndex, target.index);
|
||||
}
|
||||
});
|
||||
|
||||
if (node.lowIndex === node.index) {
|
||||
var component = [], currentNode;
|
||||
do {
|
||||
currentNode = nodeStack.pop()
|
||||
currentNode.onStack = false;
|
||||
component.push(currentNode);
|
||||
} while (currentNode != node);
|
||||
components.push({
|
||||
root: node,
|
||||
scc: component
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
components.forEach(function(component, i){
|
||||
component.index = i;
|
||||
component.scc.forEach(function(node) {
|
||||
node.component = i;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Assign the breadth (x-position) for each strongly connected component,
|
||||
// followed by assigning breadth within the component.
|
||||
function computeNodeBreadths() {
|
||||
|
||||
layerComponents();
|
||||
|
||||
components.forEach(function(component, i){
|
||||
bfs(component.root, function(node){
|
||||
var result = node.sourceLinks
|
||||
.filter(function(sourceLink){
|
||||
return sourceLink.target.component == i;
|
||||
})
|
||||
.map(function(sourceLink){
|
||||
return sourceLink.target;
|
||||
});
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
var max = 0;
|
||||
var componentsByBreadth = d3.nest()
|
||||
.key(function(d) { return d.x; })
|
||||
.sortKeys(d3.ascending)
|
||||
.entries(components)
|
||||
.map(function(d) { return d.values; });
|
||||
|
||||
var max = -1, nextMax = -1;
|
||||
componentsByBreadth.forEach(function(c){
|
||||
c.forEach(function(component){
|
||||
component.x = max + 1;
|
||||
component.scc.forEach(function(node){
|
||||
node.x = component.x + node.x;
|
||||
nextMax = Math.max(nextMax, node.x);
|
||||
});
|
||||
});
|
||||
max = nextMax;
|
||||
});
|
||||
|
||||
|
||||
nodes
|
||||
.filter(function(node) {
|
||||
var outLinks = node.sourceLinks.filter(function(link){ return link.source.name != link.target.name; });
|
||||
return (outLinks.length == 0);
|
||||
})
|
||||
.forEach(function(node) { node.x = max; })
|
||||
|
||||
scaleNodeBreadths((size[0] - nodeWidth) / Math.max(max, 1));
|
||||
|
||||
function flatten(a) {
|
||||
return [].concat.apply([], a);
|
||||
}
|
||||
|
||||
function layerComponents() {
|
||||
var remainingComponents = components,
|
||||
nextComponents,
|
||||
visitedIndex,
|
||||
x = 0;
|
||||
|
||||
while (remainingComponents.length) {
|
||||
nextComponents = [];
|
||||
visitedIndex = {};
|
||||
|
||||
remainingComponents.forEach(function(component) {
|
||||
component.x = x;
|
||||
|
||||
component.scc.forEach(function(n) {
|
||||
n.sourceLinks.forEach(function(l) {
|
||||
if (!visitedIndex.hasOwnProperty(l.target.component) &&
|
||||
l.target.component != component.index) {
|
||||
nextComponents.push(components[l.target.component]);
|
||||
visitedIndex[l.target.component] = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
remainingComponents = nextComponents;
|
||||
++x;
|
||||
}
|
||||
}
|
||||
|
||||
function bfs(node, extractTargets) {
|
||||
var queue = [node], currentCount = 1, nextCount = 0;
|
||||
var x = 0;
|
||||
|
||||
while(currentCount > 0) {
|
||||
var currentNode = queue.shift();
|
||||
currentCount--;
|
||||
|
||||
if (!currentNode.hasOwnProperty('x')) {
|
||||
currentNode.x = x;
|
||||
currentNode.dx = nodeWidth;
|
||||
|
||||
var targets = extractTargets(currentNode);
|
||||
|
||||
queue = queue.concat(targets);
|
||||
nextCount += targets.length;
|
||||
}
|
||||
|
||||
|
||||
if (currentCount == 0) { // level change
|
||||
x++;
|
||||
currentCount = nextCount;
|
||||
nextCount = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveSourcesRight() {
|
||||
nodes.forEach(function(node) {
|
||||
if (!node.targetLinks.length) {
|
||||
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function moveSinksRight(x) {
|
||||
nodes.forEach(function(node) {
|
||||
if (!node.sourceLinks.length) {
|
||||
node.x = x - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scaleNodeBreadths(kx) {
|
||||
nodes.forEach(function(node) {
|
||||
node.x *= kx;
|
||||
});
|
||||
}
|
||||
|
||||
function computeNodeDepths(iterations) {
|
||||
var nodesByBreadth = d3.nest()
|
||||
.key(function(d) { return d.x; })
|
||||
.sortKeys(d3.ascending)
|
||||
.entries(nodes)
|
||||
.map(function(d) { return d.values; });
|
||||
|
||||
//
|
||||
initializeNodeDepth();
|
||||
resolveCollisions();
|
||||
for (var alpha = 1; iterations > 0; --iterations) {
|
||||
relaxRightToLeft(alpha *= .99);
|
||||
resolveCollisions();
|
||||
relaxLeftToRight(alpha);
|
||||
resolveCollisions();
|
||||
}
|
||||
|
||||
function initializeNodeDepth() {
|
||||
var ky = d3.min(nodesByBreadth, function(nodes) {
|
||||
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
|
||||
});
|
||||
|
||||
nodesByBreadth.forEach(function(nodes) {
|
||||
nodes.forEach(function(node, i) {
|
||||
node.y = i;
|
||||
node.dy = node.value * ky;
|
||||
});
|
||||
});
|
||||
|
||||
links.forEach(function(link) {
|
||||
link.dy = link.value * ky;
|
||||
});
|
||||
}
|
||||
|
||||
function relaxLeftToRight(alpha) {
|
||||
nodesByBreadth.forEach(function(nodes, breadth) {
|
||||
nodes.forEach(function(node) {
|
||||
if (node.targetLinks.length) {
|
||||
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
|
||||
node.y += (y - center(node)) * alpha;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function weightedSource(link) {
|
||||
return center(link.source) * link.value;
|
||||
}
|
||||
}
|
||||
|
||||
function relaxRightToLeft(alpha) {
|
||||
nodesByBreadth.slice().reverse().forEach(function(nodes) {
|
||||
nodes.forEach(function(node) {
|
||||
if (node.sourceLinks.length) {
|
||||
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
|
||||
node.y += (y - center(node)) * alpha;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function weightedTarget(link) {
|
||||
return center(link.target) * link.value;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCollisions() {
|
||||
nodesByBreadth.forEach(function(nodes) {
|
||||
var node,
|
||||
dy,
|
||||
y0 = 0,
|
||||
n = nodes.length,
|
||||
i;
|
||||
|
||||
// Push any overlapping nodes down.
|
||||
nodes.sort(ascendingDepth);
|
||||
for (i = 0; i < n; ++i) {
|
||||
node = nodes[i];
|
||||
dy = y0 - node.y;
|
||||
if (dy > 0) node.y += dy;
|
||||
y0 = node.y + node.dy + nodePadding;
|
||||
}
|
||||
|
||||
// If the bottommost node goes outside the bounds, push it back up.
|
||||
dy = y0 - nodePadding - size[1];
|
||||
if (dy > 0) {
|
||||
y0 = node.y -= dy;
|
||||
|
||||
// Push any overlapping nodes back up.
|
||||
for (i = n - 2; i >= 0; --i) {
|
||||
node = nodes[i];
|
||||
dy = node.y + node.dy + nodePadding - y0;
|
||||
if (dy > 0) node.y -= dy;
|
||||
y0 = node.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ascendingDepth(a, b) {
|
||||
return a.y - b.y;
|
||||
}
|
||||
}
|
||||
|
||||
function computeLinkDepths() {
|
||||
nodes.forEach(function(node) {
|
||||
node.sourceLinks.sort(ascendingTargetDepth);
|
||||
node.targetLinks.sort(ascendingSourceDepth);
|
||||
});
|
||||
nodes.forEach(function(node) {
|
||||
var sy = 0, ty = 0;
|
||||
node.sourceLinks.forEach(function(link) {
|
||||
link.sy = sy;
|
||||
sy += link.dy;
|
||||
});
|
||||
node.targetLinks.forEach(function(link) {
|
||||
link.ty = ty;
|
||||
ty += link.dy;
|
||||
});
|
||||
});
|
||||
|
||||
function ascendingSourceDepth(a, b) {
|
||||
return a.source.y - b.source.y;
|
||||
}
|
||||
|
||||
function ascendingTargetDepth(a, b) {
|
||||
return a.target.y - b.target.y;
|
||||
}
|
||||
}
|
||||
|
||||
function center(node) {
|
||||
return node.y + node.dy / 2;
|
||||
}
|
||||
|
||||
function value(link) {
|
||||
return link.value;
|
||||
}
|
||||
|
||||
return sankey;
|
||||
};
|
||||
|
||||
/// END LIBRARY CODE
|
||||
|
||||
return d3.sankey;
|
||||
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
.sankey-diagram .node rect {
|
||||
cursor: move;
|
||||
fill-opacity: 0.9;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.sankey-diagram .node text {
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
.sankey-diagram .link {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-opacity: 0.2;
|
||||
}
|
||||
|
||||
.sankey-diagram .link:hover, .sankey-diagram .link.hovering {
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
|
||||
.sankey-diagram .link.my-selected {
|
||||
stroke: yellow;
|
||||
}
|
||||
|
||||
.sankey-diagram scrollable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -0,0 +1,277 @@
|
||||
// AppFramework Sankey Plug-In
|
||||
// ---------------------------
|
||||
//
|
||||
// Provide an easy-to-use plug-in that takes data that relates a
|
||||
// many-to-many relationship with scores into a Sankey display, a form
|
||||
// of flow display. Any relationship between two fields can be
|
||||
// illustrated, although the most common is
|
||||
// "stats count by field1, field2"
|
||||
|
||||
define(function(require, exports, module) {
|
||||
var _ = require("underscore");
|
||||
var $ = require("jquery");
|
||||
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
|
||||
var d3 = require("../d3/d3");
|
||||
// Load D3 Sankey plugin
|
||||
require("./contrib/d3-sankey");
|
||||
|
||||
// Import CSS for the sankey chart.
|
||||
require("css!./sankey.css");
|
||||
|
||||
var SankeyChart = SimpleSplunkView.extend({
|
||||
moduleId: module.id,
|
||||
|
||||
className: "sankey-diagram",
|
||||
|
||||
options: {
|
||||
managerid: null,
|
||||
data: "preview",
|
||||
formatLabel: _.identity,
|
||||
height: 300,
|
||||
formatTooltip: function(d) {
|
||||
return (d.source.name + ' -> ' + d.target.name + ': ' + d.value);
|
||||
}
|
||||
},
|
||||
|
||||
// This is how we extend the SimpleSplunkView's options value for
|
||||
// this object, so that these values are available when
|
||||
// SimpleSplunkView initializes.
|
||||
initialize: function() {
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this.settings.on("change:formatLabel change:formatTooltip", this.render, this);
|
||||
|
||||
|
||||
// Set up resize callback.
|
||||
$(window).resize(_.debounce(_.bind(this._handleResize, this), 20));
|
||||
},
|
||||
|
||||
_handleResize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
// The object this method returns will be passed to the
|
||||
// updateView() method as the first argument, to be
|
||||
// manipulated according to the data and the visualization's
|
||||
// needs.
|
||||
createView: function() {
|
||||
var margin = {top: 10, right: 10, bottom: 10, left: 10};
|
||||
var availableWidth = parseInt(this.settings.get("width") || this.$el.width());
|
||||
var availableHeight = parseInt(this.settings.get("height") || this.$el.height());
|
||||
|
||||
this.$el.html("");
|
||||
|
||||
var svg = d3.select(this.el)
|
||||
.append("svg")
|
||||
.attr("width", availableWidth)
|
||||
.attr("height", availableHeight)
|
||||
.attr("pointer-events", "all");
|
||||
|
||||
return { svg: svg, margin: margin};
|
||||
},
|
||||
|
||||
// Where the data and the visualization meet. Both 'viz' and
|
||||
// 'data' are the data structures returned from their
|
||||
// respective construction methods, createView() above and
|
||||
// onData(), below.
|
||||
updateView: function(viz, data) {
|
||||
var that = this;
|
||||
var containerHeight = this.$el.height();
|
||||
var containerWidth = this.$el.width();
|
||||
|
||||
// Clear svg
|
||||
var svg = $(viz.svg[0]);
|
||||
svg.empty();
|
||||
svg.height(containerHeight);
|
||||
svg.width(containerWidth);
|
||||
|
||||
// Add the graph group as a child of the main svg
|
||||
var graphWidth = containerWidth - viz.margin.left - viz.margin.right;
|
||||
var graphHeight = containerHeight - viz.margin.top - viz.margin.bottom;
|
||||
var graph = viz.svg
|
||||
.append("g")
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")");
|
||||
|
||||
var formatLabel = this.settings.get('formatLabel') || _.identity;
|
||||
var formatTooltip = this.settings.get('formatTooltip');
|
||||
|
||||
var sankey = d3.sankey()
|
||||
.nodeWidth(15)
|
||||
.nodePadding(10)
|
||||
.size([graphWidth, graphHeight]);
|
||||
|
||||
var path = sankey.link();
|
||||
|
||||
sankey.nodes(data.nodes)
|
||||
.links(data.links)
|
||||
.layout(1);
|
||||
|
||||
var link = graph.append("g").selectAll(".link")
|
||||
.data(data.links)
|
||||
.enter().append("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", path)
|
||||
.style("stroke-width", function(d) {
|
||||
return Math.max(1, d.dy);
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
return b.dy - a.dy;
|
||||
});
|
||||
|
||||
link.append("title")
|
||||
.text(function(d) {
|
||||
return formatTooltip(d);
|
||||
});
|
||||
|
||||
var node = graph.append("g").selectAll(".node")
|
||||
.data(data.nodes)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
});
|
||||
|
||||
var color = d3.scale.category20();
|
||||
|
||||
// Draw the rectangles at each end of the link that
|
||||
// correspond to a given node, and then decorate the chart
|
||||
// with the names for each node.
|
||||
node.append("rect")
|
||||
.attr("height", function(d) {
|
||||
return d.dy;
|
||||
})
|
||||
.attr("width", sankey.nodeWidth())
|
||||
.style("fill", function(d) {
|
||||
d.color = color(d.name.replace(/ .*/, ""));
|
||||
return d.color;
|
||||
})
|
||||
.style("stroke", function(d) {
|
||||
return d3.rgb(d.color).darker(2);
|
||||
})
|
||||
.on("mouseover", function(node) {
|
||||
var linksToHighlight = link.filter(function(d) {
|
||||
return d.source.name === node.name || d.target.name === node.name;
|
||||
});
|
||||
linksToHighlight.classed('hovering', true);
|
||||
})
|
||||
.on("mouseout", function(node) {
|
||||
var linksToHighlight = link.filter(function(d) {
|
||||
return d.source.name === node.name || d.target.name === node.name;
|
||||
});
|
||||
linksToHighlight.classed('hovering', false);
|
||||
})
|
||||
.append("title")
|
||||
.text(function(d) {
|
||||
return formatLabel(d.name) + "\n" + d.value;
|
||||
});
|
||||
|
||||
node.attr("transform", function(d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
})
|
||||
.call(d3.behavior.drag()
|
||||
.origin(function(d) {
|
||||
return d;
|
||||
})
|
||||
.on("dragstart", function() {
|
||||
this.parentNode.appendChild(this);
|
||||
})
|
||||
.on("drag", dragmove));
|
||||
|
||||
node.append("text")
|
||||
.attr("x", -6)
|
||||
.attr("y", function(d) {
|
||||
return d.dy / 2;
|
||||
})
|
||||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("transform", null)
|
||||
.text(function(d) {
|
||||
return formatLabel(d.name);
|
||||
})
|
||||
.filter(function(d) {
|
||||
return d.x < graphWidth / 2;
|
||||
})
|
||||
.attr("x", 6 + sankey.nodeWidth())
|
||||
.attr("text-anchor", "start");
|
||||
|
||||
// This view publishes the 'click:link' event that
|
||||
// other Splunk views can then use to drill down
|
||||
// further into the data. We return the source and target
|
||||
// names as values to be used in further Splunk searches.
|
||||
// This allows us to accept events from the visualization
|
||||
// library and provide them consistently to other Splunk
|
||||
// views.
|
||||
var format_event_data = function(e) {
|
||||
return {
|
||||
source: e.source.name,
|
||||
target: e.target.name,
|
||||
value: e.value
|
||||
};
|
||||
};
|
||||
|
||||
function dragmove(d) {
|
||||
d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(graphHeight - d.dy, d3.event.y))) + ")");
|
||||
sankey.relayout();
|
||||
link.attr("d", path);
|
||||
}
|
||||
|
||||
link.on('click', function(e) {
|
||||
that.trigger('click:link', format_event_data(e));
|
||||
});
|
||||
|
||||
node.on('mousedown', function(e) {
|
||||
var linksToNodes = function(links, type) {
|
||||
return _.map(links, function(link) {
|
||||
return {
|
||||
name: link[type].name,
|
||||
value: link[type].value
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var clickEvent = {
|
||||
name: e.name,
|
||||
value: e.value,
|
||||
incomingLinks: linksToNodes(e.targetLinks, "source"),
|
||||
outgoingLinks: linksToNodes(e.sourceLinks, "target")
|
||||
};
|
||||
that.trigger('click:node', clickEvent);
|
||||
});
|
||||
},
|
||||
|
||||
// This function turns the three expected data items into data
|
||||
// structures Sankey understands, and then calls
|
||||
// updateView(). This is the function that is called when
|
||||
// new data is available, and triggers the actual rendering of
|
||||
// the visualization above. The data passed here corresponds
|
||||
// to the basic format requested by the view.
|
||||
formatData: function(data) {
|
||||
var nodeList = _.uniq(_.pluck(data, 0).concat(_.pluck(data, 1)));
|
||||
|
||||
var links = _.map(data, function(item) {
|
||||
return {
|
||||
source: nodeList.indexOf(item[0]),
|
||||
target: nodeList.indexOf(item[1]),
|
||||
value: parseInt(item[2], 10)
|
||||
};
|
||||
});
|
||||
|
||||
var nodes = _.map(nodeList, function(node) {
|
||||
return {
|
||||
name: node
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: nodes, links: links };
|
||||
},
|
||||
render: function() {
|
||||
this.$el.height(this.settings.get('height'));
|
||||
return SimpleSplunkView.prototype.render.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
return SankeyChart;
|
||||
});
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
--------------
|
||||
SccTakeTheTour
|
||||
--------------
|
||||
*/
|
||||
|
||||
/* Navigation*/
|
||||
.scc-takethetour-nav {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav button,
|
||||
.scc-takethetour-nav div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav div button{
|
||||
border-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav button.prev{
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav button.next{
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav button.prev .caret {
|
||||
transform: rotate(90deg);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.scc-takethetour-nav button.next .caret {
|
||||
transform: rotate(270deg);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Slides */
|
||||
.scc-takethetour-slides {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scc-takethetour-slides li{
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* Various */
|
||||
.scc-takethetour .credits {
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scc-takethetour.modal .modal-footer {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.scc-takethetour .never-show-again {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.scc-takethetour .never-show-again input {
|
||||
margin-top: 0;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
/*
|
||||
--------------
|
||||
SccTakeTheTour
|
||||
--------------
|
||||
*/
|
||||
|
||||
define(function(require, exports, module) {
|
||||
var $ = require('jquery');
|
||||
var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview');
|
||||
var SplunkUtils = require('splunkjs/mvc/utils');
|
||||
require("css!./scc_takethetour.css");
|
||||
|
||||
var SccTakeTheTour = SimpleSplunkView.extend({
|
||||
className: "scc-takethetour",
|
||||
options: {
|
||||
title: "Take the tour",
|
||||
width: 60,
|
||||
close_btn_label: "Close",
|
||||
no_more_label: "Do not show this tour again",
|
||||
hide_container: false,
|
||||
show_credits: false,
|
||||
cookie_name_suffix: "",
|
||||
backdrop_close: true,
|
||||
keyboard_close: true
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
|
||||
// Treat "hide_container" setting
|
||||
if(this.settings.get('hide_container') == true){
|
||||
this.$el.parent().css({
|
||||
'padding':0,
|
||||
'margin':0,
|
||||
'height':'0px',
|
||||
'width':'0px'
|
||||
})
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html('');
|
||||
|
||||
// Cookie stuff
|
||||
var cookie_name_suffix = this.settings.get('cookie_name_suffix') == "" ? "" : '_' + this.settings.get('cookie_name_suffix');
|
||||
var ck_name = 'scc_takethetour_' + SplunkUtils.getCurrentApp() + cookie_name_suffix;
|
||||
|
||||
// set expiration for 1 year
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (365*24*60*60*1000));
|
||||
var ck_expires = "expires="+d.toUTCString();
|
||||
|
||||
function getCookie(ck_name) {
|
||||
var oRegex = new RegExp("(?:; )?" + ck_name + "=([^;]*);?");
|
||||
return oRegex.test(document.cookie) ? decodeURIComponent(RegExp["$1"]) : null;
|
||||
}
|
||||
|
||||
function setCookie(ck_name) {
|
||||
document.cookie = ck_name + "= scc-takethetour" + ";" + ck_expires;
|
||||
}
|
||||
|
||||
// GENERATE BOOTSTRAP MODAL
|
||||
// ------------------------
|
||||
if(typeof this.settings.get('user_slides') !== 'undefined'
|
||||
&& $('#'+this.settings.get('user_slides')).length == 1
|
||||
&& getCookie(ck_name) == null){
|
||||
|
||||
var html_tpl = '<div class="scc-takethetour modal fade" id="scc-takethetour-modal" tabindex="-1" role="dialog" aria-labelledby="scc-takethetour-modal">'
|
||||
+ '<div class="modal-dialog" role="document">'
|
||||
+ '<div class="modal-content">'
|
||||
+ '<div class="modal-header">'
|
||||
+ '<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>'
|
||||
+ '<h4 class="modal-title" id="myModalLabel">' + this.settings.get('title') + '</h4>'
|
||||
+ '</div>'
|
||||
+ '<div class="modal-body">'
|
||||
+ '<div class="scc-takethetour-nav">'
|
||||
+ '<button class="prev btn btn-default disabled"><span class="caret"></span></button>'
|
||||
+ '<div></div>'
|
||||
+ '<button class="next btn btn-default"><span class="caret"></span></button>'
|
||||
+ '</div>'
|
||||
+ '<ul class="scc-takethetour-slides"></ul>'
|
||||
+ '</div>'
|
||||
+ '<div class="modal-footer">'
|
||||
+ '<label class="never-show-again"><input type="checkbox">' + this.settings.get('no_more_label') + '</label>'
|
||||
+ '<button type="button" class="btn btn-default" data-dismiss="modal">' + this.settings.get('close_btn_label') + '</button>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
this.$el.html(html_tpl);
|
||||
|
||||
var $user_slides = $('#'+this.settings.get('user_slides')+' li');
|
||||
|
||||
var $modal = this.$el.find('#scc-takethetour-modal');
|
||||
$modal.css({width:this.settings.get('width') + "%", "margin-left": "-" + this.settings.get('width')/2 + "%"});
|
||||
|
||||
var $modal_body = this.$el.find('#scc-takethetour-modal .modal-body');
|
||||
var $slides = this.$el.find('.scc-takethetour-slides');
|
||||
var $slides_nav = this.$el.find('.scc-takethetour-nav');
|
||||
var $slides_nav_prev = $slides_nav.find('.prev');
|
||||
var $slides_nav_next = $slides_nav.find('.next');
|
||||
var $slides_nav_links = $slides_nav.find('div');
|
||||
var $checkbox = this.$el.find('.never-show-again input');
|
||||
|
||||
var active_slide_id = 0;
|
||||
var max_slide_id = $user_slides.length - 1;
|
||||
|
||||
// Treat "show_credits" setting
|
||||
if(this.settings.get('show_credits') === true){
|
||||
var html_credits = '<div class="credits">'
|
||||
+ 'Powered by <a href="http://www.splunk.com" target="_blank">Splunk</a> and <a href="https://github.com/ftoulouse/splunk-components-collection" target="_blank">Scc</a>'
|
||||
+ '</div>';
|
||||
$modal.find('.modal-footer').append(html_credits);
|
||||
}
|
||||
|
||||
$user_slides.each(function(idx){
|
||||
$slides_nav_links.append('<button class="btn btn-default" data-slide-id="' + idx + '">' + (idx + 1) + '</button>');
|
||||
$slides.append($user_slides.eq(idx));
|
||||
|
||||
idx == 0 ? $slides_nav_links.find('button').eq(idx).addClass('btn-primary') : $slides.find('li').eq(idx).addClass('hide');
|
||||
});
|
||||
$('#'+this.settings.get('user_slides')).remove();
|
||||
|
||||
|
||||
// EVENTS
|
||||
// ------
|
||||
$slides_nav_prev.on('click', function(){
|
||||
if(!$(this).hasClass('disabled')){
|
||||
slidesManagr(active_slide_id - 1);
|
||||
}
|
||||
});
|
||||
$slides_nav_next.on('click', function(){
|
||||
if(!$(this).hasClass('disabled')){
|
||||
slidesManagr(active_slide_id + 1);
|
||||
}
|
||||
});
|
||||
$slides_nav_links.on('click', function(e){
|
||||
slidesManagr($(e.target).attr('data-slide-id'));
|
||||
});
|
||||
|
||||
// Add / remove vertical scrollbar on page resize
|
||||
function isScrollNeeded(){
|
||||
var overflow_y = $slides_nav.outerHeight(true) + $slides.outerHeight() > $modal_body.height() ? 'scroll' : 'none';
|
||||
$modal_body.css('overflow-y', overflow_y);
|
||||
};
|
||||
|
||||
// Hide / show slides and slides navigation
|
||||
function slidesManagr(slide_id){
|
||||
slide_id = parseInt(slide_id);
|
||||
|
||||
// Manage slides
|
||||
$slides.find('li').eq(active_slide_id).addClass('hide');
|
||||
$slides_nav_links.find('button').eq(active_slide_id).removeClass('btn-primary')
|
||||
|
||||
$slides.find('li').eq(slide_id).removeClass('hide');
|
||||
$slides_nav_links.find('button').eq(slide_id).addClass('btn-primary');
|
||||
|
||||
// Manage nav
|
||||
if(slide_id == 0){
|
||||
if(!$slides_nav_prev.hasClass('disabled')){
|
||||
$slides_nav_prev.addClass('disabled');
|
||||
}
|
||||
if($slides_nav_next.hasClass('disabled')){
|
||||
$slides_nav_next.removeClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
else if(slide_id == max_slide_id){
|
||||
if(!$slides_nav_next.hasClass('disabled')){
|
||||
$slides_nav_next.addClass('disabled');
|
||||
}
|
||||
if($slides_nav_prev.hasClass('disabled')){
|
||||
$slides_nav_prev.removeClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
else{
|
||||
if($slides_nav_prev.hasClass('disabled')){
|
||||
$slides_nav_prev.removeClass('disabled');
|
||||
}
|
||||
if($slides_nav_next.hasClass('disabled')){
|
||||
$slides_nav_next.removeClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
active_slide_id = slide_id;
|
||||
isScrollNeeded();
|
||||
}
|
||||
|
||||
$(window).resize(function(){
|
||||
isScrollNeeded();
|
||||
});
|
||||
|
||||
// Treat "backdrop_close" and "keyboard_close" settings
|
||||
var modal_backdrop = this.settings.get('backdrop_close') === true ? true : 'static';
|
||||
var modal_keyboard = this.settings.get('keyboard_close') === true ? true : false;
|
||||
|
||||
$modal
|
||||
.on('shown.bs.modal', function(e){
|
||||
// Prevents page body scrolling if modal content is scrollable
|
||||
$('body').css({overflow:"hidden", position:"fixed", width: "100%"});
|
||||
isScrollNeeded();
|
||||
})
|
||||
.on('hide.bs.modal', function(e){
|
||||
// Give back inherited overflow attributes to the body
|
||||
$('body').css({overflow:"inherit", position:"inherit", width: "inherit"});
|
||||
|
||||
// Never show the tour again until navigator cookie is not deleted by user
|
||||
if($checkbox.is(':checked')){
|
||||
setCookie(ck_name);
|
||||
}
|
||||
})
|
||||
.modal({
|
||||
backdrop: modal_backdrop,
|
||||
keyboard: modal_keyboard
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return SccTakeTheTour;
|
||||
});
|
||||
@ -0,0 +1,61 @@
|
||||
require.config({
|
||||
paths: {
|
||||
prettify: '../app/simple_xml_examples/components/srcviewer/contrib/prettify'
|
||||
}
|
||||
})
|
||||
define([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'backbone',
|
||||
'prettify',
|
||||
'css!contrib/google-code-prettify/prettify.css'
|
||||
], function(_, $, Backbone, prettify) {
|
||||
|
||||
var CodeView = Backbone.View.extend({
|
||||
options: {
|
||||
stripI18n: true
|
||||
},
|
||||
initialize: function() {
|
||||
this.listenTo(this.model, 'change:content', this.render, this);
|
||||
},
|
||||
getContent: function() {
|
||||
var content = this.model.get('content');
|
||||
if (content && this.options.stripI18n) {
|
||||
content = this.stripInjectedI18n(content);
|
||||
}
|
||||
return content;
|
||||
},
|
||||
stripInjectedI18n: function(source) {
|
||||
var lines = source.split(/\r\n|\r|\n/);
|
||||
var i = 0, start = 0;
|
||||
while (i < lines.length) {
|
||||
if (lines[i].indexOf('i18n_register') === 0) {
|
||||
i++;
|
||||
while (!lines[i]) {
|
||||
i++;
|
||||
}
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return lines.slice(start).join('\n');
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
content: this.getContent(),
|
||||
lang: this.model.get('lang')
|
||||
}));
|
||||
this.$el.attr({ "class": "tab-pane", id: this.model.get("id") });
|
||||
prettify(function(){}, this.el);
|
||||
return this;
|
||||
},
|
||||
template: _.template(
|
||||
'<pre class="prettyprint linenums <% if(lang) { %>lang-<%-lang%><% } %>">' +
|
||||
'<code><%- content %></code>' +
|
||||
'</pre>'
|
||||
)
|
||||
});
|
||||
|
||||
return CodeView;
|
||||
});
|
||||
@ -0,0 +1,106 @@
|
||||
require.config({
|
||||
paths: {
|
||||
showdown: '../app/simple_xml_examples/components/srcviewer/contrib/showdown'
|
||||
}
|
||||
});
|
||||
|
||||
define([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'backbone',
|
||||
'./codeview',
|
||||
'showdown',
|
||||
'bootstrap.tab'
|
||||
], function(_, $, Backbone, CodeView, Showdown) {
|
||||
|
||||
var markdown = new Showdown.converter();
|
||||
|
||||
var SourceCodeViewer = Backbone.View.extend({
|
||||
className: 'sourcecode-viewer',
|
||||
options: {
|
||||
title: 'Dashboard Source Code'
|
||||
},
|
||||
initialize: function(){
|
||||
this.items = [];
|
||||
this.listenTo(this.collection, 'reset remove', this.render, this);
|
||||
this.listenTo(this.collection, 'add', this.addItem, this);
|
||||
this.listenTo(this.model, 'change', this.render, this);
|
||||
},
|
||||
events: {
|
||||
'click .nav>li>a': function(e){
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).tab('show');
|
||||
}
|
||||
},
|
||||
addItem: function(model){
|
||||
if(!model.has('id')) {
|
||||
model.set('id', _.uniqueId('tab_'));
|
||||
}
|
||||
var id = model.get('id');
|
||||
var filename = model.get('name');
|
||||
var fileUrl = model.get('url') || '';
|
||||
var link = $('<a class="tab-title-text"></a>').text(filename).attr('href', fileUrl + '#' + id)
|
||||
var li = $('<li></li>').append(link);
|
||||
this.$('.nav-tabs').append(li);
|
||||
|
||||
var item = new CodeView({
|
||||
model: model,
|
||||
el: $('<div></div>').appendTo(this.$('.tab-content'))
|
||||
});
|
||||
item.render();
|
||||
|
||||
if(this.items.length == 0) {
|
||||
// Activate the tab, if it's the first item
|
||||
li.find('a').click();
|
||||
}
|
||||
|
||||
this.items.push(item);
|
||||
return item;
|
||||
},
|
||||
render: function(){
|
||||
_(this.items).invoke('remove');
|
||||
this.items.length = 0; // Clear items array
|
||||
|
||||
var model = _.extend({
|
||||
_: _,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
related_links: null
|
||||
}, this.model.toJSON(), this.options);
|
||||
|
||||
if(model.description) {
|
||||
model.description = markdown.makeHtml(model.description);
|
||||
}
|
||||
|
||||
this.$el.addClass(this.className);
|
||||
this.$el.html(this.template(model));
|
||||
this.collection.map(_(this.addItem).bind(this));
|
||||
|
||||
return this;
|
||||
},
|
||||
template: _.template(
|
||||
'<div class="dashboard-description">' +
|
||||
'<div class="description-title"><h3><%= _("Description").t() %></h3></div>' +
|
||||
'<div class="example-info">' +
|
||||
'<p class="description"><%= description %></p>' +
|
||||
'<% if(related_links && related_links.length) { %>' +
|
||||
'<h5><%= _("Related examples:").t() %></h5>' +
|
||||
'<ul class="related-links">' +
|
||||
'<% _.each(related_links, function(link) { %>' +
|
||||
'<li><a href="<%- link.href %>"><%- link.label %></a></li>' +
|
||||
'<% }); %>' +
|
||||
'</ul>' +
|
||||
'<% } %>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="showsource-container">' +
|
||||
'<ul class="nav nav-tabs">' +
|
||||
'<li class="nav-title">Source Code</li>' +
|
||||
'</ul>' +
|
||||
'<div class="tab-content source-content"></div>' +
|
||||
'</div>'
|
||||
)
|
||||
});
|
||||
|
||||
return SourceCodeViewer;
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
.tagcloud-viz {
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
max-width: 60%;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.tagcloud-viz .link {
|
||||
padding: 2px 3px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Simple TagCloud visualization
|
||||
* This view is an example for a simple visualization based on search results
|
||||
*/
|
||||
define(function(require, module) {
|
||||
var _ = require('underscore'), $ = require('jquery');
|
||||
var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview');
|
||||
var Drilldown = require('splunkjs/mvc/drilldown');
|
||||
require('css!./tagcloud.css');
|
||||
|
||||
var TagCloud = SimpleSplunkView.extend({
|
||||
moduleId: module.id,
|
||||
className: 'tagcloud-viz',
|
||||
options: {
|
||||
labelField: 'label',
|
||||
valueField: 'count',
|
||||
minFontSize: 8,
|
||||
maxFontSize: 36,
|
||||
data: 'preview'
|
||||
},
|
||||
output_mode: 'json',
|
||||
events: {
|
||||
'click a': function(e) {
|
||||
e.preventDefault();
|
||||
// Perform automatic drilldown on click on a tag
|
||||
Drilldown.handleDrilldown({
|
||||
name: this.settings.get('labelField'),
|
||||
value: $.trim($(e.target).text())
|
||||
}, 'row', this.manager);
|
||||
}
|
||||
},
|
||||
initialize: function() {
|
||||
SimpleSplunkView.prototype.initialize.apply(this, arguments);
|
||||
// Make sure we re-render the visualization when our settings change
|
||||
this.listenTo(this.settings, 'change:labelField change:valueField change:minFontSize change:maxFontSize', this._updateView);
|
||||
},
|
||||
createView: function() {
|
||||
return true;
|
||||
},
|
||||
updateView: function(viz, data) {
|
||||
var labelField = this.settings.get('labelField');
|
||||
var valueField = this.settings.get('valueField');
|
||||
var minFontSize = parseFloat(this.settings.get('minFontSize'));
|
||||
var maxFontSize = parseFloat(this.settings.get('maxFontSize'));
|
||||
|
||||
// Clear the current view
|
||||
var el = this.$el.empty().css('line-height', Math.ceil(maxFontSize * 0.55) + 'px');
|
||||
var minMagnitude = Infinity, maxMagnitude = -Infinity;
|
||||
|
||||
_(data).chain().map(function(result) {
|
||||
// Extract and convert the magnitude field value
|
||||
var magnitude = parseFloat(result[valueField]);
|
||||
// Find the maximum and minimum of the magnitude field values
|
||||
minMagnitude = magnitude < minMagnitude ? magnitude : minMagnitude;
|
||||
maxMagnitude = magnitude > maxMagnitude ? magnitude : maxMagnitude;
|
||||
return {
|
||||
label: result[labelField],
|
||||
magnitude: magnitude
|
||||
};
|
||||
}).each(function(result) {
|
||||
// Calculate relative size of each tag
|
||||
var size = minFontSize + ((result.magnitude - minMagnitude) / maxMagnitude * (maxFontSize - minFontSize));
|
||||
// Render the tag
|
||||
$('<a class="link" href="#" /> ').text(result.label + ' ').css({
|
||||
'font-size': size
|
||||
}).appendTo(el);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return TagCloud;
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2016-2017, OctoInsight Inc., All rights reserved.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2017, OctoInsight Inc., All rights reserved.
|
||||
* Authored by Ryan Thibodeaux
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
.toggledopen {
|
||||
font-family: Arial, Helvetica, Arial, sans-serif;
|
||||
padding: 9px 6px 6px 6px;
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.toggledopen:before {
|
||||
color: #d3d3d3;
|
||||
content: "\25B2";
|
||||
}
|
||||
|
||||
.toggledclosed {
|
||||
font-family: Arial, Helvetica, Arial, sans-serif;
|
||||
padding: 9px 6px 6px 6px;
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.toggledclosed:before {
|
||||
color: #d3d3d3;
|
||||
content: "\25Bc";
|
||||
}
|
||||
|
||||
.togglepanel-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @fileoverview Class definition for TogglePanel or Accordion panel feature
|
||||
* @author Ryan Thibodeaux
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2016-2017, OctoInsight Inc., All rights reserved.
|
||||
* Authored by Ryan Thibodeaux
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Definition of custom TogglePanel class.
|
||||
* This turns a Splunk "panel" element into
|
||||
* an Accordion panel or toggle-able panel.
|
||||
*
|
||||
* NOTE: this is not an extension of the panel
|
||||
* base class; instead, it allows one to retroactively
|
||||
* turn an existing panel into a toggle-able one. This
|
||||
* makes managing panels easier in Simple XML dashboards
|
||||
* because the Simple XML dashboards don't have to know
|
||||
* about this class, i.e., it is done in JS for the
|
||||
* dashboard
|
||||
*/
|
||||
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var $ = require('jquery');
|
||||
var mvc = require("splunkjs/mvc");
|
||||
|
||||
// The require-css plugin is inconsistent at determining the path
|
||||
// for loading CSS files. We have to hardcode for now,
|
||||
require('css!/static/app/metricator-for-nmon/components/togglepanel/togglepanel.css');
|
||||
|
||||
|
||||
// default settings for TogglePanel object
|
||||
var defaults = {
|
||||
openWidth : undefined, // width of toggle panel when open
|
||||
closeWidth : undefined, // width of toggle panel when closed
|
||||
toggleSpeed : 500, // toggle animation duration in milliseconds
|
||||
};
|
||||
|
||||
// constructor of TogglePanel object
|
||||
// parent can be an HTML id or jquery selector
|
||||
function TogglePanel(parent, openWidth, closeWidth, toggleSpeed) {
|
||||
if (!(this instanceof TogglePanel)) {
|
||||
throw new TypeError("TogglePanel constructor cannot be called as a function.");
|
||||
}
|
||||
|
||||
// handle parent parameter passed in as HTML id or jquery selector.
|
||||
// this.parent should point to jquery selector of TogglePanel object
|
||||
// this.parentId should contain the parent's HTML id
|
||||
if (typeof parent === 'undefined') {
|
||||
throw new TypeError("TogglePanel constructor cannot be called without a parent.");
|
||||
} else if (typeof parent === 'string' || parent instanceof String) {
|
||||
this.parentId = parent.replace(/^\#+/g, '');
|
||||
this.parent = $('#' + parent);
|
||||
if (!this.parentId.length) {
|
||||
throw new TypeError("TogglePanel constructor cannot find specified parent.");
|
||||
}
|
||||
} else {
|
||||
this.parentId = $(parent).attr('id');
|
||||
this.parent = $(parent);
|
||||
}
|
||||
|
||||
// set TogglePanel parameters
|
||||
this.openWidth = (typeof openWidth !== 'undefined' ? openWidth : defaults.openWidth);
|
||||
this.closeWidth = (typeof closeWidth !== 'undefined' ? closeWidth : defaults.closeWidth);
|
||||
this.toggleSpeed = (typeof toggleSpeed !== 'undefined' ? toggleSpeed : defaults.toggleSpeed);
|
||||
this.fieldset = undefined;
|
||||
this.childrenMvc = undefined;
|
||||
}
|
||||
|
||||
// create() wrapper for Toggle Panel constructor
|
||||
TogglePanel.create = function(parent, openWidth, closeWidth, toggleSpeed) {
|
||||
return (new TogglePanel(parent, openWidth, closeWidth, toggleSpeed));
|
||||
};
|
||||
|
||||
// static CSS class names to use for the open and close
|
||||
// state of the TogglePanel
|
||||
TogglePanel.OPENED_CLASS = 'toggledopen';
|
||||
TogglePanel.CLOSED_CLASS = 'toggledclosed';
|
||||
|
||||
|
||||
// define the TogglePanel class, which is a wholly new class
|
||||
// and not an extension of another
|
||||
TogglePanel.prototype = {
|
||||
constructor: TogglePanel,
|
||||
|
||||
// toggle child state
|
||||
toggleChild: function(el, wait) {
|
||||
if (!!wait) {
|
||||
el.slideToggle(this.toggleSpeed, function(){return;});
|
||||
} else {
|
||||
el.slideToggle(this.toggleSpeed);
|
||||
}
|
||||
el.resize();
|
||||
},
|
||||
|
||||
// animate hiding of child
|
||||
slideUpChild: function(el, wait) {
|
||||
if (!!wait) {
|
||||
el.slideUp(this.toggleSpeed, function(){return;});
|
||||
} else {
|
||||
el.slideUp(this.toggleSpeed);
|
||||
}
|
||||
},
|
||||
|
||||
// animate showing of child
|
||||
slideDownChild: function(el, wait) {
|
||||
if (!!wait) {
|
||||
el.slideDown(this.toggleSpeed, function(){return;});
|
||||
} else {
|
||||
el.slideDown(this.toggleSpeed);
|
||||
}
|
||||
},
|
||||
|
||||
// resize Splunk MVC element
|
||||
resizeChild: function(el) {
|
||||
el.resize();
|
||||
},
|
||||
|
||||
// call toggleChild for all known child elements
|
||||
toggleAllChildren: function() {
|
||||
var self = this;
|
||||
this.childrenMvc.forEach(function(child) {
|
||||
self.toggleChild(child.$el);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// call slideUp for all known child elements
|
||||
hideAllChildren: function() {
|
||||
var self = this;
|
||||
this.childrenMvc.forEach(function(child) {
|
||||
self.slideUpChild(child.$el);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// call slideDown for all known child elements
|
||||
// where we let the animation complete before moving
|
||||
// to the next and then we resize after all done
|
||||
showAllChildren: function() {
|
||||
var self = this;
|
||||
this.childrenMvc.forEach(function(child) {
|
||||
self.slideDownChild(child.$el, true);
|
||||
});
|
||||
this.childrenMvc.forEach(function(child) {
|
||||
self.resizeChild(child.$el);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// high-level function to start the toggle process on,
|
||||
// element el which should toggle all dashboard elements inside
|
||||
// of the toggle panel (el) and its fieldset element
|
||||
toggle: function(el) {
|
||||
|
||||
if (!el.data('parent')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set the toggled state on the contained fieldset element
|
||||
// and set the appropriate width of the TogglePanel object el
|
||||
var parent = $('#' + el.data('parent'));
|
||||
if (parent.length) {
|
||||
|
||||
var fieldset = this.fieldset;
|
||||
|
||||
// if toggling open, set width, toggle the fieldset,
|
||||
// and then toggle the elements
|
||||
if (el.attr("class") === TogglePanel.CLOSED_CLASS) {
|
||||
if (typeof this.openWidth !== 'undefined') {
|
||||
parent.css('width', this.openWidth);
|
||||
}
|
||||
|
||||
// toggle the fieldset
|
||||
if (fieldset.length > 0) {
|
||||
fieldset.removeClass("togglepanel-hidden");
|
||||
fieldset.slideDown(this.toggleSpeed);
|
||||
}
|
||||
|
||||
// call toggle open on all children
|
||||
this.showAllChildren();
|
||||
|
||||
// set new toggle icon
|
||||
el.attr('class', TogglePanel.OPENED_CLASS);
|
||||
} else {
|
||||
// if toggling closed, toggle elements,
|
||||
// and then toggle the fieldset
|
||||
|
||||
// call slideUp on all children
|
||||
this.hideAllChildren();
|
||||
|
||||
// hide the fieldset, where we use slideUp but
|
||||
// we also add a class after the animation is complete
|
||||
// where this class will force it to be hidden
|
||||
if (fieldset.length > 0) {
|
||||
fieldset.slideUp(this.toggleSpeed, function() {
|
||||
fieldset.addClass("togglepanel-hidden");
|
||||
});
|
||||
}
|
||||
|
||||
// set the close width
|
||||
if (typeof this.closeWidth !== 'undefined') {
|
||||
parent.css('width', this.closeWidth);
|
||||
}
|
||||
|
||||
// set new toggle icon
|
||||
el.attr('class', TogglePanel.CLOSED_CLASS);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// initial function to setup the TogglePanel object
|
||||
// by inserting the toggle element at the beginning
|
||||
// of the panel title and setting the appropriate state
|
||||
// of the TogglePanel based on the hide parameter
|
||||
setup: function(hide) {
|
||||
|
||||
// setup html element and its attributes for the toggle icon
|
||||
var title = this.parent.find('.panel-title')
|
||||
var toggleDiv = $('<div> </div>');
|
||||
toggleDiv.attr('class', TogglePanel.OPENED_CLASS);
|
||||
toggleDiv.attr('id', "toggle_panel_div_" + this.parentId);
|
||||
this.parent.children('.dashboard-panel').prepend(toggleDiv);
|
||||
toggleDiv.attr('alt', '#' + this.parentId).data('parent', this.parentId);
|
||||
this.$el = toggleDiv
|
||||
|
||||
// save fieldset selector if the panel has one
|
||||
this.fieldset = this.parent.find('.fieldset');
|
||||
|
||||
// find all MVC elements and save them in childrenMvc
|
||||
var children = [];
|
||||
this.parent.find('.dashboard-element').each(function() {
|
||||
var k = mvc.Components.get(this.id);
|
||||
if (typeof k !== 'undefined') {
|
||||
children.push(k);
|
||||
}
|
||||
});
|
||||
this.childrenMvc = children;
|
||||
|
||||
|
||||
// hide all children if "hide" is true
|
||||
if (!!hide) {
|
||||
this.toggle(this.$el);
|
||||
}
|
||||
|
||||
// setup click listener on toggle switch and panel title
|
||||
toggleDiv.on("click", $.proxy(this.toggle, this, this.$el));
|
||||
if (title.length > 0) {
|
||||
title.first().on("click", $.proxy(this.toggle, this, this.$el));
|
||||
title.first().css("cursor", "pointer");
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
return TogglePanel;
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2017, OctoInsight Inc., All rights reserved.
|
||||
* Authored by Ryan Thibodeaux
|
||||
* see included LICENSE file (BSD 3-clause)
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file implements the autodiscover function
|
||||
* that finds panels in SimpleXML that should
|
||||
* be turned into Toggle Panels based on their
|
||||
* HTML IDs matching a specific pattern.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
require([
|
||||
"underscore",
|
||||
"jquery",
|
||||
"splunkjs/mvc",
|
||||
"TogglePanel",
|
||||
], function(_, $, mvc, TogglePanel) {
|
||||
|
||||
const regex = /_togglepanel/i;
|
||||
const regexHide = /_togglepanel_true/i;
|
||||
|
||||
_(mvc.Components.toJSON())
|
||||
.chain()
|
||||
.filter(function(el) {
|
||||
var id = $(el).attr("id");
|
||||
var dom = $(el).attr("$el");
|
||||
if (typeof id !== "undefined" && typeof dom !== "undefined") {
|
||||
if (id.match(regex) !== null && dom.hasClass('dashboard-cell')) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}).each(function(el) {
|
||||
var id = $(el).attr("id");
|
||||
var hide = (id.match(regexHide) !== null ? true : false);
|
||||
new TogglePanel(id).setup(hide);
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
@ -0,0 +1,10 @@
|
||||
require(['jquery', 'splunkjs/mvc/simplexml/ready!'], function($) {
|
||||
$("[id*=setWidth]").each(function() {
|
||||
var match = /setWidth_(\d+(?:_\d+)?)/.exec($(this).attr('id'));
|
||||
if (match[1]) {
|
||||
$(this).closest(".dashboard-cell").css('width', match[1].replace("_", ".") + '%');
|
||||
}
|
||||
});
|
||||
// Force visualizations (esp. charts) to be redrawn with their new size
|
||||
$(window).trigger('resize');
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
/* darks css theme from: http://www.brainfold.net/2016/04/splunk-dashboards-looks-more-beautiful.html */
|
||||
|
||||
body,.dashboard-body,.footer,.header,.dashboard-cell {
|
||||
background: #1C1E23 !important;
|
||||
}
|
||||
|
||||
.dashboard-panel {
|
||||
background: #292C33 !important;
|
||||
}
|
||||
|
||||
svg text {
|
||||
fill: #fff !important;
|
||||
}
|
||||
.single-value .single-result {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dashboard-header h2, p.description, .nav-footer>li>a {
|
||||
color: #ddd;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.dashboard-row .dashboard-panel h2.panel-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button, input, label, select, textarea {
|
||||
color: #808080 !important;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
/*
|
||||
*
|
||||
* This is the default CSS for all
|
||||
* pages in the app.
|
||||
*
|
||||
* This file will automatically be loaded
|
||||
* for all dashboards.
|
||||
*
|
||||
*/
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
/////////////////////////////////////////////////
|
||||
//
|
||||
// This is the default entry point for all
|
||||
// pages in the app.
|
||||
//
|
||||
// This file will automatically be loaded
|
||||
// for all dashboards.
|
||||
//
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
(function() {
|
||||
var appName, appPath;
|
||||
var pageOptions, pageName;
|
||||
var urlAppComponents, requireRoot;
|
||||
|
||||
// anonymous function to breakdown the URL into app name and page name
|
||||
urlAppComponents = (function() {
|
||||
var comps = (location.pathname.split('?')[0]).split('/');
|
||||
var idx = comps.indexOf('app');
|
||||
var app = comps[idx + 1];
|
||||
var page = comps[idx + 2];
|
||||
return [app, page];
|
||||
})();
|
||||
|
||||
// obtain values from previous anonymous function
|
||||
appName = urlAppComponents[0];
|
||||
pageName = urlAppComponents[1];
|
||||
|
||||
// save global entities
|
||||
pageOptions = {
|
||||
"pageStartTime": new Date().valueOf(), // save time of when page is loaded
|
||||
"appName" : appName, // app name
|
||||
"pageName" : pageName, // dashboard page name
|
||||
};
|
||||
|
||||
// setup paths for rest of the configuration
|
||||
requireRoot = "../app";
|
||||
appPath = requireRoot + "/" + appName;
|
||||
|
||||
// configure RequrieJS Paths and Options
|
||||
require.config({
|
||||
paths: {
|
||||
"app" : requireRoot,
|
||||
"appOptions" : appPath + "/components/lib/options",
|
||||
"appUtils" : appPath + "/components/lib/utils",
|
||||
},
|
||||
config: {
|
||||
"appOptions": {
|
||||
"options": pageOptions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// load the important modules for the dashboards, where we load CSS first
|
||||
// and than everything else
|
||||
require([], function() {
|
||||
require([
|
||||
"appOptions",
|
||||
"appUtils",
|
||||
], function(ignored, appUtils) {
|
||||
// call initialization routine
|
||||
appUtils.initiliazeApp(true);
|
||||
}, function(err) {
|
||||
// error callback
|
||||
// the error has a list of modules that failed
|
||||
var failedId = err.requireModules && err.requireModules[0];
|
||||
requirejs.undef(failedId);
|
||||
console.error("Error when loading dependency", err);
|
||||
});
|
||||
}, function(err) {
|
||||
// error callback
|
||||
// the error has a list of modules that failed
|
||||
var failedId = err.requireModules && err.requireModules[0];
|
||||
requirejs.undef(failedId);
|
||||
console.error("Error when loading CSS dependency", err);
|
||||
});
|
||||
}).call(this);
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,3 @@
|
||||
.dashboard-header h2 {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
/* completely hide panel footers */
|
||||
|
||||
.dashboard-row .dashboard-panel .dashboard-element .panel-footer {
|
||||
display: none;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
.dashboard-row .dashboard-panel .refresh-time-indicator {
|
||||
font-size: 11px;
|
||||
color: #555555;
|
||||
cursor: default;
|
||||
padding: 10px 10px 6px 10px;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
.html h2 {
|
||||
color: #adbacd !important;
|
||||
}
|
||||
|
||||
a.tryitbtn-blue,
|
||||
a.tryitbtn-blue:link,
|
||||
a.tryitbtn-blue:visited,
|
||||
a.tryitbtn-blue,
|
||||
a.tryitbtn-blue:link,
|
||||
a.tryitbtn-blue:visited {
|
||||
display: inline-block;
|
||||
color: #71b1c1;
|
||||
background-color: #1f1f1f;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 4px;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
/* margin-left: 5px; */
|
||||
margin-right: 10px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #aaaaaa;
|
||||
border: 1px solid #71b1c1;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.tryitbtn-blue:hover,
|
||||
a.tryitbtn-blue:active,
|
||||
a.tryitbtn-blue:hover,
|
||||
a.tryitbtn-blue:active {
|
||||
background-color: #71b1c1;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
/* main category icons and titles */
|
||||
|
||||
.imgheader img {
|
||||
float: left;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.imgheader h1 {
|
||||
color: #adbacd;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
top: 9px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.imgheader h2 {
|
||||
position: relative;
|
||||
top: 18px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.imgminiheader img {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.1 KiB |