add support of table filter controls (#101)

This commit is contained in:
Evgenii Alekseev 2023-07-21 02:10:26 +03:00 committed by GitHub
parent b7852f55c8
commit 6b3fc3a6a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 50 deletions

View File

@ -63,6 +63,8 @@
<table id="packages" class="table table-striped table-hover" <table id="packages" class="table table-striped table-hover"
data-export-options='{"fileName": "packages"}' data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
data-page-list="[10, 25, 50, 100, all]" data-page-list="[10, 25, 50, 100, all]"
data-page-size="10" data-page-size="10"
data-pagination="true" data-pagination="true"
@ -72,6 +74,7 @@
data-show-columns-search="true" data-show-columns-search="true"
data-show-columns-toggle-all="true" data-show-columns-toggle-all="true"
data-show-export="true" data-show-export="true"
data-show-filter-control-switch="true"
data-show-fullscreen="true" data-show-fullscreen="true"
data-show-search-clear-button="true" data-show-search-clear-button="true"
data-sortable="true" data-sortable="true"
@ -82,14 +85,14 @@
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-checkbox="true"></th> <th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="base">package base</th> <th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th>
<th data-sortable="true" data-field="version">version</th> <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-field="packages">packages</th> <th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th>
<th data-sortable="true" data-visible="false" data-field="groups">groups</th> <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th> <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
<th data-sortable="true" data-visible="false" data-field="packager">packager</th> <th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th>
<th data-sortable="true" data-field="timestamp">last update</th> <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th> <th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
</tr> </tr>
</thead> </thead>
</table> </table>

View File

@ -84,7 +84,7 @@
showSuccess("Success", `Key ${key} has been imported`); showSuccess("Success", `Key ${key} has been imported`);
}, },
error: (jqXHR, _, errorThrown) => { error: (jqXHR, _, errorThrown) => {
const message = _ => { return `Could not import key ${key} from ${server}`; }; const message = _ => `Could not import key ${key} from ${server}`;
showFailure("Action failed", message, jqXHR, errorThrown); showFailure("Action failed", message, jqXHR, errorThrown);
}, },
}); });

View File

@ -60,8 +60,8 @@
const packages = packageInput.val(); const packages = packageInput.val();
if (packages) { if (packages) {
packageAddModal.modal("hide"); packageAddModal.modal("hide");
const onSuccess = update => { return `Packages ${update} have been added`; }; const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => { return `Package addition failed: ${error}`; }; const onFailure = error => `Package addition failed: ${error}`;
doPackageAction("/api/v1/service/add", [packages], onSuccess, onFailure); doPackageAction("/api/v1/service/add", [packages], onSuccess, onFailure);
} }
} }
@ -70,8 +70,8 @@
const packages = packageInput.val(); const packages = packageInput.val();
if (packages) { if (packages) {
packageAddModal.modal("hide"); packageAddModal.modal("hide");
const onSuccess = update => { return `Packages ${update} have been requested`; }; const onSuccess = update => `Packages ${update} have been requested`;
const onFailure = error => { return `Package request failed: ${error}`; }; const onFailure = error => `Package request failed: ${error}`;
doPackageAction("/api/v1/service/request", [packages], onSuccess, onFailure); doPackageAction("/api/v1/service/request", [packages], onSuccess, onFailure);
} }
} }

View File

@ -61,7 +61,7 @@
error: (jqXHR, _, errorThrown) => { error: (jqXHR, _, errorThrown) => {
// show failed modal in case if first time loading // show failed modal in case if first time loading
if (isPackageBaseSet) { if (isPackageBaseSet) {
const message = error => { return `Could not load package ${packageBase} logs: ${error}`; }; const message = error => `Could not load package ${packageBase} logs: ${error}`;
showFailure("Load failure", message, jqXHR, errorThrown); showFailure("Load failure", message, jqXHR, errorThrown);
} }
}, },

View File

@ -33,8 +33,8 @@
const packages = dependencyInput.val(); const packages = dependencyInput.val();
if (packages) { if (packages) {
packageRebuildModal.modal("hide"); packageRebuildModal.modal("hide");
const onSuccess = update => { return `Repository rebuild has been run for packages which depend on ${update}`; }; const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
const onFailure = error => { return `Repository rebuild failed: ${error}`; }; const onFailure = error => `Repository rebuild failed: ${error}`;
doPackageAction("/api/v1/service/rebuild", [packages], onSuccess, onFailure); doPackageAction("/api/v1/service/rebuild", [packages], onSuccess, onFailure);
} }
} }

View File

@ -15,6 +15,25 @@
table.bootstrapTable(method, {field: "id", values: [data.id]}); table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showLogs(data.id); } else showLogs(data.id);
}); });
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
const repositoryBadge = $("#badge-repository"); const repositoryBadge = $("#badge-repository");
const statusBadge = $("#badge-status"); const statusBadge = $("#badge-status");
@ -37,21 +56,21 @@
} }
function getSelection() { function getSelection() {
return table.bootstrapTable("getSelections").map(row => { return row.id; }); return table.bootstrapTable("getSelections").map(row => row.id);
} }
function removePackages() { function removePackages() {
const onSuccess = update => { return `Packages ${update} have been removed`; }; const onSuccess = update => `Packages ${update} have been removed`;
const onFailure = error => { return `Could not remove packages: ${error}`; }; const onFailure = error => `Could not remove packages: ${error}`;
doPackageAction("/api/v1/service/remove", getSelection(), onSuccess, onFailure); doPackageAction("/api/v1/service/remove", getSelection(), onSuccess, onFailure);
} }
function updatePackages() { function updatePackages() {
const currentSelection = getSelection(); const currentSelection = getSelection();
const [url, onSuccess] = currentSelection.length === 0 const [url, onSuccess] = currentSelection.length === 0
? ["/api/v1/service/update", _ => { return "Repository update has been run"; }] ? ["/api/v1/service/update", _ => "Repository update has been run"]
: ["/api/v1/service/add", update => { return `Run update for packages ${update}`; }]; : ["/api/v1/service/add", update => `Run update for packages ${update}`];
const onFailure = error => { return `Packages update failed: ${error}`; }; const onFailure = error => `Packages update failed: ${error}`;
doPackageAction(url, currentSelection, onSuccess, onFailure); doPackageAction(url, currentSelection, onSuccess, onFailure);
} }
@ -81,13 +100,13 @@
success: response => { success: response => {
const extractListProperties = (description, property) => { const extractListProperties = (description, property) => {
return Object.values(description.packages) return Object.values(description.packages)
.map(pkg => { return pkg[property]; }) .map(pkg => pkg[property])
.reduce((left, right) => { return left.concat(right); }, []); .reduce((left, right) => left.concat(right), []);
}; };
const listToTable = data => { const listToTable = data => {
return Array.from(new Set(data)) return Array.from(new Set(data))
.sort() .sort()
.map(entry => { return safe(entry); }) .map(entry => safe(entry))
.join("<br>"); .join("<br>");
}; };
@ -121,7 +140,7 @@
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
} else { } else {
// other errors // other errors
const message = error => { return `Could not load list of packages: ${error}`; }; const message = error => `Could not load list of packages: ${error}`;
showFailure("Load failure", message, jqXHR, errorThrown); showFailure("Load failure", message, jqXHR, errorThrown);
} }
hideControls(true); hideControls(true);
@ -158,6 +177,18 @@
return {classes: cellClass(value)}; return {classes: cellClass(value)};
} }
function filterListGroups() {
return extractDataList(table.bootstrapTable("getData"), "groups");
}
function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses");
}
function filterListPackagers() {
return extractDataList(table.bootstrapTable("getData"), "packager");
}
$(() => { $(() => {
table.bootstrapTable({}); table.bootstrapTable({});
statusBadge.popover(); statusBadge.popover();

View File

@ -31,6 +31,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<div class="container"> <div class="container">
<table id="packages" class="table table-striped table-hover" <table id="packages" class="table table-striped table-hover"
data-export-options='{"fileName": "packages"}' data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
data-page-list="[10, 25, 50, 100, all]" data-page-list="[10, 25, 50, 100, all]"
data-page-size="10" data-page-size="10"
data-pagination="true" data-pagination="true"
@ -40,6 +42,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-columns-search="true" data-show-columns-search="true"
data-show-columns-toggle-all="true" data-show-columns-toggle-all="true"
data-show-export="true" data-show-export="true"
data-show-filter-control-switch="true"
data-show-fullscreen="true" data-show-fullscreen="true"
data-show-search-clear-button="true" data-show-search-clear-button="true"
data-sortable="true" data-sortable="true"
@ -48,17 +51,17 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-toggle="table"> data-toggle="table">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-sortable="true" data-switchable="false">package</th> <th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
<th data-sortable="true">version</th> <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-visible="false">architecture</th> <th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
<th data-sortable="true" data-visible="false">description</th> <th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
<th data-sortable="true" data-visible="false">upstream url</th> <th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
<th data-sortable="true" data-visible="false">licenses</th> <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
<th data-sortable="true" data-visible="false">groups</th> <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
<th data-sortable="true" data-visible="false">depends</th> <th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
<th data-sortable="true">archive size</th> <th data-sortable="true" data-field="archive_size">archive size</th>
<th data-sortable="true">installed size</th> <th data-sortable="true" data-field="installed_size">installed size</th>
<th data-sortable="true">build date</th> <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
</tr> </tr>
</thead> </thead>
@ -96,6 +99,27 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</div> </div>
<script> <script>
const table = $("#packages");
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
const pacmanConf = $("#pacman-conf"); const pacmanConf = $("#pacman-conf");
const pacmanConfCopyButton = $("#copy-btn"); const pacmanConfCopyButton = $("#copy-btn");
@ -103,6 +127,18 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
const conf = pacmanConf.text(); const conf = pacmanConf.text();
await copyToClipboard(conf, pacmanConfCopyButton); await copyToClipboard(conf, pacmanConfCopyButton);
} }
function filterListDepends() {
return extractDataList(table.bootstrapTable("getData"), "depends");
}
function filterListGroups() {
return extractDataList(table.bootstrapTable("getData"), "groups");
}
function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses");
}
</script> </script>
</body> </body>

View File

@ -1,16 +1,21 @@
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js" integrity="sha384-NXgwF8Kv9SSAr+jemKKcbvQsz+teULH/a5UNJvZc6kP47hZgl62M1vGnw6gHQhb1" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-u4eJN1VWrTf/FnYYQJo2kqJyVxEQf5UmWY4iUcNAoLenOEtEuCkfwc5bKvZOWBi5" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.min.js" integrity="sha384-1Rz4Kz/y1rSWw+ZsjTcxB684XgofbO8iizY+UFIzCwFeQ+QUyhBNWBMh/STOyomI" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" integrity="sha384-IazMVNyYoUNx6357fWJoqtHYUWWCNHIXxFVtbpVgvImQNWuRP2WbHPaIb3QF8j97" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.min.js" integrity="sha384-IDwe1+LCz02ROU9k972gdyvl+AESN10+x7tBKgc9I5HFtuNz0wWnPclzo6p9vxnk" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/export/bootstrap-table-export.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" integrity="sha384-GVLHfbEvuGA/RFiQ3MK2ClEJkWYJXABg55t9LpoDPZFGIsSq8xhFlQydm5poV2jW" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-jeldDadm+qM2RwGER3qVqxFgWVpAEJ7Jie+0rlYj8ni3KkQA654T8TSXDtol022X" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-B6xNXlSOaOFxjlKo9OW3htbox+9/DcaEcjPPEi1+pTMwH5Tzc/s2wNTYriHz7Tb8" crossorigin="anonymous" type="application/javascript"></script>
<script> <script>
async function copyToClipboard(text, button) { async function copyToClipboard(text, button) {
@ -38,4 +43,28 @@
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;"); .replace(/"/g, "&quot;");
} }
function extractDataList(data, column) {
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
return Array.from(new Set(elements)).sort();
}
function filterContains(text, value) {
return value.includes(text.toLowerCase().trim());
}
function filterDateRange(text, value) {
const asOfStartOfDay = date => date.setUTCHours(0, 0, 0, 0);
const [minDate, maxDate] = text.split(" - ");
const buildDate = asOfStartOfDay(new Date(value));
return (buildDate >= asOfStartOfDay(new Date(minDate))) && (buildDate <= asOfStartOfDay(new Date(maxDate)));
}
function filterList(index, value, field, data) {
const dataList = extractDataList(data, field);
// the library removes all symbols from string, so it is just string
return value.includes(dataList[index].toLowerCase());
}
</script> </script>

