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 |