2 typeof define === "function" ? function (m) { define("kismet-ui-js", m); } :
3 typeof exports === "object" ? function (m) { module.exports = m(); } :
4 function(m){ this.kismet_ui = m(); }
9 var local_uri_prefix = "";
10 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
11 local_uri_prefix = KISMET_URI_PREFIX;
15 exports.window_visible = true;
17 // Load spectrum css and js
23 href: local_uri_prefix + 'css/spectrum.css'
28 type: 'text/javascript',
29 src: local_uri_prefix + 'js/spectrum.js'
33 exports.last_timestamp = 0;
35 // Set panels to close on escape system-wide
36 jsPanel.closeOnEscape = true;
49 /* Add a view option that the user can pick for the main device table;
50 * view is expected to be a component of the /devices/views/ api
52 exports.AddDeviceView = function(name, view, priority, group = 'none') {
53 DeviceViews.push({name: name, view: view, priority: priority, group: group});
56 exports.BuildDeviceViewSelector = function(element) {
57 var grouped_views = [];
59 // Pre-sort the array so that as we build our nested stuff we do it in order
60 DeviceViews.sort(function(a, b) {
61 if (a.priority < b.priority)
63 if (b.priority > a.priority)
69 // This isn't efficient but happens rarely, so who cares
70 for (var i in DeviceViews) {
71 if (DeviceViews[i]['group'] == 'none') {
72 // If there's no group, immediately add it to the grouped view
73 grouped_views.push(DeviceViews[i]);
75 // Otherwise look for the group already in the view
77 for (var g in grouped_views) {
78 if (Array.isArray(grouped_views[g])) {
79 if (grouped_views[g][0]['group'] == DeviceViews[i]['group']) {
86 // Make a new sub-array if we don't exist, otherwise append to the existing array
87 if (existing_g == -1) {
88 grouped_views.push([DeviceViews[i]]);
90 grouped_views[existing_g].push(DeviceViews[i]);
97 name: 'devices_views_select',
98 id: 'devices_views_select'
101 for (var i in grouped_views) {
102 if (!Array.isArray(grouped_views[i])) {
105 value: grouped_views[i]['view']
106 }).html(grouped_views[i]['name'])
111 label: grouped_views[i][0]['group']
114 for (var og in grouped_views[i]) {
117 value: grouped_views[i][og]['view']
118 }).html(grouped_views[i][og]['name'])
122 selector.append(optgroup);
126 var selected_option = kismet.getStorage('kismet.ui.deviceview.selected', 'all');
127 $('option[value="' + selected_option + '"]', selector).prop("selected", "selected");
129 selector.on("selectmenuselect", function(evt, elem) {
130 kismet.putStorage('kismet.ui.deviceview.selected', elem.item.value);
132 if (device_dt != null) {
133 device_dt.ajax.url(local_uri_prefix + "devices/views/" + elem.item.value + "/devices.json");
137 element.empty().append(selector);
139 selector.selectmenu()
140 .selectmenu("menuWidget")
141 .addClass("selectoroverflow");
144 // Local maps of views for phys and datasources we've already added
145 var existing_views = {};
146 var view_list_updater_tid = 0;
148 function deviceview_selector_dynamic_update() {
149 clearTimeout(view_list_updater_tid);
150 view_list_updater_tid = setTimeout(deviceview_selector_dynamic_update, 5000);
152 if (!exports.window_visible)
155 var ds_priority = -5000;
156 var phy_priority = -1000;
158 $.get(local_uri_prefix + "devices/views/all_views.json")
159 .done(function(data) {
160 var ds_promises = [];
162 var f_datasource_closure = function(uuid) {
163 var ds_promise = $.Deferred();
165 $.get(local_uri_prefix + "datasource/by-uuid/" + uuid + "/source.json")
166 .done(function(dsdata) {
167 var dsdata = kismet.sanitizeObject(dsdata);
168 var synth_view = 'seenby-' + dsdata['kismet.datasource.uuid'];
170 existing_views[synth_view] = 1;
172 exports.AddDeviceView(dsdata['kismet.datasource.name'], synth_view, ds_priority, 'Datasources');
173 ds_priority = ds_priority - 1;
176 ds_promise.resolve();
179 return ds_promise.promise();
182 data = kismet.sanitizeObject(data);
184 for (var v in data) {
185 if (data[v]['kismet.devices.view.id'] in existing_views)
188 if (data[v]['kismet.devices.view.id'].substr(0, 7) === 'seenby-') {
189 var uuid = data[v]['kismet.devices.view.id'].substr(7);
190 ds_promises.push(f_datasource_closure(uuid));
191 // ds_promises.push($.get(local_uri_prefix + "datasource/by-uuid/" + uuid + "/source.json"));
194 if (data[v]['kismet.devices.view.id'].substr(0, 4) === 'phy-') {
195 existing_views[data[v]['kismet.devices.view.id']] = 1;
196 exports.AddDeviceView(data[v]['kismet.devices.view.description'], data[v]['kismet.devices.view.id'], phy_priority, 'Phy types');
197 phy_priority = phy_priority - 1;
201 // Complete all the DS queries
202 $.when(ds_promises).then(function(pi) {
206 // Skip generating this round if the menu is open
207 if ($("div.viewselector > .ui-selectmenu-button").hasClass("ui-selectmenu-button-open")) {
210 exports.BuildDeviceViewSelector($('div.viewselector'));
215 deviceview_selector_dynamic_update();
217 // List of datatable columns we have available
218 var DeviceColumns = new Array();
220 // Device row highlights, consisting of fields, function, name, and color
221 var DeviceRowHighlights = new Array();
223 /* Add a jquery datatable column that the user can pick from, with various
226 * sTitle: datatable column title
227 * name: datatable 'name' field (optional)
228 * field: Kismet field path, array pair of field path and name, array of fields,
229 * or a function returning one of the above.
230 * fields: Multiple fields. When multiple fields are defined, ONE field MUST be defined in the
231 * 'field' parameter. Additional multiple fields may be defined in this parameter.
232 * renderfunc: string name of datatable render function, taking DT arguments
233 * (data, type, row, meta), (optional)
234 * drawfunc: string name of a draw function, taking arguments:
235 * dyncolumn - The dynamic column (this)
236 * datatable - A DataTable() object of the table we're operating on
237 * row - The row we're operating on, which should be visible
238 * This will be called during the drawCallback
239 * stage of the table, on visible rows. (optional)
241 exports.AddDeviceColumn = function(id, options) {
244 sTitle: options.sTitle,
249 if ('field' in options) {
250 coldef.field = options.field;
253 if ('fields' in options) {
254 coldef.fields = options.fields;
257 if ('description' in options) {
258 coldef.description = options.description;
261 if ('name' in options) {
262 coldef.name = options.name;
265 if ('orderable' in options) {
266 coldef.bSortable = options.orderable;
269 if ('visible' in options) {
270 coldef.bVisible = options.visible;
272 coldef.bVisible = true;
275 if ('selectable' in options) {
276 coldef.user_selectable = options.selectable;
278 coldef.user_selectable = true;
281 if ('searchable' in options) {
282 coldef.bSearchable = options.searchable;
285 if ('width' in options)
286 coldef.width = options.width;
288 if ('sClass' in options)
289 coldef.sClass = options.sClass;
292 if (typeof(coldef.field) === 'string') {
293 var fs = coldef.field.split("/");
294 f = fs[fs.length - 1];
295 } else if (Array.isArray(coldef.field)) {
299 // Bypass datatable/jquery pathing
300 coldef.mData = function(row, type, set) {
301 return kismet.ObjectByString(row, f);
304 // Datatable render function
305 if ('renderfunc' in options) {
306 coldef.mRender = options.renderfunc;
309 // Set an arbitrary draw hook we call ourselves during the draw loop later
310 if ('drawfunc' in options) {
311 coldef.kismetdrawfunc = options.drawfunc;
314 DeviceColumns.push(coldef);
317 /* Add a row highlighter for coloring rows; expects an options dictionary containing:
319 * description: Longer description
320 * priority: Priority for assigning color
321 * defaultcolor: rgb default color
322 * defaultenable: optional bool, should be turned on by default
323 * fields: *array* of field definitions, each of which may be a single or two-element
324 * field definition/path. A *single* field must still be represented as an array,
325 * ie, ['some.field.def']. Multiple fields and complex fields could be represented
326 * as ['some.field.def', 'some.second.field', ['some.complex/field.path', 'field.foo']]
327 * selector: function(data) returning true for color or false for ignore
329 exports.AddDeviceRowHighlight = function(options) {
331 // Load enable preference
333 kismet.getStorage('kismet.rowhighlight.enable' + options.name, 'NONE');
335 if (storedenable === 'NONE') {
336 if ('defaultenable' in options) {
337 options['enable'] = options['defaultenable'];
339 options['enable'] = true;
342 options['enable'] = storedenable;
345 // Load color preference
347 kismet.getStorage('kismet.rowhighlight.color' + options.name, 'NONE');
349 if (storedcolor !== 'NONE') {
350 options['color'] = storedcolor;
352 options['color'] = options['defaultcolor'];
355 DeviceRowHighlights.push(options);
357 DeviceRowHighlights.sort(function(a, b) {
358 if (a.priority < b.priority)
360 if (b.priority > a.priority)
367 /* Return columns from the selected list of column IDs */
368 exports.GetDeviceColumns = function(showall = false) {
369 var ret = new Array();
371 var order = kismet.getStorage('kismet.datatable.columns', []);
373 // If we don't have an order saved
374 if (order.length == 0) {
375 // Sort invisible columns to the end
376 for (var i in DeviceColumns) {
377 if (!DeviceColumns[i].bVisible)
379 ret.push(DeviceColumns[i]);
381 for (var i in DeviceColumns) {
382 if (DeviceColumns[i].bVisible)
384 ret.push(DeviceColumns[i]);
389 // Otherwise look for all the columns we have enabled
390 for (var oi in order) {
396 // Find the column that matches the ID in the master list of columns
397 var dc = DeviceColumns.find(function(e, i, a) {
398 if (e.kismetId === o.id)
403 if (dc != undefined && dc.user_selectable) {
409 // If we didn't find anything, default to the normal behavior - something is wrong
410 if (ret.length == 0) {
411 // Sort invisible columsn to the end
412 for (var i in DeviceColumns) {
413 if (!DeviceColumns[i].bVisible)
415 ret.push(DeviceColumns[i]);
417 for (var i in DeviceColumns) {
418 if (DeviceColumns[i].bVisible)
420 ret.push(DeviceColumns[i]);
425 // If we're showing everything, find any other columns we don't have selected,
426 // now that we've added the visible ones in the right order.
428 for (var dci in DeviceColumns) {
429 var dc = DeviceColumns[dci];
432 if (!dc.user_selectable)
436 var rc = ret.find(function(e, i, a) {
437 if (e.kismetId === dc.kismetId)
442 if (rc == undefined) {
448 // Return the list w/out adding the non-user-selectable stuff
452 // Then append all the columns the user can't select because we need them for
453 // fetching data or providing hidden sorting
454 for (var dci in DeviceColumns) {
455 if (!DeviceColumns[dci].user_selectable) {
456 ret.push(DeviceColumns[dci]);
463 // Generate a map of column number to field array so we can tell Kismet what fields
464 // are in what column for sorting
465 exports.GetDeviceColumnMap = function(columns) {
468 for (var ci in columns) {
469 var fields = new Array();
471 if ('field' in columns[ci])
472 fields.push(columns[ci]['field']);
474 if ('fields' in columns[ci])
475 fields.push.apply(fields, columns[ci]['fields']);
484 /* Return field arrays for the device list; aggregates fields from device columns,
485 * widget columns, and color highlight columns.
487 exports.GetDeviceFields = function(selected) {
488 var rawret = new Array();
489 var cols = exports.GetDeviceColumns();
491 for (var i in cols) {
492 if ('field' in cols[i] && cols[i]['field'] != null)
493 rawret.push(cols[i]['field']);
495 if ('fields' in cols[i] && cols[i]['fields'] != null)
496 rawret.push.apply(rawret, cols[i]['fields']);
499 for (var i in DeviceRowHighlights) {
500 rawret.push.apply(rawret, DeviceRowHighlights[i]['fields']);
503 // De-dupe the list of fields/field aliases
504 var ret = rawret.filter(function(item, pos, self) {
505 return self.indexOf(item) == pos;
511 exports.AddDetail = function(container, id, title, pos, options) {
512 var settings = $.extend({
527 container.sort(function(a, b) {
528 return a.position - b.position;
532 exports.DetailWindow = function(key, title, options, window_cb, close_cb) {
533 // Generate a unique ID for this dialog
534 var dialogid = "detaildialog" + key;
535 var dialogmatch = '#' + dialogid;
537 if (jsPanel.activePanels.list.indexOf(dialogid) != -1) {
538 jsPanel.activePanels.getPanel(dialogid).front();
542 var h = $(window).height() - 5;
544 // If we're on a wide-screen browser, try to split it into 3 details windows
545 var w = ($(window).width() / 3) - 10;
547 // If we can't, split it into 2. This seems to look better when people
548 // don't run full-size browser windows.
550 w = ($(window).width() / 2) - 5;
553 // Finally make it full-width if we're still narrow
555 w = $(window).width() - 5;
558 var panel = $.jsPanel({
564 controls: 'closeonly',
573 "autoposition": "RIGHT"
579 stop: function(event, ui) {
580 $('div#accordion', ui.element).accordion("refresh");
584 onmaximized: function() {
585 $('div#accordion', this.content).accordion("refresh");
588 onnormalized: function() {
589 $('div#accordion', this.content).accordion("refresh");
592 onclosed: function() {
593 close_cb(this, options);
596 callback: function() {
597 window_cb(this, options);
602 callback: function(panel) {
603 $('div#accordion', this.content).accordion("refresh");
607 // Did we creep off the screen in our autopositioning? Put this panel in
608 // the left if so (or if it's a single-panel situation like mobile, just
609 // put it front and center)
610 if (panel.offset().left + panel.width() > $(window).width()) {
622 exports.DeviceDetails = new Array();
624 /* Register a device detail accordion panel, taking an id for the panel
625 * content, a title presented to the user, a position in the list, and
626 * options. Because details are directly rendered all the time and
627 * can't be moved around / saved as configs like columns can, callbacks
628 * are just direct functions here.
630 * filter and render take one argument, the data to be shown
631 * filter: function(data) {
635 * render: function(data) {
636 * return "Some content";
639 * draw takes the device data and a container element as an argument:
640 * draw: function(data, element) {
644 exports.AddDeviceDetail = function(id, title, pos, options) {
645 exports.AddDetail(exports.DeviceDetails, id, title, pos, options);
648 exports.GetDeviceDetails = function() {
649 return exports.DeviceDetails;
652 exports.DeviceDetailWindow = function(key) {
653 exports.DetailWindow(key, "Device Details",
658 function(panel, options) {
659 var content = panel.content;
663 window['storage_devlist_' + key] = {};
665 window['storage_devlist_' + key]['foobar'] = 'bar';
667 panel.updater = function() {
668 if (exports.window_visible) {
669 $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
670 .done(function(fulldata) {
671 fulldata = kismet.sanitizeObject(fulldata);
673 panel.headerTitle("Device: " + kismet.censorMAC(fulldata['kismet.device.base.commonname']));
675 var accordion = $('div#accordion', content);
677 if (accordion.length == 0) {
678 accordion = $('<div />', {
682 content.append(accordion);
685 var detailslist = kismet_ui.GetDeviceDetails();
687 for (var dii in detailslist) {
688 var di = detailslist[dii];
691 if ('filter' in di.options &&
692 typeof(di.options.filter) === 'function') {
693 if (di.options.filter(fulldata) == false) {
698 var vheader = $('h3#header_' + di.id, accordion);
700 if (vheader.length == 0) {
701 vheader = $('<h3>', {
702 id: "header_" + di.id,
706 accordion.append(vheader);
709 var vcontent = $('div#' + di.id, accordion);
711 if (vcontent.length == 0) {
712 vcontent = $('<div>', {
715 accordion.append(vcontent);
718 // Do we have pre-rendered content?
719 if ('render' in di.options &&
720 typeof(di.options.render) === 'function') {
721 vcontent.html(di.options.render(fulldata));
724 if ('draw' in di.options &&
725 typeof(di.options.draw) === 'function') {
726 di.options.draw(fulldata, vcontent, options, 'storage_devlist_' + key);
729 if ('finalize' in di.options &&
730 typeof(di.options.finalize) === 'function') {
731 di.options.finalize(fulldata, vcontent, options, 'storage_devlist_' + key);
734 accordion.accordion({ heightStyle: 'fill' });
736 .fail(function(jqxhr, texterror) {
737 content.html("<div style=\"padding: 10px;\"><h1>Oops!</h1><p>An error occurred loading device details for key <code>" + key +
738 "</code>: HTTP code <code>" + jqxhr.status + "</code>, " + texterror + "</div>");
741 panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
744 panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
751 new ClipboardJS('.copyuri');
754 function(panel, options) {
755 clearTimeout(panel.timerid);
756 panel.active = false;
757 window['storage_devlist_' + key] = {};
762 exports.RenderTrimmedTime = function(opts) {
763 return (new Date(opts['value'] * 1000).toString()).substring(4, 25);
766 exports.RenderHumanSize = function(opts) {
767 return kismet.HumanReadableSize(opts['value']);
770 // Central location to register channel conversion lists. Conversion can
771 // be a function or a fixed dictionary.
772 exports.freq_channel_list = { };
773 exports.human_freq_channel_list = { };
775 exports.AddChannelList = function(phyname, humanname, channellist) {
776 exports.freq_channel_list[phyname] = channellist;
777 exports.human_freq_channel_list[humanname] = channellist;
780 // Get a list of human frequency conversions
781 exports.GetChannelListKeys = function() {
782 return Object.keys(exports.human_freq_channel_list);
785 // Get a converted channel name, or the raw frequency if we can't help
786 exports.GetConvertedChannel = function(humanname, frequency) {
787 if (humanname in exports.human_freq_channel_list) {
788 var conv = exports.human_freq_channel_list[humanname];
790 if (typeof(conv) === "function") {
791 // Call the conversion function if one exists
792 return conv(frequency);
793 } else if (frequency in conv) {
794 // Return the mapped value
795 return conv[frequency];
799 // Return the frequency if we couldn't figure out what to do
803 // Get a converted channel name, or the raw frequency if we can't help
804 exports.GetPhyConvertedChannel = function(phyname, frequency) {
805 if (phyname in exports.freq_channel_list) {
806 var conv = exports.freq_channel_list[phyname];
808 if (typeof(conv) === "function") {
809 // Call the conversion function if one exists
810 return conv(frequency);
811 } else if (frequency in conv) {
812 // Return the mapped value
813 return conv[frequency];
817 // Return the frequency if we couldn't figure out what to do
818 return kismet.HumanReadableFrequency(frequency);
821 exports.connection_error = 0;
822 exports.connection_error_panel = null;
824 exports.HealthCheck = function() {
827 if (exports.window_visible) {
828 $.get(local_uri_prefix + "system/status.json")
829 .done(function(data) {
830 data = kismet.sanitizeObject(data);
832 if (exports.connection_error && exports.connection_error_panel) {
834 exports.connection_error_panel.close();
835 exports.connection_error_panel = null;
841 exports.connection_error = 0;
843 exports.last_timestamp = data['kismet.system.timestamp.sec'];
846 if (exports.connection_error >= 3 && exports.connection_error_panel == null) {
847 exports.connection_error_panel = $.jsPanel({
848 id: "connection-alert",
849 headerTitle: 'Cannot Connect to Kismet',
854 contentSize: "auto auto",
856 content: '<div style="padding: 10px;"><h3><i class="fa fa-exclamation-triangle" style="color: red;" /> Sorry!</h3><p>Cannot connect to the Kismet webserver. Make sure Kismet is still running on this host!<p><i class="fa fa-refresh fa-spin" style="margin-right: 5px" /> Connecting to the Kismet server...</div>',
860 exports.connection_error++;
863 if (exports.connection_error)
864 timerid = setTimeout(exports.HealthCheck, 1000);
866 timerid = setTimeout(exports.HealthCheck, 5000);
869 if (exports.connection_error)
870 timerid = setTimeout(exports.HealthCheck, 1000);
872 timerid = setTimeout(exports.HealthCheck, 5000);
878 exports.DegToDir = function(deg) {
880 "N", "NNE", "NE", "ENE",
881 "E", "ESE", "SE", "SSE",
882 "S", "SSW", "SW", "WSW",
883 "W", "WNW", "NW", "NNW"
893 for (var p = 1; p < degrees.length; p++) {
894 if (deg < degrees[p])
895 return directions[p - 1];
898 return directions[directions.length - 1];
901 // Use our settings to make some conversion functions for distance and temperature
902 exports.renderDistance = function(k, precision = 5) {
903 if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
904 kismet.getStorage('kismet.base.unit.distance') === '') {
906 return (k * 1000).toFixed(precision) + ' m';
909 return k.toFixed(precision) + ' km';
911 var m = (k * 0.621371);
914 return (5280 * m).toFixed(precision) + ' feet';
916 return (k * 0.621371).toFixed(precision) + ' miles';
920 // Use our settings to make some conversion functions for distance and temperature
921 exports.renderHeightDistance = function(m, precision = 5) {
922 if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
923 kismet.getStorage('kismet.base.unit.distance') === '') {
925 return m.toFixed(precision) + ' m';
928 return (m / 1000).toFixed(precision) + ' km';
930 var f = (m * 3.2808399);
933 return f.toFixed(precision) + ' feet';
935 return (f / 5280).toFixed(precision) + ' miles';
939 exports.renderSpeed = function(kph, precision = 5) {
940 if (kismet.getStorage('kismet.base.unit.speed') === 'metric' ||
941 kismet.getStorage('kismet.base.unit.speed') === '') {
942 return kph.toFixed(precision) + ' KPH';
944 return (kph * 0.621371).toFixed(precision) + ' MPH';
948 exports.renderTemperature = function(c, precision = 5) {
949 if (kismet.getStorage('kismet.base.unit.temp') === 'celsius' ||
950 kismet.getStorage('kismet.base.unit.temp') === '') {
951 return c.toFixed(precision) + '° C';
953 return (c * (9/5) + 32).toFixed(precision) + '° F';
959 var devicetableElement = null;
961 function ScheduleDeviceSummary() {
963 if (exports.window_visible && devicetableElement.is(":visible")) {
965 var dt = devicetableElement.DataTable();
967 // Save the state. We can't use proper state saving because it seems to break
968 // the table position
969 kismet.putStorage('kismet.base.devicetable.order', JSON.stringify(dt.order()));
970 kismet.putStorage('kismet.base.devicetable.search', JSON.stringify(dt.search()));
972 // Snapshot where we are, because the 'don't reset page' in ajax.reload
973 // DOES still reset the scroll position
975 'top': $(dt.settings()[0].nScrollBody).scrollTop(),
976 'left': $(dt.settings()[0].nScrollBody).scrollLeft()
978 dt.ajax.reload(function(d) {
979 // Restore our scroll position
980 $(dt.settings()[0].nScrollBody).scrollTop( prev_pos.top );
981 $(dt.settings()[0].nScrollBody).scrollLeft( prev_pos.left );
989 // Set our timer outside of the datatable callback so that we get called even
990 // if the ajax load fails
991 deviceTid = setTimeout(ScheduleDeviceSummary, 2000);
996 function CancelDeviceSummary() {
997 clearTimeout(deviceTid);
1000 /* Create the device table */
1001 exports.CreateDeviceTable = function(element) {
1002 devicetableElement = element;
1003 var statuselement = $('#' + element.attr('id') + '_status');
1005 var dt = exports.InitializeDeviceTable(element);
1009 // Start the auto-updating
1010 ScheduleDeviceSummary();
1013 exports.InitializeDeviceTable = function(element) {
1014 var statuselement = $('#' + element.attr('id') + '_status');
1016 /* Make the fields list json and set the wrapper object to aData to make the DT happy */
1017 var cols = exports.GetDeviceColumns();
1018 var colmap = exports.GetDeviceColumnMap(cols);
1019 var fields = exports.GetDeviceFields();
1027 if ($.fn.dataTable.isDataTable(element)) {
1028 element.DataTable().destroy();
1033 .on('xhr.dt', function (e, settings, json, xhr) {
1034 json = kismet.sanitizeObject(json);
1036 if (json['recordsFiltered'] != json['recordsTotal'])
1037 statuselement.html(json['recordsTotal'] + " devices (" + json['recordsFiltered'] + " shown after filter)");
1039 statuselement.html(json['recordsTotal'] + " devices");
1052 dom: '<"viewselector">ft',
1055 lengthChange: false,
1058 loadingIndicator: true,
1061 // Create a complex post to get our summary fields only
1063 url: local_uri_prefix + "devices/views/" + kismet.getStorage('kismet.ui.deviceview.selected', 'all') + "/devices.json",
1065 json: JSON.stringify(json)
1067 error: function(jqxhr, status, error) {
1068 // Catch missing views and reset
1069 if (jqxhr.status == 404) {
1070 device_dt.ajax.url(local_uri_prefix + "devices/views/all/devices.json");
1071 kismet.putStorage('kismet.ui.deviceview.selected', 'all');
1072 exports.BuildDeviceViewSelector($('div.viewselector'));
1079 // Get our dynamic columns
1083 { className: "dt_td", targets: "_all" },
1089 // Map our ID into the row
1090 createdRow : function( row, data, index ) {
1091 row.id = data['kismet.device.base.key'];
1094 // Opportunistic draw on new rows
1095 drawCallback: function( settings ) {
1096 var dt = this.api();
1100 }).every(function(rowIdx, tableLoop, rowLoop) {
1101 for (var c in DeviceColumns) {
1102 var col = DeviceColumns[c];
1104 if (!('kismetdrawfunc' in col)) {
1108 // Call the draw callback if one exists
1110 col.kismetdrawfunc(col, dt, this);
1116 for (var r in DeviceRowHighlights) {
1118 var rowh = DeviceRowHighlights[r];
1120 if (rowh['enable']) {
1121 if (rowh['selector'](this.data())) {
1122 $('td', this.node()).css('background-color', rowh['color']);
1136 device_dt = element.DataTable();
1137 // var dt_base_height = element.height();
1139 // $('div.viewselector').html("View picker");
1140 exports.BuildDeviceViewSelector($('div.viewselector'));
1142 // Restore the order
1143 var saved_order = kismet.getStorage('kismet.base.devicetable.order', "");
1144 if (saved_order !== "")
1145 device_dt.order(JSON.parse(saved_order));
1147 // Restore the search
1148 var saved_search = kismet.getStorage('kismet.base.devicetable.search', "");
1149 if (saved_search !== "")
1150 device_dt.search(JSON.parse(saved_search));
1152 // Set an onclick handler to spawn the device details dialog
1153 $('tbody', element).on('click', 'tr', function () {
1154 kismet_ui.DeviceDetailWindow(this.id);
1156 // Use the ID above we insert in the row creation, instead of looking in the
1158 // Fetch the data of the row that got clicked
1159 // var device_dt = element.DataTable();
1160 // var data = device_dt.row( this ).data();
1161 // var key = data['kismet.device.base.key'];
1162 // kismet_ui.DeviceDetailWindow(key);
1166 .on( 'mouseenter', 'td', function () {
1168 var device_dt = element.DataTable();
1170 if (typeof(device_dt.cell(this).index()) === 'Undefined')
1173 var colIdx = device_dt.cell(this).index().column;
1174 var rowIdx = device_dt.cell(this).index().row;
1176 // Remove from all cells
1177 $(device_dt.cells().nodes()).removeClass('kismet-highlight');
1178 // Highlight the td in this row
1179 $('td', device_dt.row(rowIdx).nodes()).addClass('kismet-highlight');
1189 exports.ResizeDeviceTable = function(element) {
1190 // console.log(element.height());
1191 // exports.ResetDeviceTable(element);
1194 exports.ResetDeviceTable = function(element) {
1195 CancelDeviceSummary();
1197 exports.InitializeDeviceTable(element);
1199 ScheduleDeviceSummary();
1202 kismet_ui_settings.AddSettingsPane({
1203 id: 'core_devicelist_columns',
1204 listTitle: 'Device List Columns',
1205 create: function(elem) {
1209 id: 'k-c-p-rowcontainer'
1212 var cols = exports.GetDeviceColumns(true);
1214 for (var ci in cols) {
1217 if (! c.user_selectable)
1222 class: 'k-c-p-column',
1227 class: 'k-c-p-c-mover fa fa-arrows-v'
1232 class: 'k-c-p-c-enable',
1237 id: 'k-c-p-c-enable'
1239 .on('change', function() {
1240 kismet_ui_settings.SettingsModified();
1246 class: 'k-c-p-c-name',
1248 .text(c.description)
1252 class: 'k-c-p-c-title',
1258 class: 'k-c-p-c-notes',
1259 id: 'k-c-p-c-notes',
1263 var notes = new Array;
1265 if (c.bVisible != false) {
1266 $('#k-c-p-c-enable', crow).prop('checked', true);
1269 if (c.bSortable != false) {
1270 notes.push("sortable");
1273 if (c.bSearchable != false) {
1274 notes.push("searchable");
1277 $('#k-c-p-c-notes', crow).html(notes.join(", "));
1279 rowcontainer.append(crow);
1286 .html('Drag and drop columns to re-order the device display table. Columns may also be shown or hidden individually.')
1291 class: 'k-c-p-header',
1295 class: 'k-c-p-c-mover fa fa-arrows-v',
1296 style: 'color: transparent !important',
1301 class: 'k-c-p-c-enable',
1311 class: 'k-c-p-c-name',
1313 .html('<i>Column</i>')
1317 class: 'k-c-p-c-title',
1319 .html('<i>Title</i>')
1323 class: 'k-c-p-c-notes',
1325 .html('<i>Info</i>')
1329 elem.append(rowcontainer);
1331 rowcontainer.sortable({
1332 change: function(event, ui) {
1333 kismet_ui_settings.SettingsModified();
1339 save: function(elem) {
1340 // Generate a config array of objects which defines the user config for
1341 // the datatable; save it; then kick the datatable redraw
1342 var col_defs = new Array();
1344 $('.k-c-p-column', elem).each(function(i, e) {
1346 id: $(this).attr('id'),
1347 enable: $('#k-c-p-c-enable', $(this)).is(':checked')
1351 kismet.putStorage('kismet.datatable.columns', col_defs);
1352 exports.ResetDeviceTable(devicetableElement);
1356 // Add the row highlighting
1357 kismet_ui_settings.AddSettingsPane({
1358 id: 'core_device_row_highlights',
1359 listTitle: 'Device Row Highlighting',
1360 create: function(elem) {
1371 .html('Device Row Highlights')
1375 id: "devicerow_table",
1393 .html("Description")
1400 $('#form', elem).on('change', function() {
1401 kismet_ui_settings.SettingsModified();
1404 for (var ri in DeviceRowHighlights) {
1405 var rh = DeviceRowHighlights[ri];
1409 .attr('hlname', rh['name'])
1415 class: "k-dt-enable",
1429 class: "k-dt-colorwidget"
1435 .html(rh['description'])
1438 $('#devicerow_table', elem).append(row);
1441 $('.k-dt-enable', row).prop('checked', true);
1444 $(".k-dt-colorwidget", row).spectrum({
1446 preferredFormat: "rgb",
1451 save: function(elem) {
1452 $('tr', elem).each(function() {
1453 kismet.putStorage('kismet.rowhighlight.color' + $(this).attr('hlname'), $('.k-dt-colorwidget', $(this)).val());
1455 kismet.putStorage('kismet.rowhighlight.enable' + $(this).attr('hlname'), $('.k-dt-enable', $(this)).is(':checked'));
1457 for (var ri in DeviceRowHighlights) {
1458 if (DeviceRowHighlights[ri]['name'] === $(this).attr('hlname')) {
1459 DeviceRowHighlights[ri]['color'] = $('.k-dt-colorwidget', $(this)).val();
1460 DeviceRowHighlights[ri]['enable'] = $('.k-dt-enable', $(this)).is(':checked');