View File

@ -1,11 +1,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css" integrity="sha384-LrVLJJYk9OiJmjNDakUBU7kS9qCT8wk1j2OU7ncpsfB3QS37UPdkCuq3ZD1MugNY" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.1/dist/bootstrap-table.min.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/bootstrap-table.min.css" integrity="sha384-pTEAhytv7JmEG2D7qiW5gY0lI5EKZ9n3CNmj6Qp+U3qhnmH2qnnN9KJbVwbtMHN0" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" integrity="sha384-1sLxvR8mXzjhvFY9f8mzXl97DNLepeZ0PnRiMMdm/rQsKjsrPZPJxYle2wwT2PMg" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.2.2/dist/cosmo/bootstrap.min.css" integrity="sha256-5t++JZpgVLzo9vF7snO5Qw0y3fA5/NkoJENWB7kpg0E=" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.2.3/dist/cosmo/bootstrap.min.css" integrity="sha384-P1PBFVifKf1Ww0gS5B8A0siIeDpcFd4uU7S68LA1XMdE0R+y1WN3DR4HcLc9csRC" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" integrity="sha384-zLkQsiLfAQqGeIJeKLC+rcCR1YoYaQFLCL7cLDUoKE1ajKJzySpjzWGfYS2vjSG+" crossorigin="anonymous" type="text/css">
<style> <style>
.pre-scrollable { .pre-scrollable {