geforkt von mirrored/vaultwarden
Merge pull request #3288 from BlackDex/admin-interface-updates
Some Admin Interface updates
Dieser Commit ist enthalten in:
Commit
4556f668de
13 geänderte Dateien mit 199 neuen und 64 gelöschten Zeilen
|
@ -300,8 +300,9 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
|
async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
|
||||||
let mut users_json = Vec::new();
|
let users = User::get_all(&mut conn).await;
|
||||||
for u in User::get_all(&mut conn).await {
|
let mut users_json = Vec::with_capacity(users.len());
|
||||||
|
for u in users {
|
||||||
let mut usr = u.to_json(&mut conn).await;
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
usr["UserEnabled"] = json!(u.enabled);
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
|
@ -313,8 +314,9 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
|
||||||
|
|
||||||
#[get("/users/overview")]
|
#[get("/users/overview")]
|
||||||
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let mut users_json = Vec::new();
|
let users = User::get_all(&mut conn).await;
|
||||||
for u in User::get_all(&mut conn).await {
|
let mut users_json = Vec::with_capacity(users.len());
|
||||||
|
for u in users {
|
||||||
let mut usr = u.to_json(&mut conn).await;
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await);
|
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await);
|
||||||
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await);
|
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await);
|
||||||
|
@ -490,11 +492,15 @@ async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyRes
|
||||||
|
|
||||||
#[get("/organizations/overview")]
|
#[get("/organizations/overview")]
|
||||||
async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let mut organizations_json = Vec::new();
|
let organizations = Organization::get_all(&mut conn).await;
|
||||||
for o in Organization::get_all(&mut conn).await {
|
let mut organizations_json = Vec::with_capacity(organizations.len());
|
||||||
|
for o in organizations {
|
||||||
let mut org = o.to_json();
|
let mut org = o.to_json();
|
||||||
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
|
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
|
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
|
||||||
|
org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await);
|
||||||
|
org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await);
|
||||||
|
org["event_count"] = json!(Event::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await);
|
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32));
|
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32));
|
||||||
organizations_json.push(org);
|
organizations_json.push(org);
|
||||||
|
@ -525,10 +531,20 @@ struct GitCommit {
|
||||||
sha: String,
|
sha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
#[derive(Deserialize)]
|
||||||
let github_api = get_reqwest_client();
|
struct TimeApi {
|
||||||
|
year: u16,
|
||||||
|
month: u8,
|
||||||
|
day: u8,
|
||||||
|
hour: u8,
|
||||||
|
minute: u8,
|
||||||
|
seconds: u8,
|
||||||
|
}
|
||||||
|
|
||||||
Ok(github_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
|
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
|
let json_api = get_reqwest_client();
|
||||||
|
|
||||||
|
Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn has_http_access() -> bool {
|
async fn has_http_access() -> bool {
|
||||||
|
@ -548,14 +564,13 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) ->
|
||||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
if has_http_access {
|
if has_http_access {
|
||||||
(
|
(
|
||||||
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest")
|
match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest")
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => r.tag_name,
|
Ok(r) => r.tag_name,
|
||||||
_ => "-".to_string(),
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await
|
match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await {
|
||||||
{
|
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
c.sha.truncate(8);
|
c.sha.truncate(8);
|
||||||
c.sha
|
c.sha
|
||||||
|
@ -567,7 +582,7 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) ->
|
||||||
if running_within_docker {
|
if running_within_docker {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
} else {
|
} else {
|
||||||
match get_github_api::<GitRelease>(
|
match get_json_api::<GitRelease>(
|
||||||
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -582,6 +597,24 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_ntp_time(has_http_access: bool) -> String {
|
||||||
|
if has_http_access {
|
||||||
|
if let Ok(ntp_time) = get_json_api::<TimeApi>("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await
|
||||||
|
{
|
||||||
|
return format!(
|
||||||
|
"{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC",
|
||||||
|
year = ntp_time.year,
|
||||||
|
month = ntp_time.month,
|
||||||
|
day = ntp_time.day,
|
||||||
|
hour = ntp_time.hour,
|
||||||
|
minute = ntp_time.minute,
|
||||||
|
seconds = ntp_time.seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::from("Unable to fetch NTP time.")
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
#[get("/diagnostics")]
|
||||||
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
|
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
@ -610,7 +643,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||||
// Check if we are able to resolve DNS entries
|
// Check if we are able to resolve DNS entries
|
||||||
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
||||||
Ok(Some(a)) => a.ip().to_string(),
|
Ok(Some(a)) => a.ip().to_string(),
|
||||||
_ => "Could not resolve domain name.".to_string(),
|
_ => "Unable to resolve domain name.".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (latest_release, latest_commit, latest_web_build) =
|
let (latest_release, latest_commit, latest_web_build) =
|
||||||
|
@ -644,7 +677,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||||
"host_arch": std::env::consts::ARCH,
|
"host_arch": std::env::consts::ARCH,
|
||||||
"host_os": std::env::consts::OS,
|
"host_os": std::env::consts::OS,
|
||||||
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
||||||
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
|
||||||
|
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference
|
||||||
});
|
});
|
||||||
|
|
||||||
let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?;
|
let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?;
|
||||||
|
|
|
@ -234,6 +234,17 @@ impl Collection {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
collections::table
|
||||||
|
.filter(collections::org_uuid.eq(org_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
collections::table
|
collections::table
|
||||||
|
|
|
@ -263,6 +263,17 @@ impl Event {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
event::table
|
||||||
|
.filter(event::org_uuid.eq(org_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_org_and_user_org(
|
pub async fn find_by_org_and_user_org(
|
||||||
org_uuid: &str,
|
org_uuid: &str,
|
||||||
user_org_uuid: &str,
|
user_org_uuid: &str,
|
||||||
|
|
|
@ -168,6 +168,17 @@ impl Group {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
groups::table
|
||||||
|
.filter(groups::organizations_uuid.eq(organizations_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
groups::table
|
groups::table
|
||||||
|
|
6
src/static/scripts/admin.css
gevendort
6
src/static/scripts/admin.css
gevendort
|
@ -25,10 +25,14 @@ img {
|
||||||
min-width: 85px;
|
min-width: 85px;
|
||||||
max-width: 85px;
|
max-width: 85px;
|
||||||
}
|
}
|
||||||
#users-table .vw-items, #orgs-table .vw-items, #orgs-table .vw-users {
|
#users-table .vw-ciphers, #orgs-table .vw-users, #orgs-table .vw-ciphers {
|
||||||
min-width: 35px;
|
min-width: 35px;
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
}
|
}
|
||||||
|
#orgs-table .vw-misc {
|
||||||
|
min-width: 65px;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
#users-table .vw-attachments, #orgs-table .vw-attachments {
|
#users-table .vw-attachments, #orgs-table .vw-attachments {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
|
|
32
src/static/scripts/admin_diagnostics.js
gevendort
32
src/static/scripts/admin_diagnostics.js
gevendort
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
var dnsCheck = false;
|
var dnsCheck = false;
|
||||||
var timeCheck = false;
|
var timeCheck = false;
|
||||||
|
var ntpTimeCheck = false;
|
||||||
var domainCheck = false;
|
var domainCheck = false;
|
||||||
var httpsCheck = false;
|
var httpsCheck = false;
|
||||||
|
|
||||||
|
@ -90,7 +91,8 @@ async function generateSupportString(event, dj) {
|
||||||
supportString += `* Internet access: ${dj.has_http_access}\n`;
|
supportString += `* Internet access: ${dj.has_http_access}\n`;
|
||||||
supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
|
supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
|
||||||
supportString += `* DNS Check: ${dnsCheck}\n`;
|
supportString += `* DNS Check: ${dnsCheck}\n`;
|
||||||
supportString += `* Time Check: ${timeCheck}\n`;
|
supportString += `* Browser/Server Time Check: ${timeCheck}\n`;
|
||||||
|
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
|
||||||
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
||||||
supportString += `* HTTPS Check: ${httpsCheck}\n`;
|
supportString += `* HTTPS Check: ${httpsCheck}\n`;
|
||||||
supportString += `* Database type: ${dj.db_type}\n`;
|
supportString += `* Database type: ${dj.db_type}\n`;
|
||||||
|
@ -136,16 +138,17 @@ function copyToClipboard(event) {
|
||||||
new BSN.Toast("#toastClipboardCopy").show();
|
new BSN.Toast("#toastClipboardCopy").show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTimeDrift(browserUTC, serverUTC) {
|
function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) {
|
||||||
const timeDrift = (
|
const timeDrift = (
|
||||||
Date.parse(serverUTC.replace(" ", "T").replace(" UTC", "")) -
|
Date.parse(utcTimeA.replace(" ", "T").replace(" UTC", "")) -
|
||||||
Date.parse(browserUTC.replace(" ", "T").replace(" UTC", ""))
|
Date.parse(utcTimeB.replace(" ", "T").replace(" UTC", ""))
|
||||||
) / 1000;
|
) / 1000;
|
||||||
if (timeDrift > 20 || timeDrift < -20) {
|
if (timeDrift > 15 || timeDrift < -15) {
|
||||||
document.getElementById("time-warning").classList.remove("d-none");
|
document.getElementById(`${statusPrefix}-warning`).classList.remove("d-none");
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("time-success").classList.remove("d-none");
|
document.getElementById(`${statusPrefix}-success`).classList.remove("d-none");
|
||||||
timeCheck = true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +198,18 @@ function checkDns(dns_resolved) {
|
||||||
function init(dj) {
|
function init(dj) {
|
||||||
// Time check
|
// Time check
|
||||||
document.getElementById("time-browser-string").innerText = browserUTC;
|
document.getElementById("time-browser-string").innerText = browserUTC;
|
||||||
checkTimeDrift(browserUTC, dj.server_time);
|
|
||||||
|
// Check if we were able to fetch a valid NTP Time
|
||||||
|
// If so, compare both browser and server with NTP
|
||||||
|
// Else, compare browser and server.
|
||||||
|
if (dj.ntp_time.indexOf("UTC") !== -1) {
|
||||||
|
timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time");
|
||||||
|
checkTimeDrift(dj.ntp_time, browserUTC, "ntp-browser");
|
||||||
|
ntpTimeCheck = checkTimeDrift(dj.ntp_time, dj.server_time, "ntp-server");
|
||||||
|
} else {
|
||||||
|
timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time");
|
||||||
|
ntpTimeCheck = "n/a";
|
||||||
|
}
|
||||||
|
|
||||||
// Domain check
|
// Domain check
|
||||||
const browserURL = location.href.toLowerCase();
|
const browserURL = location.href.toLowerCase();
|
||||||
|
|
2
src/static/scripts/admin_organizations.js
gevendort
2
src/static/scripts/admin_organizations.js
gevendort
|
@ -54,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
|
||||||
],
|
],
|
||||||
"pageLength": -1, // Default show all
|
"pageLength": -1, // Default show all
|
||||||
"columnDefs": [{
|
"columnDefs": [{
|
||||||
"targets": 4,
|
"targets": [4,5],
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"orderable": false
|
"orderable": false
|
||||||
}]
|
}]
|
||||||
|
|
2
src/static/scripts/admin_users.js
gevendort
2
src/static/scripts/admin_users.js
gevendort
|
@ -244,7 +244,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
|
||||||
[-1, 2, 5, 10, 25, 50],
|
[-1, 2, 5, 10, 25, 50],
|
||||||
["All", 2, 5, 10, 25, 50]
|
["All", 2, 5, 10, 25, 50]
|
||||||
],
|
],
|
||||||
"pageLength": 2, // Default show all
|
"pageLength": -1, // Default show all
|
||||||
"columnDefs": [{
|
"columnDefs": [{
|
||||||
"targets": [1, 2],
|
"targets": [1, 2],
|
||||||
"type": "date-iso"
|
"type": "date-iso"
|
||||||
|
|
29
src/static/scripts/datatables.css
gevendort
29
src/static/scripts/datatables.css
gevendort
|
@ -4,13 +4,19 @@
|
||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-1.13.1
|
* https://datatables.net/download/#bs5/dt-1.13.2
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.13.1
|
* DataTables 1.13.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
|
:root {
|
||||||
|
--dt-row-selected: 13, 110, 253;
|
||||||
|
--dt-row-selected-text: 255, 255, 255;
|
||||||
|
--dt-row-selected-link: 9, 10, 11;
|
||||||
|
}
|
||||||
|
|
||||||
table.dataTable td.dt-control {
|
table.dataTable td.dt-control {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -126,7 +132,7 @@ div.dataTables_processing > div:last-child > div {
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(13, 110, 253, 0.9);
|
background: 13 110 253;
|
||||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
}
|
}
|
||||||
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
||||||
|
@ -284,23 +290,28 @@ table.dataTable > tbody > tr {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
table.dataTable > tbody > tr.selected > * {
|
table.dataTable > tbody > tr.selected > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9);
|
box-shadow: inset 0 0 0 9999px rgb(13, 110, 253);
|
||||||
color: white;
|
box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected));
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
color: rgb(var(--dt-row-selected-text));
|
||||||
}
|
}
|
||||||
table.dataTable > tbody > tr.selected a {
|
table.dataTable > tbody > tr.selected a {
|
||||||
color: #090a0b;
|
color: rgb(9, 10, 11);
|
||||||
|
color: rgb(var(--dt-row-selected-link));
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped > tbody > tr.odd > * {
|
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
|
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95);
|
||||||
}
|
}
|
||||||
table.dataTable.table-hover > tbody > tr:hover > * {
|
table.dataTable.table-hover > tbody > tr:hover > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075);
|
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075);
|
||||||
}
|
}
|
||||||
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dataTables_wrapper div.dataTables_length label {
|
div.dataTables_wrapper div.dataTables_length label {
|
||||||
|
@ -374,9 +385,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
div.dataTables_wrapper div.dataTables_length,
|
div.dataTables_wrapper div.dataTables_length,
|
||||||
div.dataTables_wrapper div.dataTables_filter,
|
div.dataTables_wrapper div.dataTables_filter,
|
||||||
div.dataTables_wrapper div.dataTables_info,
|
div.dataTables_wrapper div.dataTables_info,
|
||||||
div.dataTables_wrapper div.dataTables_paginate {
|
div.dataTables_wrapper div.dataTables_paginate {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
||||||
|
|
64
src/static/scripts/datatables.js
gevendort
64
src/static/scripts/datatables.js
gevendort
|
@ -4,20 +4,20 @@
|
||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-1.13.1
|
* https://datatables.net/download/#bs5/dt-1.13.2
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.13.1
|
* DataTables 1.13.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 1.13.1
|
/*! DataTables 1.13.2
|
||||||
* ©2008-2022 SpryMedia Ltd - datatables.net/license
|
* ©2008-2023 SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary DataTables
|
* @summary DataTables
|
||||||
* @description Paginate, search and order HTML tables
|
* @description Paginate, search and order HTML tables
|
||||||
* @version 1.13.1
|
* @version 1.13.2
|
||||||
* @author SpryMedia Ltd
|
* @author SpryMedia Ltd
|
||||||
* @contact www.datatables.net
|
* @contact www.datatables.net
|
||||||
* @copyright SpryMedia Ltd.
|
* @copyright SpryMedia Ltd.
|
||||||
|
@ -1382,7 +1382,12 @@
|
||||||
|
|
||||||
|
|
||||||
var _isNumber = function ( d, decimalPoint, formatted ) {
|
var _isNumber = function ( d, decimalPoint, formatted ) {
|
||||||
var strType = typeof d === 'string';
|
let type = typeof d;
|
||||||
|
var strType = type === 'string';
|
||||||
|
|
||||||
|
if ( type === 'number' || type === 'bigint') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// If empty return immediately so there must be a number if it is a
|
// If empty return immediately so there must be a number if it is a
|
||||||
// formatted string (this stops the string "k", or "kr", etc being detected
|
// formatted string (this stops the string "k", or "kr", etc being detected
|
||||||
|
@ -6789,8 +6794,15 @@
|
||||||
|
|
||||||
if ( eventName !== null ) {
|
if ( eventName !== null ) {
|
||||||
var e = $.Event( eventName+'.dt' );
|
var e = $.Event( eventName+'.dt' );
|
||||||
|
var table = $(settings.nTable);
|
||||||
|
|
||||||
$(settings.nTable).trigger( e, args );
|
table.trigger( e, args );
|
||||||
|
|
||||||
|
// If not yet attached to the document, trigger the event
|
||||||
|
// on the body directly to sort of simulate the bubble
|
||||||
|
if (table.parents('body').length === 0) {
|
||||||
|
$('body').trigger( e, args );
|
||||||
|
}
|
||||||
|
|
||||||
ret.push( e.result );
|
ret.push( e.result );
|
||||||
}
|
}
|
||||||
|
@ -7256,7 +7268,7 @@
|
||||||
|
|
||||||
pluck: function ( prop )
|
pluck: function ( prop )
|
||||||
{
|
{
|
||||||
let fn = DataTable.util.get(prop);
|
var fn = DataTable.util.get(prop);
|
||||||
|
|
||||||
return this.map( function ( el ) {
|
return this.map( function ( el ) {
|
||||||
return fn(el);
|
return fn(el);
|
||||||
|
@ -8353,10 +8365,9 @@
|
||||||
|
|
||||||
$(document).on('plugin-init.dt', function (e, context) {
|
$(document).on('plugin-init.dt', function (e, context) {
|
||||||
var api = new _Api( context );
|
var api = new _Api( context );
|
||||||
|
var namespace = 'on-plugin-init';
|
||||||
const namespace = 'on-plugin-init';
|
var stateSaveParamsEvent = 'stateSaveParams.' + namespace;
|
||||||
const stateSaveParamsEvent = `stateSaveParams.${namespace}`;
|
var destroyEvent = 'destroy. ' + namespace;
|
||||||
const destroyEvent = `destroy.${namespace}`;
|
|
||||||
|
|
||||||
api.on( stateSaveParamsEvent, function ( e, settings, d ) {
|
api.on( stateSaveParamsEvent, function ( e, settings, d ) {
|
||||||
// This could be more compact with the API, but it is a lot faster as a simple
|
// This could be more compact with the API, but it is a lot faster as a simple
|
||||||
|
@ -8375,7 +8386,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
api.on( destroyEvent, function () {
|
api.on( destroyEvent, function () {
|
||||||
api.off(`${stateSaveParamsEvent} ${destroyEvent}`);
|
api.off(stateSaveParamsEvent + ' ' + destroyEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
var loaded = api.state.loaded();
|
var loaded = api.state.loaded();
|
||||||
|
@ -9697,7 +9708,7 @@
|
||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "1.13.1";
|
DataTable.version = "1.13.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
|
@ -14121,7 +14132,7 @@
|
||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
build:"bs5/dt-1.13.1",
|
build:"bs5/dt-1.13.2",
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14830,10 +14841,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( btnDisplay !== null ) {
|
if ( btnDisplay !== null ) {
|
||||||
node = $('<a>', {
|
var tag = settings.oInit.pagingTag || 'a';
|
||||||
|
var disabled = btnClass.indexOf(disabledClass) !== -1;
|
||||||
|
|
||||||
|
|
||||||
|
node = $('<'+tag+'>', {
|
||||||
'class': classes.sPageButton+' '+btnClass,
|
'class': classes.sPageButton+' '+btnClass,
|
||||||
'aria-controls': settings.sTableId,
|
'aria-controls': settings.sTableId,
|
||||||
|
'aria-disabled': disabled ? 'true' : null,
|
||||||
'aria-label': aria[ button ],
|
'aria-label': aria[ button ],
|
||||||
|
'aria-role': 'link',
|
||||||
|
'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null,
|
||||||
'data-dt-idx': button,
|
'data-dt-idx': button,
|
||||||
'tabindex': tabIndex,
|
'tabindex': tabIndex,
|
||||||
'id': idx === 0 && typeof button === 'string' ?
|
'id': idx === 0 && typeof button === 'string' ?
|
||||||
|
@ -14965,6 +14983,12 @@
|
||||||
if ( d !== 0 && (!d || d === '-') ) {
|
if ( d !== 0 && (!d || d === '-') ) {
|
||||||
return -Infinity;
|
return -Infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let type = typeof d;
|
||||||
|
|
||||||
|
if (type === 'number' || type === 'bigint') {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
// If a decimal place other than `.` is used, it needs to be given to the
|
// If a decimal place other than `.` is used, it needs to be given to the
|
||||||
// function so we can detect it and replace with a `.` which is the only
|
// function so we can detect it and replace with a `.` which is the only
|
||||||
|
@ -15647,7 +15671,6 @@
|
||||||
require('datatables.net')(root, $);
|
require('datatables.net')(root, $);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return factory( $, root, root.document );
|
return factory( $, root, root.document );
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -15755,6 +15778,8 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( btnDisplay ) {
|
if ( btnDisplay ) {
|
||||||
|
var disabled = btnClass.indexOf('disabled') !== -1;
|
||||||
|
|
||||||
node = $('<li>', {
|
node = $('<li>', {
|
||||||
'class': classes.sPageButton+' '+btnClass,
|
'class': classes.sPageButton+' '+btnClass,
|
||||||
'id': idx === 0 && typeof button === 'string' ?
|
'id': idx === 0 && typeof button === 'string' ?
|
||||||
|
@ -15762,9 +15787,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
||||||
null
|
null
|
||||||
} )
|
} )
|
||||||
.append( $('<a>', {
|
.append( $('<a>', {
|
||||||
'href': '#',
|
'href': disabled ? null : '#',
|
||||||
'aria-controls': settings.sTableId,
|
'aria-controls': settings.sTableId,
|
||||||
|
'aria-disabled': disabled ? 'true' : null,
|
||||||
'aria-label': aria[ button ],
|
'aria-label': aria[ button ],
|
||||||
|
'aria-role': 'link',
|
||||||
|
'aria-current': btnClass === 'active' ? 'page' : null,
|
||||||
'data-dt-idx': button,
|
'data-dt-idx': button,
|
||||||
'tabindex': settings.iTabIndex,
|
'tabindex': settings.iTabIndex,
|
||||||
'class': 'page-link'
|
'class': 'page-link'
|
||||||
|
|
|
@ -144,10 +144,15 @@
|
||||||
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Date & Time (UTC)
|
<dt class="col-sm-5">Date & Time (UTC)
|
||||||
<span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 20 seconds of each other.">Ok</span>
|
<span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 20 seconds apart.">Error</span>
|
<span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span>
|
||||||
|
<span class="badge bg-success d-none" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span>
|
||||||
|
<span class="badge bg-danger d-none" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span>
|
||||||
|
<span class="badge bg-success d-none" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span>
|
||||||
|
<span class="badge bg-danger d-none" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
|
<span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_time}}</span></span>
|
||||||
<span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span>
|
<span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span>
|
||||||
<span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span>
|
<span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="vw-org-details">Organization</th>
|
<th class="vw-org-details">Organization</th>
|
||||||
<th class="vw-users">Users</th>
|
<th class="vw-users">Users</th>
|
||||||
<th class="vw-items">Items</th>
|
<th class="vw-ciphers">Ciphers</th>
|
||||||
<th class="vw-attachments">Attachments</th>
|
<th class="vw-attachments">Attachments</th>
|
||||||
|
<th class="vw-misc">Misc</th>
|
||||||
<th class="vw-actions">Actions</th>
|
<th class="vw-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -37,8 +38,13 @@
|
||||||
<span class="d-block"><strong>Size:</strong> {{attachment_size}}</span>
|
<span class="d-block"><strong>Size:</strong> {{attachment_size}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-block"><strong>Collections:</strong> {{collection_count}}</span>
|
||||||
|
<span class="d-block"><strong>Groups:</strong> {{group_count}}</span>
|
||||||
|
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
|
||||||
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-0 small">
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button><br>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<th class="vw-account-details">User</th>
|
<th class="vw-account-details">User</th>
|
||||||
<th class="vw-created-at">Created at</th>
|
<th class="vw-created-at">Created at</th>
|
||||||
<th class="vw-last-active">Last Active</th>
|
<th class="vw-last-active">Last Active</th>
|
||||||
<th class="vw-items">Items</th>
|
<th class="vw-ciphers">Ciphers</th>
|
||||||
<th class="vw-attachments">Attachments</th>
|
<th class="vw-attachments">Attachments</th>
|
||||||
<th class="vw-organizations">Organizations</th>
|
<th class="vw-organizations">Organizations</th>
|
||||||
<th class="vw-actions">Actions</th>
|
<th class="vw-actions">Actions</th>
|
||||||
|
@ -63,14 +63,14 @@
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-0 small">
|
||||||
<span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}">
|
<span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}">
|
||||||
{{#if TwoFactorEnabled}}
|
{{#if TwoFactorEnabled}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br>
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br>
|
||||||
{{#if user_enabled}}
|
{{#if user_enabled}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
Laden …
In neuem Issue referenzieren