2026-02-04 15:26:44 -05:00
{ #
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% import "forms.twig" as forms %}
{% block title %} {{ "DataSets" | trans }} | {% endblock %}
Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
2026-02-06 23:54:21 -05:00
{% block actionMenu %} {% endblock %}
2026-02-04 15:26:44 -05:00
{% block pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1> {% trans "DataSets" %} </h1>
<p class="text-muted"> {% trans "Manage structured data sources." %} </p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id=" {{ random ( ) }} " data-grid-name="dataSetView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title"> {% trans "Filter DataSets" %} </h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title=" {% trans 'Toggle filter panel' %} ">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline" onsubmit="return false">
{% set title %} {% trans "Name" %} {% endset %}
{{ inline .inputNameGrid ( 'dataSet' , title ) }}
{% set title %} {% trans "Code" %} {% endset %}
{% set helpText %} {% trans "Show items which match the provided code" %} {% endset %}
{{ inline .input ( "code" , title , "" , helpText ) }}
{% set title %} {% trans "Owner" %} {% endset %}
{% set helpText %} {% trans "Show items owned by the selected User." %} {% endset %}
{% set attributes = [
Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
2026-02-06 23:54:21 -05:00
{ name: "data-width", value: "100%" },
2026-02-04 15:26:44 -05:00
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline .dropdown ( "userId" , "single" , title , "" , null , "userId" , "userName" , helpText , "pagedSelect" , "" , "" , "" , attributes ) }}
{{ inline .hidden ( "folderId" ) }}
</form>
</div>
</div>
Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
2026-02-06 23:54:21 -05:00
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-card ots-folder-tree" id="grid-folder-filter">
2026-02-04 15:26:44 -05:00
<input id="jstree-search" class="form-control" type="text" placeholder=" {% trans "Search" %} ">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title=" {% trans "Search in all folders" %} "> {% trans "All Folders" %} </label>
</div>
<div class="folder-search-no-results d-none">
<p> {% trans 'No Folders matching the search term' %} </p>
</div>
<div id="container-folder-tree"></div>
</div>
Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
2026-02-06 23:54:21 -05:00
<div class="folder-controller d-none ots-grid-controller">
2026-02-04 15:26:44 -05:00
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title=" {% trans "Open / Close Folder Search options" %} "><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
2026-02-06 23:54:21 -05:00
<div class="ots-table-toolbar">
{% if currentUser .featureEnabled ( "dataset.add" ) %}
<button class="btn btn-sm btn-success XiboFormButton" title=" {% trans "Add a new DataSet" %} " href=" {{ url_for ( "dataSet.add.form" ) }} "><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add DataSet" %} </button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title=" {% trans "Refresh the Table" %} " href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
2026-02-04 15:26:44 -05:00
<table id="datasets" class="table table-striped" data-state-preference-name="dataSetGrid" style="width: 100%;">
<thead>
<tr>
<th> {% trans "ID" %} </th>
<th> {% trans "Name" %} </th>
<th> {% trans "Description" %} </th>
<th> {% trans "Code" %} </th>
<th> {% trans "Remote?" %} </th>
<th> {% trans "Real time?" %} </th>
<th> {% trans "Owner" %} </th>
<th> {% trans "Sharing" %} </th>
<th> {% trans "Last Sync" %} </th>
<th> {% trans "Data Last Modified" %} </th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce=" {{ cspNonce }} ">
var table;
$(document).ready(function() {
{% if not currentUser .featureEnabled ( "folder.view" ) %}
disableFolders();
{% endif %}
table = $("#datasets").DataTable( {
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 0, "asc"]],
ajax: {
"url": " {{ url_for ( "dataSet.search" ) }} ",
"data": function(d) {
$.extend(d, $("#datasets").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "dataSetId", responsivePriority: 2 },
{ "data": "dataSet", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
{ "data": "description", responsivePriority: 4 },
{ "data": "code", responsivePriority: 3 },
{
"data": "isRemote",
responsivePriority: 3,
"render": dataTableTickCrossColumn
},
{
data: 'isRealTime',
responsivePriority: 3,
render: dataTableTickCrossColumn,
},
{ "data": "owner", responsivePriority: 3 },
{
"data": "groupsWithPermissions",
responsivePriority: 3,
"render": dataTableCreatePermissions
},
{
"data": "lastSync",
responsivePriority: 4,
"render": dataTableDateFromUnix
},
{
"data": "lastDataEdit",
responsivePriority: 4,
"render": dataTableDateFromUnix
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', function(e, settings) {
dataTableDraw(e, settings);
// Upload form
$(".dataSetImportForm").click(function(e) {
e.preventDefault();
var template = Handlebars.compile($("#template-dataset-upload").html());
var data = table.row($(this).closest("tr")).data();
var columns = [];
var i = 1;
$.each(data.columns, function (index, element) {
if (element.dataSetColumnTypeId === 1) {
element.index = i;
columns.push(element);
i++;
}
});
// Handle bars and open a dialog
bootbox.dialog( {
message: template( {
trans: {
addFiles: " {% trans "Add CSV Files" %} ",
startUpload: " {% trans "Start upload" %} ",
cancelUpload: " {% trans "Cancel upload" %} ",
processing: " {% trans "Processing..." %} "
},
upload: {
maxSize: {{ libraryUpload .maxSize }} ,
maxSizeMessage: " {{ libraryUpload .maxSizeMessage }} ",
validExt: " {{ libraryUpload .validExt }} ",
utf8Message: " {% trans "If the CSV file contains non-ASCII characters please ensure the file is UTF-8 encoded" %} "
},
columns: columns
}),
title: " {% trans "CSV Import" %} ",
size: 'large',
buttons: {
main: {
label: " {% trans "Done" %} ",
className: "btn-primary btn-bb-main",
callback: function() {
table.ajax.reload();
XiboDialogClose();
}
}
}
}).on('shown.bs.modal', function() {
// Configure the upload form
var url = " {{ url_for ( "dataSet.import" , { id : ':id' } ) }} ".replace(":id", data.dataSetId);
var form = $(this).find("form");
var refreshSessionInterval;
// Initialize the jQuery File Upload widget:
form.fileupload( {
url: url,
disableImageResize: true
});
// Upload server status check for browsers with CORS support:
if ($.support.cors) {
$.ajax( {
url: url,
type: 'HEAD'
}).fail(function () {
$('<span class="alert alert-error"/>')
.text('Upload server currently unavailable - ' + new Date())
.appendTo(form);
});
}
// Enable iframe cross-domain access via redirect option:
form.fileupload(
'option',
'redirect',
window.location.href.replace(
/\/[^\/]*$/,
'/cors/result.html?%s'
)
);
form.bind('fileuploadsubmit', function (e, data) {
var inputs = data.context.find(':input');
if (inputs.filter('[required][value=""]').first().focus().length) {
return false;
}
data.formData = inputs.serializeArray().concat(form.serializeArray());
inputs.filter("input").prop("disabled", true);
}).bind('fileuploadstart', function (e, data) {
// Show progress data
form.find('.fileupload-progress .progress-extended').show();
form.find('.fileupload-progress .progress-end').hide();
if (form.fileupload("active") <= 0)
refreshSessionInterval = setInterval("XiboPing('" + pingUrl + "?refreshSession=true')", 1000 * 60 * 3);
return true;
}).bind('fileuploaddone', function (e, data) {
if (refreshSessionInterval != null && form.fileupload("active") <= 0)
clearInterval(refreshSessionInterval);
}).bind('fileuploadprogressall', function (e, data) {
// Hide progress data and show processing
if(data.total > 0 && data.loaded == data.total) {
form.find('.fileupload-progress .progress-extended').hide();
form.find('.fileupload-progress .progress-end').show();
}
}).bind('fileuploadadded fileuploadcompleted fileuploadfinished', function (e, data) {
// Get uploaded and downloaded files and toggle Done button
var filesToUploadCount = form.find('tr.template-upload').length;
var $button = form.parents('.modal:first').find('button.btn-bb-main');
if(filesToUploadCount == 0) {
$button.removeAttr('disabled');
} else {
$button.attr('disabled', 'disabled');
}
});
});
});
});
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#datasets_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
function dataSetFormOpen(dialog) {
// Bind the remote dataset test button
$(dialog).find("#dataSetRemoteTestButton").on('click', function() {
var $form = $(dialog).find("form");
XiboRemoteRequest(" {{ url_for ( "dataSet.test.remote" ) }} ", $form.serializeObject(), function(response) {
if (!response.success || !$.trim(response.data.entries)) {
response.data = response.message;
}
$("#datasetRemoteTestRequestResult").html('<pre style="height: 300px; overflow: scroll">' + JSON.stringify(response.data, null, 3) + '</pre>');
});
});
// Set up some dependencies between the isRemote checkbox and the tabs related to remote datasets
onRemoteFieldChanged(dialog);
// show data source dropdown if real time is checked
onIsRealTimeFieldChanged(dialog);
$(dialog).find("#isRemote").on('change', function() {
onRemoteFieldChanged(dialog);
});
$(dialog).find("#isRealTime").on('change', function() {
onIsRealTimeFieldChanged(dialog);
});
// Auth field
onAuthenticationFieldChanged(dialog);
$(dialog).find("#authentication").on('change', function() {
onAuthenticationFieldChanged(dialog);
});
// remote DataSet source
onSourceFieldChanged(dialog);
$(dialog).find('#sourceId').on('change', function() {
onSourceFieldChanged(dialog);
});
// Validate form manually because
// uri field depends on isRemote being checked
if (forms != undefined) {
const $form = $(dialog).find('form');
forms.validateForm(
$form, // form
$form.parent(), // container
{
submitHandler: XiboFormSubmit,
rules: {
uri: {
required: function(element) {
return $form.find('#isRemote').is(':checked')
},
},
},
},
);
}
}
function onIsRealTimeFieldChanged(dialog) {
var isRealTime = $(dialog).find("#isRealTime").is(":checked");
var dataSourceField = $(dialog).find("#dataSourceField");
var dataConnectorSource = $(dialog).find("#dataConnectorSource");
if (isRealTime) {
// show and enable data connector source
dataSourceField.removeClass("d-none");
dataConnectorSource.prop('disabled', false)
} else {
// hide and disable data connector source
dataSourceField.addClass("d-none");
dataConnectorSource.prop('disabled', true)
}
}
function onRemoteFieldChanged(dialog) {
var isRemote = $(dialog).find("#isRemote").is(":checked");
var $remoteTabs = $(dialog).find(".tabForRemoteDataSet");
if (isRemote) {
$remoteTabs.removeClass("d-none");
} else {
$remoteTabs.addClass("d-none");
}
}
function onAuthenticationFieldChanged(dialog) {
var authentication = $(dialog).find("#authentication").val();
var $authFieldUserName = $(dialog).find(".auth-field-username");
var $authFieldPassword = $(dialog).find(".auth-field-password");
if (authentication === "none") {
$authFieldUserName.addClass("d-none");
$authFieldPassword.addClass("d-none");
} else if (authentication === "bearer") {
$authFieldUserName.addClass("d-none");
$authFieldPassword.removeClass("d-none");
} else {
$authFieldUserName.removeClass("d-none");
$authFieldPassword.removeClass("d-none");
}
}
function onSourceFieldChanged(dialog) {
var sourceId = $(dialog).find('#sourceId').val();
var $jsonSource = $(dialog).find(".json-source-field");
var $csvSource = $(dialog).find(".csv-source-field");
if (sourceId == 1) {
$jsonSource.removeClass('d-none');
$csvSource.addClass('d-none');
} else {
$jsonSource.addClass('d-none');
$csvSource.removeClass('d-none');
}
}
function deleteMultiSelectFormOpen(dialog) {
{% set message = 'Delete any associated data?' %}
var $input = $('<input type=checkbox id="deleteData" name="deleteData"> {{ message | trans | e }} </input>');
$input.on('change', function() {
dialog.data().commitData = { deleteData: $(this).val()};
});
$(dialog).find('.modal-body').append($input);
}
</script>
{% endblock %}
{% block javaScriptTemplates %}
{{ parent ( ) }}
{% verbatim %}
<script type="text/x-handlebars-template" id="template-dataset-upload">
<form class="form-horizontal" method="post" enctype="multipart/form-data" data-max-file-size=" {{ upload .maxSize }} " data-accept-file-types="/(\.|\/)csv/i">
<div class="row fileupload-buttonbar">
<div class="card p-3 mb-3 bg-light">
{{ upload .maxSizeMessage }} <br>
{{ upload .utf8Message }}
</div>
<div class="col-md-7">
<!-- The fileinput-button span is used to style the file input field as button -->
<span class="btn btn-success fileinput-button">
<i class="fa fa-plus"></i>
<span> {{ trans .addFiles }} </span>
<input type="file" name="files">
</span>
<button type="submit" class="btn btn-primary start">
<i class="fa fa-upload"></i>
<span> {{ trans .startUpload }} </span>
</button>
<button type="reset" class="btn btn-warning cancel">
<i class="fa fa-ban"></i>
<span> {{ trans .cancelUpload }} </span>
</button>
<!-- The loading indicator is shown during file processing -->
<span class="fileupload-loading"></span>
</div>
<!-- The global progress information -->
<div class="col-md-4 fileupload-progress fade">
<!-- The global progress bar -->
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
<div class="sr-only"></div>
</div>
</div>
<!-- The extended global progress information -->
<div class="progress-extended"> </div>
<!-- Processing info container -->
<div class="progress-end" style="display:none;"> {{ trans .processing }} </div>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% endverbatim %}
{% set title %} {% trans "Overwrite existing data?" %} {% endset %}
{% set helpText %} {% trans "Erase all content in this DataSet and overwrite it with the new content in this import." %} {% endset %}
{{ forms .checkbox ( "overwrite" , title , "" , helpText ) }}
{% set title %} {% trans "Ignore first row?" %} {% endset %}
{% set helpText %} {% trans "Ignore the first row? Useful if the CSV has headings." %} {% endset %}
{{ forms .checkbox ( "ignorefirstrow" , title , "" , helpText ) }}
{% set message %} {% trans "In the fields below please enter the column number in the CSV file that corresponds to the Column Heading listed. This should be done before Adding the file." %} {% endset %}
{{ forms .message ( message ) }}
{% verbatim %}
{{ # each columns }}
<div class="form-group row">
<label class="col-sm-2 control-label" for="csvImport_ {{ dataSetColumnId }} "> {{ heading }} </label>
<div class="col-sm-10">
<input class="form-control" name="csvImport_ {{ dataSetColumnId }} " type="number" id="csvImport_ {{ dataSetColumnId }} " value=" {{ index }} " />
</div>
</div>
{{ / each }}
</div>
</div>
<!-- The table listing the files available for upload/download -->
<table role="presentation" class="table table-striped"><tbody class="files"></tbody></table>
</form>
</script>
<!-- The template to display files available for upload -->
<script id="template-dataset-upload" type="text/x-tmpl">
{% for ( var i = 0 , file ; file = o .files [ i ] ; i + + ) { %}
<tr class="template-upload">
<td>
<span class="fileupload-preview"></span>
</td>
<td class="title">
{% if ( file .error ) { %}
<div><span class="label label-danger"> { %=file.error%}</span></div>
{ % } %}
{% if ( ! file .error ) { %}
<label for="name[]"><input name="name[]" type="text" id="name" value="" /></label>
{ % } %}
</td>
<td>
<p class="size"> { %=o.formatFileSize(file.size)%}</p>
{% if ( ! o .files .error ) { %}
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
<div class="sr-only"></div>
</div>
</div>
</div>
{ % } %}
</td>
<td class="btn-group">
{% if ( ! o .files .error & & ! i & & ! o .options .autoUpload ) { %}
<button class="btn btn-primary start">
<i class="fa fa-upload"></i>
</button>
{ % } %}
{% if ( ! i ) { %}
<button class="btn btn-warning cancel">
<i class="fa fa-ban"></i>
</button>
{ % } %}
</td>
</tr>
{ % } %}
</script>
<!-- The template to display files available for download -->
<script id="template-dataset-download" type="text/x-tmpl">
{% for ( var i = 0 , file ; file = o .files [ i ] ; i + + ) { %}
<tr class="template-download">
<td>
<p class="name" id=" { %=file.storedas%}" status=" {% if ( file .error ) { %} error { % } %}">
{ %=file.name%}
</p>
{% if ( file .error ) { %}
<div><span class="label label-danger"> { %=file.error%}</span></div>
{ % } %}
</td>
<td>
<span class="size"> { %=o.formatFileSize(file.size)%}</span>
</td>
</tr>
{ % } %}
</script>
{% endverbatim %}
</div>
</div>
</div>
{% endblock %}