mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
Multiselect user filters (#2090)
* Extract user filter generation code into method * Extend make_user_cond to allow lists of user IDs * Update documentation for stats APIs to indicate handling of ID lists * Use multiselect dropdown for user filter on graphs page Use standard concatenation Fix select style Move settings to JS constructor Change text for no users checked Don't call selectAll on page init Add it back Remove attributes Fix emptiness check Allow deselect all Only refresh if user id changed * Show "N users" starting at 2 users Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> * Use helper function split_strip Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> * Move make_user_cond at bottom and make private * Add new user picker to history page * Fix copy-paste error * Again * Add CSS for bootstrap-select --------- Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
This commit is contained in:
parent
b144e6527f
commit
343a3e9281
7 changed files with 125 additions and 82 deletions
6
data/interfaces/default/css/bootstrap-select.min.css
vendored
Normal file
6
data/interfaces/default/css/bootstrap-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -2914,7 +2914,7 @@ a .home-platforms-list-cover-face:hover
|
|||
margin-bottom: -20px;
|
||||
width: 100%;
|
||||
max-width: 1750px;
|
||||
overflow: hidden;
|
||||
display: flow-root;
|
||||
}
|
||||
.table-card-back td {
|
||||
font-size: 12px;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<%inherit file="base.html"/>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="${http_root}css/bootstrap-select.min.css">
|
||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.min.css">
|
||||
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||
</%def>
|
||||
|
@ -14,9 +15,7 @@
|
|||
<div class="button-bar">
|
||||
<div class="btn-group" id="user-selection">
|
||||
<label>
|
||||
<select name="graph-user" id="graph-user" class="btn" style="color: inherit;">
|
||||
<option value="">All Users</option>
|
||||
<option disabled>────────────</option>
|
||||
<select name="graph-user" id="graph-user" multiple>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -225,6 +224,7 @@
|
|||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="${http_root}js/bootstrap-select.min.js"></script>
|
||||
<script src="${http_root}js/highcharts.min.js"></script>
|
||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
|
@ -373,14 +373,35 @@
|
|||
type: 'get',
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
var select = $('#graph-user');
|
||||
let select = $('#graph-user');
|
||||
let by_id = {};
|
||||
data.sort(function(a, b) {
|
||||
return a.friendly_name.localeCompare(b.friendly_name);
|
||||
});
|
||||
data.forEach(function(item) {
|
||||
select.append('<option value="' + item.user_id + '">' +
|
||||
item.friendly_name + '</option>');
|
||||
by_id[item.user_id] = item.friendly_name;
|
||||
});
|
||||
select.selectpicker({
|
||||
countSelectedText: function(sel, total) {
|
||||
if (sel === 0 || sel === total) {
|
||||
return 'All users';
|
||||
} else if (sel > 1) {
|
||||
return sel + ' users';
|
||||
} else {
|
||||
return select.val().map(function(id) {
|
||||
return by_id[id];
|
||||
}).join(', ');
|
||||
}
|
||||
},
|
||||
style: 'btn-dark',
|
||||
actionsBox: true,
|
||||
selectedTextFormat: 'count',
|
||||
noneSelectedText: 'All users'
|
||||
});
|
||||
select.selectpicker('render');
|
||||
select.selectpicker('selectAll');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -602,11 +623,6 @@
|
|||
$('#nav-tabs-total').tab('show');
|
||||
}
|
||||
|
||||
// Set initial state
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
|
||||
// Tab1 opened
|
||||
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
|
@ -652,9 +668,20 @@
|
|||
$('.months').text(current_month_range);
|
||||
});
|
||||
|
||||
let graph_user_last_id = undefined;
|
||||
|
||||
// User changed
|
||||
$('#graph-user').on('change', function() {
|
||||
selected_user_id = $(this).val() || null;
|
||||
let val = $(this).val();
|
||||
if (val.length === 0 || val.length === $(this).children().length) {
|
||||
selected_user_id = null; // if all users are selected, just send an empty list
|
||||
} else {
|
||||
selected_user_id = val.join(",");
|
||||
}
|
||||
if (selected_user_id === graph_user_last_id) {
|
||||
return;
|
||||
}
|
||||
graph_user_last_id = selected_user_id;
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<%inherit file="base.html"/>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="${http_root}css/bootstrap-select.min.css">
|
||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.min.css">
|
||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||
|
@ -31,9 +32,7 @@
|
|||
% if _session['user_group'] == 'admin':
|
||||
<div class="btn-group" id="user-selection">
|
||||
<label>
|
||||
<select name="history-user" id="history-user" class="btn" style="color: inherit;">
|
||||
<option value="">All Users</option>
|
||||
<option disabled>────────────</option>
|
||||
<select name="history-user" id="history-user" multiple>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -121,6 +120,7 @@
|
|||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="${http_root}js/bootstrap-select.min.js"></script>
|
||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
|
@ -134,17 +134,40 @@
|
|||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
var select = $('#history-user');
|
||||
let select = $('#history-user');
|
||||
let by_id = {};
|
||||
data.sort(function (a, b) {
|
||||
return a.friendly_name.localeCompare(b.friendly_name);
|
||||
});
|
||||
data.forEach(function (item) {
|
||||
select.append('<option value="' + item.user_id + '">' +
|
||||
item.friendly_name + '</option>');
|
||||
by_id[item.user_id] = item.friendly_name;
|
||||
});
|
||||
select.selectpicker({
|
||||
countSelectedText: function(sel, total) {
|
||||
if (sel === 0 || sel === total) {
|
||||
return 'All users';
|
||||
} else if (sel > 1) {
|
||||
return sel + ' users';
|
||||
} else {
|
||||
return select.val().map(function(id) {
|
||||
return by_id[id];
|
||||
}).join(', ');
|
||||
}
|
||||
},
|
||||
style: 'btn-dark',
|
||||
actionsBox: true,
|
||||
selectedTextFormat: 'count',
|
||||
noneSelectedText: 'All users'
|
||||
});
|
||||
select.selectpicker('render');
|
||||
select.selectpicker('selectAll');
|
||||
}
|
||||
});
|
||||
|
||||
let history_user_last_id = undefined;
|
||||
|
||||
function loadHistoryTable(media_type, transcode_decision, selected_user_id) {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
|
@ -187,7 +210,16 @@
|
|||
});
|
||||
|
||||
$('#history-user').on('change', function () {
|
||||
selected_user_id = $(this).val() || null;
|
||||
let val = $(this).val();
|
||||
if (val.length === 0 || val.length === $(this).children().length) {
|
||||
selected_user_id = null; // if all users are selected, just send an empty list
|
||||
} else {
|
||||
selected_user_id = val.join(",");
|
||||
}
|
||||
if (selected_user_id === history_user_last_id) {
|
||||
return;
|
||||
}
|
||||
history_user_last_id = selected_user_id;
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
|
9
data/interfaces/default/js/bootstrap-select.min.js
vendored
Normal file
9
data/interfaces/default/js/bootstrap-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -51,11 +51,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -171,11 +167,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = "AND session_history.user_id = %s " % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = "AND session_history.user_id = %s " % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -308,11 +300,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -427,11 +415,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 12
|
||||
timestamp = arrow.get(helpers.timestamp()).shift(months=-time_range).floor('month').timestamp()
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -554,11 +538,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -653,11 +633,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -763,11 +739,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -860,11 +832,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -941,11 +909,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -1048,11 +1012,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -1128,11 +1088,7 @@ class Graphs(object):
|
|||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id and user_id.isdigit():
|
||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||
user_cond = self._make_user_cond(user_id)
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
@ -1212,3 +1168,16 @@ class Graphs(object):
|
|||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
|
||||
return output
|
||||
|
||||
def _make_user_cond(self, user_id):
|
||||
"""
|
||||
Expects user_id to be a comma-separated list of ints.
|
||||
"""
|
||||
user_cond = ''
|
||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||
elif user_id:
|
||||
user_ids = helpers.split_strip(user_id)
|
||||
if all(id.isdigit() for id in user_ids):
|
||||
user_cond = 'AND session_history.user_id IN (%s) ' % ','.join(user_ids)
|
||||
return user_cond
|
||||
|
|
|
@ -2258,7 +2258,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2302,7 +2302,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2346,7 +2346,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2390,7 +2390,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of months of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2434,7 +2434,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2478,7 +2478,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2522,7 +2522,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2565,7 +2565,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2608,7 +2608,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2651,7 +2651,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
@ -2694,7 +2694,7 @@ class WebInterface(object):
|
|||
Optional parameters:
|
||||
time_range (str): The number of days of data to return
|
||||
y_axis (str): "plays" or "duration"
|
||||
user_id (str): The user id to filter the data
|
||||
user_id (str): Comma separated list of user id to filter the data
|
||||
grouping (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue