From b87ed834f0610d8e8d8c116bbccf8018116cbd7e Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Fri, 24 Nov 2023 14:55:45 +0300 Subject: [PATCH 01/11] add validation errors to js Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/static/js/clipboard.js | 91 +++++++++++++++++++++++++++++++++++ ui/web/template/users.html | 29 ++++++++--- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/ui/web/static/js/clipboard.js b/ui/web/static/js/clipboard.js index 4d7b50006..dd8768806 100644 --- a/ui/web/static/js/clipboard.js +++ b/ui/web/static/js/clipboard.js @@ -21,3 +21,94 @@ function copyToClipboard(button) { }, ); } + +// Form validation functions + +function validateName(name, errorDiv, event) { + removeErrorMessage(errorDiv); + if (name.trim() === "") { + event.preventDefault(); + displayErrorMessage("Name is Required", errorDiv); + } +} + +const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; +function validateEmail(email, errorDiv, event) { + removeErrorMessage(errorDiv); + if (email.trim() === "") { + event.preventDefault(); + displayErrorMessage("Email is Required", errorDiv); + } else if (!email.match(emailRegex)) { + event.preventDefault(); + displayErrorMessage("Invalid email format", errorDiv); + } +} + +const minLength = 8; +function validatePassword(password, errorDiv, event) { + removeErrorMessage(errorDiv); + if (password.trim().length < minLength) { + event.preventDefault(); + var errorMessage = `Password must be at least ${minLength} characters long`; + displayErrorMessage(errorMessage, errorDiv); + } +} + +function validateMetadata(metadata, errorDiv, event) { + removeErrorMessage(errorDiv); + try { + if (metadata.trim() !== "") { + JSON.parse(metadata); + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("Metadata is not a valid JSON object", errorDiv); + } +} + +function validateTags(tags, errorDiv, event) { + removeErrorMessage(errorDiv); + var tagsArray; + try { + if (tags.trim() !== "") { + tagsArray = JSON.parse(tags); + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("tags must be a string array", errorDiv); + } + if ( + !Array.isArray(tagsArray) || + !tagsArray.every(function (tag) { + return typeof tag === "string"; + }) + ) { + event.preventDefault(); + displayErrorMessage("tags must be strings", errorDiv); + } +} + +function displayErrorMessage(errorMessage, divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "block"; + errorDiv.innerHTML = errorMessage; +} + +function removeErrorMessage(divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "none"; +} + +function attachValidationListener(config) { + const button = document.getElementById(config.buttonId); + + button.addEventListener("click", function (event) { + for (const key in config.validations) { + if (config.validations.hasOwnProperty(key)) { + const validationFunc = config.validations[key]; + const elementValue = document.getElementById(key).value; + validationFunc(elementValue, config.errorDivs[key], event); + } + } + }); +} diff --git a/ui/web/template/users.html b/ui/web/template/users.html index d03e2d25c..26db5a8cc 100644 --- a/ui/web/template/users.html +++ b/ui/web/template/users.html @@ -47,8 +47,8 @@ <h5 class="modal-title" id="addUserModalLabel"> <div class="modal-body"> <div id="alertMessage"></div> <form - method="post" - onsubmit="return submitUserForm()" + + > <div class="mb-3"> <label for="name" class="form-label">Name</label> @@ -58,8 +58,8 @@ <h5 class="modal-title" id="addUserModalLabel"> name="name" id="name" placeholder="User Name" - required /> + <div id="nameError" class="text-danger"></div> </div> <div class="mb-3"> <label for="identity" class="form-label"> @@ -71,8 +71,8 @@ <h5 class="modal-title" id="addUserModalLabel"> name="identity" id="identity" placeholder="User Identity" - required /> + <div id="identityError" class="text-danger"></div> </div> <div class="mb-3"> <label for="secret" class="form-label"> @@ -84,7 +84,6 @@ <h5 class="modal-title" id="addUserModalLabel"> name="secret" id="secret" placeholder="User Secret" - required /> <div id="secretError" class="text-danger"></div> </div> @@ -122,7 +121,7 @@ <h5 class="modal-title" id="addUserModalLabel"> <button type="submit" class="btn body-button" - onclick="return validateForm()" + id="create-user-button" > Submit </button> @@ -258,6 +257,24 @@ <h5 class="modal-title" id="addUsersModalLabel"> </div> {{ template "footer" }} <script> + attachValidationListener({ + buttonId: "create-user-button", + errorDivs: { + name: "nameError", + identity: "identityError", + secret: "secretError", + metadata: "metadataError", + tags: "tagsError", + }, + validations: { + name: validateName, + identity: validateEmail, + secret: validatePassword, + metadata: validateMetadata, + tags: validateTags, + } + }) + function validateForm() { var secret = document.getElementById("secret").value; var tagsInput = document.getElementById("tags").value; From a808c199299f9d4a1515f59a12c8d8cec924c309 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Mon, 27 Nov 2023 12:56:55 +0300 Subject: [PATCH 02/11] add users scripts to javascript file Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/api/endpoint.go | 15 +- ui/api/transport.go | 2 + ui/web/static/js/clipboard.js | 114 ----------- ui/web/static/js/main.js | 344 ++++++++++++++++++++++++++++++++++ ui/web/template/footer.html | 1 - ui/web/template/header.html | 1 + ui/web/template/user.html | 196 +++---------------- ui/web/template/users.html | 154 ++------------- 8 files changed, 392 insertions(+), 435 deletions(-) delete mode 100644 ui/web/static/js/clipboard.js create mode 100644 ui/web/static/js/main.js diff --git a/ui/api/endpoint.go b/ui/api/endpoint.go index 396c31235..52adf2d2c 100644 --- a/ui/api/endpoint.go +++ b/ui/api/endpoint.go @@ -273,8 +273,7 @@ func createUserEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": usersAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -291,8 +290,7 @@ func createUsersEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": usersAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -353,8 +351,7 @@ func updateUserEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": usersAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -375,8 +372,7 @@ func updateUserTagsEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": usersAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -400,8 +396,7 @@ func updateUserIdentityEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": usersAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } diff --git a/ui/api/transport.go b/ui/api/transport.go index bda2d8cf5..a2fcb6f53 100644 --- a/ui/api/transport.go +++ b/ui/api/transport.go @@ -975,6 +975,8 @@ func decodeUserUpdate(_ context.Context, r *http.Request) (interface{}, error) { Metadata: data.Metadata, } + fmt.Println(req) + return req, nil } diff --git a/ui/web/static/js/clipboard.js b/ui/web/static/js/clipboard.js deleted file mode 100644 index dd8768806..000000000 --- a/ui/web/static/js/clipboard.js +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -//function to copy the ID to the clipboard -function copyToClipboard(button) { - var clientIDElement = button.previousElementSibling.firstChild; - var clientId = clientIDElement.textContent; - - navigator.clipboard.writeText(clientId).then( - function () { - //change the copy icon to indicate success - button.innerHTML = `<i class="fas fa-check success-icon">`; - setTimeout(function () { - //revert the copy icon after a short delay - button.innerHTML = `<i class ="far fa-copy">`; - }, 1000); - }, - function (error) { - //handle error - console.error("failed to copy to clipboard: ", error); - }, - ); -} - -// Form validation functions - -function validateName(name, errorDiv, event) { - removeErrorMessage(errorDiv); - if (name.trim() === "") { - event.preventDefault(); - displayErrorMessage("Name is Required", errorDiv); - } -} - -const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; -function validateEmail(email, errorDiv, event) { - removeErrorMessage(errorDiv); - if (email.trim() === "") { - event.preventDefault(); - displayErrorMessage("Email is Required", errorDiv); - } else if (!email.match(emailRegex)) { - event.preventDefault(); - displayErrorMessage("Invalid email format", errorDiv); - } -} - -const minLength = 8; -function validatePassword(password, errorDiv, event) { - removeErrorMessage(errorDiv); - if (password.trim().length < minLength) { - event.preventDefault(); - var errorMessage = `Password must be at least ${minLength} characters long`; - displayErrorMessage(errorMessage, errorDiv); - } -} - -function validateMetadata(metadata, errorDiv, event) { - removeErrorMessage(errorDiv); - try { - if (metadata.trim() !== "") { - JSON.parse(metadata); - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("Metadata is not a valid JSON object", errorDiv); - } -} - -function validateTags(tags, errorDiv, event) { - removeErrorMessage(errorDiv); - var tagsArray; - try { - if (tags.trim() !== "") { - tagsArray = JSON.parse(tags); - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("tags must be a string array", errorDiv); - } - if ( - !Array.isArray(tagsArray) || - !tagsArray.every(function (tag) { - return typeof tag === "string"; - }) - ) { - event.preventDefault(); - displayErrorMessage("tags must be strings", errorDiv); - } -} - -function displayErrorMessage(errorMessage, divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "block"; - errorDiv.innerHTML = errorMessage; -} - -function removeErrorMessage(divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "none"; -} - -function attachValidationListener(config) { - const button = document.getElementById(config.buttonId); - - button.addEventListener("click", function (event) { - for (const key in config.validations) { - if (config.validations.hasOwnProperty(key)) { - const validationFunc = config.validations[key]; - const elementValue = document.getElementById(key).value; - validationFunc(elementValue, config.errorDivs[key], event); - } - } - }); -} diff --git a/ui/web/static/js/main.js b/ui/web/static/js/main.js new file mode 100644 index 000000000..3f0d45b0e --- /dev/null +++ b/ui/web/static/js/main.js @@ -0,0 +1,344 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//function to copy the ID to the clipboard +function copyToClipboard(button) { + var clientIDElement = button.previousElementSibling.firstChild; + var clientId = clientIDElement.textContent; + + navigator.clipboard.writeText(clientId).then( + function () { + //change the copy icon to indicate success + button.innerHTML = `<i class="fas fa-check success-icon">`; + setTimeout(function () { + //revert the copy icon after a short delay + button.innerHTML = `<i class ="far fa-copy">`; + }, 1000); + }, + function (error) { + //handle error + console.error("failed to copy to clipboard: ", error); + }, + ); +} + +// Form validation functions + +function validateName(name, errorDiv, event) { + removeErrorMessage(errorDiv); + if (name.trim() === "") { + event.preventDefault(); + displayErrorMessage("Name is Required", errorDiv); + return false; + } + return true; +} + +const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; +function validateEmail(email, errorDiv, event) { + removeErrorMessage(errorDiv); + if (email.trim() === "") { + event.preventDefault(); + displayErrorMessage("Email is Required", errorDiv); + return false; + } else if (!email.match(emailRegex)) { + event.preventDefault(); + displayErrorMessage("Invalid email format", errorDiv); + return false; + } + return true; +} + +const minLength = 8; +function validatePassword(password, errorDiv, event) { + removeErrorMessage(errorDiv); + if (password.trim().length < minLength) { + event.preventDefault(); + var errorMessage = `Password must be at least ${minLength} characters long`; + displayErrorMessage(errorMessage, errorDiv); + return false; + } + return true; +} + +function validateMetadata(metadata, errorDiv, event) { + removeErrorMessage(errorDiv); + try { + if (metadata.trim() !== "") { + JSON.parse(metadata); + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("Metadata is not a valid JSON object", errorDiv); + return false; + } + return true; +} + +function validateTags(tags, errorDiv, event) { + removeErrorMessage(errorDiv); + var tagsArray; + try { + if (tags.trim() !== "") { + tagsArray = JSON.parse(tags); + } + if ( + !Array.isArray(tagsArray) || + !tagsArray.every(function (tag) { + return typeof tag === "string"; + }) + ) { + event.preventDefault(); + displayErrorMessage("tags must be strings in an array", errorDiv); + return false; + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("tags must be a string array", errorDiv); + return false; + } + + return true; +} + +function displayErrorMessage(errorMessage, divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "block"; + errorDiv.innerHTML = errorMessage; +} + +function removeErrorMessage(divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "none"; +} + +function attachValidationListener(config) { + const button = document.getElementById(config.buttonId); + + button.addEventListener("click", function (event) { + for (const key in config.validations) { + if (config.validations.hasOwnProperty(key)) { + const validationFunc = config.validations[key]; + const elementValue = document.getElementById(key).value; + validationFunc(elementValue, config.errorDivs[key], event); + } + } + }); +} + +// Form subsmission functions +// config parameters are: formId, url, alertDiv, modal +function submitCreateForm(config) { + const form = document.getElementById(config.formId); + form.addEventListener("submit", function (event) { + event.preventDefault(); + const formData = new FormData(form); + + fetch(config.url, { + method: "POST", + body: formData, + }) + .then(function (response) { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + case 400: + showAlert("invalid file contents!", config.alertDiv); + break; + case 415: + showAlert("invalid file type!", config.alertDiv); + break; + default: + form.reset(); + config.modal.hide(); + window.location.reload(); + } + }) + .catch((error) => { + console.error("error submitting form: ", error); + }); + }); +} + +function showAlert(errorMessage, alertDiv) { + const alert = document.getElementById(alertDiv); + alert.innerHTML = ` + <div class="alert alert-danger alert-dismissable fade show d-flex flex-row justify-content-between" role="alert"> + <div>${errorMessage}</div> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> + </div> `; +} + +// Functions to make a row editable. + +// make a cell editable. +function makeEditable(cell) { + cell.setAttribute("contenteditable", "true"); + cell.dataset.originalContent = cell.innerHTML; +} + +// make cell uneditable. +function makeUneditable(cell) { + const originalContent = cell.dataset.originalContent; + cell.innerHTML = originalContent; + cell.setAttribute("contenteditable", "false"); +} + +// function show the save/cancel buttons and hide the edit button. +function showSaveCancelButtons(editBtn, saveCancelBtn) { + editBtn.style.display = "none"; + saveCancelBtn.style.display = "inline-block"; +} + +// function to show the edit button anf hide the save/cancel buttons. +function showEditButton(editBtn, saveCancelBtn) { + editBtn.style.display = "inline-block"; + saveCancelBtn.style.display = "none"; +} + +// config parameters are: button, field +function editRow(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + makeEditable(config.cell); + showSaveCancelButtons(config.editBtn, config.saveCancelBtn); + }); +} + +function cancelEditRow(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + makeUneditable(config.cell); + showEditButton(config.editBtn, config.saveCancelBtn); + removeErrorMessage(config.alertDiv); + }); +} + +function submitUpdateForm(config) { + fetch(config.url, { + method: "POST", + body: config.data, + headers: { + "Content-Type": "application/json", + }, + }).then((response) => { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + default: + window.location.reload(); + } + }); +} + +function updateName(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateName(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateIdentity(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateEmail(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/identity`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateMetadata(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateMetadata(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateTags(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateTags(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/tags`; + const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function attachEditRowListener(config) { + for (const key in config.rows) { + if (config.rows.hasOwnProperty(key)) { + const cell = document.querySelector(`td[data-field="${key}"]`); + const editBtn = cell.parentNode.querySelector(".edit-btn"); + const saveCancelBtn = cell.parentNode.querySelector( + ".save-cancel-buttons", + ); + editRow({ + button: `edit-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + }); + cancelEditRow({ + button: `cancel-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + alertDiv: config.errorDiv, + }); + const saveRow = config.rows[key]; + saveRow({ + button: `save-${key}`, + field: key, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + id: config.id, + entity: config.entity, + alertDiv: config.errorDiv, + }); + } + } +} diff --git a/ui/web/template/footer.html b/ui/web/template/footer.html index 6ae7bd7fd..c64b769fa 100644 --- a/ui/web/template/footer.html +++ b/ui/web/template/footer.html @@ -5,7 +5,6 @@ <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> - <script src="/js/clipboard.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" diff --git a/ui/web/template/header.html b/ui/web/template/header.html index e504c37e0..07c3534bf 100644 --- a/ui/web/template/header.html +++ b/ui/web/template/header.html @@ -21,5 +21,6 @@ integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous" /> + <script src="/js/main.js"></script> </head> {{ end }} diff --git a/ui/web/template/user.html b/ui/web/template/user.html index fe9b420c3..aea0349bb 100644 --- a/ui/web/template/user.html +++ b/ui/web/template/user.html @@ -58,7 +58,7 @@ <td> <button class="edit-btn" - onclick="editRow('name')" + id="edit-name" {{ if $disableButton }} @@ -71,13 +71,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('name')"> + <button class="save-btn" id="save-name"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('name')" - > + <button class="cancel-btn" id="cancel-name"> Cancel </button> </div> @@ -105,7 +102,7 @@ <td> <button class="edit-btn" - onclick="editRow('identity')" + id="edit-identity" {{ if $disableButton }} @@ -118,16 +115,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('identity')" - > + <button class="save-btn" id="save-identity"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('identity')" - > + <button class="cancel-btn" id="cancel-identity"> Cancel </button> </div> @@ -145,7 +136,7 @@ <td> <button class="edit-btn" - onclick="editRow('tags')" + id="edit-tags" {{ if $disableButton }} @@ -158,13 +149,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('tags')"> + <button class="save-btn" id="save-tags"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('tags')" - > + <button class="cancel-btn" id="cancel-tags"> Cancel </button> </div> @@ -182,7 +170,7 @@ <td> <button class="edit-btn" - onclick="editRow('metadata')" + id="edit-metadata" {{ if $disableButton }} @@ -195,16 +183,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('metadata')" - > + <button class="save-btn" id="save-metadata"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('metadata')" - > + <button class="cancel-btn" id="cancel-metadata"> Cancel </button> </div> @@ -221,149 +203,19 @@ </div> {{ template "footer" }} <script> - function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; - } - - function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); - } - - function showButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; - } - - function hideButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; - } - - function editRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - - // Make the row editable - makeEditable(cell); - - // Hide the edit button and show save and cancel buttons - showButtons(editBtn, saveCancelBtn); - } - - function saveRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - // Get the updated value from the nameCell - const updatedValue = cell.textContent.trim(); - - // Email format validation regular expression - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - // Send the updated value to the server using a POST request - let url; - let data; - if (field === "name") { - url = "/users/{{.UserID}}"; - data = { [field]: updatedValue }; - } else if (field == "metadata") { - try { - const metadata = JSON.parse(updatedValue); - url = "/users/{{.UserID}}"; - data = { [field]: metadata }; - } catch (error) { - errorMessage.textContent = "Metadata must be a valid JSON object!"; - return; - } - } else if (field === "identity") { - if (!emailRegex.test(updatedValue)) { - errorMessage.textContent = "Identity should be in email format!"; - return; - } - url = "/users/{{.UserID}}/identity"; - data = { [field]: updatedValue }; - } else if (field === "tags") { - try { - const tags = JSON.parse(updatedValue); - if ( - !Array.isArray(tags) || - !tags.every(function (tag) { - return typeof tag === "string"; - }) - ) { - errorMessage.textContent = "Tags must be a string array"; - return; - } - url = "/users/{{.UserID}}/tags"; - data = { [field]: tags }; - } catch (error) { - errorMessage.textContent = "Tags must be a valid string array"; - return; - } - } - - errorMessage.textContent = ""; - - if (url) { - errorMessage.textContent=""; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", + attachEditRowListener( + { + entity: "users", + id: "{{ .UserID }}", + rows: { + name:updateName, + identity:updateIdentity, + tags:updateTags, + metadata:updateMetadata, }, - }) - .then((response) => { - if (response.ok) { - // Make the row uneditable - cell.setAttribute("contenteditable", "false"); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } else { - throw new Error("Response not OK"); - } - }) - .catch((error) => { - // Restore original values in the row if there is an error - makeUneditable(cell); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - - console.error("Error", error); - errorMessage.textContent = "Entity already exists"; - }); - } else { - console.error("Invalid field:", field); - } - } - - function cancelEditRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - errorMessage.textContent = ""; - // Restore original values in the row - makeUneditable(cell); - - // Show the edit button and hide the save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } + errorDiv: "error-message", + } + ); </script> </body> </html> diff --git a/ui/web/template/users.html b/ui/web/template/users.html index 26db5a8cc..ed7791c50 100644 --- a/ui/web/template/users.html +++ b/ui/web/template/users.html @@ -46,10 +46,7 @@ <h5 class="modal-title" id="addUserModalLabel"> </div> <div class="modal-body"> <div id="alertMessage"></div> - <form - - - > + <form id="userform"> <div class="mb-3"> <label for="name" class="form-label">Name</label> <input @@ -150,9 +147,8 @@ <h5 class="modal-title" id="addUsersModalLabel"> <div class="modal-body"> <div id="alertBulkMessage"></div> <form - method="post" enctype="multipart/form-data" - onsubmit="return submitBulkUsersForm()" + id="bulkusersform" > <div class="form-group"> <label for="usersFile"> @@ -272,59 +268,8 @@ <h5 class="modal-title" id="addUsersModalLabel"> secret: validatePassword, metadata: validateMetadata, tags: validateTags, - } - }) - - function validateForm() { - var secret = document.getElementById("secret").value; - var tagsInput = document.getElementById("tags").value; - var metadataInput = document.getElementById("metadata").value; - var secretError = document.getElementById("secretError"); - var tagsError = document.getElementById("tagsError"); - var metadataError = document.getElementById("metadataError"); - var submit = document.getElementById("submit"); - - var tags; - var metadata; - - secretError.innerHTML = ""; - tagsError.innerHTML = ""; - metadataError.innerHTML = ""; - - var isValid = true; - - if (secret.length < 8) { - secretError.innerHTML = - "secret must have a minimum of 8 characters"; - isValid = false; - } - try { - tags = JSON.parse(tagsInput); - } catch (error) { - tagsError.innerHTML = "please enter valid tags as a string array!"; - isValid = false; - } - - if ( - !Array.isArray(tags) || - !tags.every(function (tag) { - return typeof tag === "string"; - }) - ) { - tagsError.innerHTML = "tags must be a string array!"; - isValid = false; - } - - try { - metadata = JSON.parse(metadataInput); - } catch (error) { - metadataError.innerHTML = - "please enter valid metadata in JSON format!"; - isValid = false; - } - - return isValid; - } + }, + }); const userModal = new bootstrap.Modal( document.getElementById("addUserModal"), @@ -341,86 +286,19 @@ <h5 class="modal-title" id="addUsersModalLabel"> } } - function submitUserForm() { - event.preventDefault(); - var form = event.target; - fetch("/users", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 409) { - errorMessage = "user already exists!"; - showAlert(errorMessage, "single"); - return false; - } else { - form.reset(); - userModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("error submitting user form: ", error); - return false; - }); - } - - function submitBulkUsersForm() { - event.preventDefault(); - var form = event.target; - fetch("/users/bulk", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - switch (response.status) { - case 409: - errorMessage = "users already exist!"; - showAlert(errorMessage, "bulk"); - return false; - case 400: - errorMessage = "invalid file contents!"; - showAlert(errorMessage, "bulk"); - return false; - case 415: - errorMessage = "file must be csv!"; - showAlert(errorMessage, "bulk"); - return false; - default: - form.reset(); - usersModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("Error submitting bulk users form: ", error); - return false; - }); - } + submitCreateForm({ + url: "/users", + formId: "userform", + alertDiv: "alertMessage", + modal: userModal, + }); - function showAlert(errorMessage, modal) { - if (modal === "single") { - var alertMessage = document.getElementById("alertMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } else if (modal === "bulk") { - var alertMessage = document.getElementById("alertBulkMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } - } - - function closeAlertPopup() { - document.getElementById("alert-popup").style.display = "none"; - } + submitCreateForm({ + url: "/users/bulk", + formId: "bulkusersform", + alertDiv: "alertBulkMessage", + modal: usersModal, + }); </script> </body> </html> From 700865d2b9ff80f75f334e410722d42ee8c7fecd Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Mon, 27 Nov 2023 14:36:14 +0300 Subject: [PATCH 03/11] update things, channels and groups Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/api/endpoint.go | 18 +-- ui/api/transport.go | 2 - ui/web/static/js/main.js | 122 +++++++++++++++++ ui/web/template/channel.html | 156 +++------------------- ui/web/template/channels.html | 243 +++++----------------------------- ui/web/template/group.html | 157 +++------------------- ui/web/template/groups.html | 233 +++++--------------------------- ui/web/template/thing.html | 206 ++++------------------------ ui/web/template/things.html | 178 +++++-------------------- 9 files changed, 293 insertions(+), 1022 deletions(-) diff --git a/ui/api/endpoint.go b/ui/api/endpoint.go index 52adf2d2c..327bff60e 100644 --- a/ui/api/endpoint.go +++ b/ui/api/endpoint.go @@ -707,8 +707,7 @@ func createThingEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -725,8 +724,7 @@ func createThingsEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -787,8 +785,7 @@ func updateThingEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -809,8 +806,7 @@ func updateThingTagsEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -827,8 +823,7 @@ func updateThingSecretEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -849,8 +844,7 @@ func updateThingOwnerEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": thingsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } diff --git a/ui/api/transport.go b/ui/api/transport.go index a2fcb6f53..bda2d8cf5 100644 --- a/ui/api/transport.go +++ b/ui/api/transport.go @@ -975,8 +975,6 @@ func decodeUserUpdate(_ context.Context, r *http.Request) (interface{}, error) { Metadata: data.Metadata, } - fmt.Println(req) - return req, nil } diff --git a/ui/web/static/js/main.js b/ui/web/static/js/main.js index 3f0d45b0e..068a5e63d 100644 --- a/ui/web/static/js/main.js +++ b/ui/web/static/js/main.js @@ -307,6 +307,56 @@ function updateTags(config) { }); } +function updateSecret(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validatePassword(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/secret`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateOwner(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}/owner`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateDescription(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + function attachEditRowListener(config) { for (const key in config.rows) { if (config.rows.hasOwnProperty(key)) { @@ -342,3 +392,75 @@ function attachEditRowListener(config) { } } } + +function fetchIndividualEntity(config) { + document.addEventListener("DOMContentLoaded", function () { + getEntities(config.item, ""); + infiniteScroll(config.item); + }); + + const input = document.getElementById(config.input); + + input.addEventListener("input", function (event) { + const itemSelect = document.getElementById(config.itemSelect); + if (event.target.value === "") { + itemSelect.innerHTML = `<option disabled>select a ${config.type}</option>`; + getEntities(config.item, ""); + infiniteScroll(config.item); + } else { + itemSelect.innerHTML = ""; + getEntities(config.item, event.target.value); + } + }); +} + +function getEntities(item, name) { + fetchData(item, name, 1); +} + +function infiniteScroll(item) { + var selectElement = document.getElementById("infiniteScroll"); + var singleOptionHeight = selectElement.querySelector("option").offsetHeight; + var selectBoxHeight = selectElement.offsetHeight; + var numOptionsBeforeLoad = 2; + var lastScrollTop = 0; + var currentPageNo = 1; + var currentScroll = 0; + + selectElement.addEventListener("scroll", function () { + var st = selectElement.scrollTop; + var totalHeight = + selectElement.querySelectorAll("option").length * singleOptionHeight; + + if (st > lastScrollTop) { + currentScroll = st + selectBoxHeight; + if ( + currentScroll + numOptionsBeforeLoad * singleOptionHeight >= + totalHeight + ) { + currentPageNo++; + fetchData(item, "", currentPageNo); + } + } + + lastScrollTop = st; + }); +} + +let limit = 5; +function fetchData(item, name, page) { + fetch(`/entities?item=${item}&limit=${limit}&name=${name}&page=${page}`, { + method: "GET", + }) + .then((response) => response.json()) + .then((data) => { + const selectElement = document.getElementById("infiniteScroll"); + data.data.forEach((entity) => { + const option = document.createElement("option"); + option.value = entity.id; + option.text = entity.name; + selectElement.appendChild(option); + }); + }) + .catch((error) => console.error("Error:", error)); +} diff --git a/ui/web/template/channel.html b/ui/web/template/channel.html index e1c7f4db5..7d9af95a0 100644 --- a/ui/web/template/channel.html +++ b/ui/web/template/channel.html @@ -56,7 +56,7 @@ <td> <button class="edit-btn" - onclick="editRow('name')" + id="edit-name" {{ if $disableButton }} @@ -69,13 +69,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('name')"> + <button class="save-btn" id="save-name"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('name')" - > + <button class="cancel-btn" id="cancel-name"> Cancel </button> </div> @@ -103,7 +100,7 @@ <td> <button class="edit-btn" - onclick="editRow('description')" + id="edit-description" {{ if $disableButton }} @@ -116,16 +113,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('description')" - > + <button class="save-btn" id="save-description"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('description')" - > + <button class="cancel-btn" id="cancel-description"> Cancel </button> </div> @@ -143,7 +134,7 @@ <td> <button class="edit-btn" - onclick="editRow('metadata')" + id="edit-metadata" {{ if $disableButton }} @@ -156,16 +147,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('metadata')" - > + <button class="save-btn" id="save-metadata"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('metadata')" - > + <button class="cancel-btn" id="cancel-metadata"> Cancel </button> </div> @@ -182,119 +167,18 @@ </div> {{ template "footer" }} <script> - function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; - } - - function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); - } - - function showButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; - } - function hideButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; - } - - function editRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - - //make the row editable - makeEditable(cell); - - //Hide edit button and show save and cancel buttons - showButtons(editBtn, saveCancelBtn); - } - - function cancelEditRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - errorMessage.textContent = ""; - - //Restore original values in the row - makeUneditable(cell); - - //show the edit button and hide the save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } - - function saveRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - //Get the updated value from the nameCell - const updatedValue = cell.textContent.trim(); - - //send the updated value to the server using a post request - let url; - let data; - if (field === "name" || field === "description") { - url = "/channels/{{.Channel.ID}}"; - data = { [field]: updatedValue }; - } else if (field === "metadata") { - try { - const metadata = JSON.parse(updatedValue); - url = "/channels/{{.Channel.ID}}"; - data = { [field]: metadata }; - } catch (error) { - errorMessage.textContent = "Metadata must be a valid JSON object!"; - return; - } - } - - if (url) { - errorMessage.textContent=""; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", + attachEditRowListener( + { + entity: "channels", + id: "{{ .Channel.ID }}", + rows: { + name:updateName, + description: updateDescription, + metadata:updateMetadata, }, - }) - .then((response) => { - if (response.ok) { - //make the row uneditable - cell.setAttribute("contenteditable", "false"); - - //show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } else { - throw new Error("Response not OK"); - } - }) - .catch((error) => { - //Restore original values in the row if there is an error - makeUneditable(cell); - - //show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - - console.error("Error", error); - errorMessage.textContent = "Entity already exists"; - }); - } else { - console.error("Invalid field:", field); - } - } + errorDiv: "error-message", + } + ); </script> </body> </html> diff --git a/ui/web/template/channels.html b/ui/web/template/channels.html index 3e5f3d768..f0079449f 100644 --- a/ui/web/template/channels.html +++ b/ui/web/template/channels.html @@ -40,10 +40,7 @@ <h5 class="modal-title" id="addChannelModalLabel"> </div> <div class="modal-body"> <div id="alertMessage"></div> - <form - method="post" - onsubmit="return submitChannelForm()" - > + <form method="post" id="channelform"> <div class="mb-3"> <label for="name" class="form-label">Name</label> <input @@ -52,8 +49,8 @@ <h5 class="modal-title" id="addChannelModalLabel"> name="name" id="name" placeholder="Channel Name" - required /> + <div id="nameError" class="text-danger"></div> </div> <div class="mb-3"> <label for="infiniteScroll" class="form-label"> @@ -65,7 +62,6 @@ <h5 class="modal-title" id="addChannelModalLabel"> name="parentFilter" id="parentFilter" placeholder="Filter by parent name" - oninput="fetchIndividualEntity(this.value, 'infiniteScroll')" /> <select class="form-select" @@ -102,15 +98,12 @@ <h5 class="modal-title" id="addChannelModalLabel"> <div id="metadataHelp" class="form-text"> Enter channel metadata in JSON format. </div> - <div - id="metadataError" - class="error-message" - ></div> + <div id="metadataError" class="text-danger"></div> </div> <button type="submit" class="btn body-button" - onclick="return validateForm()" + id="create-channel-button" > Submit </button> @@ -150,7 +143,7 @@ <h5 class="modal-title" id="addChannelsModalLabel"> <form method="post" enctype="multipart/form-data" - onsubmit="return submitBulkChannelsForm()" + id="bulkchannelsform" > <div class="form-group"> <label for="channelsFile"> @@ -255,45 +248,18 @@ <h5 class="modal-title" id="addChannelsModalLabel"> </div> {{ template "footer" }} <script> - function filterItem(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - const options = itemSelect.getElementsByTagName("option"); - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - const itemId = option.value; - const itemName = option.innerHTML; - const shouldShow = - itemId.includes(filterValue) || - itemName.toLowerCase().includes(filterValue.toLowerCase()); - option.style.display = shouldShow ? "" : "none"; - } - } - function validateForm() { - var name = document.getElementById("name").value; - var metadataInput = document.getElementById("metadata").value; - var nameError = document.getElementById("nameError"); - var metadataError = document.getElementById("metadataError"); - var metadata; - - nameError.innerHTML = ""; - metadataError.innerHTML = ""; + attachValidationListener({ + buttonId: "create-channel-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + }, + validations: { + name: validateName, + metadata: validateMetadata, + }, + }); - var isValid = true; - if (name === "") { - nameError.innerHTML = "Field Required!"; - isValid = false; - } - try { - metadata = JSON.parse(metadataInput); - } catch (error) { - metadataError.innerHTML = - "Please enter a valid metadaat in JSON format!"; - isValid = false; - } - - return isValid; - } const channelModal = new bootstrap.Modal( document.getElementById("addChannelModal"), ); @@ -304,173 +270,30 @@ <h5 class="modal-title" id="addChannelsModalLabel"> function openModal(modal) { if (modal === "single") { channelModal.show(); - getChannels(""); - getAdditionalChannels(); } else if (modal === "bulk") { channelsModal.show(); } } - function submitChannelForm() { - event.preventDefault(); - var form = event.target; - fetch("/channels", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 409) { - errorMessage = "channel already exists!"; - showAlert(errorMessage, "single"); - return false; - } else { - form.reset(); - channelModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("error submitting channel form: ", error); - return false; - }); - } - - function submitBulkChannelsForm() { - event.preventDefault(); - var form = event.target; - fetch("/channels/bulk", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - switch (response.status) { - case 409: - errorMessage = "channels already exist!"; - showAlert(errorMessage, "bulk"); - return false; - case 400: - errorMessage = "invalid file contents!"; - showAlert(errorMessage, "bulk"); - return false; - case 415: - errorMessage = "file must be csv!"; - showAlert(errorMessage, "bulk"); - return false; - case 500: - errorMessage = "try again!"; - showAlert(errorMessage, "bulk"); - return false; - default: - form.reset(); - channelsModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("Error submitting bulk channels form: ", error); - return false; - }); - } + submitCreateForm({ + url: "/channels", + formId: "channelform", + alertDiv: "alertMessage", + modal: channelModal, + }); - function showAlert(errorMessage, modal) { - if (modal === "single") { - var alertMessage = document.getElementById("alertMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } else if (modal === "bulk") { - var alertMessage = document.getElementById("alertBulkMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } - } + submitCreateForm({ + url: "/channels/bulk", + formId: "bulkchannelsform", + alertDiv: "alertBulkMessage", + modal: channelsModal, + }); - function closeAlertPopup() { - document.getElementById("alert-popup").style.display = "none"; - } - - function hideAlert() { - var alertElement = document.querySelector(".alert-danger"); - alertElement.style.display = "none"; - } - - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a channel</option>"; - getChannels(""); - getAdditionalChannels(); - } else { - itemSelect.innerHTML = ""; - getChannels(filterValue); - } - } - - let limit = 5; - - function getChannels(name) { - fetchData(name, 1); - } - - function getAdditionalChannels() { - var selectElement = document.getElementById("infiniteScroll"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - - function fetchData(name, page) { - fetch( - `/entities?item=channels&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("infiniteScroll"); - data.data.forEach((group) => { - const option = document.createElement("option"); - option.value = group.id; - option.text = group.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "parentFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); </script> </body> </html> diff --git a/ui/web/template/group.html b/ui/web/template/group.html index 6edbe74a3..ecca37d44 100644 --- a/ui/web/template/group.html +++ b/ui/web/template/group.html @@ -49,7 +49,7 @@ <td> <button class="edit-btn" - onclick="editRow('name')" + id="edit-name" {{ if $disableButton }} @@ -62,13 +62,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('name')"> + <button class="save-btn" id="save-name"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('name')" - > + <button class="cancel-btn" id="cancel-name"> Cancel </button> </div> @@ -91,7 +88,7 @@ <td> <button class="edit-btn" - onclick="editRow('description')" + id="edit-description" {{ if $disableButton }} @@ -104,16 +101,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('description')" - > + <button class="save-btn" id="save-description"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('description')" - > + <button class="cancel-btn" id="cancel-description"> Cancel </button> </div> @@ -131,7 +122,7 @@ <td> <button class="edit-btn" - onclick="editRow('metadata')" + id="edit-metadata" {{ if $disableButton }} @@ -144,16 +135,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('metadata')" - > + <button class="save-btn" id="save-metadata"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('metadata')" - > + <button class="cancel-btn" id="cancel-metadata"> Cancel </button> </div> @@ -170,120 +155,18 @@ </div> {{ template "footer" }} <script> - function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; - } - - function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); - } - - function showButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; - } - function hideButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; - } - - function editRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - - //make the row editable - makeEditable(cell); - - //Hide edit button and show save and cancel buttons - showButtons(editBtn, saveCancelBtn); - } - - function cancelEditRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - errorMessage.textContent = ""; - - //Restore original values in the row - makeUneditable(cell); - - //show the edit button and hide the save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } - - function saveRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - //Get the updated value from the nameCell - const updatedValue = cell.textContent.trim(); - - //send the updated value to the server using a post request - let url; - let data; - if (field === "name" || field === "description") { - url = "/groups/{{.Group.ID}}"; - data = { [field]: updatedValue }; - } else if (field === "metadata") { - try { - const metadata = JSON.parse(updatedValue); - url = "/groups/{{.Group.ID}}"; - data = { [field]: metadata }; - } catch (error) { - errorMessage.textContent = "Metadata must be a valid JSON object!"; - return; - } - } - - if (url) { - errorMessage.textContent=""; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", + attachEditRowListener( + { + entity: "groups", + id: "{{ .Group.ID }}", + rows: { + name:updateName, + description: updateDescription, + metadata:updateMetadata, }, - }) - .then((response) => { - if (response.ok) { - //make the row uneditable - cell.setAttribute("contenteditable", "false"); - - //show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } else { - throw new Error(response.statusText); - console.log(response.statusText); - } - }) - .catch((error) => { - //Restore original values in the row if there is an error - makeUneditable(cell); - - //show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - - console.error("Error", error); - errorMessage.textContent = "Entity already exists"; - }); - } else { - console.error("Invalid field:", field); - } - } + errorDiv: "error-message", + } + ); </script> </body> </html> diff --git a/ui/web/template/groups.html b/ui/web/template/groups.html index 6ce468fa7..da96eaf46 100644 --- a/ui/web/template/groups.html +++ b/ui/web/template/groups.html @@ -40,10 +40,7 @@ <h5 class="modal-title" id="addGroupModalLabel"> </div> <div class="modal-body"> <div id="alertMessage"></div> - <form - method="post" - onsubmit="return submitGroupForm()" - > + <form method="post" id="groupform"> <div class="mb-3"> <label for="name" class="form-label">Name</label> <input @@ -52,8 +49,8 @@ <h5 class="modal-title" id="addGroupModalLabel"> name="name" id="name" placeholder="Group Name" - required /> + <div id="nameError" class="text-danger"></div> </div> <div class="mb-3"> <label for="description" class="form-label"> @@ -77,7 +74,6 @@ <h5 class="modal-title" id="addGroupModalLabel"> name="parentFilter" id="parentFilter" placeholder="Filter by parent name" - oninput="fetchIndividualEntity(this.value, 'infiniteScroll')" /> <select class="form-select" @@ -102,15 +98,12 @@ <h5 class="modal-title" id="addGroupModalLabel"> <div id="metadataHelp" class="form-text"> Enter groups metadata in JSON format. </div> - <div - id="metadataError" - class="error-message" - ></div> + <div id="metadataError" class="text-danger"></div> </div> <button type="submit" class="btn body-button" - onclick="return validateForm()" + id="create-group-button" > Submit </button> @@ -146,9 +139,8 @@ <h5 class="modal-title" id="addGroupsModalLabel"> <div class="modal-body"> <div id="alertBulkMessage"></div> <form - method="post" enctype="multipart/form-data" - onsubmit="return submitBulkGroupForm()" + id="bulkgroupsform" > <div class="form-group"> <label for="groupsFile"> @@ -253,38 +245,17 @@ <h5 class="modal-title" id="addGroupsModalLabel"> {{ template "footer" }} <script> - function validateForm() { - var metadataInput = document.getElementById("metadata").value; - var metadataError = document.getElementById("metadataError"); - var metadata; - - metadataError.innerHTML = ""; - - var isValid = true; - try { - metadata = JSON.parse(metadataInput); - } catch (error) { - metadataError.innerHTML = - "Please enter a valid metadata in JSON format!"; - isValid = false; - } - - return isValid; - } - function filterItem(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - const options = itemSelect.getElementsByTagName("option"); - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - const itemId = option.value; - const itemName = option.innerHTML; - const shouldShow = - itemId.includes(filterValue) || - itemName.toLowerCase().includes(filterValue.toLowerCase()); - option.style.display = shouldShow ? "" : "none"; - } - } + attachValidationListener({ + buttonId: "create-group-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + }, + validations: { + name: validateName, + metadata: validateMetadata, + }, + }); const groupModal = new bootstrap.Modal( document.getElementById("addGroupModal"), @@ -296,168 +267,30 @@ <h5 class="modal-title" id="addGroupsModalLabel"> function openModal(modal) { if (modal === "single") { groupModal.show(); - getGroups(""); - getAdditionalGroups(); } else if (modal === "bulk") { groupsModal.show(); } } - function submitGroupForm() { - event.preventDefault(); - var form = event.target; - fetch("/groups", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 409) { - errorMessage = "group already exists!"; - showAlert(errorMessage, "single"); - return false; - } else { - form.reset(); - groupModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("error submitting group form: ", error); - return false; - }); - } - - function submitBulkGroupForm() { - event.preventDefault(); - var form = event.target; - fetch("/groups/bulk", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - switch (response.status) { - case 409: - errorMessage = "groups already exist!"; - showAlert(errorMessage, "bulk"); - return false; - case 400: - errorMessage = "invalid file contents!"; - showAlert(errorMessage, "bulk"); - return false; - case 415: - errorMessage = "file must be csv!"; - showAlert(errorMessage, "bulk"); - return false; - case 500: - errorMessage = "try again!"; - showAlert(errorMessage, "bulk"); - return false; - default: - form.reset(); - groupsModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("Error submitting bulk groups form: ", error); - return false; - }); - } - - function showAlert(errorMessage, modal) { - if (modal === "single") { - var alertMessage = document.getElementById("alertMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } else if (modal === "bulk") { - var alertMessage = document.getElementById("alertBulkMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } - } - - function closeAlertPopup() { - document.getElementById("alert-popup").style.display = "none"; - } + submitCreateForm({ + url: "/groups", + formId: "groupform", + alertDiv: "alertMessage", + modal: groupModal, + }); - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a group</option>"; - getGroups(""); - getAdditionalGroups(); - } else { - itemSelect.innerHTML = ""; - getGroups(filterValue); - } - } - - let limit = 5; - - function getGroups(name) { - fetchData(name, 1); - } + submitCreateForm({ + url: "/groups/bulk", + formId: "bulkgroupsform", + alertDiv: "alertBulkMessage", + modal: groupsModal, + }); - function getAdditionalGroups() { - var selectElement = document.getElementById("infiniteScroll"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - - function fetchData(name, page) { - fetch( - `/entities?item=groups&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("infiniteScroll"); - data.data.forEach((group) => { - const option = document.createElement("option"); - option.value = group.id; - option.text = group.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "parentFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); </script> </body> </html> diff --git a/ui/web/template/thing.html b/ui/web/template/thing.html index 1840bdf5f..c75ebb9c7 100644 --- a/ui/web/template/thing.html +++ b/ui/web/template/thing.html @@ -49,7 +49,7 @@ <td> <button class="edit-btn" - onclick="editRow('name')" + id="edit-name" {{ if $disableButton }} @@ -62,13 +62,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('name')"> + <button class="save-btn" id="save-name"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('name')" - > + <button class="cancel-btn" id="cancel-name"> Cancel </button> </div> @@ -91,7 +88,7 @@ <td> <button class="edit-btn" - onclick="editRow('secret')" + id="edit-secret" {{ if $disableButton }} @@ -104,16 +101,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('secret')" - > + <button class="save-btn" id="save-secret"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('secret')" - > + <button class="cancel-btn" id="cancel-secret"> Cancel </button> </div> @@ -131,7 +122,7 @@ <td> <button class="edit-btn" - onclick="editRow('tags')" + id="edit-tags" {{ if $disableButton }} @@ -144,13 +135,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('tags')"> + <button class="save-btn" id="save-tags"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('tags')" - > + <button class="cancel-btn" id="cancel-tags"> Cancel </button> </div> @@ -168,7 +156,7 @@ <td> <button class="edit-btn" - onclick="editRow('owner')" + id="edit-owner" {{ if $disableButton }} @@ -181,13 +169,10 @@ class="save-cancel-buttons" style="display: none" > - <button class="save-btn" onclick="saveRow('owner')"> + <button class="save-btn" id="save-owner"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('owner')" - > + <button class="cancel-btn" id="cancel-owner"> Cancel </button> </div> @@ -205,7 +190,7 @@ <td> <button class="edit-btn" - onclick="editRow('metadata')" + id="edit-metadata" {{ if $disableButton }} @@ -218,16 +203,10 @@ class="save-cancel-buttons" style="display: none" > - <button - class="save-btn" - onclick="saveRow('metadata')" - > + <button class="save-btn" id="save-metadata"> Save </button> - <button - class="cancel-btn" - onclick="cancelEditRow('metadata')" - > + <button class="cancel-btn" id="cancel-metadata"> Cancel </button> </div> @@ -244,149 +223,20 @@ </div> {{ template "footer" }} <script> - function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; - } - - function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); - } - - function showButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; - } - - function hideButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; - } - - function editRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - - // Make the row editable - makeEditable(cell); - - // Hide the edit button and show save and cancel buttons - showButtons(editBtn, saveCancelBtn); - } - - function saveRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - // Get the updated value from the nameCell - const updatedValue = cell.textContent.trim(); - - // Send the updated value to the server using a POST request - let url; - let data; - if (field === "name") { - url = "/things/{{.Thing.ID}}"; - data = { [field]: updatedValue }; - } else if (field == "metadata") { - try { - const metadata = JSON.parse(updatedValue); - url = "/things/{{.Thing.ID}}"; - data = { [field]: metadata }; - } catch (error) { - errorMessage.textContent = "Metadata must be a valid JSON object!"; - return; - } - } else if (field === "secret") { - if (updatedValue.length < 8) { - errorMessage.textContent = - "Secret must have a minimum of 8 characters!"; - return; - } - url = "/things/{{.Thing.ID}}/secret"; - data = { [field]: updatedValue }; - } else if (field === "tags") { - try { - const tags = JSON.parse(updatedValue); - if ( - !Array.isArray(tags) || - !tags.every(function (tag) { - return typeof tag === "string"; - }) - ) { - errorMessage.textContent = "Tags must be a string array!"; - return; - } - url = "/things/{{.Thing.ID}}/tags"; - data = { [field]: JSON.parse(updatedValue) }; - } catch (error) { - errorMessage.textContent = "Tags must be a valid string array"; - return; - } - } else if (field === "owner") { - url = "/things/{{.Thing.ID}}/owner"; - data = { [field]: updatedValue }; - } - - errorMessage.textContent = ""; - - if (url) { - errorMessage.textContent=""; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", + attachEditRowListener( + { + entity: "things", + id: "{{ .Thing.ID }}", + rows: { + name:updateName, + secret: updateSecret, + tags:updateTags, + owner: updateOwner, + metadata:updateMetadata, }, - }) - .then((response) => { - if (response.ok) { - // Make the row uneditable - cell.setAttribute("contenteditable", "false"); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } - }) - .catch((error) => { - // Restore original values in the row if there is an error - makeUneditable(cell); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - - console.error("Error", error); - errorMessage.textContent=error; - }); - } else { - console.error("Invalid field:", field); - } - } - - function cancelEditRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - errorMessage.textContent = ""; - - // Restore original values in the row - makeUneditable(cell); - - // Show the edit button and hide the save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } + errorDiv: "error-message", + } + ); </script> </body> </html> diff --git a/ui/web/template/things.html b/ui/web/template/things.html index 7bc5d336e..76dbc8b85 100644 --- a/ui/web/template/things.html +++ b/ui/web/template/things.html @@ -40,10 +40,7 @@ <h5 class="modal-title" id="addThingModalLabel"> </div> <div class="modal-body"> <div id="alertMessage"></div> - <form - method="post" - onsubmit="return submitThingForm()" - > + <form id="thingform"> <div class="mb-3"> <label for="name" class="form-label">Name</label> <input @@ -52,8 +49,8 @@ <h5 class="modal-title" id="addThingModalLabel"> name="name" id="name" placeholder="Device Name" - required /> + <div id="nameError" class="text-danger"></div> </div> <div class="mb-3"> <label for="thingID" class="form-label"> @@ -105,7 +102,7 @@ <h5 class="modal-title" id="addThingModalLabel"> <div id="tagHelp" class="form-text"> Enter device tags as a string slice. </div> - <div id="tagsError" class="error-message"></div> + <div id="tagsError" class="text-danger"></div> </div> <div class="mb-3"> <label for="metadata" class="form-label"> @@ -121,15 +118,12 @@ <h5 class="modal-title" id="addThingModalLabel"> <div id="metadataHelp" class="form-text"> Enter device metadata in JSON format. </div> - <div - id="metadataError" - class="error-message" - ></div> + <div id="metadataError" class="text-danger"></div> </div> <button type="submit" class="btn body-button" - onclick="return validateForm()" + id="create-thing-button" > Submit </button> @@ -169,7 +163,7 @@ <h5 class="modal-title" id="addThingsModalLabel"> <form method="post" enctype="multipart/form-data" - onsubmit="return submitBulkThingsForm()" + id="bulkThingsForm" > <div class="form-group"> <label for="thingsFile"> @@ -274,58 +268,19 @@ <h5 class="modal-title" id="addThingsModalLabel"> </div> {{ template "footer" }} <script> - function validateForm() { - var secret = document.getElementById("secret").value; - var tagsInput = document.getElementById("tags").value; - var metadataInput = document.getElementById("metadata").value; - - var secretError = document.getElementById("secretError"); - var tagsError = document.getElementById("tagsError"); - var metadataError = document.getElementById("metadataError"); - - var tags; - var metadata; - - secretError.innerHTML = ""; - tagsError.innerHTML = ""; - metadataError.innerHTML = ""; - - var isValid = true; - - if (secret.trim() != "") { - if (secret.length < 8) { - secretError.innerHTML = - "Secret must have a minimum of 8 characters"; - isValid = false; - } - } - try { - tags = JSON.parse(tagsInput); - } catch (error) { - tagsError.innerHTML = "Please enter valid tags as a string array!"; - isValid = false; - } - - if ( - !Array.isArray(tags) || - !tags.every(function (tag) { - return typeof tag === "string"; - }) - ) { - tagsError.innerHTML = "Tags must be a string array!"; - isValid = false; - } - - try { - metadata = JSON.parse(metadataInput); - } catch (error) { - metadataError.innerHTML = - "Please enter valid metadata in JSON format!"; - isValid = false; - } - - return isValid; - } + attachValidationListener({ + buttonId: "create-thing-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + tags: "tagsError", + }, + validations: { + name: validateName, + metadata: validateMetadata, + tags: validateTags, + }, + }); const thingModal = new bootstrap.Modal( document.getElementById("addThingModal"), @@ -342,90 +297,19 @@ <h5 class="modal-title" id="addThingsModalLabel"> } } - function submitThingForm() { - event.preventDefault(); - var form = event.target; - fetch("/things", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 409) { - errorMessage = "thing already exists!"; - showAlert(errorMessage, "single"); - return false; - } else { - form.reset(); - thingModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("error submitting thing form: ", error); - return false; - }); - } - - function submitBulkThingsForm() { - event.preventDefault(); - var form = event.target; - fetch("/things/bulk", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - switch (response.status) { - case 409: - errorMessage = "things already exist!"; - showAlert(errorMessage, "bulk"); - return false; - case 400: - errorMessage = "invalid file contents!"; - showAlert(errorMessage, "bulk"); - return false; - case 415: - errorMessage = "file must be csv!"; - showAlert(errorMessage, "bulk"); - return false; - case 500: - errorMessage = "try again!"; - showAlert(errorMessage, "bulk"); - return false; - default: - form.reset(); - thingsModal.hide(); - window.location.reload(); - return true; - } - }) - .catch((error) => { - console.error("Error submitting bulk things form: ", error); - return false; - }); - } + submitCreateForm({ + url: "/things", + formId: "thingform", + alertDiv: "alertMessage", + modal: thingModal, + }); - function showAlert(errorMessage, modal) { - if (modal === "single") { - var alertMessage = document.getElementById("alertMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } else if (modal === "bulk") { - var alertMessage = document.getElementById("alertBulkMessage"); - alertMessage.innerHTML = ` - <div id="alert-popup" class="alert alert-danger"> - <span class="close-button" onclick="closeAlertPopup()">×</span> - <h5 class="alert-messsage" id="alert-message">${errorMessage}</h5> - </div> `; - } - } - - function closeAlertPopup() { - document.getElementById("alert-popup").style.display = "none"; - } + submitCreateForm({ + url: "/things/bulk", + formId: "bulkThingsForm", + alertDiv: "alertBulkMessage", + modal: thingsModal, + }); </script> </body> </html> From 83a5d3255ed99642f31966c93957b1296dce1d05 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Mon, 27 Nov 2023 15:58:54 +0300 Subject: [PATCH 04/11] update entity dependency templates Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/template/bootstraps.html | 92 ++---------------------------- ui/web/template/channelgroups.html | 80 +++----------------------- ui/web/template/channelthings.html | 80 +++----------------------- ui/web/template/channelusers.html | 79 +++---------------------- ui/web/template/groupchannels.html | 80 +++----------------------- ui/web/template/groupusers.html | 79 +++---------------------- ui/web/template/thingchannels.html | 80 +++----------------------- ui/web/template/thingusers.html | 81 +++----------------------- ui/web/template/userchannels.html | 79 +++---------------------- ui/web/template/usergroups.html | 80 +++----------------------- ui/web/template/userthings.html | 83 +++------------------------ 11 files changed, 86 insertions(+), 807 deletions(-) diff --git a/ui/web/template/bootstraps.html b/ui/web/template/bootstraps.html index e8fe11a84..5e14dc3c6 100644 --- a/ui/web/template/bootstraps.html +++ b/ui/web/template/bootstraps.html @@ -62,7 +62,6 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> name="thingFilter" id="thingFilter" placeholder="Filter by Thing name" - oninput="fetchIndividualEntity(this.value,'infiniteScroll')" /> <select class="form-select" @@ -276,20 +275,6 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> return isValid; } - function filterItem(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - const options = itemSelect.getElementsByTagName("option"); - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - const itemId = option.value; - const itemName = option.innerHTML; - const shouldShow = - itemId.includes(filterValue) || - itemName.toLowerCase().includes(filterValue.toLowerCase()); - option.style.display = shouldShow ? "" : "none"; - } - } const bootstrapsModal = new bootstrap.Modal( document.getElementById("addBootstrapModal"), @@ -297,80 +282,13 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> function openModal() { bootstrapsModal.show(); - getThings(""); - getAdditionalThings(); - } - - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a group</option>"; - getThings(""); - getAdditionalThings(); - } else { - itemSelect.innerHTML = ""; - getThings(filterValue); - } - } - - let limit = 5; - - function getThings(name) { - fetchData(name, 1); } - function getAdditionalThings() { - var selectElement = document.getElementById("infiniteScroll"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - - function fetchData(name, page) { - fetch( - `/entities?item=things&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("infiniteScroll"); - data.data.forEach((group) => { - const option = document.createElement("option"); - option.value = group.id; - option.text = group.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "infiniteScroll", + item: "things", + }); </script> </body> </html> diff --git a/ui/web/template/channelgroups.html b/ui/web/template/channelgroups.html index 3946a72a0..eea84035b 100644 --- a/ui/web/template/channelgroups.html +++ b/ui/web/template/channelgroups.html @@ -75,20 +75,19 @@ <h4>Channel Groups</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="groups" class="form-label"> + <label for="infiniteScroll" class="form-label"> Group ID </label> <input type="text" name="groupFilter" - id="groupFilterAssign" + id="groupFilter" placeholder="Filter by Group ID" - oninput="fetchIndividualEntity(this.value,'groups')" /> <select class="form-select" name="groupID" - id="groups" + id="infiniteScroll" size="5" required > @@ -191,76 +190,13 @@ <h4>Channel Groups</h4> ); function openGroupModal() { groupModal.show(); - getGroup(""); - getAdditionalGroups(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a group</option>"; - getGroup(""); - getAdditionalGroups(); - } else { - itemSelect.innerHTML = ""; - getGroup(filterValue); - } - } - let limit = 5; - - function getGroup(name) { - fetchGroupData(name, 1); - } - function getAdditionalGroups() { - var selectElement = document.getElementById("groups"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchGroupData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchGroupData(name, page) { - fetch( - `/entities?item=groups&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("groups"); - data.data.forEach((group) => { - const option = document.createElement("option"); - option.value = group.id; - option.text = group.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "groupFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); </script> </body> </html> diff --git a/ui/web/template/channelthings.html b/ui/web/template/channelthings.html index 8b86c57a5..1334325c4 100644 --- a/ui/web/template/channelthings.html +++ b/ui/web/template/channelthings.html @@ -75,20 +75,19 @@ <h4>Channel Things</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="things" class="form-label"> + <label for="infiniteScroll" class="form-label"> Thing ID </label> <input type="text" name="thingFilter" - id="thingFilterAssign" + id="thingFilter" placeholder="Filter by Thing ID" - oninput="fetchIndividualEntity(this.value,'things')" /> <select class="form-select" name="thingID" - id="things" + id="infiniteScroll" size="5" required > @@ -191,76 +190,13 @@ <h4>Channel Things</h4> ); function openThingModal() { thingModal.show(); - getThing(""); - getAdditionalThings(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a thing</option>"; - getThing(""); - getAdditionalThings(); - } else { - itemSelect.innerHTML = ""; - getThing(filterValue); - } - } - let limit = 5; - - function getThing(name) { - fetchThingData(name, 1); - } - function getAdditionalThings() { - var selectElement = document.getElementById("things"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchThingData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchThingData(name, page) { - fetch( - `/entities?item=things&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("things"); - data.data.forEach((thing) => { - const option = document.createElement("option"); - option.value = thing.id; - option.text = thing.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "infiniteScroll", + item: "things", + }); </script> </body> </html> diff --git a/ui/web/template/channelusers.html b/ui/web/template/channelusers.html index 43397599d..c66619721 100644 --- a/ui/web/template/channelusers.html +++ b/ui/web/template/channelusers.html @@ -72,20 +72,19 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> > <div class="modal-body"> <div class="mb-3"> - <label for="users" class="form-label"> + <label for="infiniteScroll" class="form-label"> User ID </label> <input type="text" name="userFilter" - id="userFilterAssign" + id="userFilter" placeholder="Filter by User ID" - oninput="fetchIndividualEntity(this.value,'users')" /> <select class="form-select" name="userID" - id="users" + id="infiniteScroll" size="5" required > @@ -520,76 +519,14 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> ); function openUserModal() { userModal.show(); - getUser(""); - getAdditionalUsers(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a user</option>"; - getUser(""); - getAdditionalUsers(); - } else { - itemSelect.innerHTML = ""; - getUser(filterValue); - } - } - let limit = 5; - - function getUser(name) { - fetchUserData(name, 1); - } - function getAdditionalUsers() { - var selectElement = document.getElementById("users"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchUserData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchUserData(name, page) { - fetch( - `/entities?item=users&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("users"); - data.data.forEach((user) => { - const option = document.createElement("option"); - option.value = user.id; - option.text = user.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } function openTab(relation) { event.preventDefault(); var channelID = '{{.ChannelID}}'; diff --git a/ui/web/template/groupchannels.html b/ui/web/template/groupchannels.html index 2a7f37d04..6973610c8 100644 --- a/ui/web/template/groupchannels.html +++ b/ui/web/template/groupchannels.html @@ -68,20 +68,19 @@ <h4>Group Channels</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="channels" class="form-label"> + <label for="infiniteScroll" class="form-label"> Channel ID </label> <input type="text" name="channelFilter" - id="channelFilterAssign" + id="channelFilter" placeholder="Filter by Channel ID" - oninput="fetchIndividualEntity(this.value,'channels')" /> <select class="form-select" name="channelID" - id="channels" + id="infiniteScroll" size="5" required > @@ -184,76 +183,13 @@ <h4>Group Channels</h4> ); function openChannelModal() { channelModal.show(); - getChannel(""); - getAdditionalChannels(""); } - function fetchIndividualGroup(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a channel</option>"; - getChannel(""); - getAdditionalChannels(); - } else { - itemSelect.innerHTML = ""; - getChannel(filterValue); - } - } - let limit = 5; - - function getChannel(name) { - fetchChannelData(name, 1); - } - function getAdditionalChannels() { - var selectElement = document.getElementById("channels"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchChannelData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchChannelData(name, page) { - fetch( - `/entities?item=channels&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("channels"); - data.data.forEach((channel) => { - const option = document.createElement("option"); - option.value = channel.id; - option.text = channel.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); </script> </body> </html> diff --git a/ui/web/template/groupusers.html b/ui/web/template/groupusers.html index 11903623f..ea72a05d9 100644 --- a/ui/web/template/groupusers.html +++ b/ui/web/template/groupusers.html @@ -65,20 +65,19 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> > <div class="modal-body"> <div class="mb-3"> - <label for="users" class="form-label"> + <label for="infiniteScroll" class="form-label"> User ID </label> <input type="text" name="userFilter" - id="userFilterAssign" + id="userFilter" placeholder="Filter by User ID" - oninput="fetchIndividualEntity(this.value,'users')" /> <select class="form-select" name="userID" - id="users" + id="infiniteScroll" size="5" required > @@ -514,76 +513,14 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> ); function openUserModal() { userModal.show(); - getUser(""); - getAdditionalUsers(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a user</option>"; - getUser(""); - getAdditionalUsers(); - } else { - itemSelect.innerHTML = ""; - getUser(filterValue); - } - } - let limit = 5; - - function getUser(name) { - fetchUserData(name, 1); - } - function getAdditionalUsers() { - var selectElement = document.getElementById("users"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchUserData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchUserData(name, page) { - fetch( - `/entities?item=users&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("users"); - data.data.forEach((user) => { - const option = document.createElement("option"); - option.value = user.id; - option.text = user.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } function openTab(relation) { event.preventDefault(); var groupID = '{{.GroupID}}'; diff --git a/ui/web/template/thingchannels.html b/ui/web/template/thingchannels.html index 44a751333..774b5b417 100644 --- a/ui/web/template/thingchannels.html +++ b/ui/web/template/thingchannels.html @@ -68,20 +68,19 @@ <h4>Thing Channels</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="channels" class="form-label"> + <label for="infiniteScroll" class="form-label"> Channel ID </label> <input type="text" name="channelFilter" - id="channelFilterAssign" + id="channelFilter" placeholder="Filter by Channel ID" - oninput="fetchIndividualEntity(this.value,'channels')" /> <select class="form-select" name="channelID" - id="channels" + id="infiniteScroll" size="5" required > @@ -265,76 +264,13 @@ <h4>Thing Channels</h4> ); function openChannelModal() { channelModal.show(); - getChannel(""); - getAdditionalChannels(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a channel</option>"; - getChannel(""); - getAdditionalChannels(); - } else { - itemSelect.innerHTML = ""; - getChannel(filterValue); - } - } - let limit = 5; - - function getChannel(name) { - fetchChannelData(name, 1); - } - function getAdditionalChannels() { - var selectElement = document.getElementById("channels"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchChannelData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchChannelData(name, page) { - fetch( - `/entities?item=channels&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("channels"); - data.data.forEach((channel) => { - const option = document.createElement("option"); - option.value = channel.id; - option.text = channel.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); </script> </body> </html> diff --git a/ui/web/template/thingusers.html b/ui/web/template/thingusers.html index a5d0c02d8..72038a9c0 100644 --- a/ui/web/template/thingusers.html +++ b/ui/web/template/thingusers.html @@ -65,20 +65,19 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> > <div class="modal-body"> <div class="mb-3"> - <label for="users" class="form-label"> + <label for="infiniteScroll" class="form-label"> User ID </label> <input type="text" name="userFilter" - id="userFilterAssign" + id="userFilter" placeholder="Filter by User ID" - oninput="fetchIndividualEntity(this.value,'users')" /> <select class="form-select" name="userID" - id="users" + id="infiniteScroll" size="5" required > @@ -209,76 +208,12 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> ); function openUserModal() { userModal.show(); - getUser(""); - getAdditionalUsers(""); - } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a user</option>"; - getUser(""); - getAdditionalUsers(); - } else { - itemSelect.innerHTML = ""; - getUser(filterValue); - } - } - let limit = 5; - - function getUser(name) { - fetchUserData(name, 1); - } - function getAdditionalUsers() { - var selectElement = document.getElementById("users"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchUserData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchUserData(name, page) { - fetch( - `/entities?item=users&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("users"); - data.data.forEach((user) => { - const option = document.createElement("option"); - option.value = user.id; - option.text = user.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); } + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); </script> </body> </html> diff --git a/ui/web/template/userchannels.html b/ui/web/template/userchannels.html index 466bc77e5..0be2dd44c 100644 --- a/ui/web/template/userchannels.html +++ b/ui/web/template/userchannels.html @@ -75,20 +75,19 @@ <h4>User Channels</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="channels" class="form-label"> + <label for="infiniteScroll" class="form-label"> Channel ID </label> <input type="text" name="channelFilter" - id="channelFilterAssign" + id="channelFilter" placeholder="Filter by Channel ID" - oninput="fetchIndividualEntity(this.value,'channels')" /> <select class="form-select" name="channelID" - id="channels" + id="infiniteScroll" size="5" required > @@ -523,76 +522,14 @@ <h4>User Channels</h4> ); function openChannelModal() { channelModal.show(); - getChannel(""); - getAdditionalChannels(""); } - function fetchIndividualGroup(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a channel</option>"; - getChannel(""); - getAdditionalChannels(); - } else { - itemSelect.innerHTML = ""; - getChannel(filterValue); - } - } - let limit = 5; - - function getChannel(name) { - fetchChannelData(name, 1); - } - function getAdditionalChannels() { - var selectElement = document.getElementById("channels"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchChannelData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchChannelData(name, page) { - fetch( - `/entities?item=channels&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("channels"); - data.data.forEach((channel) => { - const option = document.createElement("option"); - option.value = channel.id; - option.text = channel.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } function openTab(relation) { event.preventDefault(); var userID = '{{.UserID}}'; diff --git a/ui/web/template/usergroups.html b/ui/web/template/usergroups.html index d294c3e47..16f9f3cd5 100644 --- a/ui/web/template/usergroups.html +++ b/ui/web/template/usergroups.html @@ -75,20 +75,19 @@ <h4>User Groups</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="groups" class="form-label"> + <label for="infiniteScroll" class="form-label"> Group ID </label> <input type="text" name="groupFilter" - id="groupFilterAssign" + id="groupFilter" placeholder="Filter by Group ID" - oninput="fetchIndividualEntity(this.value,'groups')" /> <select class="form-select" name="groupID" - id="groups" + id="infiniteScroll" size="5" required > @@ -524,76 +523,13 @@ <h4>User Groups</h4> ); function openGroupModal() { groupModal.show(); - getGroup(""); - getAdditionalGroups(""); } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a group</option>"; - getGroup(""); - getAdditionalGroups(); - } else { - itemSelect.innerHTML = ""; - getGroup(filterValue); - } - } - let limit = 5; - - function getGroup(name) { - fetchGroupData(name, 1); - } - function getAdditionalGroups() { - var selectElement = document.getElementById("groups"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchGroupData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchGroupData(name, page) { - fetch( - `/entities?item=groups&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("groups"); - data.data.forEach((group) => { - const option = document.createElement("option"); - option.value = group.id; - option.text = group.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); - } + fetchIndividualEntity({ + input: "groupFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); function openTab(relation) { event.preventDefault(); diff --git a/ui/web/template/userthings.html b/ui/web/template/userthings.html index 984f85460..afcd4c842 100644 --- a/ui/web/template/userthings.html +++ b/ui/web/template/userthings.html @@ -21,7 +21,7 @@ User </a> <a - href="/users/{{ .UserID }}/things" + href="/users/{{ .UserID }}/groups" type="button" class="btn body-button" > @@ -75,20 +75,19 @@ <h4>User Things</h4> > <div class="modal-body"> <div class="mb-3"> - <label for="things" class="form-label"> + <label for="infiniteScroll" class="form-label"> Thing ID </label> <input type="text" name="thingFilter" - id="thingFilterAssign" + id="thingFilter" placeholder="Filter by Thing ID" - oninput="fetchIndividualEntity(this.value,'things')" /> <select class="form-select" name="thingID" - id="things" + id="infiniteScroll" size="5" required > @@ -219,76 +218,12 @@ <h4>User Things</h4> ); function openThingModal() { thingModal.show(); - getThing(""); - getAdditionalThings(""); - } - function fetchIndividualEntity(filterValue, selectId) { - const itemSelect = document.getElementById(selectId); - if (filterValue === "") { - itemSelect.innerHTML = "<option disabled>select a thing</option>"; - getThing(""); - getAdditionalThings(); - } else { - itemSelect.innerHTML = ""; - getThing(filterValue); - } - } - let limit = 5; - - function getThing(name) { - fetchThingData(name, 1); - } - function getAdditionalThings() { - var selectElement = document.getElementById("things"); - var singleOptionHeight = - selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", onselectScroll); - - function onselectScroll(event) { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * - singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchThingData("", currentPageNo); - } - } - - lastScrollTop = st; - } - } - function fetchThingData(name, page) { - fetch( - `/entities?item=things&limit=${limit}&name=${name}&page=${page}`, - { - method: "GET", - }, - ) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("things"); - data.data.forEach((thing) => { - const option = document.createElement("option"); - option.value = thing.id; - option.text = thing.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); } + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "things", + item: "things", + }); </script> </body> </html> From f0f8badcd7683ab01da36ac7eaf25b94e371daad Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Mon, 27 Nov 2023 20:00:43 +0300 Subject: [PATCH 05/11] update formatting to 2 spaces Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- .prettierrc | 12 +- ui/web/template/bootstrap.html | 378 ++++------- ui/web/template/bootstraps.html | 513 +++++++------- ui/web/template/channel.html | 295 ++++----- ui/web/template/channelgroups.html | 364 +++++----- ui/web/template/channels.html | 280 ++++---- ui/web/template/channelthings.html | 364 +++++----- ui/web/template/channelusers.html | 994 +++++++++++++-------------- ui/web/template/error.html | 42 +- ui/web/template/footer.html | 29 +- ui/web/template/group.html | 267 ++++---- ui/web/template/groupchannels.html | 352 +++++----- ui/web/template/groups.html | 300 ++++----- ui/web/template/groupusers.html | 982 +++++++++++++-------------- ui/web/template/header.html | 40 +- ui/web/template/index.html | 174 ++--- ui/web/template/login.html | 335 +++++----- ui/web/template/messagesread.html | 247 ++++--- ui/web/template/navbar.html | 369 +++++------ ui/web/template/resetpassword.html | 168 +++-- ui/web/template/tablefooter.html | 114 ++-- ui/web/template/tableheader.html | 82 +-- ui/web/template/terminal.html | 108 +-- ui/web/template/thing.html | 381 +++++------ ui/web/template/thingchannels.html | 502 +++++++------- ui/web/template/things.html | 304 ++++----- ui/web/template/thingusers.html | 393 +++++------ ui/web/template/updatepassword.html | 207 +++--- ui/web/template/user.html | 344 ++++------ ui/web/template/userchannels.html | 995 +++++++++++++--------------- ui/web/template/usergroups.html | 995 +++++++++++++--------------- ui/web/template/users.html | 282 ++++---- ui/web/template/userthings.html | 406 +++++------- 33 files changed, 5357 insertions(+), 6261 deletions(-) diff --git a/.prettierrc b/.prettierrc index d5e92c768..6ceed361a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,20 +1,16 @@ { - "plugins": [ - "prettier-plugin-go-template" - ], + "plugins": ["prettier-plugin-go-template"], "overrides": [ { - "files": [ - "*.html" - ], + "files": ["*.html"], "options": { "parser": "go-template" } } ], "goTemplateBracketSpacing": true, - "useTabs": true, - "printWidth": 80, + "useTabs": false, + "printWidth": 100, "semi": true, "tabWidth": 2, "jsxSingleQuote": false, diff --git a/ui/web/template/bootstrap.html b/ui/web/template/bootstrap.html index e89ec34c9..61a6cb1e5 100644 --- a/ui/web/template/bootstrap.html +++ b/ui/web/template/bootstrap.html @@ -2,241 +2,145 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "bootstrap" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <button - type="button" - class="btn body-button" - onclick="location.href='/bootstraps/{{ .Bootstrap.ThingID }}/terminal'" - > - Remote Terminal - </button> - </div> - <div class="table-responsive table-container"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="row">Bootstrap</th> - </tr> - </thead> - <tbody> - <tr> - <th>Name</th> - <td - class="editable" - contenteditable="false" - data-field="name" - > - {{ .Bootstrap.Name }} - </td> - <td> - <button class="edit-btn" onclick="editRow('name')"> - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" onclick="saveRow('name')"> - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('name')" - > - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Content</th> - <td - class="editable" - contenteditable="false" - data-field="content" - > - {{ .Bootstrap.Content }} - </td> - <td> - <button class="edit-btn" onclick="editRow('content')"> - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button - class="save-btn" - onclick="saveRow('content')" - > - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('content')" - > - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Channels</th> - <td - class="editable" - contenteditable="false" - data-field="channels" - > - {{ toSlice .Bootstrap.Channels }} - </td> - <td> - <button - class="edit-btn" - onclick="editRow('channels')" - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button - class="save-btn" - onclick="saveRow('channels')" - > - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('channels')" - > - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Client Cert</th> - <td - class="editable" - contenteditable="false" - data-field="clientCert" - > - {{ .Bootstrap.ClientCert }} - </td> - <td> - <button - class="edit-btn" - onclick="editRow('clientCert')" - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button - class="save-btn" - onclick="saveRow('clientCert')" - > - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('clientCert')" - > - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Client Key</th> - <td - class="editable" - contenteditable="false" - data-field="clientKey" - > - {{ .Bootstrap.ClientKey }} - </td> - <td> - <button - class="edit-btn" - onclick="editRow('clientKey')" - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button - class="save-btn" - onclick="saveRow('clientKey')" - > - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('clientKey')" - > - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>CA Cert</th> - <td - class="editable" - contenteditable="false" - data-field="CACert" - > - {{ .Bootstrap.CACert }} - </td> - <td> - <button class="edit-btn" onclick="editRow('CACert')"> - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button - class="save-btn" - onclick="saveRow('CACert')" - > - Save - </button> - <button - class="cancel-btn" - onclick="cancelEditRow('CACert')" - > - Cancel - </button> - </div> - </td> - </tr> - </tbody> - </table> - <div id="error-message" class="text-danger"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <button + type="button" + class="btn body-button" + onclick="location.href='/bootstraps/{{ .Bootstrap.ThingID }}/terminal'" + > + Remote Terminal + </button> + </div> + <div class="table-responsive table-container"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="row">Bootstrap</th> + </tr> + </thead> + <tbody> + <tr> + <th>Name</th> + <td class="editable" contenteditable="false" data-field="name"> + {{ .Bootstrap.Name }} + </td> + <td> + <button class="edit-btn" onclick="editRow('name')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('name')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('name')"> + Cancel + </button> + </div> + </td> + </tr> + <tr> + <th>Content</th> + <td class="editable" contenteditable="false" data-field="content"> + {{ .Bootstrap.Content }} + </td> + <td> + <button class="edit-btn" onclick="editRow('content')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('content')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('content')"> + Cancel + </button> + </div> + </td> + </tr> + <tr> + <th>Channels</th> + <td class="editable" contenteditable="false" data-field="channels"> + {{ toSlice .Bootstrap.Channels }} + </td> + <td> + <button class="edit-btn" onclick="editRow('channels')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('channels')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('channels')"> + Cancel + </button> + </div> + </td> + </tr> + <tr> + <th>Client Cert</th> + <td class="editable" contenteditable="false" data-field="clientCert"> + {{ .Bootstrap.ClientCert }} + </td> + <td> + <button class="edit-btn" onclick="editRow('clientCert')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('clientCert')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('clientCert')"> + Cancel + </button> + </div> + </td> + </tr> + <tr> + <th>Client Key</th> + <td class="editable" contenteditable="false" data-field="clientKey"> + {{ .Bootstrap.ClientKey }} + </td> + <td> + <button class="edit-btn" onclick="editRow('clientKey')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('clientKey')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('clientKey')"> + Cancel + </button> + </div> + </td> + </tr> + <tr> + <th>CA Cert</th> + <td class="editable" contenteditable="false" data-field="CACert"> + {{ .Bootstrap.CACert }} + </td> + <td> + <button class="edit-btn" onclick="editRow('CACert')"> + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" onclick="saveRow('CACert')">Save</button> + <button class="cancel-btn" onclick="cancelEditRow('CACert')"> + Cancel + </button> + </div> + </td> + </tr> + </tbody> + </table> + <div id="error-message" class="text-danger"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> function makeEditable(cell) { cell.setAttribute("contenteditable", "true"); @@ -376,6 +280,6 @@ hideButtons(editBtn, saveCancelBtn); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/bootstraps.html b/ui/web/template/bootstraps.html index 5e14dc3c6..1e0f7e70e 100644 --- a/ui/web/template/bootstraps.html +++ b/ui/web/template/bootstraps.html @@ -2,294 +2,253 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "bootstraps" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <button - type="button" - class="btn body-button" - onclick="openModal()" - > - Add - </button> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <button type="button" class="btn body-button" onclick="openModal()">Add</button> - <!-- Modal --> - <div - class="modal fade" - id="addBootstrapModal" - tabindex="-1" - role="dialog" - aria-labelledby="addBootstrapModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addBootstrapModalLabel"> - Add Bootstrap Config - </h5> - </div> - <div class="modal-body"> - <form method="post" onsubmit="return validateForm()"> - <div class="mb-3"> - <label for="name" class="form-label"> - Bootstrap Name - </label> - <input - type="text" - class="form-control" - name="name" - id="name" - aria-describedby="tagHelp" - placeholder="Name" - /> - </div> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Thing - </label> - <input - type="text" - class="itemsFilter" - name="thingFilter" - id="thingFilter" - placeholder="Filter by Thing name" - /> - <select - class="form-select" - name="thingID" - id="infiniteScroll" - required - > - <option disabled>select a group</option> - </select> - </div> - <div class="mb-3"> - <label for="externalID" class="form-label"> - External ID - </label> - <input - type="text" - class="form-control" - name="externalID" - id="externalID" - placeholder="External ID" - required - /> - </div> - <div class="mb-3"> - <label for="externalKey" class="form-label"> - External Key - </label> - <input - type="text" - class="form-control" - name="externalKey" - id="externalKey" - placeholder="External Key" - required - /> - </div> - <div class="mb-3"> - <label for="channels" class="form-label"> - Channels - </label> - <input - type="text" - class="form-control" - name="channels" - id="channels" - value="[]" - required - /> - <div id="channelsHelp" class="form-text"> - Enter channels as a string slice. - </div> - <div - id="channelsError" - class="error-message" - ></div> - </div> + <!-- Modal --> + <div + class="modal fade" + id="addBootstrapModal" + tabindex="-1" + role="dialog" + aria-labelledby="addBootstrapModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addBootstrapModalLabel"> + Add Bootstrap Config + </h5> + </div> + <div class="modal-body"> + <form method="post" onsubmit="return validateForm()"> + <div class="mb-3"> + <label for="name" class="form-label">Bootstrap Name</label> + <input + type="text" + class="form-control" + name="name" + id="name" + aria-describedby="tagHelp" + placeholder="Name" + /> + </div> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Thing</label> + <input + type="text" + class="itemsFilter" + name="thingFilter" + id="thingFilter" + placeholder="Filter by Thing name" + /> + <select + class="form-select" + name="thingID" + id="infiniteScroll" + required + > + <option disabled>select a group</option> + </select> + </div> + <div class="mb-3"> + <label for="externalID" class="form-label">External ID</label> + <input + type="text" + class="form-control" + name="externalID" + id="externalID" + placeholder="External ID" + required + /> + </div> + <div class="mb-3"> + <label for="externalKey" class="form-label">External Key</label> + <input + type="text" + class="form-control" + name="externalKey" + id="externalKey" + placeholder="External Key" + required + /> + </div> + <div class="mb-3"> + <label for="channels" class="form-label">Channels</label> + <input + type="text" + class="form-control" + name="channels" + id="channels" + value="[]" + required + /> + <div id="channelsHelp" class="form-text"> + Enter channels as a string slice. + </div> + <div id="channelsError" class="error-message"></div> + </div> - <div class="mb-3"> - <label for="content" class="form-label"> - Content - </label> - <input - type="text" - class="form-control" - name="content" - id="content" - value="{}" - /> - <div id="contentHelp" class="form-text"> - Enter content in JSON format. - </div> - <div - id="contentError" - class="error-message" - ></div> - </div> - <div class="mb-3"> - <label for="clientCert" class="form-label"> - Client Cert - </label> - <input - type="text" - class="form-control" - name="clientCert" - id="clientCert" - aria-describedby="tagHelp" - value="clientCert" - /> - </div> - <div class="mb-3"> - <label for="clientKey" class="form-label"> - Client Key - </label> - <input - type="text" - class="form-control" - name="clientKey" - id="clientKey" - aria-describedby="tagHelp" - value="clientKey" - /> - </div> - <div class="mb-3"> - <label for="CACert" class="form-label"> - CA Cert - </label> - <input - type="text" - class="form-control" - name="CACert" - id="CACert" - aria-describedby="tagHelp" - value="CACert" - /> - </div> - <button type="submit" class="btn body-button"> - Submit - </button> - </form> - </div> - </div> - </div> - </div> - </div> - <div class="table-responsive table-container"> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">Thing ID</th> - <th scope="col">External ID</th> - </tr> - </thead> - <tbody> - {{ range $i, $t := .Bootstraps }} - <tr> - <td>{{ $t.Name }}</td> - <td> - <div class="copy-con-container"> - <a - href="{{ printf "/bootstraps/%s" $t.ThingID }}" - > - {{ $t.ThingID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td>{{ $t.ExternalID }}</td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - function validateForm() { - var channelsInput = document.getElementById("channels").value; - var contentInput = document.getElementById("content").value; + <div class="mb-3"> + <label for="content" class="form-label">Content</label> + <input + type="text" + class="form-control" + name="content" + id="content" + value="{}" + /> + <div id="contentHelp" class="form-text"> + Enter content in JSON format. + </div> + <div id="contentError" class="error-message"></div> + </div> + <div class="mb-3"> + <label for="clientCert" class="form-label">Client Cert</label> + <input + type="text" + class="form-control" + name="clientCert" + id="clientCert" + aria-describedby="tagHelp" + value="clientCert" + /> + </div> + <div class="mb-3"> + <label for="clientKey" class="form-label">Client Key</label> + <input + type="text" + class="form-control" + name="clientKey" + id="clientKey" + aria-describedby="tagHelp" + value="clientKey" + /> + </div> + <div class="mb-3"> + <label for="CACert" class="form-label">CA Cert</label> + <input + type="text" + class="form-control" + name="CACert" + id="CACert" + aria-describedby="tagHelp" + value="CACert" + /> + </div> + <button type="submit" class="btn body-button">Submit</button> + </form> + </div> + </div> + </div> + </div> + </div> + <div class="table-responsive table-container"> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Thing ID</th> + <th scope="col">External ID</th> + </tr> + </thead> + <tbody> + {{ range $i, $t := .Bootstraps }} + <tr> + <td>{{ $t.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/bootstraps/%s" $t.ThingID }}"> + {{ $t.ThingID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td>{{ $t.ExternalID }}</td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + function validateForm() { + var channelsInput = document.getElementById("channels").value; + var contentInput = document.getElementById("content").value; - var channelsError = document.getElementById("channelsError"); - var contentError = document.getElementById("contentError"); + var channelsError = document.getElementById("channelsError"); + var contentError = document.getElementById("contentError"); - channelsError.innerHTML = ""; - contentError.innerHTML = ""; + channelsError.innerHTML = ""; + contentError.innerHTML = ""; - var content; - var channels; + var content; + var channels; - var isValid = true; + var isValid = true; - try { - channels = JSON.parse(channelsInput); - } catch (error) { - channelsError.innerHTML = - "Please enter valid channels as a string array!"; - isValid = false; - } + try { + channels = JSON.parse(channelsInput); + } catch (error) { + channelsError.innerHTML = "Please enter valid channels as a string array!"; + isValid = false; + } - if ( - !Array.isArray(channels) || - !channels.every(function (channels) { - return typeof channels === "string"; - }) - ) { - channelsError.innerHTML = "Channels must be a string array!"; - isValid = false; - } + if ( + !Array.isArray(channels) || + !channels.every(function (channels) { + return typeof channels === "string"; + }) + ) { + channelsError.innerHTML = "Channels must be a string array!"; + isValid = false; + } - try { - content = JSON.parse(contentInput); - content = contentInput; - } catch (error) { - contentError.innerHTML = - "Please enter valid content in JSON format!"; - isValid = false; - } + try { + content = JSON.parse(contentInput); + content = contentInput; + } catch (error) { + contentError.innerHTML = "Please enter valid content in JSON format!"; + isValid = false; + } - return isValid; - } + return isValid; + } - const bootstrapsModal = new bootstrap.Modal( - document.getElementById("addBootstrapModal"), - ); + const bootstrapsModal = new bootstrap.Modal(document.getElementById("addBootstrapModal")); - function openModal() { - bootstrapsModal.show(); - } + function openModal() { + bootstrapsModal.show(); + } - fetchIndividualEntity({ - input: "thingFilter", - itemSelect: "infiniteScroll", - item: "things", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "infiniteScroll", + item: "things", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/channel.html b/ui/web/template/channel.html index 7d9af95a0..a9d7a6c2d 100644 --- a/ui/web/template/channel.html +++ b/ui/web/template/channel.html @@ -2,170 +2,133 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "channel" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/channels/{{ .Channel.ID }}/things" - type="button" - class="btn body-button" - > - Channel Things - </a> - <a - href="/channels/{{ .Channel.ID }}/users" - type="button" - class="btn body-button" - > - Channel Users - </a> - <a - href="/channels/{{ .Channel.ID }}/groups" - type="button" - class="btn body-button" - > - Channel Groups - </a> - </div> - <div class="table-responsive table-container"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="row">CHANNEL</th> - </tr> - </thead> - <tbody> - {{ $disableButton := false }} - <tr> - <th>NAME</th> - <td - class="editable" - contenteditable="false" - data-field="name" - > - {{ .Channel.Name }} - </td> - <td> - <button - class="edit-btn" - id="edit-name" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-name"> - Save - </button> - <button class="cancel-btn" id="cancel-name"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>ID</th> - <td>{{ .Channel.ID }}</td> - <td></td> - </tr> - <tr> - <th>Owner</th> - <td>{{ .Channel.OwnerID }}</td> - <td></td> - </tr> - <tr> - <th>Description</th> - <td - class="editable" - contenteditable="false" - data-field="description" - > - {{ .Channel.Description }} - </td> - <td> - <button - class="edit-btn" - id="edit-description" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-description"> - Save - </button> - <button class="cancel-btn" id="cancel-description"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Metadata</th> - <td - class="editable" - contenteditable="false" - data-field="metadata" - > - {{ toJSON .Channel.Metadata }} - </td> - <td> - <button - class="edit-btn" - id="edit-metadata" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-metadata"> - Save - </button> - <button class="cancel-btn" id="cancel-metadata"> - Cancel - </button> - </div> - </td> - </tr> - </tbody> - </table> - <div id="error-message" class="text-danger"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a + href="/channels/{{ .Channel.ID }}/things" + type="button" + class="btn body-button" + > + Channel Things + </a> + <a href="/channels/{{ .Channel.ID }}/users" type="button" class="btn body-button"> + Channel Users + </a> + <a + href="/channels/{{ .Channel.ID }}/groups" + type="button" + class="btn body-button" + > + Channel Groups + </a> + </div> + <div class="table-responsive table-container"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="row">CHANNEL</th> + </tr> + </thead> + <tbody> + {{ $disableButton := false }} + <tr> + <th>NAME</th> + <td class="editable" contenteditable="false" data-field="name"> + {{ .Channel.Name }} + </td> + <td> + <button + class="edit-btn" + id="edit-name" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-name">Save</button> + <button class="cancel-btn" id="cancel-name">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>ID</th> + <td>{{ .Channel.ID }}</td> + <td></td> + </tr> + <tr> + <th>Owner</th> + <td>{{ .Channel.OwnerID }}</td> + <td></td> + </tr> + <tr> + <th>Description</th> + <td class="editable" contenteditable="false" data-field="description"> + {{ .Channel.Description }} + </td> + <td> + <button + class="edit-btn" + id="edit-description" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-description">Save</button> + <button class="cancel-btn" id="cancel-description">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Metadata</th> + <td class="editable" contenteditable="false" data-field="metadata"> + {{ toJSON .Channel.Metadata }} + </td> + <td> + <button + class="edit-btn" + id="edit-metadata" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-metadata">Save</button> + <button class="cancel-btn" id="cancel-metadata">Cancel</button> + </div> + </td> + </tr> + </tbody> + </table> + <div id="error-message" class="text-danger"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> attachEditRowListener( { @@ -180,6 +143,6 @@ } ); </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/channelgroups.html b/ui/web/template/channelgroups.html index eea84035b..62d8f9fc6 100644 --- a/ui/web/template/channelgroups.html +++ b/ui/web/template/channelgroups.html @@ -2,202 +2,172 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "channelgroups" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/channels/{{ .ChannelID }}" - type="button" - class="btn body-button" - > - Channel - </a> - <a - href="/channels/{{ .ChannelID }}/users" - type="button" - class="btn body-button" - > - Channel Users - </a> - <a - href="/channels/{{ .ChannelID }}/things" - type="button" - class="btn body-button" - > - Channel Things - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Channel Groups</h4> - <button - role="button" - class="btn body-button" - onclick="openGroupModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addGroupModal" - tabindex="-1" - aria-labelledby="addGroupModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addGroupModalLabel" - > - Add Group - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/channels/{{ .ChannelID }}/groups/assign?item=channels" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Group ID - </label> - <input - type="text" - name="groupFilter" - id="groupFilter" - placeholder="Filter by Group ID" - /> - <select - class="form-select" - name="groupID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a group</option> - </select> - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $channelID := .ChannelID }} - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col">{{ toJSON $g.Metadata }}</td> - <td class="created-col">{{ $g.CreatedAt }}</td> - <td class="text-center"> - <form - action="/channels/{{ $channelID }}/groups/unassign?item=channels" - method="post" - > - <input - type="hidden" - name="groupID" - id="groupID" - value="{{ $g.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const groupModal = new bootstrap.Modal( - document.getElementById("addGroupModal"), - ); - function openGroupModal() { - groupModal.show(); - } + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/channels/{{ .ChannelID }}" type="button" class="btn body-button"> + Channel + </a> + <a href="/channels/{{ .ChannelID }}/users" type="button" class="btn body-button"> + Channel Users + </a> + <a href="/channels/{{ .ChannelID }}/things" type="button" class="btn body-button"> + Channel Things + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Channel Groups</h4> + <button role="button" class="btn body-button" onclick="openGroupModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addGroupModal" + tabindex="-1" + aria-labelledby="addGroupModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addGroupModalLabel">Add Group</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/channels/{{ .ChannelID }}/groups/assign?item=channels" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Group ID</label> + <input + type="text" + name="groupFilter" + id="groupFilter" + placeholder="Filter by Group ID" + /> + <select + class="form-select" + name="groupID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a group</option> + </select> + </div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $channelID := .ChannelID }} + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col">{{ toJSON $g.Metadata }}</td> + <td class="created-col">{{ $g.CreatedAt }}</td> + <td class="text-center"> + <form + action="/channels/{{ $channelID }}/groups/unassign?item=channels" + method="post" + > + <input + type="hidden" + name="groupID" + id="groupID" + value="{{ $g.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const groupModal = new bootstrap.Modal(document.getElementById("addGroupModal")); + function openGroupModal() { + groupModal.show(); + } - fetchIndividualEntity({ - input: "groupFilter", - itemSelect: "infiniteScroll", - item: "groups", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "groupFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/channels.html b/ui/web/template/channels.html index f0079449f..8d7908443 100644 --- a/ui/web/template/channels.html +++ b/ui/web/template/channels.html @@ -2,125 +2,109 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "channels" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <!-- Button trigger modal --> - <button - type="button" - class="btn body-button" - onclick="openModal('single')" - > - Add Channel - </button> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <!-- Button trigger modal --> + <button type="button" class="btn body-button" onclick="openModal('single')"> + Add Channel + </button> - <!-- Modal --> - <div - class="modal fade" - id="addChannelModal" - tabindex="-1" - role="dialog" - aria-labelledby="addChannelModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addChannelModalLabel"> - Add Channel - </h5> - </div> - <div class="modal-body"> - <div id="alertMessage"></div> - <form method="post" id="channelform"> - <div class="mb-3"> - <label for="name" class="form-label">Name</label> - <input - type="text" - class="form-control" - name="name" - id="name" - placeholder="Channel Name" - /> - <div id="nameError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Parent Name - </label> - <input - type="text" - class="itemsFilter" - name="parentFilter" - id="parentFilter" - placeholder="Filter by parent name" - /> - <select - class="form-select" - name="parentID" - id="infiniteScroll" - size="5" - > - <option disabled>select a channel</option> - </select> - </div> - <div class="mb-3"> - <label for="description" class="form-label"> - Description - </label> - <input - type="text" - class="form-control" - name="description" - id="description" - placeholder="Channel Description" - /> - </div> - <div class="mb-3"> - <label for="metadata" class="form-label"> - Metadata - </label> - <input - type="text" - class="form-control" - name="metadata" - id="metadata" - value="{}" - /> - <div id="metadataHelp" class="form-text"> - Enter channel metadata in JSON format. - </div> - <div id="metadataError" class="text-danger"></div> - </div> - <button - type="submit" - class="btn body-button" - id="create-channel-button" - > - Submit - </button> - </form> - </div> - </div> - </div> - </div> + <!-- Modal --> + <div + class="modal fade" + id="addChannelModal" + tabindex="-1" + role="dialog" + aria-labelledby="addChannelModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addChannelModalLabel">Add Channel</h5> + </div> + <div class="modal-body"> + <div id="alertMessage"></div> + <form method="post" id="channelform"> + <div class="mb-3"> + <label for="name" class="form-label">Name</label> + <input + type="text" + class="form-control" + name="name" + id="name" + placeholder="Channel Name" + /> + <div id="nameError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Parent Name</label> + <input + type="text" + class="itemsFilter" + name="parentFilter" + id="parentFilter" + placeholder="Filter by parent name" + /> + <select + class="form-select" + name="parentID" + id="infiniteScroll" + size="5" + > + <option disabled>select a channel</option> + </select> + </div> + <div class="mb-3"> + <label for="description" class="form-label">Description</label> + <input + type="text" + class="form-control" + name="description" + id="description" + placeholder="Channel Description" + /> + </div> + <div class="mb-3"> + <label for="metadata" class="form-label">Metadata</label> + <input + type="text" + class="form-control" + name="metadata" + id="metadata" + value="{}" + /> + <div id="metadataHelp" class="form-text"> + Enter channel metadata in JSON format. + </div> + <div id="metadataError" class="text-danger"></div> + </div> + <button + type="submit" + class="btn body-button" + id="create-channel-button" + > + Submit + </button> + </form> + </div> + </div> + </div> + </div> - <!-- Button trigger modal --> - <button - type="button" - class="btn body-button" - onclick="openModal('bulk')" - > - Add Channels - </button> + <!-- Button trigger modal --> + <button type="button" class="btn body-button" onclick="openModal('bulk')"> + Add Channels + </button> <!-- Modal --> <div @@ -260,41 +244,37 @@ <h5 class="modal-title" id="addChannelsModalLabel"> }, }); - const channelModal = new bootstrap.Modal( - document.getElementById("addChannelModal"), - ); - const channelsModal = new bootstrap.Modal( - document.getElementById("addChannelsModal"), - ); + const channelModal = new bootstrap.Modal(document.getElementById("addChannelModal")); + const channelsModal = new bootstrap.Modal(document.getElementById("addChannelsModal")); - function openModal(modal) { - if (modal === "single") { - channelModal.show(); - } else if (modal === "bulk") { - channelsModal.show(); - } - } + function openModal(modal) { + if (modal === "single") { + channelModal.show(); + } else if (modal === "bulk") { + channelsModal.show(); + } + } - submitCreateForm({ - url: "/channels", - formId: "channelform", - alertDiv: "alertMessage", - modal: channelModal, - }); + submitCreateForm({ + url: "/channels", + formId: "channelform", + alertDiv: "alertMessage", + modal: channelModal, + }); - submitCreateForm({ - url: "/channels/bulk", - formId: "bulkchannelsform", - alertDiv: "alertBulkMessage", - modal: channelsModal, - }); + submitCreateForm({ + url: "/channels/bulk", + formId: "bulkchannelsform", + alertDiv: "alertBulkMessage", + modal: channelsModal, + }); - fetchIndividualEntity({ - input: "parentFilter", - itemSelect: "infiniteScroll", - item: "channels", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "parentFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/channelthings.html b/ui/web/template/channelthings.html index 1334325c4..a0e6c32c8 100644 --- a/ui/web/template/channelthings.html +++ b/ui/web/template/channelthings.html @@ -2,202 +2,172 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "channelthings" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/channels/{{ .ChannelID }}" - type="button" - class="btn body-button" - > - Channel - </a> - <a - href="/channels/{{ .ChannelID }}/users" - type="button" - class="btn body-button" - > - Channel Users - </a> - <a - href="/channels/{{ .ChannelID }}/groups" - type="button" - class="btn body-button" - > - Channel Groups - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Channel Things</h4> - <button - role="button" - class="btn body-button" - onclick="openThingModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addThingModal" - tabindex="-1" - aria-labelledby="addThingModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addThingModalLabel" - > - Add Thing - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/channels/{{ .ChannelID }}/things/connect?item=channels" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Thing ID - </label> - <input - type="text" - name="thingFilter" - id="thingFilter" - placeholder="Filter by Thing ID" - /> - <select - class="form-select" - name="thingID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a thing</option> - </select> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Connect - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $channelID := .ChannelID }} - {{ range $i, $t := .Things }} - {{ $disableButton := false }} - <tr> - <td>{{ $t.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/things/%s" $t.ID }}"> - {{ $t.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $t.Tags }}</td> - <td class="meta-col">{{ toJSON $t.Metadata }}</td> - <td class="created-col">{{ $t.CreatedAt }}</td> - <td class="text-center"> - <form - action="/channels/{{ $channelID }}/things/disconnect?item=channels" - method="post" - > - <input - type="hidden" - name="thingID" - id="thingID" - value="{{ $t.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const thingModal = new bootstrap.Modal( - document.getElementById("addThingModal"), - ); - function openThingModal() { - thingModal.show(); - } + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/channels/{{ .ChannelID }}" type="button" class="btn body-button"> + Channel + </a> + <a href="/channels/{{ .ChannelID }}/users" type="button" class="btn body-button"> + Channel Users + </a> + <a href="/channels/{{ .ChannelID }}/groups" type="button" class="btn body-button"> + Channel Groups + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Channel Things</h4> + <button role="button" class="btn body-button" onclick="openThingModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addThingModal" + tabindex="-1" + aria-labelledby="addThingModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addThingModalLabel">Add Thing</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/channels/{{ .ChannelID }}/things/connect?item=channels" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Thing ID</label> + <input + type="text" + name="thingFilter" + id="thingFilter" + placeholder="Filter by Thing ID" + /> + <select + class="form-select" + name="thingID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a thing</option> + </select> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Connect</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $channelID := .ChannelID }} + {{ range $i, $t := .Things }} + {{ $disableButton := false }} + <tr> + <td>{{ $t.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/things/%s" $t.ID }}"> + {{ $t.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $t.Tags }}</td> + <td class="meta-col">{{ toJSON $t.Metadata }}</td> + <td class="created-col">{{ $t.CreatedAt }}</td> + <td class="text-center"> + <form + action="/channels/{{ $channelID }}/things/disconnect?item=channels" + method="post" + > + <input + type="hidden" + name="thingID" + id="thingID" + value="{{ $t.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const thingModal = new bootstrap.Modal(document.getElementById("addThingModal")); + function openThingModal() { + thingModal.show(); + } - fetchIndividualEntity({ - input: "thingFilter", - itemSelect: "infiniteScroll", - item: "things", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "infiniteScroll", + item: "things", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/channelusers.html b/ui/web/template/channelusers.html index c66619721..12d383c14 100644 --- a/ui/web/template/channelusers.html +++ b/ui/web/template/channelusers.html @@ -2,517 +2,471 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "channelusers" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/channels/{{ .ChannelID }}" - role="button" - > - Channel - </a> - <a - class="btn body-button" - href="/channels/{{ .ChannelID }}/things" - role="button" - > - Channel Things - </a> - <a - class="btn body-button" - href="/channels/{{ .ChannelID }}/groups" - role="button" - > - Channel Groups - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Channel Users</h4> - <button - role="button" - class="btn body-button" - onclick="openUserModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addUserModal" - tabindex="-1" - aria-labelledby="addUserModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 class="modal-title fs-5" id="addUserModalLabel"> - Add User - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/channels/{{ .ChannelID }}/users/assign?item=channels" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - User ID - </label> - <input - type="text" - name="userFilter" - id="userFilter" - placeholder="Filter by User ID" - /> - <select - class="form-select" - name="userID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a User</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $i, $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/channels/{{ .ChannelID }}" role="button"> + Channel + </a> + <a class="btn body-button" href="/channels/{{ .ChannelID }}/things" role="button"> + Channel Things + </a> + <a class="btn body-button" href="/channels/{{ .ChannelID }}/groups" role="button"> + Channel Groups + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Channel Users</h4> + <button role="button" class="btn body-button" onclick="openUserModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addUserModal" + tabindex="-1" + aria-labelledby="addUserModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/channels/{{ .ChannelID }}/users/assign?item=channels" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">User ID</label> + <input + type="text" + name="userFilter" + id="userFilter" + placeholder="Filter by User ID" + /> + <select + class="form-select" + name="userID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a User</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $i, $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> - <ul class="nav nav-tabs" id="roleTab" role="tablist"> - <li class="nav-item" role="presentation"> - {{ $tabActive := "" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="view-tab" - data-bs-toggle="tab" - data-bs-target="#view-tab-pane" - type="button" - role="tab" - aria-controls="view-tab-pane" - aria-selected="true" - onclick="openTab('')" - > - All - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "admin" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="admin-tab" - data-bs-toggle="tab" - data-bs-target="#admin-tab-pane" - type="button" - role="tab" - aria-controls="admin-tab-pane" - aria-selected="true" - onclick="openTab('admin')" - > - Admin - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "editor" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="editor-tab" - data-bs-toggle="tab" - data-bs-target="#editor-tab-pane" - type="button" - role="tab" - aria-controls="editor-tab-pane" - aria-selected="false" - onclick="openTab('editor')" - > - Editor - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "viewer" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="viewer-tab" - data-bs-toggle="tab" - data-bs-target="#viewer-tab-pane" - type="button" - role="tab" - aria-controls="viewer-tab-pane" - aria-selected="false" - onclick="openTab('viewer')" - > - Viewer - </button> - </li> - </ul> - <div class="tab-content mt-3" id="roleTabContent"> - {{ $channelID := .ChannelID }} - <div - class="tab-pane active" - id="view-tab-pane" - role="tabpanel" - aria-labelledby="view-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="admin-tab-pane" - role="tabpanel" - aria-labelledby="admin-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/channels/{{ $channelID }}/users/unassign?item=channels" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="admin" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane " - id="editor-tab-pane" - role="tabpanel" - aria-labelledby="editor-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/channels/{{ $channelID }}/users/unassign?item=channels" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="editor" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="viewer-tab-pane" - role="tabpanel" - aria-labelledby="viewer-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/channels/{{ $channelID }}/users/unassign?item=channels" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="viewer" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <ul class="nav nav-tabs" id="roleTab" role="tablist"> + <li class="nav-item" role="presentation"> + {{ $tabActive := "" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="view-tab" + data-bs-toggle="tab" + data-bs-target="#view-tab-pane" + type="button" + role="tab" + aria-controls="view-tab-pane" + aria-selected="true" + onclick="openTab('')" + > + All + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "admin" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="admin-tab" + data-bs-toggle="tab" + data-bs-target="#admin-tab-pane" + type="button" + role="tab" + aria-controls="admin-tab-pane" + aria-selected="true" + onclick="openTab('admin')" + > + Admin + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "editor" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="editor-tab" + data-bs-toggle="tab" + data-bs-target="#editor-tab-pane" + type="button" + role="tab" + aria-controls="editor-tab-pane" + aria-selected="false" + onclick="openTab('editor')" + > + Editor + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "viewer" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="viewer-tab" + data-bs-toggle="tab" + data-bs-target="#viewer-tab-pane" + type="button" + role="tab" + aria-controls="viewer-tab-pane" + aria-selected="false" + onclick="openTab('viewer')" + > + Viewer + </button> + </li> + </ul> + <div class="tab-content mt-3" id="roleTabContent"> + {{ $channelID := .ChannelID }} + <div + class="tab-pane active" + id="view-tab-pane" + role="tabpanel" + aria-labelledby="view-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="admin-tab-pane" + role="tabpanel" + aria-labelledby="admin-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/channels/{{ $channelID }}/users/unassign?item=channels" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="admin" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane " + id="editor-tab-pane" + role="tabpanel" + aria-labelledby="editor-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/channels/{{ $channelID }}/users/unassign?item=channels" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="editor" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="viewer-tab-pane" + role="tabpanel" + aria-labelledby="viewer-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/channels/{{ $channelID }}/users/unassign?item=channels" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="viewer" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> const userModal = new bootstrap.Modal( document.getElementById("addUserModal"), @@ -527,18 +481,18 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> item: "users", }); - function openTab(relation) { + function openTab(relation) { event.preventDefault(); - var channelID = '{{.ChannelID}}'; - fetch(`/channels/${channelID}/users?relation=${relation}`, { - method: "GET", - }) - .then((response) => { + var channelID = '{{.ChannelID}}'; + fetch(`/channels/${channelID}/users?relation=${relation}`, { + method: "GET", + }) + .then((response) => { - }) - .catch((error) => console.error("Error:", error)); + }) + .catch((error) => console.error("Error:", error)); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/error.html b/ui/web/template/error.html index 601e2ea5b..63c0ad3d9 100644 --- a/ui/web/template/error.html +++ b/ui/web/template/error.html @@ -1,27 +1,29 @@ <!-- Copyright (c) Abstract Machines SPDX-License-Identifier: Apache-2.0 --> -{{define "error"}} -<!DOCTYPE html> -<html lang="en"> -{{template "header"}} -<body> - {{template "navbar"}} - <div class="main-content"> +{{ define "error" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" }} + <div class="main-content"> <div class="main"> - <div class="row"> - <div class="col-lg-8 mx-auto py-5"> - <div class="row"> - <div class="error-header py-5"> - <h1 class = "error-header">Sorry, there seems to be an issue with your request</h1> - </div> - <div class="alert alert-danger alert-dismissible">{{.Error}}</div> - <a type="button" href="javascript:window. history. back();" class="btn body-button">Back</a> - </div> + <div class="row"> + <div class="col-lg-8 mx-auto py-5"> + <div class="row"> + <div class="error-header py-5"> + <h1 class="error-header">Sorry, there seems to be an issue with your request</h1> </div> + <div class="alert alert-danger alert-dismissible">{{ .Error }}</div> + <a type="button" href="javascript:window. history. back();" class="btn body-button"> + Back + </a> + </div> </div> + </div> </div> - </div> -</body> -</html> -{{end}} + </div> + </body> + </html> +{{ end }} diff --git a/ui/web/template/footer.html b/ui/web/template/footer.html index c64b769fa..0c6e2e736 100644 --- a/ui/web/template/footer.html +++ b/ui/web/template/footer.html @@ -2,21 +2,18 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "footer" }} - <!-- Bootstrap core JavaScript + <!-- Bootstrap core JavaScript ================================================== --> - <!-- Placed at the end of the document so the pages load faster --> - <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" - ></script> - <script - src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.min.js" - integrity="sha384-Rx+T1VzGupg4BHQYs2gCW9It+akI2MM/mndMCy36UVfodzcJcF0GGLxZIzObiEfa" - crossorigin="anonymous" - ></script> - <script - type="text/javascript" - src="https://www.gstatic.com/charts/loader.js" - ></script> + <!-- Placed at the end of the document so the pages load faster --> + <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" + ></script> + <script + src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.min.js" + integrity="sha384-Rx+T1VzGupg4BHQYs2gCW9It+akI2MM/mndMCy36UVfodzcJcF0GGLxZIzObiEfa" + crossorigin="anonymous" + ></script> + <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> {{ end }} diff --git a/ui/web/template/group.html b/ui/web/template/group.html index ecca37d44..e7ae42567 100644 --- a/ui/web/template/group.html +++ b/ui/web/template/group.html @@ -2,158 +2,117 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "group" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/groups/{{ .Group.ID }}/users" - role="button" - > - Group Users - </a> - <a - class="btn body-button" - href="/groups/{{ .Group.ID }}/channels" - role="button" - > - Group Channels - </a> - </div> - <div class="table-responsive table-container"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="row">GROUP</th> - </tr> - </thead> - <tbody> - {{ $disableButton := false }} - <tr> - <th>NAME</th> - <td - class="editable" - contenteditable="false" - data-field="name" - > - {{ .Group.Name }} - </td> - <td> - <button - class="edit-btn" - id="edit-name" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-name"> - Save - </button> - <button class="cancel-btn" id="cancel-name"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>ID</th> - <td>{{ .Group.ID }}</td> - <td></td> - </tr> - <tr> - <th>Description</th> - <td - class="editable" - contenteditable="false" - data-field="description" - > - {{ .Group.Description }} - </td> - <td> - <button - class="edit-btn" - id="edit-description" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-description"> - Save - </button> - <button class="cancel-btn" id="cancel-description"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Metadata</th> - <td - class="editable" - contenteditable="false" - data-field="metadata" - > - {{ toJSON .Group.Metadata }} - </td> - <td> - <button - class="edit-btn" - id="edit-metadata" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-metadata"> - Save - </button> - <button class="cancel-btn" id="cancel-metadata"> - Cancel - </button> - </div> - </td> - </tr> - </tbody> - </table> - <div id="error-message" class="text-danger"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/groups/{{ .Group.ID }}/users" role="button"> + Group Users + </a> + <a class="btn body-button" href="/groups/{{ .Group.ID }}/channels" role="button"> + Group Channels + </a> + </div> + <div class="table-responsive table-container"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="row">GROUP</th> + </tr> + </thead> + <tbody> + {{ $disableButton := false }} + <tr> + <th>NAME</th> + <td class="editable" contenteditable="false" data-field="name"> + {{ .Group.Name }} + </td> + <td> + <button + class="edit-btn" + id="edit-name" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-name">Save</button> + <button class="cancel-btn" id="cancel-name">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>ID</th> + <td>{{ .Group.ID }}</td> + <td></td> + </tr> + <tr> + <th>Description</th> + <td class="editable" contenteditable="false" data-field="description"> + {{ .Group.Description }} + </td> + <td> + <button + class="edit-btn" + id="edit-description" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-description">Save</button> + <button class="cancel-btn" id="cancel-description">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Metadata</th> + <td class="editable" contenteditable="false" data-field="metadata"> + {{ toJSON .Group.Metadata }} + </td> + <td> + <button + class="edit-btn" + id="edit-metadata" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-metadata">Save</button> + <button class="cancel-btn" id="cancel-metadata">Cancel</button> + </div> + </td> + </tr> + </tbody> + </table> + <div id="error-message" class="text-danger"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> attachEditRowListener( { @@ -168,6 +127,6 @@ } ); </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/groupchannels.html b/ui/web/template/groupchannels.html index 6973610c8..26b5af2fa 100644 --- a/ui/web/template/groupchannels.html +++ b/ui/web/template/groupchannels.html @@ -2,195 +2,167 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "groupchannels" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/groups/{{ .GroupID }}" - role="button" - > - Group - </a> - <a - class="btn body-button" - href="/groups/{{ .GroupID }}/users" - role="button" - > - Group Users - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Group Channels</h4> - <button - role="button" - class="btn body-button" - onclick="openChannelModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addChannelModal" - tabindex="-1" - aria-labelledby="addChannelModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addChannelModalLabel" - > - Add Channel - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/groups/{{ .GroupID }}/channels/assign?item=groups" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Channel ID - </label> - <input - type="text" - name="channelFilter" - id="channelFilter" - placeholder="Filter by Channel ID" - /> - <select - class="form-select" - name="channelID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a channel</option> - </select> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Add Channel - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $groupID := .GroupID }} - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col">{{ toJSON $c.Metadata }}</td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="text-center"> - <form - action="/groups/{{ $groupID }}/channels/unassign?item=groups" - method="post" - > - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const channelModal = new bootstrap.Modal( - document.getElementById("addChannelModal"), - ); - function openChannelModal() { - channelModal.show(); - } + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/groups/{{ .GroupID }}" role="button">Group</a> + <a class="btn body-button" href="/groups/{{ .GroupID }}/users" role="button"> + Group Users + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Group Channels</h4> + <button role="button" class="btn body-button" onclick="openChannelModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addChannelModal" + tabindex="-1" + aria-labelledby="addChannelModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/groups/{{ .GroupID }}/channels/assign?item=groups" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Channel ID</label> + <input + type="text" + name="channelFilter" + id="channelFilter" + placeholder="Filter by Channel ID" + /> + <select + class="form-select" + name="channelID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a channel</option> + </select> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Add Channel</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $groupID := .GroupID }} + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col">{{ toJSON $c.Metadata }}</td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="text-center"> + <form + action="/groups/{{ $groupID }}/channels/unassign?item=groups" + method="post" + > + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const channelModal = new bootstrap.Modal(document.getElementById("addChannelModal")); + function openChannelModal() { + channelModal.show(); + } - fetchIndividualEntity({ - input: "channelFilter", - itemSelect: "infiniteScroll", - item: "channels", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/groups.html b/ui/web/template/groups.html index da96eaf46..aae2b5943 100644 --- a/ui/web/template/groups.html +++ b/ui/web/template/groups.html @@ -2,123 +2,103 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "groups" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <!-- Button trigger modal --> - <button - type="button" - class="btn body-button" - onclick="openModal('single')" - > - Add Group - </button> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <!-- Button trigger modal --> + <button type="button" class="btn body-button" onclick="openModal('single')"> + Add Group + </button> - <!-- Modal --> - <div - class="modal fade" - id="addGroupModal" - tabindex="-1" - role="dialog" - aria-labelledby="addGroupModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addGroupModalLabel"> - Add Group - </h5> - </div> - <div class="modal-body"> - <div id="alertMessage"></div> - <form method="post" id="groupform"> - <div class="mb-3"> - <label for="name" class="form-label">Name</label> - <input - type="text" - class="form-control" - name="name" - id="name" - placeholder="Group Name" - /> - <div id="nameError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="description" class="form-label"> - Description - </label> - <input - type="text" - class="form-control" - name="description" - id="description" - placeholder="Group Description" - /> - </div> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Parent Name - </label> - <input - type="text" - class="itemsFilter" - name="parentFilter" - id="parentFilter" - placeholder="Filter by parent name" - /> - <select - class="form-select" - name="parentID" - id="infiniteScroll" - size="5" - > - <option disabled>select a group</option> - </select> - </div> - <div class="mb-3"> - <label for="metadata" class="form-label"> - Metadata - </label> - <input - type="text" - class="form-control" - name="metadata" - id="metadata" - value="{}" - /> - <div id="metadataHelp" class="form-text"> - Enter groups metadata in JSON format. - </div> - <div id="metadataError" class="text-danger"></div> - </div> - <button - type="submit" - class="btn body-button" - id="create-group-button" - > - Submit - </button> - </form> - </div> - </div> - </div> - </div> - <button - type="button" - class="btn body-button" - onclick="openModal('bulk')" - > - Add Groups - </button> + <!-- Modal --> + <div + class="modal fade" + id="addGroupModal" + tabindex="-1" + role="dialog" + aria-labelledby="addGroupModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addGroupModalLabel">Add Group</h5> + </div> + <div class="modal-body"> + <div id="alertMessage"></div> + <form method="post" id="groupform"> + <div class="mb-3"> + <label for="name" class="form-label">Name</label> + <input + type="text" + class="form-control" + name="name" + id="name" + placeholder="Group Name" + /> + <div id="nameError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="description" class="form-label">Description</label> + <input + type="text" + class="form-control" + name="description" + id="description" + placeholder="Group Description" + /> + </div> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Parent Name</label> + <input + type="text" + class="itemsFilter" + name="parentFilter" + id="parentFilter" + placeholder="Filter by parent name" + /> + <select + class="form-select" + name="parentID" + id="infiniteScroll" + size="5" + > + <option disabled>select a group</option> + </select> + </div> + <div class="mb-3"> + <label for="metadata" class="form-label">Metadata</label> + <input + type="text" + class="form-control" + name="metadata" + id="metadata" + value="{}" + /> + <div id="metadataHelp" class="form-text"> + Enter groups metadata in JSON format. + </div> + <div id="metadataError" class="text-danger"></div> + </div> + <button type="submit" class="btn body-button" id="create-group-button"> + Submit + </button> + </form> + </div> + </div> + </div> + </div> + <button type="button" class="btn body-button" onclick="openModal('bulk')"> + Add Groups + </button> <!-- Modal --> <div @@ -243,55 +223,51 @@ <h5 class="modal-title" id="addGroupsModalLabel"> </div> </div> - {{ template "footer" }} - <script> - attachValidationListener({ - buttonId: "create-group-button", - errorDivs: { - name: "nameError", - metadata: "metadataError", - }, - validations: { - name: validateName, - metadata: validateMetadata, - }, - }); + {{ template "footer" }} + <script> + attachValidationListener({ + buttonId: "create-group-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + }, + validations: { + name: validateName, + metadata: validateMetadata, + }, + }); - const groupModal = new bootstrap.Modal( - document.getElementById("addGroupModal"), - ); - const groupsModal = new bootstrap.Modal( - document.getElementById("addGroupsModal"), - ); + const groupModal = new bootstrap.Modal(document.getElementById("addGroupModal")); + const groupsModal = new bootstrap.Modal(document.getElementById("addGroupsModal")); - function openModal(modal) { - if (modal === "single") { - groupModal.show(); - } else if (modal === "bulk") { - groupsModal.show(); - } - } + function openModal(modal) { + if (modal === "single") { + groupModal.show(); + } else if (modal === "bulk") { + groupsModal.show(); + } + } - submitCreateForm({ - url: "/groups", - formId: "groupform", - alertDiv: "alertMessage", - modal: groupModal, - }); + submitCreateForm({ + url: "/groups", + formId: "groupform", + alertDiv: "alertMessage", + modal: groupModal, + }); - submitCreateForm({ - url: "/groups/bulk", - formId: "bulkgroupsform", - alertDiv: "alertBulkMessage", - modal: groupsModal, - }); + submitCreateForm({ + url: "/groups/bulk", + formId: "bulkgroupsform", + alertDiv: "alertBulkMessage", + modal: groupsModal, + }); - fetchIndividualEntity({ - input: "parentFilter", - itemSelect: "infiniteScroll", - item: "groups", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "parentFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/groupusers.html b/ui/web/template/groupusers.html index ea72a05d9..ae60dc189 100644 --- a/ui/web/template/groupusers.html +++ b/ui/web/template/groupusers.html @@ -2,511 +2,467 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "groupusers" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/groups/{{ .GroupID }}" - role="button" - > - Group - </a> - <a - class="btn body-button" - href="/groups/{{ .GroupID }}/channels" - role="button" - > - Group Channels - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Group Users</h4> - <button - role="button" - class="btn body-button" - onclick="openUserModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addUserModal" - tabindex="-1" - aria-labelledby="addUserModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 class="modal-title fs-5" id="addUserModalLabel"> - Add User - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/groups/{{ .GroupID }}/users/assign?item=groups" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - User ID - </label> - <input - type="text" - name="userFilter" - id="userFilter" - placeholder="Filter by User ID" - /> - <select - class="form-select" - name="userID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a User</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $i, $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/groups/{{ .GroupID }}" role="button">Group</a> + <a class="btn body-button" href="/groups/{{ .GroupID }}/channels" role="button"> + Group Channels + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Group Users</h4> + <button role="button" class="btn body-button" onclick="openUserModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addUserModal" + tabindex="-1" + aria-labelledby="addUserModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/groups/{{ .GroupID }}/users/assign?item=groups" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">User ID</label> + <input + type="text" + name="userFilter" + id="userFilter" + placeholder="Filter by User ID" + /> + <select + class="form-select" + name="userID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a User</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $i, $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> - <ul class="nav nav-tabs" id="roleTab" role="tablist"> - <li class="nav-item" role="presentation"> - {{ $tabActive := "" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="view-tab" - data-bs-toggle="tab" - data-bs-target="#view-tab-pane" - type="button" - role="tab" - aria-controls="view-tab-pane" - aria-selected="true" - onclick="openTab('')" - > - All - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "admin" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="admin-tab" - data-bs-toggle="tab" - data-bs-target="#admin-tab-pane" - type="button" - role="tab" - aria-controls="admin-tab-pane" - aria-selected="true" - onclick="openTab('admin')" - > - Admin - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "editor" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="editor-tab" - data-bs-toggle="tab" - data-bs-target="#editor-tab-pane" - type="button" - role="tab" - aria-controls="editor-tab-pane" - aria-selected="false" - onclick="openTab('editor')" - > - Editor - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "viewer" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="viewer-tab" - data-bs-toggle="tab" - data-bs-target="#viewer-tab-pane" - type="button" - role="tab" - aria-controls="viewer-tab-pane" - aria-selected="false" - onclick="openTab('viewer')" - > - Viewer - </button> - </li> - </ul> - <div class="tab-content mt-3" id="roleTabContent"> - {{ $groupID:= .GroupID }} - <div - class="tab-pane active" - id="view-tab-pane" - role="tabpanel" - aria-labelledby="view-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - </tr> - </thead> - <tbody> - {{ $groupID:= .GroupID }} - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="admin-tab-pane" - role="tabpanel" - aria-labelledby="admin-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/groups/{{ $groupID }}/users/unassign?item=groups" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="admin" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane " - id="editor-tab-pane" - role="tabpanel" - aria-labelledby="editor-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/groups/{{ $groupID }}/users/unassign?item=groups" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="editor" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="viewer-tab-pane" - role="tabpanel" - aria-labelledby="viewer-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col"> - {{ toJSON $u.Metadata }} - </td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/groups/{{ $groupID }}/users/unassign?item=groups" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="viewer" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <ul class="nav nav-tabs" id="roleTab" role="tablist"> + <li class="nav-item" role="presentation"> + {{ $tabActive := "" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="view-tab" + data-bs-toggle="tab" + data-bs-target="#view-tab-pane" + type="button" + role="tab" + aria-controls="view-tab-pane" + aria-selected="true" + onclick="openTab('')" + > + All + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "admin" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="admin-tab" + data-bs-toggle="tab" + data-bs-target="#admin-tab-pane" + type="button" + role="tab" + aria-controls="admin-tab-pane" + aria-selected="true" + onclick="openTab('admin')" + > + Admin + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "editor" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="editor-tab" + data-bs-toggle="tab" + data-bs-target="#editor-tab-pane" + type="button" + role="tab" + aria-controls="editor-tab-pane" + aria-selected="false" + onclick="openTab('editor')" + > + Editor + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "viewer" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="viewer-tab" + data-bs-toggle="tab" + data-bs-target="#viewer-tab-pane" + type="button" + role="tab" + aria-controls="viewer-tab-pane" + aria-selected="false" + onclick="openTab('viewer')" + > + Viewer + </button> + </li> + </ul> + <div class="tab-content mt-3" id="roleTabContent"> + {{ $groupID:= .GroupID }} + <div + class="tab-pane active" + id="view-tab-pane" + role="tabpanel" + aria-labelledby="view-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + </tr> + </thead> + <tbody> + {{ $groupID:= .GroupID }} + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="admin-tab-pane" + role="tabpanel" + aria-labelledby="admin-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/groups/{{ $groupID }}/users/unassign?item=groups" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="admin" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane " + id="editor-tab-pane" + role="tabpanel" + aria-labelledby="editor-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/groups/{{ $groupID }}/users/unassign?item=groups" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="editor" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="viewer-tab-pane" + role="tabpanel" + aria-labelledby="viewer-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col"> + {{ toJSON $u.Metadata }} + </td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/groups/{{ $groupID }}/users/unassign?item=groups" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="viewer" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> const userModal = new bootstrap.Modal( document.getElementById("addUserModal"), @@ -523,16 +479,16 @@ <h1 class="modal-title fs-5" id="addUserModalLabel"> function openTab(relation) { event.preventDefault(); - var groupID = '{{.GroupID}}'; - fetch(`/groups/${groupID}/users?relation=${relation}`, { - method: "GET", - }) - .then((response) => { + var groupID = '{{.GroupID}}'; + fetch(`/groups/${groupID}/users?relation=${relation}`, { + method: "GET", + }) + .then((response) => { - }) - .catch((error) => console.error("Error:", error)); + }) + .catch((error) => console.error("Error:", error)); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/header.html b/ui/web/template/header.html index 07c3534bf..e906b381b 100644 --- a/ui/web/template/header.html +++ b/ui/web/template/header.html @@ -2,25 +2,25 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "header" }} - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="description" content="" /> - <meta name="author" content="" /> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="description" content="" /> + <meta name="author" content="" /> - <title>Magistrala</title> - <link rel="stylesheet" href="/css/styles.css" /> - <link rel="icon" type="image/x-icon" href="/images/favicon.ico" /> - <link - rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" - /> - <link - href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" - rel="stylesheet" - integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" - crossorigin="anonymous" - /> - <script src="/js/main.js"></script> - </head> + <title>Magistrala</title> + <link rel="stylesheet" href="/css/styles.css" /> + <link rel="icon" type="image/x-icon" href="/images/favicon.ico" /> + <link + rel="stylesheet" + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" + /> + <link + href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" + rel="stylesheet" + integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" + crossorigin="anonymous" + /> + <script src="/js/main.js"></script> + </head> {{ end }} diff --git a/ui/web/template/index.html b/ui/web/template/index.html index eb97da44b..ad5a656e6 100644 --- a/ui/web/template/index.html +++ b/ui/web/template/index.html @@ -2,91 +2,91 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "index" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content mt-5 pt-5"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row mb-5"> - <div class="col-lg-3 col-md-6 mb-4"> - <div class="card text-bg-light mb-3"> - <div - class="card-single card-body d-flex justify-content-between p-5" - data-card="users" - > - <div> - <h1>{{ .Summary.TotalUsers }}</h1> - <span>Users</span> - </div> - <div> - <span class="fas fa-users"></span> - </div> - </div> - </div> - </div> - <div class="col-lg-3 col-md-6 mb-4"> - <div class="card text-bg-light mb-3"> - <div - class="card-single card-body d-flex justify-content-between p-5" - data-card="things" - > - <div> - <h1>{{ .Summary.TotalThings }}</h1> - <span>Things</span> - </div> - <div> - <span class="fas fa-microchip"></span> - </div> - </div> - </div> - </div> - <div class="col-lg-3 col-md-6 mb-4"> - <div class="card text-bg-light mb-3"> - <div - class="card-single card-body d-flex justify-content-between p-5" - data-card="groups" - > - <div> - <h1>{{ .Summary.TotalGroups }}</h1> - <span>Groups</span> - </div> - <div> - <span class="fas fa-layer-group"></span> - </div> - </div> - </div> - </div> - <div class="col-lg-3 col-md-6 mb-4"> - <div class="card text-bg-light mb-3"> - <div - class="card-single card-body d-flex justify-content-between p-5" - data-card="channels" - > - <div> - <h1>{{ .Summary.TotalChannels }}</h1> - <span>Channels</span> - </div> - <div> - <span class="fas fa-microchip"></span> - </div> - </div> - </div> - </div> - </div> - <div class="row"> - <div class="card text-bg-light selected-grid"> - <div class="default-content"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content mt-5 pt-5"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row mb-5"> + <div class="col-lg-3 col-md-6 mb-4"> + <div class="card text-bg-light mb-3"> + <div + class="card-single card-body d-flex justify-content-between p-5" + data-card="users" + > + <div> + <h1>{{ .Summary.TotalUsers }}</h1> + <span>Users</span> + </div> + <div> + <span class="fas fa-users"></span> + </div> + </div> + </div> + </div> + <div class="col-lg-3 col-md-6 mb-4"> + <div class="card text-bg-light mb-3"> + <div + class="card-single card-body d-flex justify-content-between p-5" + data-card="things" + > + <div> + <h1>{{ .Summary.TotalThings }}</h1> + <span>Things</span> + </div> + <div> + <span class="fas fa-microchip"></span> + </div> + </div> + </div> + </div> + <div class="col-lg-3 col-md-6 mb-4"> + <div class="card text-bg-light mb-3"> + <div + class="card-single card-body d-flex justify-content-between p-5" + data-card="groups" + > + <div> + <h1>{{ .Summary.TotalGroups }}</h1> + <span>Groups</span> + </div> + <div> + <span class="fas fa-layer-group"></span> + </div> + </div> + </div> + </div> + <div class="col-lg-3 col-md-6 mb-4"> + <div class="card text-bg-light mb-3"> + <div + class="card-single card-body d-flex justify-content-between p-5" + data-card="channels" + > + <div> + <h1>{{ .Summary.TotalChannels }}</h1> + <span>Channels</span> + </div> + <div> + <span class="fas fa-microchip"></span> + </div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="card text-bg-light selected-grid"> + <div class="default-content"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> var enabledUsers = parseInt("{{.Summary.EnabledUsers}}"); var disabledUsers = parseInt("{{.Summary.DisabledUsers}}"); @@ -232,6 +232,6 @@ <h1>{{ .Summary.TotalChannels }}</h1> chart.draw(chartData, options); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/login.html b/ui/web/template/login.html index 96739f861..1866189f4 100644 --- a/ui/web/template/login.html +++ b/ui/web/template/login.html @@ -2,184 +2,169 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "login" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body class="login-body"> - <div class="container mt-5 pt-5"> - <div class="row p-5"> - <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> - <div - class="row text-center mb-4 d-flex flex-column align-items-center" - > - <div class="mb-3 border-bottom pb-3"> - <div class="d-flex justify-content-center mt-2"> - <h1 class="mx-3">Magistrala</h1> - </div> - </div> - <div class="login-header mb-4"> - <h2>LOGIN</h2> - </div> - <form - method="post" - id="form" - class="row border-bottom pb-3 mb-3" - onsubmit="submitLoginForm()" - > - <div class="col-md-12"> - <div class="row mb-3"> - <div class="col-md-12 input-field email-field"> - <i class="fas fa-solid fa-envelope"></i> - <input - class="p-3 w-100" - type="email" - name="username" - id="username" - placeholder="Email Address" - /> - </div> - <div id="emailError" class="text-danger"></div> - </div> - <div class="row mb-3"> - <div class="col-md-12 input-field password-field"> - <i class="fas fa-solid fa-lock"></i> - <input - class="p-3 w-100" - type="password" - name="password" - id="password" - placeholder="Password" - /> - </div> - <div id="passwordError" class="text-danger"></div> - </div> - <div id="loginError" class="text-danger"></div> - </div> - <div class="col-md-12 d-grid py-3"> - <button - type="submit" - class="login-btn py-3" - onclick="return validateForm()" - > - Log In - </button> - </div> - </form> - <div class="forgot-password"> - <button - type="button" - id="fp-button" - class="btn btn-link text-light" - data-bs-toggle="modal" - data-bs-target="#forgotPasswordModal" - > - Forgot Password? - </button> - </div> - </div> - </div> - </div> - <div - class="modal fade" - id="forgotPasswordModal" - tabindex="-1" - role="dialog" - aria-labelledby="forgotPasswordModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="forgotPasswordModalLabel"> - Enter email - </h5> - </div> - <div class="modal-body"> - <form - method="post" - action="/reset-request" - class="reset-pw-form" - > - <div class="mb-3"> - <label for="email" class="form-label">Email</label> - <input - type="email" - class="form-control" - name="email" - id="email" - placeholder="Email" - required - /> - </div> - <button type="submit" class="btn submit-button"> - Submit - </button> - </form> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const passwordField = document.querySelector(".password-field"); - const emailField = document.querySelector(".email-field"); - const loginError = document.getElementById("loginError"); - const passwordError = document.getElementById("passwordError"); - const emailError = document.getElementById("emailError"); + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body class="login-body"> + <div class="container mt-5 pt-5"> + <div class="row p-5"> + <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> + <div class="row text-center mb-4 d-flex flex-column align-items-center"> + <div class="mb-3 border-bottom pb-3"> + <div class="d-flex justify-content-center mt-2"> + <h1 class="mx-3">Magistrala</h1> + </div> + </div> + <div class="login-header mb-4"> + <h2>LOGIN</h2> + </div> + <form + method="post" + id="form" + class="row border-bottom pb-3 mb-3" + onsubmit="submitLoginForm()" + > + <div class="col-md-12"> + <div class="row mb-3"> + <div class="col-md-12 input-field email-field"> + <i class="fas fa-solid fa-envelope"></i> + <input + class="p-3 w-100" + type="email" + name="username" + id="username" + placeholder="Email Address" + /> + </div> + <div id="emailError" class="text-danger"></div> + </div> + <div class="row mb-3"> + <div class="col-md-12 input-field password-field"> + <i class="fas fa-solid fa-lock"></i> + <input + class="p-3 w-100" + type="password" + name="password" + id="password" + placeholder="Password" + /> + </div> + <div id="passwordError" class="text-danger"></div> + </div> + <div id="loginError" class="text-danger"></div> + </div> + <div class="col-md-12 d-grid py-3"> + <button type="submit" class="login-btn py-3" onclick="return validateForm()"> + Log In + </button> + </div> + </form> + <div class="forgot-password"> + <button + type="button" + id="fp-button" + class="btn btn-link text-light" + data-bs-toggle="modal" + data-bs-target="#forgotPasswordModal" + > + Forgot Password? + </button> + </div> + </div> + </div> + </div> + <div + class="modal fade" + id="forgotPasswordModal" + tabindex="-1" + role="dialog" + aria-labelledby="forgotPasswordModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="forgotPasswordModalLabel">Enter email</h5> + </div> + <div class="modal-body"> + <form method="post" action="/reset-request" class="reset-pw-form"> + <div class="mb-3"> + <label for="email" class="form-label">Email</label> + <input + type="email" + class="form-control" + name="email" + id="email" + placeholder="Email" + required + /> + </div> + <button type="submit" class="btn submit-button">Submit</button> + </form> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const passwordField = document.querySelector(".password-field"); + const emailField = document.querySelector(".email-field"); + const loginError = document.getElementById("loginError"); + const passwordError = document.getElementById("passwordError"); + const emailError = document.getElementById("emailError"); - function validateForm() { - var password = document.getElementById("password").value; - var email = document.getElementById("username").value; + function validateForm() { + var password = document.getElementById("password").value; + var email = document.getElementById("username").value; - loginError.innerHTML = ""; - passwordError.innerHTML = ""; - emailError.innerHTML = ""; - passwordField.classList.remove("border-red"); - emailField.classList.remove("border-red"); - var isValid = true; + loginError.innerHTML = ""; + passwordError.innerHTML = ""; + emailError.innerHTML = ""; + passwordField.classList.remove("border-red"); + emailField.classList.remove("border-red"); + var isValid = true; - if (email === "") { - emailError.innerHTML = "email is required!"; - emailField.classList.add("border-red"); - isValid = false; - } - if (password.length < 8) { - passwordError.innerHTML = - "Password must have a minimum of 8 characters!"; - passwordField.classList.add("border-red"); - isValid = false; - } - return isValid; - } + if (email === "") { + emailError.innerHTML = "email is required!"; + emailField.classList.add("border-red"); + isValid = false; + } + if (password.length < 8) { + passwordError.innerHTML = "Password must have a minimum of 8 characters!"; + passwordField.classList.add("border-red"); + isValid = false; + } + return isValid; + } - function submitLoginForm() { - event.preventDefault(); - var form = event.target; - fetch("/login", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 401) { - passwordField.classList.add("border-red"); - emailField.classList.add("border-red"); - errorMessage = "invalid email or password. Please try again!"; - showAlert(errorMessage); - } else { - form.reset(); - window.location.href = "/"; - } - }) - .catch((error) => { - console.error("error submitting login form: ", error); - }); - } + function submitLoginForm() { + event.preventDefault(); + var form = event.target; + fetch("/login", { + method: "POST", + body: new FormData(form), + }) + .then((response) => { + if (response.status === 401) { + passwordField.classList.add("border-red"); + emailField.classList.add("border-red"); + errorMessage = "invalid email or password. Please try again!"; + showAlert(errorMessage); + } else { + form.reset(); + window.location.href = "/"; + } + }) + .catch((error) => { + console.error("error submitting login form: ", error); + }); + } - function showAlert(errorMessage) { - loginError.innerHTML = errorMessage; - } - </script> - </body> - </html> + function showAlert(errorMessage) { + loginError.innerHTML = errorMessage; + } + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/messagesread.html b/ui/web/template/messagesread.html index a122e61ee..5f060ca77 100644 --- a/ui/web/template/messagesread.html +++ b/ui/web/template/messagesread.html @@ -1,143 +1,140 @@ <!-- Copyright (c) Abstract Machines SPDX-License-Identifier: Apache-2.0 --> -{{define "messagesread"}} -<!doctype html> -<html lang="en"> - {{template "header"}} +{{ define "messagesread" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} - <body> - {{template "navbar" .}} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <button type="button" class="btn body-button" onclick="openModal()"> - Read Message - </button> - </div> - <!-- Modal --> - <div - class="modal fade" - id="sendMessageModal" - tabindex="-1" - role="dialog" - aria-labelledby="sendMessageModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="sendMessageModalLabel"> - Read Message - </h5> - </div> - <div class="modal-body"> - <div id="alertMessage"></div> - <form method="get"> - <div class="mb-3"> - <label for="chanID" class="form-label">Channel ID</label> - <input - type="text" - class="form-control" - name="chanID" - id="chanID" - aria-describedby="chanIDHelp" - /> - <div id="chanIDHelp" class="form-text"> - Enter Channel ID. + <body> + {{ template "navbar" . }} + + + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <button type="button" class="btn body-button" onclick="openModal()"> + Read Message + </button> + </div> + <!-- Modal --> + <div + class="modal fade" + id="sendMessageModal" + tabindex="-1" + role="dialog" + aria-labelledby="sendMessageModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="sendMessageModalLabel">Read Message</h5> + </div> + <div class="modal-body"> + <div id="alertMessage"></div> + <form method="get"> + <div class="mb-3"> + <label for="chanID" class="form-label">Channel ID</label> + <input + type="text" + class="form-control" + name="chanID" + id="chanID" + aria-describedby="chanIDHelp" + /> + <div id="chanIDHelp" class="form-text">Enter Channel ID.</div> </div> - </div> - <div class="mb-3"> - <label for="thingKey" class="form-label">Thing Key</label> - <input - type="text" - class="form-control" - name="thingKey" - id="thingKey" - aria-describedby="thingKeyHelp" - /> - <div id="thingKeyHelp" class="form-text"> - Enter thing key. + <div class="mb-3"> + <label for="thingKey" class="form-label">Thing Key</label> + <input + type="text" + class="form-control" + name="thingKey" + id="thingKey" + aria-describedby="thingKeyHelp" + /> + <div id="thingKeyHelp" class="form-text">Enter thing key.</div> </div> - </div> - <button type="submit" class="btn body-button">Submit</button> - </form> + <button type="submit" class="btn body-button">Submit</button> + </form> + </div> </div> </div> </div> - </div> - <div class="table-responsive table-container"> - <div> - <span> Channel ID: {{.ChanID}} </span> - </div> - {{template "tableheader" .}} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">#</th> - <th scope="col">Subtopic</th> - <th scope="col">Publisher</th> - <th scope="col">Protocol</th> - <th scope="col">Name</th> - <th scope="col">Unit</th> - <th scope="col">Value</th> - <th scope="col">StringValue</th> - <th scope="col">BoolValue</th> - <th scope="col">DataValue</th> - <th scope="col">Sum</th> - <th scope="col">Time</th> - <th scope="col">UpdateTime</th> - </tr> - </thead> - <tbody> - {{range $i, $c := .Msg}} - <tr> - <td>{{$i}}</td> - <td>{{$c.Subtopic}}</td> - <td>{{$c.Publisher}}</td> - <td>{{$c.Protocol}}</td> - <td>{{$c.Name}}</td> - <td>{{$c.Unit}}</td> - <td>{{if $c.Value}}{{$c.Value}}{{end}}</td> - <td>{{if $c.StringValue}}{{$c.StringValue}}{{end}}</td> - <td>{{if $c.BoolValue}}{{$c.BoolValue}}{{end}}</td> - <td>{{if $c.DataValue}}{{$c.DataValue}}{{end}}</td> - <td>{{if $c.Sum}}{{$c.Sum}}{{end}}</td> - <td>{{unixTimeToHumanTime $c.Time}}</td> - <td> - {{if $c.UpdateTime}}{{ unixTimeToHumanTime - $c.UpdateTime}}{{end}} - </td> - </tr> - {{end}} - </tbody> - </table> + <div class="table-responsive table-container"> + <div> + <span>Channel ID: {{ .ChanID }}</span> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Subtopic</th> + <th scope="col">Publisher</th> + <th scope="col">Protocol</th> + <th scope="col">Name</th> + <th scope="col">Unit</th> + <th scope="col">Value</th> + <th scope="col">StringValue</th> + <th scope="col">BoolValue</th> + <th scope="col">DataValue</th> + <th scope="col">Sum</th> + <th scope="col">Time</th> + <th scope="col">UpdateTime</th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Msg }} + <tr> + <td>{{ $i }}</td> + <td>{{ $c.Subtopic }}</td> + <td>{{ $c.Publisher }}</td> + <td>{{ $c.Protocol }}</td> + <td>{{ $c.Name }}</td> + <td>{{ $c.Unit }}</td> + <td>{{ if $c.Value }}{{ $c.Value }}{{ end }}</td> + <td>{{ if $c.StringValue }}{{ $c.StringValue }}{{ end }}</td> + <td>{{ if $c.BoolValue }}{{ $c.BoolValue }}{{ end }}</td> + <td>{{ if $c.DataValue }}{{ $c.DataValue }}{{ end }}</td> + <td>{{ if $c.Sum }}{{ $c.Sum }}{{ end }}</td> + <td>{{ unixTimeToHumanTime $c.Time }}</td> + <td> + {{ if $c.UpdateTime }} + {{ unixTimeToHumanTime + $c.UpdateTime + }} + {{ end }} + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} </div> - {{ template "tablefooter" .}} </div> </div> - </div> - </div> + </div> + </div> </div> - </div> - {{template "footer"}} + {{ template "footer" }} - <script> - const messageModal = new bootstrap.Modal( - document.getElementById("sendMessageModal"), - ); + <script> + const messageModal = new bootstrap.Modal(document.getElementById("sendMessageModal")); - function openModal() { - messageModal.show(); - } - </script> - </body> -</html> -{{end}} + function openModal() { + messageModal.show(); + } + </script> + </body> + </html> +{{ end }} diff --git a/ui/web/template/navbar.html b/ui/web/template/navbar.html index 368753888..cd7cd264d 100644 --- a/ui/web/template/navbar.html +++ b/ui/web/template/navbar.html @@ -2,204 +2,187 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "navbar" }} - <!-- Sidebbar --> - <ul - class="navbar-nav bg-gradient-primary sidebar sidebar-dark" - id="accordionSidebar" - > - <!-- Sidebar Brand --> - <a class="sidebar-brand d-flex justify-content-center pt-2" href="/"> - <h1 class="mx-3">Magistrala</h1> - </a> + <!-- Sidebbar --> + <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark" id="accordionSidebar"> + <!-- Sidebar Brand --> + <a class="sidebar-brand d-flex justify-content-center pt-2" href="/"> + <h1 class="mx-3">Magistrala</h1> + </a> - <!-- Divider --> - <hr class="sidebar-divider my-0 text-light mb-3" /> + <!-- Divider --> + <hr class="sidebar-divider my-0 text-light mb-3" /> - <li class="nav-item mb-2 mx-2"> - {{ $active := "dashboard" }} - <a - href="/" - class="nav-link {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-home"></i> - <span>Dashboard</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "users" }} - <a - href="/users" - class=" nav-link {{ if (serviceUnavailable "users") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-users"></i> - <span>Users</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "groups" }} - <a - href="/groups" - class="nav-link {{ if (serviceUnavailable "users") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-layer-group"></i> - <span>Groups</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "things" }} - <a - href="/things" - class="nav-link {{ if (serviceUnavailable "things") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-microchip"></i> - <span>Things</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "channels" }} - <a - href="/channels" - class="nav-link {{ if (serviceUnavailable "things") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-exchange-alt"></i> - <span>Channels</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "readmessages" }} - <a - href="/messages/read" - class="nav-link {{ if (serviceUnavailable "things") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-envelope-open"></i> - <span>Messages</span> - </a> - </li> - <li class="nav-item mb-2 mx-2"> - {{ $active = "bootstraps" }} - <a - href="/bootstraps" - class="nav-link {{ if (serviceUnavailable "bootstrap") }} - disabled-item - {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" - > - <i class="fas fa-solid fa-broadcast-tower"></i> - <span>Bootstrap</span> - </a> - </li> + <li class="nav-item mb-2 mx-2"> + {{ $active := "dashboard" }} + <a href="/" class="nav-link {{ if eq .NavbarActive $active }}active{{ end }}"> + <i class="fas fa-home"></i> + <span>Dashboard</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "users" }} + <a + href="/users" + class=" nav-link {{ if (serviceUnavailable "users") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-users"></i> + <span>Users</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "groups" }} + <a + href="/groups" + class="nav-link {{ if (serviceUnavailable "users") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-layer-group"></i> + <span>Groups</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "things" }} + <a + href="/things" + class="nav-link {{ if (serviceUnavailable "things") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-microchip"></i> + <span>Things</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "channels" }} + <a + href="/channels" + class="nav-link {{ if (serviceUnavailable "things") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-exchange-alt"></i> + <span>Channels</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "readmessages" }} + <a + href="/messages/read" + class="nav-link {{ if (serviceUnavailable "things") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-envelope-open"></i> + <span>Messages</span> + </a> + </li> + <li class="nav-item mb-2 mx-2"> + {{ $active = "bootstraps" }} + <a + href="/bootstraps" + class="nav-link {{ if (serviceUnavailable "bootstrap") }} + disabled-item + {{ end }} {{ if eq .NavbarActive $active }}active{{ end }}" + > + <i class="fas fa-solid fa-broadcast-tower"></i> + <span>Bootstrap</span> + </a> + </li> - <!-- Sidebar Toggler (Sidebar) --> - <div class="d-flex justify-content-center"> - <button class="rounded-circle border-0" id="sidebarToggle"></button> - </div> - </ul> + <!-- Sidebar Toggler (Sidebar) --> + <div class="d-flex justify-content-center"> + <button class="rounded-circle border-0" id="sidebarToggle"></button> + </div> + </ul> - <!-- End of sidebar --> + <!-- End of sidebar --> - <!-- Header navbar --> - <nav class="navbar navbar-expand topbar fixed-top"> - <div class="container-fluid mb-3"> - <div class=""> - <!-- Sidebar Toggle (Topbar) --> - <button - id="sidebarToggleTop" - class="btn btn-link d-md-none rounded-circle mr-3 user-item" - > - <i class="fa fa-bars"></i> - </button> - </div> + <!-- Header navbar --> + <nav class="navbar navbar-expand topbar fixed-top"> + <div class="container-fluid mb-3"> + <div class=""> + <!-- Sidebar Toggle (Topbar) --> + <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3 user-item"> + <i class="fa fa-bars"></i> + </button> + </div> - <div class="navbarSupportedContent"> - <ul class="navbar-nav ms-auto d-flex align-items-center"> - <li class="nav-item me-2"> - <a - href="https://docs.mainflux.io/" - target="_blank" - class="doc-button btn" - role="button" - > - <span>Documentation</span> - </a> - </li> - <li class="nav-item dropstart me-3"> - <a - class="user-item nav-link d-flex flex-row" - href="#" - role="button" - data-bs-toggle="dropdown" - aria-expanded="false" - > - <span class="me-2 d-none d-lg-inline text-muted-600 small"> - {{ .User.Name }} - </span> - <i class="fa-solid fa-user"></i> - </a> - <ul class="dropdown-menu mt-5 position-absolute end-0 p-3"> - <li class="d-flex justify-content-start mb-2"> - <div class="align-self-center"> - <i class="fa-regular fa-user fs-2"></i> - </div> - <div> - <span class="dropdown-item">{{ .User.Name }}</span> - <span class="dropdown-item text-muted"> - {{ .User.Credentials.Identity }} - </span> - </div> - </li> - <li class="mb-2"><hr class="dropdown-divider" /></li> - <li class="mb-2"> - <a - href="/password" - class="dropdown-item user-item-button p-2 mb-2" - > - <i class="fas fa-solid fa-lock me-2"></i> - <span>Update password</span> - </a> - <a - href="/logout" - class="dropdown-item user-item-button p-2 mb-2" - > - <i class="fa-solid fa-right-from-bracket me-2"></i> - <span>Log out</span> - </a> - </li> - </ul> - </li> - </ul> - </div> - </div> - </nav> - <script defer> - // Toggle the side navigation - document - .querySelectorAll("#sidebarToggle, #sidebarToggleTop") - .forEach(function (element) { - element.addEventListener("click", function (e) { - document.body.classList.toggle("sidebar-toggled"); - document.querySelector(".sidebar").classList.toggle("toggled"); - }); - }); + <div class="navbarSupportedContent"> + <ul class="navbar-nav ms-auto d-flex align-items-center"> + <li class="nav-item me-2"> + <a + href="https://docs.mainflux.io/" + target="_blank" + class="doc-button btn" + role="button" + > + <span>Documentation</span> + </a> + </li> + <li class="nav-item dropstart me-3"> + <a + class="user-item nav-link d-flex flex-row" + href="#" + role="button" + data-bs-toggle="dropdown" + aria-expanded="false" + > + <span class="me-2 d-none d-lg-inline text-muted-600 small"> + {{ .User.Name }} + </span> + <i class="fa-solid fa-user"></i> + </a> + <ul class="dropdown-menu mt-5 position-absolute end-0 p-3"> + <li class="d-flex justify-content-start mb-2"> + <div class="align-self-center"> + <i class="fa-regular fa-user fs-2"></i> + </div> + <div> + <span class="dropdown-item">{{ .User.Name }}</span> + <span class="dropdown-item text-muted"> + {{ .User.Credentials.Identity }} + </span> + </div> + </li> + <li class="mb-2"><hr class="dropdown-divider" /></li> + <li class="mb-2"> + <a href="/password" class="dropdown-item user-item-button p-2 mb-2"> + <i class="fas fa-solid fa-lock me-2"></i> + <span>Update password</span> + </a> + <a href="/logout" class="dropdown-item user-item-button p-2 mb-2"> + <i class="fa-solid fa-right-from-bracket me-2"></i> + <span>Log out</span> + </a> + </li> + </ul> + </li> + </ul> + </div> + </div> + </nav> + <script defer> + // Toggle the side navigation + document.querySelectorAll("#sidebarToggle, #sidebarToggleTop").forEach(function (element) { + element.addEventListener("click", function (e) { + document.body.classList.toggle("sidebar-toggled"); + document.querySelector(".sidebar").classList.toggle("toggled"); + }); + }); - // Close any open menu accordions when window is resized below 768px - window.addEventListener("resize", function () { - // Toggle the side navigation when window is resized below 480px - if ( - window.innerWidth < 480 && - !document.querySelector(".sidebar").classList.contains("toggled") - ) { - document.body.classList.add("sidebar-toggled"); - document.querySelector(".sidebar").classList.add("toggled"); - } - }); - </script> + // Close any open menu accordions when window is resized below 768px + window.addEventListener("resize", function () { + // Toggle the side navigation when window is resized below 480px + if ( + window.innerWidth < 480 && + !document.querySelector(".sidebar").classList.contains("toggled") + ) { + document.body.classList.add("sidebar-toggled"); + document.querySelector(".sidebar").classList.add("toggled"); + } + }); + </script> {{ end }} diff --git a/ui/web/template/resetpassword.html b/ui/web/template/resetpassword.html index 8af42f4b6..380896f7b 100644 --- a/ui/web/template/resetpassword.html +++ b/ui/web/template/resetpassword.html @@ -2,93 +2,87 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "resetPassword" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body class="login-body"> - <div class="container mt-5 pt-5"> - <div class="row p-5"> - <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> - <div - class="row text-center mb-4 d-flex flex-column align-items-center" - > - <div class="mb-3 border-bottom pb-3"> - <div class="sidebar-brand d-flex justify-content-center mt-2"> - <h1 class="mx-3">Magistrala</h1> - </div> - </div> - <div class="login-header mb-4"> - <h2>Reset Password</h2> - </div> - <form - method="post" - id="form" - class="row border-bottom pb-3 mb-3" - onsubmit="return validateForm()" - > - <div class="col-md-12"> - <div class="row mb-3"> - <div class="col-md-12 input-field"> - <i class="fas fa-solid fa-lock"></i> - <input - class="p-3 w-100" - type="password" - name="password" - id="password" - placeholder="Password" - required - /> - <div id="passwordError" class="text-danger"></div> - </div> - </div> - <div class="row mb-3"> - <div class="col-md-12 input-field"> - <i class="fas fa-solid fa-lock"></i> - <input - class="p-3 w-100" - type="password" - name="confirmPassword" - id="confirmPassword" - placeholder="confirm Password" - required - /> - <div id="confirmPasswordError" class="text-danger"></div> - </div> - </div> - </div> - <div class="col-md-12 d-grid py-3"> - <button type="submit" class="login-btn py-3"> - Reset Password - </button> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - function validateForm() { - var password = document.getElementById("password").value; - var confirmPassword = - document.getElementById("confirmPassword").value; + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body class="login-body"> + <div class="container mt-5 pt-5"> + <div class="row p-5"> + <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> + <div class="row text-center mb-4 d-flex flex-column align-items-center"> + <div class="mb-3 border-bottom pb-3"> + <div class="sidebar-brand d-flex justify-content-center mt-2"> + <h1 class="mx-3">Magistrala</h1> + </div> + </div> + <div class="login-header mb-4"> + <h2>Reset Password</h2> + </div> + <form + method="post" + id="form" + class="row border-bottom pb-3 mb-3" + onsubmit="return validateForm()" + > + <div class="col-md-12"> + <div class="row mb-3"> + <div class="col-md-12 input-field"> + <i class="fas fa-solid fa-lock"></i> + <input + class="p-3 w-100" + type="password" + name="password" + id="password" + placeholder="Password" + required + /> + <div id="passwordError" class="text-danger"></div> + </div> + </div> + <div class="row mb-3"> + <div class="col-md-12 input-field"> + <i class="fas fa-solid fa-lock"></i> + <input + class="p-3 w-100" + type="password" + name="confirmPassword" + id="confirmPassword" + placeholder="confirm Password" + required + /> + <div id="confirmPasswordError" class="text-danger"></div> + </div> + </div> + </div> + <div class="col-md-12 d-grid py-3"> + <button type="submit" class="login-btn py-3">Reset Password</button> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + function validateForm() { + var password = document.getElementById("password").value; + var confirmPassword = document.getElementById("confirmPassword").value; - passwordError.innerHTML = ""; - confirmPasswordError.innerHTML = ""; - var isValid = true; + passwordError.innerHTML = ""; + confirmPasswordError.innerHTML = ""; + var isValid = true; - if (password.length < 8) { - passwordError.innerHTML = - "Password must have a minimum of 8 characters!"; - isValid = false; - } - if (confirmPassword != password) { - confirmPasswordError.innerHTML = "Passwords do not match!"; - isValid = false; - } - return isValid; - } - </script> - </body> - </html> + if (password.length < 8) { + passwordError.innerHTML = "Password must have a minimum of 8 characters!"; + isValid = false; + } + if (confirmPassword != password) { + confirmPasswordError.innerHTML = "Passwords do not match!"; + isValid = false; + } + return isValid; + } + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/tablefooter.html b/ui/web/template/tablefooter.html index 2b7e5d374..31c3e8c54 100644 --- a/ui/web/template/tablefooter.html +++ b/ui/web/template/tablefooter.html @@ -2,62 +2,62 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "tablefooter" }} - {{ $currentPage := .CurrentPage }} - {{ $totalPages := - .Pages - }} - <script> - document.addEventListener("DOMContentLoaded", function () { - var selectedLimit = localStorage.getItem("selectedLimit"); - if (selectedLimit) { - var paginationLinks = document.querySelectorAll(".pagination a"); + {{ $currentPage := .CurrentPage }} + {{ $totalPages := + .Pages + }} + <script> + document.addEventListener("DOMContentLoaded", function () { + var selectedLimit = localStorage.getItem("selectedLimit"); + if (selectedLimit) { + var paginationLinks = document.querySelectorAll(".pagination a"); - paginationLinks.forEach(function (link) { - var href = link.getAttribute("href"); - link.setAttribute("href", href + "&limit=" + selectedLimit); - }); - } - }); - </script> - <nav class="border-top" aria-label="Page navigation example"> - <ul class="pagination justify-content-end mt-3"> - {{ $prevPage := sub $currentPage 1 }} - {{ if le $prevPage 0 }} - <li class="page-item disabled"> - <span class="page-link">Previous</span> - </li> - {{ else }} - <li class="page-item"> - <a class="page-link" href="?page={{ $prevPage }}">Previous</a> - </li> - {{ end }} - {{ $startPage := max 1 (sub $currentPage 2) }} - {{ $endPage := min - $totalPages (add $currentPage 2) - }} - {{ range fromTo $startPage $endPage }} - {{ if eq . $currentPage }} - <li class="page-item active"> - <span class="page-link">{{ . }}</span> - </li> - {{ else }} - <li class="page-item"> - <a class="page-link" href="?page={{ . }}">{{ . }}</a> - </li> - {{ end }} - {{ end }} - {{ $nextPage := add $currentPage 1 }} - {{ if ge $nextPage - $totalPages - }} - <li class="page-item disabled"> - <span class="page-link">Next</span> - </li> - {{ else }} - <li class="page-item"> - <a class="page-link" href="?page={{ $nextPage }}">Next</a> - </li> - {{ end }} - </ul> - </nav> + paginationLinks.forEach(function (link) { + var href = link.getAttribute("href"); + link.setAttribute("href", href + "&limit=" + selectedLimit); + }); + } + }); + </script> + <nav class="border-top" aria-label="Page navigation example"> + <ul class="pagination justify-content-end mt-3"> + {{ $prevPage := sub $currentPage 1 }} + {{ if le $prevPage 0 }} + <li class="page-item disabled"> + <span class="page-link">Previous</span> + </li> + {{ else }} + <li class="page-item"> + <a class="page-link" href="?page={{ $prevPage }}">Previous</a> + </li> + {{ end }} + {{ $startPage := max 1 (sub $currentPage 2) }} + {{ $endPage := min + $totalPages (add $currentPage 2) + }} + {{ range fromTo $startPage $endPage }} + {{ if eq . $currentPage }} + <li class="page-item active"> + <span class="page-link">{{ . }}</span> + </li> + {{ else }} + <li class="page-item"> + <a class="page-link" href="?page={{ . }}">{{ . }}</a> + </li> + {{ end }} + {{ end }} + {{ $nextPage := add $currentPage 1 }} + {{ if ge $nextPage + $totalPages + }} + <li class="page-item disabled"> + <span class="page-link">Next</span> + </li> + {{ else }} + <li class="page-item"> + <a class="page-link" href="?page={{ $nextPage }}">Next</a> + </li> + {{ end }} + </ul> + </nav> {{ end }} diff --git a/ui/web/template/tableheader.html b/ui/web/template/tableheader.html index d2eafb1a0..373361eaf 100644 --- a/ui/web/template/tableheader.html +++ b/ui/web/template/tableheader.html @@ -2,59 +2,35 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "tableheader" }} - {{ $currentPage := .CurrentPage }} - <div class="d-flex flex-row justify-content-start mb-2"> - <div class="dropdown border"> - <button - class="btn btn-light dropdown-toggle" - type="button" - id="itemsPerPageDropdown" - data-bs-toggle="dropdown" - aria-expanded="false" - > - {{ .Limit }} - </button> - <ul class="dropdown-menu" aria-labelledby="itemsPerPageDropdown"> - <li> - <a - class="dropdown-item" - href="?page={{ $currentPage }}&limit=5" - id="limit5" - > - 5 - </a> - </li> - <li> - <a - class="dropdown-item" - href="?page={{ $currentPage }}&limit=10" - id="limit10" - > - 10 - </a> - </li> - <li> - <a - class="dropdown-item" - href="?page={{ $currentPage }}&limit=20" - id="limit20" - > - 20 - </a> - </li> - <li> - <a - class="dropdown-item" - href="?page={{ $currentPage }}&limit=50" - id="limit50" - > - 50 - </a> - </li> - </ul> - </div> - <span class="align-self-center mx-2">Items per page</span> - </div> + {{ $currentPage := .CurrentPage }} + <div class="d-flex flex-row justify-content-start mb-2"> + <div class="dropdown border"> + <button + class="btn btn-light dropdown-toggle" + type="button" + id="itemsPerPageDropdown" + data-bs-toggle="dropdown" + aria-expanded="false" + > + {{ .Limit }} + </button> + <ul class="dropdown-menu" aria-labelledby="itemsPerPageDropdown"> + <li> + <a class="dropdown-item" href="?page={{ $currentPage }}&limit=5" id="limit5">5</a> + </li> + <li> + <a class="dropdown-item" href="?page={{ $currentPage }}&limit=10" id="limit10">10</a> + </li> + <li> + <a class="dropdown-item" href="?page={{ $currentPage }}&limit=20" id="limit20">20</a> + </li> + <li> + <a class="dropdown-item" href="?page={{ $currentPage }}&limit=50" id="limit50">50</a> + </li> + </ul> + </div> + <span class="align-self-center mx-2">Items per page</span> + </div> <script> document.addEventListener("DOMContentLoaded", function () { // Get the dropdown items diff --git a/ui/web/template/terminal.html b/ui/web/template/terminal.html index fb1c7cb54..bbc15138c 100644 --- a/ui/web/template/terminal.html +++ b/ui/web/template/terminal.html @@ -2,61 +2,61 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "remoteTerminal" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <style> - .terminal { - color: #fff; - width: 100%; - height: 300px; - background-color: #272822; - padding: 10px; - overflow-y: scroll; - } + <!doctype html> + <html lang="en"> + {{ template "header" }} + <style> + .terminal { + color: #fff; + width: 100%; + height: 300px; + background-color: #272822; + padding: 10px; + overflow-y: scroll; + } - .input-line { - display: flex; - background-color: #272822; - } + .input-line { + display: flex; + background-color: #272822; + } - .input-prefix { - color: #f92672; - margin-right: 5px; - } + .input-prefix { + color: #f92672; + margin-right: 5px; + } - .input-field { - width: 90%; - flex: 1; - color: #fff; - background-color: #272822; - padding: 10px; - border: none; - outline: none; - } - </style> - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="terminal"> - <!-- Existing terminal content here --> - </div> - <div class="input-line"> - <form action="/bootstraps/1/terminal/input" method="POST"> - <span class="input-prefix">></span> - <input class="input-field" name="command" autofocus /> - </form> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + .input-field { + width: 90%; + flex: 1; + color: #fff; + background-color: #272822; + padding: 10px; + border: none; + outline: none; + } + </style> + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="terminal"> + <!-- Existing terminal content here --> + </div> + <div class="input-line"> + <form action="/bootstraps/1/terminal/input" method="POST"> + <span class="input-prefix">></span> + <input class="input-field" name="command" autofocus /> + </form> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> document.addEventListener("DOMContentLoaded", function () { var form = document.querySelector("form"); @@ -88,6 +88,6 @@ }); }); </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/thing.html b/ui/web/template/thing.html index c75ebb9c7..11efc99dc 100644 --- a/ui/web/template/thing.html +++ b/ui/web/template/thing.html @@ -2,226 +2,163 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "thing" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/things/{{ .Thing.ID }}/channels" - role="button" - > - Thing Channels - </a> - <a - class="btn body-button" - href="/things/{{ .Thing.ID }}/users" - role="button" - > - Thing Users - </a> - </div> - <div class="table-responsive table-container"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="row">THING</th> - </tr> - </thead> - <tbody> - {{ $disableButton := false }} - <tr> - <th>Name</th> - <td - class="editable" - contenteditable="false" - data-field="name" - > - {{ .Thing.Name }} - </td> - <td> - <button - class="edit-btn" - id="edit-name" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-name"> - Save - </button> - <button class="cancel-btn" id="cancel-name"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>ID</th> - <td>{{ .Thing.ID }}</td> - <td></td> - </tr> - <tr> - <th>Secret</th> - <td - class="editable" - contenteditable="false" - data-field="secret" - > - {{ .Thing.Credentials.Secret }} - </td> - <td> - <button - class="edit-btn" - id="edit-secret" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-secret"> - Save - </button> - <button class="cancel-btn" id="cancel-secret"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Tags</th> - <td - class="editable" - contenteditable="false" - data-field="tags" - > - {{ toSlice .Thing.Tags }} - </td> - <td> - <button - class="edit-btn" - id="edit-tags" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-tags"> - Save - </button> - <button class="cancel-btn" id="cancel-tags"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Owner</th> - <td - class="editable" - contenteditable="false" - data-field="owner" - > - {{ .Thing.Owner }} - </td> - <td> - <button - class="edit-btn" - id="edit-owner" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-owner"> - Save - </button> - <button class="cancel-btn" id="cancel-owner"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Metadata</th> - <td - class="editable" - contenteditable="false" - data-field="metadata" - > - {{ toJSON .Thing.Metadata }} - </td> - <td> - <button - class="edit-btn" - id="edit-metadata" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-metadata"> - Save - </button> - <button class="cancel-btn" id="cancel-metadata"> - Cancel - </button> - </div> - </td> - </tr> - </tbody> - </table> - <div id="error-message" class="text-danger"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/things/{{ .Thing.ID }}/channels" role="button"> + Thing Channels + </a> + <a class="btn body-button" href="/things/{{ .Thing.ID }}/users" role="button"> + Thing Users + </a> + </div> + <div class="table-responsive table-container"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="row">THING</th> + </tr> + </thead> + <tbody> + {{ $disableButton := false }} + <tr> + <th>Name</th> + <td class="editable" contenteditable="false" data-field="name"> + {{ .Thing.Name }} + </td> + <td> + <button + class="edit-btn" + id="edit-name" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-name">Save</button> + <button class="cancel-btn" id="cancel-name">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>ID</th> + <td>{{ .Thing.ID }}</td> + <td></td> + </tr> + <tr> + <th>Secret</th> + <td class="editable" contenteditable="false" data-field="secret"> + {{ .Thing.Credentials.Secret }} + </td> + <td> + <button + class="edit-btn" + id="edit-secret" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-secret">Save</button> + <button class="cancel-btn" id="cancel-secret">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Tags</th> + <td class="editable" contenteditable="false" data-field="tags"> + {{ toSlice .Thing.Tags }} + </td> + <td> + <button + class="edit-btn" + id="edit-tags" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-tags">Save</button> + <button class="cancel-btn" id="cancel-tags">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Owner</th> + <td class="editable" contenteditable="false" data-field="owner"> + {{ .Thing.Owner }} + </td> + <td> + <button + class="edit-btn" + id="edit-owner" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-owner">Save</button> + <button class="cancel-btn" id="cancel-owner">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Metadata</th> + <td class="editable" contenteditable="false" data-field="metadata"> + {{ toJSON .Thing.Metadata }} + </td> + <td> + <button + class="edit-btn" + id="edit-metadata" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-metadata">Save</button> + <button class="cancel-btn" id="cancel-metadata">Cancel</button> + </div> + </td> + </tr> + </tbody> + </table> + <div id="error-message" class="text-danger"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> attachEditRowListener( { @@ -238,6 +175,6 @@ } ); </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/thingchannels.html b/ui/web/template/thingchannels.html index 774b5b417..186b991e4 100644 --- a/ui/web/template/thingchannels.html +++ b/ui/web/template/thingchannels.html @@ -2,276 +2,238 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "thingchannels" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/things/{{ .Thing.ID }}" - role="button" - > - Thing - </a> - <a - class="btn body-button" - href="/things/{{ .Thing.ID }}/users" - role="button" - > - Thing Users - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Thing Channels</h4> - <button - role="button" - class="btn body-button" - onclick="openChannelModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addChannelModal" - tabindex="-1" - aria-labelledby="addChannelModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addChannelModalLabel" - > - Add Channel - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/things/{{ .Thing.ID }}/channels/connect?item=things" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Channel ID - </label> - <input - type="text" - name="channelFilter" - id="channelFilter" - placeholder="Filter by Channel ID" - /> - <select - class="form-select" - name="channelID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a channel</option> - </select> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $thing := .Thing }} - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col">{{ toJSON $c.Metadata }}</td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="d-flex flex-row"> - <button - type="button" - class="btn btn-sm doc-button" - data-bs-toggle="modal" - data-bs-target="#sendMesageModal{{ $i }}" - > - Send Message - </button> - <form - action="/things/{{ $thing.ID }}/channels/disconnect?item=things" - method="post" - > - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - <!-- modal --> - <div - class="modal fade" - id="sendMesageModal{{ $i }}" - tabindex="-1" - aria-labelledby="sendMesageModalLabel{{ $i }}" - aria-hidden="true" - > - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="sendMesageModalLabel{{ $i }}" - > - Send Message - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form action="/messages" method="post"> - <div class="modal-body"> - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c }}" - /> - <input - type="hidden" - name="thingKey" - id="thingKey" - value="{{ $thing.Credentials.Secret }}" - /> - <div class="mb-3"> - <label for="message" class="form-label"> - Message - </label> - <textarea - class="form-control" - name="message" - id="message" - aria-describedby="messageHelp" - ></textarea> - <div id="messageHelp" class="form-text"> - Enter Message. - </div> - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Close - </button> - <button - type="submit" - class="btn doc-button" - > - Send Message - </button> - </div> - </form> - </div> - </div> - </div> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/things/{{ .Thing.ID }}" role="button">Thing</a> + <a class="btn body-button" href="/things/{{ .Thing.ID }}/users" role="button"> + Thing Users + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Thing Channels</h4> + <button role="button" class="btn body-button" onclick="openChannelModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addChannelModal" + tabindex="-1" + aria-labelledby="addChannelModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/things/{{ .Thing.ID }}/channels/connect?item=things" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Channel ID</label> + <input + type="text" + name="channelFilter" + id="channelFilter" + placeholder="Filter by Channel ID" + /> + <select + class="form-select" + name="channelID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a channel</option> + </select> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $thing := .Thing }} + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col">{{ toJSON $c.Metadata }}</td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="d-flex flex-row"> + <button + type="button" + class="btn btn-sm doc-button" + data-bs-toggle="modal" + data-bs-target="#sendMesageModal{{ $i }}" + > + Send Message + </button> + <form + action="/things/{{ $thing.ID }}/channels/disconnect?item=things" + method="post" + > + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + <!-- modal --> + <div + class="modal fade" + id="sendMesageModal{{ $i }}" + tabindex="-1" + aria-labelledby="sendMesageModalLabel{{ $i }}" + aria-hidden="true" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="sendMesageModalLabel{{ $i }}"> + Send Message + </h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form action="/messages" method="post"> + <div class="modal-body"> + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c }}" + /> + <input + type="hidden" + name="thingKey" + id="thingKey" + value="{{ $thing.Credentials.Secret }}" + /> + <div class="mb-3"> + <label for="message" class="form-label">Message</label> + <textarea + class="form-control" + name="message" + id="message" + aria-describedby="messageHelp" + ></textarea> + <div id="messageHelp" class="form-text">Enter Message.</div> + </div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Close + </button> + <button type="submit" class="btn doc-button"> + Send Message + </button> + </div> + </form> + </div> + </div> + </div> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> - {{ template "footer" }} - <script> - const channelModal = new bootstrap.Modal( - document.getElementById("addChannelModal"), - ); - function openChannelModal() { - channelModal.show(); - } + {{ template "footer" }} + <script> + const channelModal = new bootstrap.Modal(document.getElementById("addChannelModal")); + function openChannelModal() { + channelModal.show(); + } - fetchIndividualEntity({ - input: "channelFilter", - itemSelect: "infiniteScroll", - item: "channels", - }); - </script> - </body> - </html> + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/things.html b/ui/web/template/things.html index 76dbc8b85..15b16af62 100644 --- a/ui/web/template/things.html +++ b/ui/web/template/things.html @@ -2,145 +2,123 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "things" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <!-- Button trigger modal --> - <button - type="button" - class="btn body-button" - onclick="openModal('single')" - > - Add Thing - </button> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <!-- Button trigger modal --> + <button type="button" class="btn body-button" onclick="openModal('single')"> + Add Thing + </button> - <!-- Modal --> - <div - class="modal fade" - id="addThingModal" - tabindex="-1" - role="dialog" - aria-labelledby="addThingModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addThingModalLabel"> - Add Thing - </h5> - </div> - <div class="modal-body"> - <div id="alertMessage"></div> - <form id="thingform"> - <div class="mb-3"> - <label for="name" class="form-label">Name</label> - <input - type="text" - class="form-control" - name="name" - id="name" - placeholder="Device Name" - /> - <div id="nameError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="thingID" class="form-label"> - Thing ID - </label> - <input - type="text" - class="form-control" - name="thingID" - id="thingID" - placeholder="Thing ID" - /> - </div> - <div class="mb-3"> - <label for="identity" class="form-label"> - Identity - </label> - <input - type="text" - class="form-control" - name="identity" - id="identity" - placeholder="Thing Identity" - /> - </div> - <div class="mb-3"> - <label for="secret" class="form-label"> - Secret - </label> - <input - type="text" - class="form-control" - name="secret" - id="secret" - placeholder="Thing Secret" - /> - <div id="secretError" class="error-message"></div> - </div> - <div class="mb-3"> - <label for="tags" class="form-label">Tags</label> - <input - type="text" - class="form-control" - name="tags" - id="tags" - aria-describedby="tagHelp" - value="[]" - /> - <div id="tagHelp" class="form-text"> - Enter device tags as a string slice. - </div> - <div id="tagsError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="metadata" class="form-label"> - Metadata - </label> - <input - type="text" - class="form-control" - name="metadata" - id="metadata" - value="{}" - /> - <div id="metadataHelp" class="form-text"> - Enter device metadata in JSON format. - </div> - <div id="metadataError" class="text-danger"></div> - </div> - <button - type="submit" - class="btn body-button" - id="create-thing-button" - > - Submit - </button> - </form> - </div> - </div> - </div> - </div> + <!-- Modal --> + <div + class="modal fade" + id="addThingModal" + tabindex="-1" + role="dialog" + aria-labelledby="addThingModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addThingModalLabel">Add Thing</h5> + </div> + <div class="modal-body"> + <div id="alertMessage"></div> + <form id="thingform"> + <div class="mb-3"> + <label for="name" class="form-label">Name</label> + <input + type="text" + class="form-control" + name="name" + id="name" + placeholder="Device Name" + /> + <div id="nameError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="thingID" class="form-label">Thing ID</label> + <input + type="text" + class="form-control" + name="thingID" + id="thingID" + placeholder="Thing ID" + /> + </div> + <div class="mb-3"> + <label for="identity" class="form-label">Identity</label> + <input + type="text" + class="form-control" + name="identity" + id="identity" + placeholder="Thing Identity" + /> + </div> + <div class="mb-3"> + <label for="secret" class="form-label">Secret</label> + <input + type="text" + class="form-control" + name="secret" + id="secret" + placeholder="Thing Secret" + /> + <div id="secretError" class="error-message"></div> + </div> + <div class="mb-3"> + <label for="tags" class="form-label">Tags</label> + <input + type="text" + class="form-control" + name="tags" + id="tags" + aria-describedby="tagHelp" + value="[]" + /> + <div id="tagHelp" class="form-text"> + Enter device tags as a string slice. + </div> + <div id="tagsError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="metadata" class="form-label">Metadata</label> + <input + type="text" + class="form-control" + name="metadata" + id="metadata" + value="{}" + /> + <div id="metadataHelp" class="form-text"> + Enter device metadata in JSON format. + </div> + <div id="metadataError" class="text-danger"></div> + </div> + <button type="submit" class="btn body-button" id="create-thing-button"> + Submit + </button> + </form> + </div> + </div> + </div> + </div> - <!-- Button trigger modal --> - <button - type="button" - class="btn body-button" - onclick="openModal('bulk')" - > - Add Things - </button> + <!-- Button trigger modal --> + <button type="button" class="btn body-button" onclick="openModal('bulk')"> + Add Things + </button> <!-- Modal --> <div @@ -282,35 +260,31 @@ <h5 class="modal-title" id="addThingsModalLabel"> }, }); - const thingModal = new bootstrap.Modal( - document.getElementById("addThingModal"), - ); - const thingsModal = new bootstrap.Modal( - document.getElementById("addThingsModal"), - ); + const thingModal = new bootstrap.Modal(document.getElementById("addThingModal")); + const thingsModal = new bootstrap.Modal(document.getElementById("addThingsModal")); - function openModal(modal) { - if (modal === "single") { - thingModal.show(); - } else if (modal === "bulk") { - thingsModal.show(); - } - } + function openModal(modal) { + if (modal === "single") { + thingModal.show(); + } else if (modal === "bulk") { + thingsModal.show(); + } + } - submitCreateForm({ - url: "/things", - formId: "thingform", - alertDiv: "alertMessage", - modal: thingModal, - }); + submitCreateForm({ + url: "/things", + formId: "thingform", + alertDiv: "alertMessage", + modal: thingModal, + }); - submitCreateForm({ - url: "/things/bulk", - formId: "bulkThingsForm", - alertDiv: "alertBulkMessage", - modal: thingsModal, - }); - </script> - </body> - </html> + submitCreateForm({ + url: "/things/bulk", + formId: "bulkThingsForm", + alertDiv: "alertBulkMessage", + modal: thingsModal, + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/thingusers.html b/ui/web/template/thingusers.html index 72038a9c0..9c460b7f6 100644 --- a/ui/web/template/thingusers.html +++ b/ui/web/template/thingusers.html @@ -2,219 +2,182 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "thingusers" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - class="btn body-button" - href="/things/{{ .ThingID }}" - role="button" - > - Thing - </a> - <a - class="btn body-button" - href="/things/{{ .ThingID }}/channels" - role="button" - > - Thing Channels - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>Thing Users</h4> - <button - role="button" - class="btn body-button" - onclick="openUserModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addUserModal" - tabindex="-1" - aria-labelledby="addUserModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 class="modal-title fs-5" id="addUserModalLabel"> - Share Thing - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/things/{{ .ThingID }}/share?item=things" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - User ID - </label> - <input - type="text" - name="userFilter" - id="userFilter" - placeholder="Filter by User ID" - /> - <select - class="form-select" - name="userID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a User</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $thingID := .ThingID }} - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col">{{ toJSON $u.Metadata }}</td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form - action="/things/{{ $thingID }}/unshare?item=things" - method="post" - > - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="owner" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const userModal = new bootstrap.Modal( - document.getElementById("addUserModal"), - ); - function openUserModal() { - userModal.show(); - } - fetchIndividualEntity({ - input: "userFilter", - itemSelect: "infiniteScroll", - item: "users", - }); - </script> - </body> - </html> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a class="btn body-button" href="/things/{{ .ThingID }}" role="button">Thing</a> + <a class="btn body-button" href="/things/{{ .ThingID }}/channels" role="button"> + Thing Channels + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>Thing Users</h4> + <button role="button" class="btn body-button" onclick="openUserModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addUserModal" + tabindex="-1" + aria-labelledby="addUserModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addUserModalLabel">Share Thing</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form action="/things/{{ .ThingID }}/share?item=things" method="post"> + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">User ID</label> + <input + type="text" + name="userFilter" + id="userFilter" + placeholder="Filter by User ID" + /> + <select + class="form-select" + name="userID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a User</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $thingID := .ThingID }} + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col">{{ toJSON $u.Metadata }}</td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form + action="/things/{{ $thingID }}/unshare?item=things" + method="post" + > + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <input type="hidden" name="relation" id="relation" value="owner" /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const userModal = new bootstrap.Modal(document.getElementById("addUserModal")); + function openUserModal() { + userModal.show(); + } + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/updatepassword.html b/ui/web/template/updatepassword.html index 36ce93d1c..858654b79 100644 --- a/ui/web/template/updatepassword.html +++ b/ui/web/template/updatepassword.html @@ -2,115 +2,108 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "updatePassword" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container mt-5 pt-5"> - <div class="row"> - <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> - <div - class="row text-center mb-4 d-flex flex-column align-items-center" - > - <div class="login-header border-bottom pb-3 mb-5 mt-3"> - <h2>Update Password</h2> - </div> - <form - class="row border-bottom pb-3 mb-3" - onsubmit="return submitUpdatePasswordForm()" - > - <div class="col-md-12"> - <div class="row mb-3"> - <div class="col-md-12 input-field"> - <i class="fas fa-solid fa-lock"></i> - <input - class="p-3 w-100" - type="password" - name="oldpass" - id="oldpass" - placeholder="Old password" - required - /> - <div id="oldpassError" class="text-danger"></div> - </div> - </div> - <div class="row mb-3"> - <div class="col-md-12 input-field"> - <i class="fas fa-solid fa-lock"></i> - <input - class="p-3 w-100" - type="password" - name="newpass" - id="newpass" - placeholder="New password" - required - /> - <div id="newpassError" class="text-danger"></div> - </div> - </div> - </div> - <div class="col-md-12 d-grid py-3"> - <button - type="submit" - class="login-btn py-3" - onclick="return validateForm()" - > - Submit - </button> - </div> - </form> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - function validateForm() { - var newpass = document.getElementById("newpass").value; + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container mt-5 pt-5"> + <div class="row"> + <div class="login-card col-lg-4 p-md-5 mx-auto mt-5"> + <div class="row text-center mb-4 d-flex flex-column align-items-center"> + <div class="login-header border-bottom pb-3 mb-5 mt-3"> + <h2>Update Password</h2> + </div> + <form + class="row border-bottom pb-3 mb-3" + onsubmit="return submitUpdatePasswordForm()" + > + <div class="col-md-12"> + <div class="row mb-3"> + <div class="col-md-12 input-field"> + <i class="fas fa-solid fa-lock"></i> + <input + class="p-3 w-100" + type="password" + name="oldpass" + id="oldpass" + placeholder="Old password" + required + /> + <div id="oldpassError" class="text-danger"></div> + </div> + </div> + <div class="row mb-3"> + <div class="col-md-12 input-field"> + <i class="fas fa-solid fa-lock"></i> + <input + class="p-3 w-100" + type="password" + name="newpass" + id="newpass" + placeholder="New password" + required + /> + <div id="newpassError" class="text-danger"></div> + </div> + </div> + </div> + <div class="col-md-12 d-grid py-3"> + <button type="submit" class="login-btn py-3" onclick="return validateForm()"> + Submit + </button> + </div> + </form> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + function validateForm() { + var newpass = document.getElementById("newpass").value; - newpassError.innerHTML = ""; - var isValid = true; + newpassError.innerHTML = ""; + var isValid = true; - if (newpass.length < 8) { - newpassError.innerHTML = - "Password must have a minimum of 8 characters!"; - isValid = false; - } - return isValid; - } + if (newpass.length < 8) { + newpassError.innerHTML = "Password must have a minimum of 8 characters!"; + isValid = false; + } + return isValid; + } - function submitUpdatePasswordForm() { - event.preventDefault(); - var form = event.target; - fetch("/password", { - method: "POST", - body: new FormData(form), - }) - .then((response) => { - if (response.status === 401) { - errorMessage = "invalid old password!"; - showAlert(errorMessage); - return false; - } else { - form.reset(); - window.location.href = "/login"; - return true; - } - }) - .catch((error) => { - console.error("error submitting password update form: ", error); - return false; - }); - } + function submitUpdatePasswordForm() { + event.preventDefault(); + var form = event.target; + fetch("/password", { + method: "POST", + body: new FormData(form), + }) + .then((response) => { + if (response.status === 401) { + errorMessage = "invalid old password!"; + showAlert(errorMessage); + return false; + } else { + form.reset(); + window.location.href = "/login"; + return true; + } + }) + .catch((error) => { + console.error("error submitting password update form: ", error); + return false; + }); + } - function showAlert(errorMessage) { - var loginError = document.getElementById("oldpassError"); - loginError.innerHTML = errorMessage; - } - </script> - </body> - </html> + function showAlert(errorMessage) { + var loginError = document.getElementById("oldpassError"); + loginError.innerHTML = errorMessage; + } + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/user.html b/ui/web/template/user.html index aea0349bb..e4ed0c3df 100644 --- a/ui/web/template/user.html +++ b/ui/web/template/user.html @@ -2,206 +2,150 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "user" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/users/{{ .UserID }}/groups" - type="button" - class="btn body-button" - > - User Groups - </a> - <a - href="/users/{{ .UserID }}/things" - type="button" - class="btn body-button" - > - User Things - </a> - <a - href="/users/{{ .UserID }}/channels" - type="button" - class="btn body-button" - > - User Channels - </a> - </div> - <div class="table-responsive table-container"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="row">USER</th> - </tr> - </thead> - <tbody> - {{ $disableButton := false }} + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/users/{{ .UserID }}/groups" type="button" class="btn body-button"> + User Groups + </a> + <a href="/users/{{ .UserID }}/things" type="button" class="btn body-button"> + User Things + </a> + <a href="/users/{{ .UserID }}/channels" type="button" class="btn body-button"> + User Channels + </a> + </div> + <div class="table-responsive table-container"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="row">USER</th> + </tr> + </thead> + <tbody> + {{ $disableButton := false }} - <tr> - <th>Name</th> - <td - class="editable" - contenteditable="false" - data-field="name" - > - {{ .ViewedUser.Name }} - </td> - <td> - <button - class="edit-btn" - id="edit-name" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-name"> - Save - </button> - <button class="cancel-btn" id="cancel-name"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>ID</th> - <td>{{ .ViewedUser.ID }}</td> - <td></td> - </tr> - <tr> - <th>Owner</th> - <td>{{ .ViewedUser.Owner }}</td> - <td></td> - </tr> - <tr> - <th>Identity</th> - <td - class="editable" - contenteditable="false" - data-field="identity" - > - {{ .ViewedUser.Credentials.Identity }} - </td> - <td> - <button - class="edit-btn" - id="edit-identity" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-identity"> - Save - </button> - <button class="cancel-btn" id="cancel-identity"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Tags</th> - <td - class="editable" - contenteditable="false" - data-field="tags" - > - {{ toSlice .ViewedUser.Tags }} - </td> - <td> - <button - class="edit-btn" - id="edit-tags" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-tags"> - Save - </button> - <button class="cancel-btn" id="cancel-tags"> - Cancel - </button> - </div> - </td> - </tr> - <tr> - <th>Metadata</th> - <td - class="editable" - contenteditable="false" - data-field="metadata" - > - {{ toJSON .ViewedUser.Metadata }} - </td> - <td> - <button - class="edit-btn" - id="edit-metadata" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-pencil-alt"></i> - </button> - <div - class="save-cancel-buttons" - style="display: none" - > - <button class="save-btn" id="save-metadata"> - Save - </button> - <button class="cancel-btn" id="cancel-metadata"> - Cancel - </button> - </div> - </td> - </tr> - </tbody> - </table> - <div id="error-message" class="text-danger"></div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <tr> + <th>Name</th> + <td class="editable" contenteditable="false" data-field="name"> + {{ .ViewedUser.Name }} + </td> + <td> + <button + class="edit-btn" + id="edit-name" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-name">Save</button> + <button class="cancel-btn" id="cancel-name">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>ID</th> + <td>{{ .ViewedUser.ID }}</td> + <td></td> + </tr> + <tr> + <th>Owner</th> + <td>{{ .ViewedUser.Owner }}</td> + <td></td> + </tr> + <tr> + <th>Identity</th> + <td class="editable" contenteditable="false" data-field="identity"> + {{ .ViewedUser.Credentials.Identity }} + </td> + <td> + <button + class="edit-btn" + id="edit-identity" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-identity">Save</button> + <button class="cancel-btn" id="cancel-identity">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Tags</th> + <td class="editable" contenteditable="false" data-field="tags"> + {{ toSlice .ViewedUser.Tags }} + </td> + <td> + <button + class="edit-btn" + id="edit-tags" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-tags">Save</button> + <button class="cancel-btn" id="cancel-tags">Cancel</button> + </div> + </td> + </tr> + <tr> + <th>Metadata</th> + <td class="editable" contenteditable="false" data-field="metadata"> + {{ toJSON .ViewedUser.Metadata }} + </td> + <td> + <button + class="edit-btn" + id="edit-metadata" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-pencil-alt"></i> + </button> + <div class="save-cancel-buttons" style="display: none"> + <button class="save-btn" id="save-metadata">Save</button> + <button class="cancel-btn" id="cancel-metadata">Cancel</button> + </div> + </td> + </tr> + </tbody> + </table> + <div id="error-message" class="text-danger"></div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> attachEditRowListener( { @@ -217,6 +161,6 @@ } ); </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/userchannels.html b/ui/web/template/userchannels.html index 0be2dd44c..53a20ef23 100644 --- a/ui/web/template/userchannels.html +++ b/ui/web/template/userchannels.html @@ -2,520 +2,469 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "userchannels" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/users/{{ .UserID }}" - type="button" - class="btn body-button" - > - User - </a> - <a - href="/users/{{ .UserID }}/groups" - type="button" - class="btn body-button" - > - User Groups - </a> - <a - href="/users/{{ .UserID }}/things" - type="button" - class="btn body-button" - > - View User things - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>User Channels</h4> - <button - role="button" - class="btn body-button" - onclick="openChannelModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addChannelModal" - tabindex="-1" - aria-labelledby="addChannelModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addChannelModalLabel" - > - Add Channel - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/users/{{ .UserID }}/channels/assign?item=users" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Channel ID - </label> - <input - type="text" - name="channelFilter" - id="channelFilter" - placeholder="Filter by Channel ID" - /> - <select - class="form-select" - name="channelID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a channel</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $i, $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/users/{{ .UserID }}" type="button" class="btn body-button">User</a> + <a href="/users/{{ .UserID }}/groups" type="button" class="btn body-button"> + User Groups + </a> + <a href="/users/{{ .UserID }}/things" type="button" class="btn body-button"> + View User things + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>User Channels</h4> + <button role="button" class="btn body-button" onclick="openChannelModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addChannelModal" + tabindex="-1" + aria-labelledby="addChannelModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/users/{{ .UserID }}/channels/assign?item=users" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Channel ID</label> + <input + type="text" + name="channelFilter" + id="channelFilter" + placeholder="Filter by Channel ID" + /> + <select + class="form-select" + name="channelID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a channel</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $i, $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> - <ul class="nav nav-tabs" id="roleTab" role="tablist"> - <li class="nav-item" role="presentation"> - {{ $tabActive := "" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="view-tab" - data-bs-toggle="tab" - data-bs-target="#view-tab-pane" - type="button" - role="tab" - aria-controls="view-tab-pane" - aria-selected="true" - onclick="openTab('')" - > - All - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "admin" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="admin-tab" - data-bs-toggle="tab" - data-bs-target="#admin-tab-pane" - type="button" - role="tab" - aria-controls="admin-tab-pane" - aria-selected="true" - onclick="openTab('admin')" - > - Admin - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "editor" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="editor-tab" - data-bs-toggle="tab" - data-bs-target="#editor-tab-pane" - type="button" - role="tab" - aria-controls="editor-tab-pane" - aria-selected="false" - onclick="openTab('editor')" - > - Editor - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "viewer" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="viewer-tab" - data-bs-toggle="tab" - data-bs-target="#viewer-tab-pane" - type="button" - role="tab" - aria-controls="viewer-tab-pane" - aria-selected="false" - onclick="openTab('viewer')" - > - Viewer - </button> - </li> - </ul> - <div class="tab-content mt-3" id="roleTabContent"> - {{ $userID := .UserID }} - <div - class="tab-pane active" - id="view-tab-pane" - role="tabpanel" - aria-labelledby="view-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - </tr> - </thead> - <tbody> - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col"> - {{ toJSON $c.Metadata }} - </td> - <td class="created-col">{{ $c.CreatedAt }}</td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="admin-tab-pane" - role="tabpanel" - aria-labelledby="admin-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col"> - {{ toJSON $c.Metadata }} - </td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/channels/unassign?item=users" - method="post" - > - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="admin" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane " - id="editor-tab-pane" - role="tabpanel" - aria-labelledby="editor-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col"> - {{ toJSON $c.Metadata }} - </td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/channels/unassign?item=users" - method="post" - > - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="editor" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="viewer-tab-pane" - role="tabpanel" - aria-labelledby="viewer-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col"> - {{ toJSON $c.Metadata }} - </td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/channels/unassign?item=users" - method="post" - > - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="viewer" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <ul class="nav nav-tabs" id="roleTab" role="tablist"> + <li class="nav-item" role="presentation"> + {{ $tabActive := "" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="view-tab" + data-bs-toggle="tab" + data-bs-target="#view-tab-pane" + type="button" + role="tab" + aria-controls="view-tab-pane" + aria-selected="true" + onclick="openTab('')" + > + All + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "admin" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="admin-tab" + data-bs-toggle="tab" + data-bs-target="#admin-tab-pane" + type="button" + role="tab" + aria-controls="admin-tab-pane" + aria-selected="true" + onclick="openTab('admin')" + > + Admin + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "editor" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="editor-tab" + data-bs-toggle="tab" + data-bs-target="#editor-tab-pane" + type="button" + role="tab" + aria-controls="editor-tab-pane" + aria-selected="false" + onclick="openTab('editor')" + > + Editor + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "viewer" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="viewer-tab" + data-bs-toggle="tab" + data-bs-target="#viewer-tab-pane" + type="button" + role="tab" + aria-controls="viewer-tab-pane" + aria-selected="false" + onclick="openTab('viewer')" + > + Viewer + </button> + </li> + </ul> + <div class="tab-content mt-3" id="roleTabContent"> + {{ $userID := .UserID }} + <div + class="tab-pane active" + id="view-tab-pane" + role="tabpanel" + aria-labelledby="view-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col"> + {{ toJSON $c.Metadata }} + </td> + <td class="created-col">{{ $c.CreatedAt }}</td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="admin-tab-pane" + role="tabpanel" + aria-labelledby="admin-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col"> + {{ toJSON $c.Metadata }} + </td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/channels/unassign?item=users" + method="post" + > + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="admin" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane " + id="editor-tab-pane" + role="tabpanel" + aria-labelledby="editor-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col"> + {{ toJSON $c.Metadata }} + </td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/channels/unassign?item=users" + method="post" + > + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="editor" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="viewer-tab-pane" + role="tabpanel" + aria-labelledby="viewer-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col"> + {{ toJSON $c.Metadata }} + </td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/channels/unassign?item=users" + method="post" + > + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="viewer" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> const channelModal = new bootstrap.Modal( document.getElementById("addChannelModal"), @@ -530,18 +479,18 @@ <h4>User Channels</h4> item: "channels", }); - function openTab(relation) { + function openTab(relation) { event.preventDefault(); - var userID = '{{.UserID}}'; - fetch(`/users/${userID}/channels?relation=${relation}`, { - method: "GET", - }) - .then((response) => { + var userID = '{{.UserID}}'; + fetch(`/users/${userID}/channels?relation=${relation}`, { + method: "GET", + }) + .then((response) => { - }) - .catch((error) => console.error("Error:", error)); + }) + .catch((error) => console.error("Error:", error)); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/usergroups.html b/ui/web/template/usergroups.html index 16f9f3cd5..30979fdb5 100644 --- a/ui/web/template/usergroups.html +++ b/ui/web/template/usergroups.html @@ -2,521 +2,470 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "usergroups" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/users/{{ .UserID }}" - type="button" - class="btn body-button" - > - User - </a> - <a - href="/users/{{ .UserID }}/things" - type="button" - class="btn body-button" - > - User Things - </a> - <a - href="/users/{{ .UserID }}/channels" - type="button" - class="btn body-button" - > - User Channels - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>User Groups</h4> - <button - role="button" - class="btn body-button" - onclick="openGroupModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addGroupModal" - tabindex="-1" - aria-labelledby="addGroupModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addGroupModalLabel" - > - Add Group - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/users/{{ .UserID }}/groups/assign?item=users" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Group ID - </label> - <input - type="text" - name="groupFilter" - id="groupFilter" - placeholder="Filter by Group ID" - /> - <select - class="form-select" - name="groupID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a group</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $i, $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/users/{{ .UserID }}" type="button" class="btn body-button">User</a> + <a href="/users/{{ .UserID }}/things" type="button" class="btn body-button"> + User Things + </a> + <a href="/users/{{ .UserID }}/channels" type="button" class="btn body-button"> + User Channels + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>User Groups</h4> + <button role="button" class="btn body-button" onclick="openGroupModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addGroupModal" + tabindex="-1" + aria-labelledby="addGroupModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addGroupModalLabel">Add Group</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form + action="/users/{{ .UserID }}/groups/assign?item=users" + method="post" + > + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Group ID</label> + <input + type="text" + name="groupFilter" + id="groupFilter" + placeholder="Filter by Group ID" + /> + <select + class="form-select" + name="groupID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a group</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $i, $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> - <ul class="nav nav-tabs" id="roleTab" role="tablist"> - <li class="nav-item" role="presentation"> - {{ $tabActive := "" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="view-tab" - data-bs-toggle="tab" - data-bs-target="#view-tab-pane" - type="button" - role="tab" - aria-controls="view-tab-pane" - aria-selected="true" - onclick="openTab('')" - > - All - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "admin" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="admin-tab" - data-bs-toggle="tab" - data-bs-target="#admin-tab-pane" - type="button" - role="tab" - aria-controls="admin-tab-pane" - aria-selected="true" - onclick="openTab('admin')" - > - Admin - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "editor" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="editor-tab" - data-bs-toggle="tab" - data-bs-target="#editor-tab-pane" - type="button" - role="tab" - aria-controls="editor-tab-pane" - aria-selected="false" - onclick="openTab('editor')" - > - Editor - </button> - </li> - <li class="nav-item" role="presentation"> - {{ $tabActive = "viewer" }} - <button - class="nav-link {{ if eq .TabActive $tabActive }} - active - {{ end }}" - id="viewer-tab" - data-bs-toggle="tab" - data-bs-target="#viewer-tab-pane" - type="button" - role="tab" - aria-controls="viewer-tab-pane" - aria-selected="false" - onclick="openTab('viewer')" - > - Viewer - </button> - </li> - </ul> - <div class="tab-content mt-3" id="roleTabContent"> - {{ $userID := .UserID }} - <div - class="tab-pane active" - id="view-tab-pane" - role="tabpanel" - aria-labelledby="view-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - </tr> - </thead> - <tbody> - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col"> - {{ toJSON $g.Metadata }} - </td> - <td class="created-col">{{ $g.CreatedAt }}</td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="admin-tab-pane" - role="tabpanel" - aria-labelledby="admin-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col"> - {{ toJSON $g.Metadata }} - </td> - <td class="created-col">{{ $g.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/groups/unassign?item=users" - method="post" - > - <input - type="hidden" - name="groupID" - id="groupID" - value="{{ $g.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="admin" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane " - id="editor-tab-pane" - role="tabpanel" - aria-labelledby="editor-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col"> - {{ toJSON $g.Metadata }} - </td> - <td class="created-col">{{ $g.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/groups/unassign?item=users" - method="post" - > - <input - type="hidden" - name="groupID" - id="groupID" - value="{{ $g.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="editor" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - <div - class="tab-pane" - id="viewer-tab-pane" - role="tabpanel" - aria-labelledby="viewer-tab" - tabindex="0" - > - {{ template "tableheader" . }} - <div class="itemsTable"> - <table class="table" id="itemsTable"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col"> - Created At - </th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $userID := .UserID }} - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col"> - {{ toJSON $g.Metadata }} - </td> - <td class="created-col">{{ $g.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/groups/unassign?item=users" - method="post" - > - <input - type="hidden" - name="groupID" - id="groupID" - value="{{ $g.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="viewer" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} + <ul class="nav nav-tabs" id="roleTab" role="tablist"> + <li class="nav-item" role="presentation"> + {{ $tabActive := "" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="view-tab" + data-bs-toggle="tab" + data-bs-target="#view-tab-pane" + type="button" + role="tab" + aria-controls="view-tab-pane" + aria-selected="true" + onclick="openTab('')" + > + All + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "admin" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="admin-tab" + data-bs-toggle="tab" + data-bs-target="#admin-tab-pane" + type="button" + role="tab" + aria-controls="admin-tab-pane" + aria-selected="true" + onclick="openTab('admin')" + > + Admin + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "editor" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="editor-tab" + data-bs-toggle="tab" + data-bs-target="#editor-tab-pane" + type="button" + role="tab" + aria-controls="editor-tab-pane" + aria-selected="false" + onclick="openTab('editor')" + > + Editor + </button> + </li> + <li class="nav-item" role="presentation"> + {{ $tabActive = "viewer" }} + <button + class="nav-link {{ if eq .TabActive $tabActive }} + active + {{ end }}" + id="viewer-tab" + data-bs-toggle="tab" + data-bs-target="#viewer-tab-pane" + type="button" + role="tab" + aria-controls="viewer-tab-pane" + aria-selected="false" + onclick="openTab('viewer')" + > + Viewer + </button> + </li> + </ul> + <div class="tab-content mt-3" id="roleTabContent"> + {{ $userID := .UserID }} + <div + class="tab-pane active" + id="view-tab-pane" + role="tabpanel" + aria-labelledby="view-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + </tr> + </thead> + <tbody> + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col"> + {{ toJSON $g.Metadata }} + </td> + <td class="created-col">{{ $g.CreatedAt }}</td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="admin-tab-pane" + role="tabpanel" + aria-labelledby="admin-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col"> + {{ toJSON $g.Metadata }} + </td> + <td class="created-col">{{ $g.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/groups/unassign?item=users" + method="post" + > + <input + type="hidden" + name="groupID" + id="groupID" + value="{{ $g.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="admin" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane " + id="editor-tab-pane" + role="tabpanel" + aria-labelledby="editor-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col"> + {{ toJSON $g.Metadata }} + </td> + <td class="created-col">{{ $g.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/groups/unassign?item=users" + method="post" + > + <input + type="hidden" + name="groupID" + id="groupID" + value="{{ $g.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="editor" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + <div + class="tab-pane" + id="viewer-tab-pane" + role="tabpanel" + aria-labelledby="viewer-tab" + tabindex="0" + > + {{ template "tableheader" . }} + <div class="itemsTable"> + <table class="table" id="itemsTable"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $userID := .UserID }} + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col"> + {{ toJSON $g.Metadata }} + </td> + <td class="created-col">{{ $g.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/groups/unassign?item=users" + method="post" + > + <input + type="hidden" + name="groupID" + id="groupID" + value="{{ $g.ID }}" + /> + <input + type="hidden" + name="relation" + id="relation" + value="viewer" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} <script> const groupModal = new bootstrap.Modal( document.getElementById("addGroupModal"), @@ -533,16 +482,16 @@ <h4>User Groups</h4> function openTab(relation) { event.preventDefault(); - var userID = '{{.UserID}}'; - fetch(`/users/${userID}/groups?relation=${relation}`, { - method: "GET", - }) - .then((response) => { + var userID = '{{.UserID}}'; + fetch(`/users/${userID}/groups?relation=${relation}`, { + method: "GET", + }) + .then((response) => { - }) - .catch((error) => console.error("Error:", error)); + }) + .catch((error) => console.error("Error:", error)); } </script> - </body> - </html> + </body> + </html> {{ end }} diff --git a/ui/web/template/users.html b/ui/web/template/users.html index ed7791c50..6348078a8 100644 --- a/ui/web/template/users.html +++ b/ui/web/template/users.html @@ -2,131 +2,111 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "users" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <button - class="btn body-button" - type="button" - onclick="openModal('single')" - > - Add User - </button> - <button - class="btn body-button" - type="button" - onclick="openModal('bulk')" - > - Add Users - </button> - <!-- modals --> - <!-- add user modal --> - <div - class="modal fade" - id="addUserModal" - tabindex="-1" - role="dialog" - aria-labelledby="addUserModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addUserModalLabel"> - Add User - </h5> - </div> - <div class="modal-body"> - <div id="alertMessage"></div> - <form id="userform"> - <div class="mb-3"> - <label for="name" class="form-label">Name</label> - <input - type="text" - class="form-control" - name="name" - id="name" - placeholder="User Name" - /> - <div id="nameError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="identity" class="form-label"> - Identity - </label> - <input - type="email" - class="form-control" - name="identity" - id="identity" - placeholder="User Identity" - /> - <div id="identityError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="secret" class="form-label"> - Secret - </label> - <input - type="text" - class="form-control" - name="secret" - id="secret" - placeholder="User Secret" - /> - <div id="secretError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="tags" class="form-label">Tags</label> - <input - type="text" - class="form-control" - name="tags" - id="tags" - aria-describedby="tagHelp" - value="[]" - /> - <div id="tagHelp" class="form-text"> - Enter user tags as a string slice. - </div> - <div id="tagsError" class="text-danger"></div> - </div> - <div class="mb-3"> - <label for="metadata" class="form-label"> - Metadata - </label> - <input - type="text" - class="form-control" - name="metadata" - id="metadata" - value="{}" - /> - <div id="metadataHelp" class="form-text"> - Enter user metadata in JSON format. - </div> - <div id="metadataError" class="text-danger"></div> - </div> - <button - type="submit" - class="btn body-button" - id="create-user-button" - > - Submit - </button> - </form> - </div> - </div> - </div> - </div> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <button class="btn body-button" type="button" onclick="openModal('single')"> + Add User + </button> + <button class="btn body-button" type="button" onclick="openModal('bulk')"> + Add Users + </button> + <!-- modals --> + <!-- add user modal --> + <div + class="modal fade" + id="addUserModal" + tabindex="-1" + role="dialog" + aria-labelledby="addUserModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addUserModalLabel">Add User</h5> + </div> + <div class="modal-body"> + <div id="alertMessage"></div> + <form id="userform"> + <div class="mb-3"> + <label for="name" class="form-label">Name</label> + <input + type="text" + class="form-control" + name="name" + id="name" + placeholder="User Name" + /> + <div id="nameError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="identity" class="form-label">Identity</label> + <input + type="email" + class="form-control" + name="identity" + id="identity" + placeholder="User Identity" + /> + <div id="identityError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="secret" class="form-label">Secret</label> + <input + type="text" + class="form-control" + name="secret" + id="secret" + placeholder="User Secret" + /> + <div id="secretError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="tags" class="form-label">Tags</label> + <input + type="text" + class="form-control" + name="tags" + id="tags" + aria-describedby="tagHelp" + value="[]" + /> + <div id="tagHelp" class="form-text"> + Enter user tags as a string slice. + </div> + <div id="tagsError" class="text-danger"></div> + </div> + <div class="mb-3"> + <label for="metadata" class="form-label">Metadata</label> + <input + type="text" + class="form-control" + name="metadata" + id="metadata" + value="{}" + /> + <div id="metadataHelp" class="form-text"> + Enter user metadata in JSON format. + </div> + <div id="metadataError" class="text-danger"></div> + </div> + <button type="submit" class="btn body-button" id="create-user-button"> + Submit + </button> + </form> + </div> + </div> + </div> + </div> <!-- add users modal --> <div @@ -271,35 +251,31 @@ <h5 class="modal-title" id="addUsersModalLabel"> }, }); - const userModal = new bootstrap.Modal( - document.getElementById("addUserModal"), - ); - const usersModal = new bootstrap.Modal( - document.getElementById("addUsersModal"), - ); + const userModal = new bootstrap.Modal(document.getElementById("addUserModal")); + const usersModal = new bootstrap.Modal(document.getElementById("addUsersModal")); - function openModal(modal) { - if (modal === "single") { - userModal.show(); - } else if (modal === "bulk") { - usersModal.show(); - } - } + function openModal(modal) { + if (modal === "single") { + userModal.show(); + } else if (modal === "bulk") { + usersModal.show(); + } + } - submitCreateForm({ - url: "/users", - formId: "userform", - alertDiv: "alertMessage", - modal: userModal, - }); + submitCreateForm({ + url: "/users", + formId: "userform", + alertDiv: "alertMessage", + modal: userModal, + }); - submitCreateForm({ - url: "/users/bulk", - formId: "bulkusersform", - alertDiv: "alertBulkMessage", - modal: usersModal, - }); - </script> - </body> - </html> + submitCreateForm({ + url: "/users/bulk", + formId: "bulkusersform", + alertDiv: "alertBulkMessage", + modal: usersModal, + }); + </script> + </body> + </html> {{ end }} diff --git a/ui/web/template/userthings.html b/ui/web/template/userthings.html index afcd4c842..0a3a85f1d 100644 --- a/ui/web/template/userthings.html +++ b/ui/web/template/userthings.html @@ -2,229 +2,185 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "userthings" }} - <!doctype html> - <html lang="en"> - {{ template "header" }} - <body> - {{ template "navbar" . }} - <div class="main-content pt-3"> - <div class="container"> - <div class="row"> - <div class="col-lg-12 mx-auto py-3"> - <div class="row"> - <div class="buttons mb-3"> - <a - href="/users/{{ .UserID }}" - type="button" - class="btn body-button" - > - User - </a> - <a - href="/users/{{ .UserID }}/groups" - type="button" - class="btn body-button" - > - User Groups - </a> - <a - href="/users/{{ .UserID }}/channels" - type="button" - class="btn body-button" - > - User Channels - </a> - </div> - <div class="table-responsive table-container"> - <div class="d-flex flex-row justify-content-between"> - <h4>User Things</h4> - <button - role="button" - class="btn body-button" - onclick="openThingModal()" - > - <i class="fa-solid fa-plus fs-4"></i> - </button> - <!-- modal --> - <div - class="modal fade" - id="addThingModal" - tabindex="-1" - aria-labelledby="addThingModalLabel" - aria-hidden="true" - > - <div class="modal-dialog modal-dialog-centered"> - <div class="modal-content"> - <div class="modal-header"> - <h1 - class="modal-title fs-5" - id="addThingModalLabel" - > - Add Thing - </h1> - <button - type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close" - ></button> - </div> - <form - action="/users/{{ .UserID }}/things/share?item=users" - method="post" - > - <div class="modal-body"> - <div class="mb-3"> - <label for="infiniteScroll" class="form-label"> - Thing ID - </label> - <input - type="text" - name="thingFilter" - id="thingFilter" - placeholder="Filter by Thing ID" - /> - <select - class="form-select" - name="thingID" - id="infiniteScroll" - size="5" - required - > - <option disabled>select a thing</option> - </select> - </div> - <div class="mb-3"> - <label for="relation" class="form-label"> - Relation - </label> - <select - class="form-control" - name="relation" - id="relation" - aria-describedby="relationHelp" - multiple - required - > - {{ range $i, $r := .Relations }} - <option value="{{ $r }}"> - {{ $r }} - </option> - {{ end }} - </select> - <div id="relationHelp" class="form-text"> - Select Relation. - </div> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn btn-secondary" - data-bs-dismiss="modal" - > - Cancel - </button> - <button type="submit" class="btn btn-primary"> - Assign - </button> - </div> - </div> - </form> - </div> - </div> - </div> - </div> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ $userID := .UserID }} - {{ range $i, $t := .Things }} - {{ $disableButton := false }} - <tr> - <td>{{ $t.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/things/%s" $t.ID }}"> - {{ $t.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $t.Tags }}</td> - <td class="meta-col">{{ toJSON $t.Metadata }}</td> - <td class="created-col">{{ $t.CreatedAt }}</td> - <td class="text-center"> - <form - action="/users/{{ $userID }}/things/unshare?item=users" - method="post" - > - <input - type="hidden" - name="thingID" - id="thingID" - value="{{ $t.ID }}" - /> - <input - type="hidden" - name="relation" - id="relation" - value="owner" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - const thingModal = new bootstrap.Modal( - document.getElementById("addThingModal"), - ); - function openThingModal() { - thingModal.show(); - } - fetchIndividualEntity({ - input: "thingFilter", - itemSelect: "things", - item: "things", - }); - </script> - </body> - </html> + <!doctype html> + <html lang="en"> + {{ template "header" }} + <body> + {{ template "navbar" . }} + <div class="main-content pt-3"> + <div class="container"> + <div class="row"> + <div class="col-lg-12 mx-auto py-3"> + <div class="row"> + <div class="buttons mb-3"> + <a href="/users/{{ .UserID }}" type="button" class="btn body-button">User</a> + <a href="/users/{{ .UserID }}/groups" type="button" class="btn body-button"> + User Groups + </a> + <a href="/users/{{ .UserID }}/channels" type="button" class="btn body-button"> + User Channels + </a> + </div> + <div class="table-responsive table-container"> + <div class="d-flex flex-row justify-content-between"> + <h4>User Things</h4> + <button role="button" class="btn body-button" onclick="openThingModal()"> + <i class="fa-solid fa-plus fs-4"></i> + </button> + <!-- modal --> + <div + class="modal fade" + id="addThingModal" + tabindex="-1" + aria-labelledby="addThingModalLabel" + aria-hidden="true" + > + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="addThingModalLabel">Add Thing</h1> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close" + ></button> + </div> + <form action="/users/{{ .UserID }}/things/share?item=users" method="post"> + <div class="modal-body"> + <div class="mb-3"> + <label for="infiniteScroll" class="form-label">Thing ID</label> + <input + type="text" + name="thingFilter" + id="thingFilter" + placeholder="Filter by Thing ID" + /> + <select + class="form-select" + name="thingID" + id="infiniteScroll" + size="5" + required + > + <option disabled>select a thing</option> + </select> + </div> + <div class="mb-3"> + <label for="relation" class="form-label">Relation</label> + <select + class="form-control" + name="relation" + id="relation" + aria-describedby="relationHelp" + multiple + required + > + {{ range $i, $r := .Relations }} + <option value="{{ $r }}"> + {{ $r }} + </option> + {{ end }} + </select> + <div id="relationHelp" class="form-text">Select Relation.</div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Assign</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ $userID := .UserID }} + {{ range $i, $t := .Things }} + {{ $disableButton := false }} + <tr> + <td>{{ $t.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/things/%s" $t.ID }}"> + {{ $t.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $t.Tags }}</td> + <td class="meta-col">{{ toJSON $t.Metadata }}</td> + <td class="created-col">{{ $t.CreatedAt }}</td> + <td class="text-center"> + <form + action="/users/{{ $userID }}/things/unshare?item=users" + method="post" + > + <input + type="hidden" + name="thingID" + id="thingID" + value="{{ $t.ID }}" + /> + <input type="hidden" name="relation" id="relation" value="owner" /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> + const thingModal = new bootstrap.Modal(document.getElementById("addThingModal")); + function openThingModal() { + thingModal.show(); + } + fetchIndividualEntity({ + input: "thingFilter", + itemSelect: "things", + item: "things", + }); + </script> + </body> + </html> {{ end }} From ad6328320bd373eac9b6700ece4aa078591a1bdd Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Mon, 27 Nov 2023 20:12:42 +0300 Subject: [PATCH 06/11] update js formatting Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/static/js/main.js | 700 +++++++++++++++++++-------------------- 1 file changed, 347 insertions(+), 353 deletions(-) diff --git a/ui/web/static/js/main.js b/ui/web/static/js/main.js index 068a5e63d..7ece41510 100644 --- a/ui/web/static/js/main.js +++ b/ui/web/static/js/main.js @@ -3,167 +3,167 @@ //function to copy the ID to the clipboard function copyToClipboard(button) { - var clientIDElement = button.previousElementSibling.firstChild; - var clientId = clientIDElement.textContent; - - navigator.clipboard.writeText(clientId).then( - function () { - //change the copy icon to indicate success - button.innerHTML = `<i class="fas fa-check success-icon">`; - setTimeout(function () { - //revert the copy icon after a short delay - button.innerHTML = `<i class ="far fa-copy">`; - }, 1000); - }, - function (error) { - //handle error - console.error("failed to copy to clipboard: ", error); - }, - ); + var clientIDElement = button.previousElementSibling.firstChild; + var clientId = clientIDElement.textContent; + + navigator.clipboard.writeText(clientId).then( + function () { + //change the copy icon to indicate success + button.innerHTML = `<i class="fas fa-check success-icon">`; + setTimeout(function () { + //revert the copy icon after a short delay + button.innerHTML = `<i class ="far fa-copy">`; + }, 1000); + }, + function (error) { + //handle error + console.error("failed to copy to clipboard: ", error); + }, + ); } // Form validation functions function validateName(name, errorDiv, event) { - removeErrorMessage(errorDiv); - if (name.trim() === "") { - event.preventDefault(); - displayErrorMessage("Name is Required", errorDiv); - return false; - } - return true; + removeErrorMessage(errorDiv); + if (name.trim() === "") { + event.preventDefault(); + displayErrorMessage("Name is Required", errorDiv); + return false; + } + return true; } const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; function validateEmail(email, errorDiv, event) { - removeErrorMessage(errorDiv); - if (email.trim() === "") { - event.preventDefault(); - displayErrorMessage("Email is Required", errorDiv); - return false; - } else if (!email.match(emailRegex)) { - event.preventDefault(); - displayErrorMessage("Invalid email format", errorDiv); - return false; - } - return true; + removeErrorMessage(errorDiv); + if (email.trim() === "") { + event.preventDefault(); + displayErrorMessage("Email is Required", errorDiv); + return false; + } else if (!email.match(emailRegex)) { + event.preventDefault(); + displayErrorMessage("Invalid email format", errorDiv); + return false; + } + return true; } const minLength = 8; function validatePassword(password, errorDiv, event) { - removeErrorMessage(errorDiv); - if (password.trim().length < minLength) { - event.preventDefault(); - var errorMessage = `Password must be at least ${minLength} characters long`; - displayErrorMessage(errorMessage, errorDiv); - return false; - } - return true; + removeErrorMessage(errorDiv); + if (password.trim().length < minLength) { + event.preventDefault(); + var errorMessage = `Password must be at least ${minLength} characters long`; + displayErrorMessage(errorMessage, errorDiv); + return false; + } + return true; } function validateMetadata(metadata, errorDiv, event) { - removeErrorMessage(errorDiv); - try { - if (metadata.trim() !== "") { - JSON.parse(metadata); - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("Metadata is not a valid JSON object", errorDiv); - return false; - } - return true; + removeErrorMessage(errorDiv); + try { + if (metadata.trim() !== "") { + JSON.parse(metadata); + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("Metadata is not a valid JSON object", errorDiv); + return false; + } + return true; } function validateTags(tags, errorDiv, event) { - removeErrorMessage(errorDiv); - var tagsArray; - try { - if (tags.trim() !== "") { - tagsArray = JSON.parse(tags); - } - if ( - !Array.isArray(tagsArray) || - !tagsArray.every(function (tag) { - return typeof tag === "string"; - }) - ) { - event.preventDefault(); - displayErrorMessage("tags must be strings in an array", errorDiv); - return false; - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("tags must be a string array", errorDiv); - return false; - } - - return true; + removeErrorMessage(errorDiv); + var tagsArray; + try { + if (tags.trim() !== "") { + tagsArray = JSON.parse(tags); + } + if ( + !Array.isArray(tagsArray) || + !tagsArray.every(function (tag) { + return typeof tag === "string"; + }) + ) { + event.preventDefault(); + displayErrorMessage("tags must be strings in an array", errorDiv); + return false; + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("tags must be a string array", errorDiv); + return false; + } + + return true; } function displayErrorMessage(errorMessage, divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "block"; - errorDiv.innerHTML = errorMessage; + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "block"; + errorDiv.innerHTML = errorMessage; } function removeErrorMessage(divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "none"; + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "none"; } function attachValidationListener(config) { - const button = document.getElementById(config.buttonId); - - button.addEventListener("click", function (event) { - for (const key in config.validations) { - if (config.validations.hasOwnProperty(key)) { - const validationFunc = config.validations[key]; - const elementValue = document.getElementById(key).value; - validationFunc(elementValue, config.errorDivs[key], event); - } - } - }); + const button = document.getElementById(config.buttonId); + + button.addEventListener("click", function (event) { + for (const key in config.validations) { + if (config.validations.hasOwnProperty(key)) { + const validationFunc = config.validations[key]; + const elementValue = document.getElementById(key).value; + validationFunc(elementValue, config.errorDivs[key], event); + } + } + }); } // Form subsmission functions // config parameters are: formId, url, alertDiv, modal function submitCreateForm(config) { - const form = document.getElementById(config.formId); - form.addEventListener("submit", function (event) { - event.preventDefault(); - const formData = new FormData(form); - - fetch(config.url, { - method: "POST", - body: formData, - }) - .then(function (response) { - switch (response.status) { - case 409: - showAlert("entity already exists!", config.alertDiv); - break; - case 400: - showAlert("invalid file contents!", config.alertDiv); - break; - case 415: - showAlert("invalid file type!", config.alertDiv); - break; - default: - form.reset(); - config.modal.hide(); - window.location.reload(); - } - }) - .catch((error) => { - console.error("error submitting form: ", error); - }); - }); + const form = document.getElementById(config.formId); + form.addEventListener("submit", function (event) { + event.preventDefault(); + const formData = new FormData(form); + + fetch(config.url, { + method: "POST", + body: formData, + }) + .then(function (response) { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + case 400: + showAlert("invalid file contents!", config.alertDiv); + break; + case 415: + showAlert("invalid file type!", config.alertDiv); + break; + default: + form.reset(); + config.modal.hide(); + window.location.reload(); + } + }) + .catch((error) => { + console.error("error submitting form: ", error); + }); + }); } function showAlert(errorMessage, alertDiv) { - const alert = document.getElementById(alertDiv); - alert.innerHTML = ` + const alert = document.getElementById(alertDiv); + alert.innerHTML = ` <div class="alert alert-danger alert-dismissable fade show d-flex flex-row justify-content-between" role="alert"> <div>${errorMessage}</div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> @@ -174,293 +174,287 @@ function showAlert(errorMessage, alertDiv) { // make a cell editable. function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; + cell.setAttribute("contenteditable", "true"); + cell.dataset.originalContent = cell.innerHTML; } // make cell uneditable. function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); + const originalContent = cell.dataset.originalContent; + cell.innerHTML = originalContent; + cell.setAttribute("contenteditable", "false"); } // function show the save/cancel buttons and hide the edit button. function showSaveCancelButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; + editBtn.style.display = "none"; + saveCancelBtn.style.display = "inline-block"; } // function to show the edit button anf hide the save/cancel buttons. function showEditButton(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; + editBtn.style.display = "inline-block"; + saveCancelBtn.style.display = "none"; } // config parameters are: button, field function editRow(config) { - const button = document.getElementById(config.button); + const button = document.getElementById(config.button); - button.addEventListener("click", function () { - makeEditable(config.cell); - showSaveCancelButtons(config.editBtn, config.saveCancelBtn); - }); + button.addEventListener("click", function () { + makeEditable(config.cell); + showSaveCancelButtons(config.editBtn, config.saveCancelBtn); + }); } function cancelEditRow(config) { - const button = document.getElementById(config.button); + const button = document.getElementById(config.button); - button.addEventListener("click", function () { - makeUneditable(config.cell); - showEditButton(config.editBtn, config.saveCancelBtn); - removeErrorMessage(config.alertDiv); - }); + button.addEventListener("click", function () { + makeUneditable(config.cell); + showEditButton(config.editBtn, config.saveCancelBtn); + removeErrorMessage(config.alertDiv); + }); } function submitUpdateForm(config) { - fetch(config.url, { - method: "POST", - body: config.data, - headers: { - "Content-Type": "application/json", - }, - }).then((response) => { - switch (response.status) { - case 409: - showAlert("entity already exists!", config.alertDiv); - break; - default: - window.location.reload(); - } - }); + fetch(config.url, { + method: "POST", + body: config.data, + headers: { + "Content-Type": "application/json", + }, + }).then((response) => { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + default: + window.location.reload(); + } + }); } function updateName(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateName(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: updatedValue }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateName(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); } function updateIdentity(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateEmail(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/identity`; - const data = JSON.stringify({ [config.field]: updatedValue }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateEmail(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/identity`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); } function updateMetadata(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateMetadata(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateMetadata(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); } function updateTags(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateTags(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/tags`; - const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateTags(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/tags`; + const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); } function updateSecret(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validatePassword(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/secret`; - const data = JSON.stringify({ [config.field]: updatedValue }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validatePassword(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/secret`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); } function updateOwner(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}/owner`; - const data = JSON.stringify({ [config.field]: updatedValue }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}/owner`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); } function updateDescription(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: updatedValue }); - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}`; + const data = JSON.stringify({ [config.field]: updatedValue }); + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); } function attachEditRowListener(config) { - for (const key in config.rows) { - if (config.rows.hasOwnProperty(key)) { - const cell = document.querySelector(`td[data-field="${key}"]`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - editRow({ - button: `edit-${key}`, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - }); - cancelEditRow({ - button: `cancel-${key}`, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - alertDiv: config.errorDiv, - }); - const saveRow = config.rows[key]; - saveRow({ - button: `save-${key}`, - field: key, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - id: config.id, - entity: config.entity, - alertDiv: config.errorDiv, - }); - } - } + for (const key in config.rows) { + if (config.rows.hasOwnProperty(key)) { + const cell = document.querySelector(`td[data-field="${key}"]`); + const editBtn = cell.parentNode.querySelector(".edit-btn"); + const saveCancelBtn = cell.parentNode.querySelector(".save-cancel-buttons"); + editRow({ + button: `edit-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + }); + cancelEditRow({ + button: `cancel-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + alertDiv: config.errorDiv, + }); + const saveRow = config.rows[key]; + saveRow({ + button: `save-${key}`, + field: key, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + id: config.id, + entity: config.entity, + alertDiv: config.errorDiv, + }); + } + } } function fetchIndividualEntity(config) { - document.addEventListener("DOMContentLoaded", function () { - getEntities(config.item, ""); - infiniteScroll(config.item); - }); - - const input = document.getElementById(config.input); - - input.addEventListener("input", function (event) { - const itemSelect = document.getElementById(config.itemSelect); - if (event.target.value === "") { - itemSelect.innerHTML = `<option disabled>select a ${config.type}</option>`; - getEntities(config.item, ""); - infiniteScroll(config.item); - } else { - itemSelect.innerHTML = ""; - getEntities(config.item, event.target.value); - } - }); + document.addEventListener("DOMContentLoaded", function () { + getEntities(config.item, ""); + infiniteScroll(config.item); + }); + + const input = document.getElementById(config.input); + + input.addEventListener("input", function (event) { + const itemSelect = document.getElementById(config.itemSelect); + if (event.target.value === "") { + itemSelect.innerHTML = `<option disabled>select a ${config.type}</option>`; + getEntities(config.item, ""); + infiniteScroll(config.item); + } else { + itemSelect.innerHTML = ""; + getEntities(config.item, event.target.value); + } + }); } function getEntities(item, name) { - fetchData(item, name, 1); + fetchData(item, name, 1); } function infiniteScroll(item) { - var selectElement = document.getElementById("infiniteScroll"); - var singleOptionHeight = selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", function () { - var st = selectElement.scrollTop; - var totalHeight = - selectElement.querySelectorAll("option").length * singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if ( - currentScroll + numOptionsBeforeLoad * singleOptionHeight >= - totalHeight - ) { - currentPageNo++; - fetchData(item, "", currentPageNo); - } - } - - lastScrollTop = st; - }); + var selectElement = document.getElementById("infiniteScroll"); + var singleOptionHeight = selectElement.querySelector("option").offsetHeight; + var selectBoxHeight = selectElement.offsetHeight; + var numOptionsBeforeLoad = 2; + var lastScrollTop = 0; + var currentPageNo = 1; + var currentScroll = 0; + + selectElement.addEventListener("scroll", function () { + var st = selectElement.scrollTop; + var totalHeight = selectElement.querySelectorAll("option").length * singleOptionHeight; + + if (st > lastScrollTop) { + currentScroll = st + selectBoxHeight; + if (currentScroll + numOptionsBeforeLoad * singleOptionHeight >= totalHeight) { + currentPageNo++; + fetchData(item, "", currentPageNo); + } + } + + lastScrollTop = st; + }); } let limit = 5; function fetchData(item, name, page) { - fetch(`/entities?item=${item}&limit=${limit}&name=${name}&page=${page}`, { - method: "GET", - }) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("infiniteScroll"); - data.data.forEach((entity) => { - const option = document.createElement("option"); - option.value = entity.id; - option.text = entity.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); + fetch(`/entities?item=${item}&limit=${limit}&name=${name}&page=${page}`, { + method: "GET", + }) + .then((response) => response.json()) + .then((data) => { + const selectElement = document.getElementById("infiniteScroll"); + data.data.forEach((entity) => { + const option = document.createElement("option"); + option.value = entity.id; + option.text = entity.name; + selectElement.appendChild(option); + }); + }) + .catch((error) => console.error("Error:", error)); } From f2ae078b66d8cc1450763cfe2a593ed1069d2d0f Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Tue, 28 Nov 2023 11:48:13 +0300 Subject: [PATCH 07/11] update bootstrap Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/static/js/main.js | 86 ++++++++++++--- ui/web/template/bootstrap.html | 195 +++++---------------------------- ui/web/template/channels.html | 2 +- ui/web/template/groups.html | 2 +- ui/web/template/things.html | 4 +- ui/web/template/users.html | 4 +- 6 files changed, 104 insertions(+), 189 deletions(-) diff --git a/ui/web/static/js/main.js b/ui/web/static/js/main.js index 7ece41510..07b4ce060 100644 --- a/ui/web/static/js/main.js +++ b/ui/web/static/js/main.js @@ -61,21 +61,21 @@ function validatePassword(password, errorDiv, event) { return true; } -function validateMetadata(metadata, errorDiv, event) { +function validateJSON(data, errorDiv, event) { removeErrorMessage(errorDiv); try { - if (metadata.trim() !== "") { - JSON.parse(metadata); + if (data.trim() !== "") { + JSON.parse(data); } } catch (error) { event.preventDefault(); - displayErrorMessage("Metadata is not a valid JSON object", errorDiv); + displayErrorMessage("not a valid JSON object", errorDiv); return false; } return true; } -function validateTags(tags, errorDiv, event) { +function validateStringArray(tags, errorDiv, event) { removeErrorMessage(errorDiv); var tagsArray; try { @@ -89,12 +89,12 @@ function validateTags(tags, errorDiv, event) { }) ) { event.preventDefault(); - displayErrorMessage("tags must be strings in an array", errorDiv); + displayErrorMessage("must be strings in an array", errorDiv); return false; } } catch (error) { event.preventDefault(); - displayErrorMessage("tags must be a string array", errorDiv); + displayErrorMessage("must be a string array", errorDiv); return false; } @@ -220,7 +220,7 @@ function cancelEditRow(config) { function submitUpdateForm(config) { fetch(config.url, { method: "POST", - body: config.data, + body: JSON.stringify(config.data), headers: { "Content-Type": "application/json", }, @@ -242,7 +242,7 @@ function updateName(config) { const updatedValue = config.cell.textContent.trim(); if (validateName(updatedValue, config.alertDiv, event)) { const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: updatedValue }); + const data = { [config.field]: updatedValue }; submitUpdateForm({ url: url, @@ -260,7 +260,7 @@ function updateIdentity(config) { const updatedValue = config.cell.textContent.trim(); if (validateEmail(updatedValue, config.alertDiv, event)) { const url = `/${config.entity}/${config.id}/identity`; - const data = JSON.stringify({ [config.field]: updatedValue }); + const data = { [config.field]: updatedValue }; submitUpdateForm({ url: url, @@ -276,9 +276,9 @@ function updateMetadata(config) { button.addEventListener("click", function (event) { const updatedValue = config.cell.textContent.trim(); - if (validateMetadata(updatedValue, config.alertDiv, event)) { + if (validateJSON(updatedValue, config.alertDiv, event)) { const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + const data = { [config.field]: JSON.parse(updatedValue) }; submitUpdateForm({ url: url, @@ -294,9 +294,9 @@ function updateTags(config) { button.addEventListener("click", function (event) { const updatedValue = config.cell.textContent.trim(); - if (validateTags(updatedValue, config.alertDiv, event)) { + if (validateStringArray(updatedValue, config.alertDiv, event)) { const url = `/${config.entity}/${config.id}/tags`; - const data = JSON.stringify({ [config.field]: JSON.parse(updatedValue) }); + const data = { [config.field]: JSON.parse(updatedValue) }; submitUpdateForm({ url: url, @@ -314,7 +314,7 @@ function updateSecret(config) { const updatedValue = config.cell.textContent.trim(); if (validatePassword(updatedValue, config.alertDiv, event)) { const url = `/${config.entity}/${config.id}/secret`; - const data = JSON.stringify({ [config.field]: updatedValue }); + const data = { [config.field]: updatedValue }; submitUpdateForm({ url: url, @@ -331,7 +331,7 @@ function updateOwner(config) { button.addEventListener("click", function () { const updatedValue = config.cell.textContent.trim(); const url = `/${config.entity}/${config.id}/owner`; - const data = JSON.stringify({ [config.field]: updatedValue }); + const data = { [config.field]: updatedValue }; submitUpdateForm({ url: url, @@ -347,7 +347,7 @@ function updateDescription(config) { button.addEventListener("click", function () { const updatedValue = config.cell.textContent.trim(); const url = `/${config.entity}/${config.id}`; - const data = JSON.stringify({ [config.field]: updatedValue }); + const data = { [config.field]: updatedValue }; submitUpdateForm({ url: url, @@ -357,6 +357,58 @@ function updateDescription(config) { }); } +// Bootstrap update functions +function updateContent(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateClientCerts(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}/certs`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateConnections(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + + if (validateStringArray(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/connections`; + const data = { [config.field]: JSON.parse(updatedValue) }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + function attachEditRowListener(config) { for (const key in config.rows) { if (config.rows.hasOwnProperty(key)) { diff --git a/ui/web/template/bootstrap.html b/ui/web/template/bootstrap.html index 61a6cb1e5..a377c346b 100644 --- a/ui/web/template/bootstrap.html +++ b/ui/web/template/bootstrap.html @@ -35,14 +35,12 @@ {{ .Bootstrap.Name }} </td> <td> - <button class="edit-btn" onclick="editRow('name')"> + <button class="edit-btn" id="edit-name"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('name')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('name')"> - Cancel - </button> + <button class="save-btn" id="save-name">Save</button> + <button class="cancel-btn" id="cancel-name">Cancel</button> </div> </td> </tr> @@ -52,14 +50,12 @@ {{ .Bootstrap.Content }} </td> <td> - <button class="edit-btn" onclick="editRow('content')"> + <button class="edit-btn" id="edit-content"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('content')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('content')"> - Cancel - </button> + <button class="save-btn" id="save-content">Save</button> + <button class="cancel-btn" id="cancel-content">Cancel</button> </div> </td> </tr> @@ -69,14 +65,12 @@ {{ toSlice .Bootstrap.Channels }} </td> <td> - <button class="edit-btn" onclick="editRow('channels')"> + <button class="edit-btn" id="edit-channels"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('channels')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('channels')"> - Cancel - </button> + <button class="save-btn" id="save-channels">Save</button> + <button class="cancel-btn" id="cancel-channels">Cancel</button> </div> </td> </tr> @@ -86,14 +80,12 @@ {{ .Bootstrap.ClientCert }} </td> <td> - <button class="edit-btn" onclick="editRow('clientCert')"> + <button class="edit-btn" id="edit-clientCert"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('clientCert')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('clientCert')"> - Cancel - </button> + <button class="save-btn" id="save-clientCert">Save</button> + <button class="cancel-btn" id="cancel-clientCert">Cancel</button> </div> </td> </tr> @@ -103,14 +95,12 @@ {{ .Bootstrap.ClientKey }} </td> <td> - <button class="edit-btn" onclick="editRow('clientKey')"> + <button class="edit-btn" id="edit-clientKey"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('clientKey')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('clientKey')"> - Cancel - </button> + <button class="save-btn" id="save-clientKey">Save</button> + <button class="cancel-btn" id="cancel-clientKey">Cancel</button> </div> </td> </tr> @@ -120,14 +110,12 @@ {{ .Bootstrap.CACert }} </td> <td> - <button class="edit-btn" onclick="editRow('CACert')"> + <button class="edit-btn" id="edit-CACert"> <i class="fas fa-pencil-alt"></i> </button> <div class="save-cancel-buttons" style="display: none"> - <button class="save-btn" onclick="saveRow('CACert')">Save</button> - <button class="cancel-btn" onclick="cancelEditRow('CACert')"> - Cancel - </button> + <button class="save-btn" id="save-CACert">Save</button> + <button class="cancel-btn" id="cancel-CACert">Cancel</button> </div> </td> </tr> @@ -142,143 +130,18 @@ </div> {{ template "footer" }} <script> - function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; - } - - function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); - } - - function showButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; - } - - function hideButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; - } - - function editRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - - // Make the row editable - makeEditable(cell); - - // Hide the edit button and show save and cancel buttons - showButtons(editBtn, saveCancelBtn); - } - - function saveRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - // Get the updated value from the nameCell - const updatedValue = cell.textContent.trim(); - - // Send the updated value to the server using a POST request - let url; - let data; - if (field === "name") { - url = "/bootstraps/{{.Bootstrap.ThingID}}"; - data = { [field]: updatedValue, content: "{{.Bootstrap.Content}}" }; - } else if (field == "content") { - try { - const content = JSON.parse(updatedValue); - url = "/bootstraps/{{.Bootstrap.ThingID}}"; - data = { [field]: updatedValue, name: "{{.Bootstrap.Name}}" }; - } catch (error) { - errorMessage.textContent = "Content must be a valid JSON object!"; - return; - } - } else if ( - field == "clientCert" || - field == "clientKey" || - field == "CACert" - ) { - url = "/bootstraps/{{.Bootstrap.ThingID}}/certs"; - data = { [field]: updatedValue }; - } else if (field === "channels") { - try { - const channels = JSON.parse(updatedValue); - if ( - !Array.isArray(channels) || - !channels.every(function (channel) { - return typeof channel === "string"; - }) - ) { - errorMessage.textContent = "Channels must be a string array"; - return; - } - url = "/bootstraps/{{.Bootstrap.ThingID}}/connections"; - data = { [field]: channels }; - } catch (error) { - errorMessage.textContent = "Channels must be a valid string array"; - return; - } + attachEditRowListener({ + entity: "bootstraps", + id: "{{ .Bootstrap.ThingID }}", + rows: { + name:updateName, + content:updateContent, + channels:updateConnections, + clientCert:updateClientCerts, + clientKey:updateClientCerts, + CACert:updateClientCerts, } - - errorMessage.textContent = ""; - - if (url) { - errorMessage.textContent=""; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => { - if (response.ok) { - // Make the row uneditable - cell.setAttribute("contenteditable", "false"); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } - }) - .catch((error) => { - // Restore original values in the row if there is an error - makeUneditable(cell); - - // Show edit button and hide save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - - console.error("Error", error); - }); - } else { - console.error("Invalid field:", field); - } - } - - function cancelEditRow(field) { - const cell = document.querySelector(`td[data-field='${field}']`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector( - ".save-cancel-buttons", - ); - const errorMessage = document.getElementById("error-message"); - - errorMessage.textContent = ""; - // Restore original values in the row - makeUneditable(cell); - - // Show the edit button and hide the save and cancel buttons - hideButtons(editBtn, saveCancelBtn); - } + }); </script> </body> </html> diff --git a/ui/web/template/channels.html b/ui/web/template/channels.html index 8d7908443..6a67066e6 100644 --- a/ui/web/template/channels.html +++ b/ui/web/template/channels.html @@ -240,7 +240,7 @@ <h5 class="modal-title" id="addChannelsModalLabel"> }, validations: { name: validateName, - metadata: validateMetadata, + metadata: validateJSON, }, }); diff --git a/ui/web/template/groups.html b/ui/web/template/groups.html index aae2b5943..33a82d047 100644 --- a/ui/web/template/groups.html +++ b/ui/web/template/groups.html @@ -233,7 +233,7 @@ <h5 class="modal-title" id="addGroupsModalLabel"> }, validations: { name: validateName, - metadata: validateMetadata, + metadata: validateJSON, }, }); diff --git a/ui/web/template/things.html b/ui/web/template/things.html index 15b16af62..dfd06a71b 100644 --- a/ui/web/template/things.html +++ b/ui/web/template/things.html @@ -255,8 +255,8 @@ <h5 class="modal-title" id="addThingsModalLabel"> }, validations: { name: validateName, - metadata: validateMetadata, - tags: validateTags, + metadata: validateJSON, + tags: validateStringArray, }, }); diff --git a/ui/web/template/users.html b/ui/web/template/users.html index 6348078a8..6c9b1c018 100644 --- a/ui/web/template/users.html +++ b/ui/web/template/users.html @@ -246,8 +246,8 @@ <h5 class="modal-title" id="addUsersModalLabel"> name: validateName, identity: validateEmail, secret: validatePassword, - metadata: validateMetadata, - tags: validateTags, + metadata: validateJSON, + tags: validateStringArray, }, }); From 3ee065c2aa72090ac0dd363e560d6d24a9f0a9d5 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Tue, 28 Nov 2023 13:51:28 +0300 Subject: [PATCH 08/11] split javascript files Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/static/js/clipboard.js | 23 ++ ui/web/static/js/errors.js | 13 + ui/web/static/js/forms.js | 63 ++++ ui/web/static/js/infinitescroll.js | 70 ++++ ui/web/static/js/main.js | 512 ---------------------------- ui/web/static/js/update.js | 278 +++++++++++++++ ui/web/static/js/validation.js | 105 ++++++ ui/web/template/bootstrap.html | 9 +- ui/web/template/bootstraps.html | 85 ++--- ui/web/template/channel.html | 35 +- ui/web/template/channelgroups.html | 9 +- ui/web/template/channels.html | 275 ++++++++------- ui/web/template/channelthings.html | 9 +- ui/web/template/channelusers.html | 21 +- ui/web/template/error.html | 5 +- ui/web/template/group.html | 10 +- ui/web/template/groupchannels.html | 9 +- ui/web/template/groups.html | 266 +++++++-------- ui/web/template/groupusers.html | 21 +- ui/web/template/header.html | 37 +- ui/web/template/index.html | 5 +- ui/web/template/login.html | 5 +- ui/web/template/messagesread.html | 9 +- ui/web/template/resetpassword.html | 5 +- ui/web/template/terminal.html | 5 +- ui/web/template/thing.html | 38 ++- ui/web/template/thingchannels.html | 9 +- ui/web/template/things.html | 282 +++++++-------- ui/web/template/thingusers.html | 10 +- ui/web/template/updatepassword.html | 6 +- ui/web/template/user.html | 37 +- ui/web/template/userchannels.html | 21 +- ui/web/template/usergroups.html | 21 +- ui/web/template/users.html | 292 ++++++++-------- ui/web/template/userthings.html | 10 +- 35 files changed, 1376 insertions(+), 1234 deletions(-) create mode 100644 ui/web/static/js/clipboard.js create mode 100644 ui/web/static/js/errors.js create mode 100644 ui/web/static/js/forms.js create mode 100644 ui/web/static/js/infinitescroll.js delete mode 100644 ui/web/static/js/main.js create mode 100644 ui/web/static/js/update.js create mode 100644 ui/web/static/js/validation.js diff --git a/ui/web/static/js/clipboard.js b/ui/web/static/js/clipboard.js new file mode 100644 index 000000000..49a71acb1 --- /dev/null +++ b/ui/web/static/js/clipboard.js @@ -0,0 +1,23 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//function to copy the ID to the clipboard +function copyToClipboard(button) { + var clientIDElement = button.previousElementSibling.firstChild; + var clientId = clientIDElement.textContent; + + navigator.clipboard.writeText(clientId).then( + function () { + //change the copy icon to indicate success + button.innerHTML = `<i class="fas fa-check success-icon">`; + setTimeout(function () { + //revert the copy icon after a short delay + button.innerHTML = `<i class ="far fa-copy">`; + }, 1000); + }, + function (error) { + //handle error + console.error("failed to copy to clipboard: ", error); + }, + ); +} diff --git a/ui/web/static/js/errors.js b/ui/web/static/js/errors.js new file mode 100644 index 000000000..156ee545c --- /dev/null +++ b/ui/web/static/js/errors.js @@ -0,0 +1,13 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +export function displayErrorMessage(errorMessage, divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "block"; + errorDiv.innerHTML = errorMessage; +} + +export function removeErrorMessage(divName) { + const errorDiv = document.getElementById(divName); + errorDiv.style.display = "none"; +} diff --git a/ui/web/static/js/forms.js b/ui/web/static/js/forms.js new file mode 100644 index 000000000..7f64a20ee --- /dev/null +++ b/ui/web/static/js/forms.js @@ -0,0 +1,63 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// config parameters are: formId, url, alertDiv, modal +export function submitCreateForm(config) { + const form = document.getElementById(config.formId); + form.addEventListener("submit", function (event) { + event.preventDefault(); + const formData = new FormData(form); + + fetch(config.url, { + method: "POST", + body: formData, + }) + .then(function (response) { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + case 400: + showAlert("invalid file contents!", config.alertDiv); + break; + case 415: + showAlert("invalid file type!", config.alertDiv); + break; + default: + form.reset(); + config.modal.hide(); + window.location.reload(); + } + }) + .catch((error) => { + console.error("error submitting form: ", error); + }); + }); +} + +export function submitUpdateForm(config) { + fetch(config.url, { + method: "POST", + body: JSON.stringify(config.data), + headers: { + "Content-Type": "application/json", + }, + }).then((response) => { + switch (response.status) { + case 409: + showAlert("entity already exists!", config.alertDiv); + break; + default: + window.location.reload(); + } + }); +} + +function showAlert(errorMessage, alertDiv) { + const alert = document.getElementById(alertDiv); + alert.innerHTML = ` + <div class="alert alert-danger alert-dismissable fade show d-flex flex-row justify-content-between" role="alert"> + <div>${errorMessage}</div> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> + </div> `; +} diff --git a/ui/web/static/js/infinitescroll.js b/ui/web/static/js/infinitescroll.js new file mode 100644 index 000000000..ab5c5e4ab --- /dev/null +++ b/ui/web/static/js/infinitescroll.js @@ -0,0 +1,70 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +export function fetchIndividualEntity(config) { + document.addEventListener("DOMContentLoaded", function () { + getEntities(config.item, ""); + infiniteScroll(config.item); + }); + + const input = document.getElementById(config.input); + + input.addEventListener("input", function (event) { + const itemSelect = document.getElementById(config.itemSelect); + if (event.target.value === "") { + itemSelect.innerHTML = `<option disabled>select a ${config.type}</option>`; + getEntities(config.item, ""); + infiniteScroll(config.item); + } else { + itemSelect.innerHTML = ""; + getEntities(config.item, event.target.value); + } + }); +} + +function getEntities(item, name) { + fetchData(item, name, 1); +} + +function infiniteScroll(item) { + var selectElement = document.getElementById("infiniteScroll"); + var singleOptionHeight = selectElement.querySelector("option").offsetHeight; + var selectBoxHeight = selectElement.offsetHeight; + var numOptionsBeforeLoad = 2; + var lastScrollTop = 0; + var currentPageNo = 1; + var currentScroll = 0; + + selectElement.addEventListener("scroll", function () { + var st = selectElement.scrollTop; + var totalHeight = selectElement.querySelectorAll("option").length * singleOptionHeight; + + if (st > lastScrollTop) { + currentScroll = st + selectBoxHeight; + if (currentScroll + numOptionsBeforeLoad * singleOptionHeight >= totalHeight) { + currentPageNo++; + fetchData(item, "", currentPageNo); + } + } + + lastScrollTop = st; + }); +} + +let limit = 5; +function fetchData(item, name, page) { + fetch(`/entities?item=${item}&limit=${limit}&name=${name}&page=${page}`, { + method: "GET", + }) + .then((response) => response.json()) + .then((data) => { + const selectElement = document.getElementById("infiniteScroll"); + data.data.forEach((entity) => { + const option = document.createElement("option"); + option.value = entity.id; + option.text = entity.name; + selectElement.appendChild(option); + }); + }) + .catch((error) => console.error("Error:", error)); +} diff --git a/ui/web/static/js/main.js b/ui/web/static/js/main.js deleted file mode 100644 index 07b4ce060..000000000 --- a/ui/web/static/js/main.js +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -//function to copy the ID to the clipboard -function copyToClipboard(button) { - var clientIDElement = button.previousElementSibling.firstChild; - var clientId = clientIDElement.textContent; - - navigator.clipboard.writeText(clientId).then( - function () { - //change the copy icon to indicate success - button.innerHTML = `<i class="fas fa-check success-icon">`; - setTimeout(function () { - //revert the copy icon after a short delay - button.innerHTML = `<i class ="far fa-copy">`; - }, 1000); - }, - function (error) { - //handle error - console.error("failed to copy to clipboard: ", error); - }, - ); -} - -// Form validation functions - -function validateName(name, errorDiv, event) { - removeErrorMessage(errorDiv); - if (name.trim() === "") { - event.preventDefault(); - displayErrorMessage("Name is Required", errorDiv); - return false; - } - return true; -} - -const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; -function validateEmail(email, errorDiv, event) { - removeErrorMessage(errorDiv); - if (email.trim() === "") { - event.preventDefault(); - displayErrorMessage("Email is Required", errorDiv); - return false; - } else if (!email.match(emailRegex)) { - event.preventDefault(); - displayErrorMessage("Invalid email format", errorDiv); - return false; - } - return true; -} - -const minLength = 8; -function validatePassword(password, errorDiv, event) { - removeErrorMessage(errorDiv); - if (password.trim().length < minLength) { - event.preventDefault(); - var errorMessage = `Password must be at least ${minLength} characters long`; - displayErrorMessage(errorMessage, errorDiv); - return false; - } - return true; -} - -function validateJSON(data, errorDiv, event) { - removeErrorMessage(errorDiv); - try { - if (data.trim() !== "") { - JSON.parse(data); - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("not a valid JSON object", errorDiv); - return false; - } - return true; -} - -function validateStringArray(tags, errorDiv, event) { - removeErrorMessage(errorDiv); - var tagsArray; - try { - if (tags.trim() !== "") { - tagsArray = JSON.parse(tags); - } - if ( - !Array.isArray(tagsArray) || - !tagsArray.every(function (tag) { - return typeof tag === "string"; - }) - ) { - event.preventDefault(); - displayErrorMessage("must be strings in an array", errorDiv); - return false; - } - } catch (error) { - event.preventDefault(); - displayErrorMessage("must be a string array", errorDiv); - return false; - } - - return true; -} - -function displayErrorMessage(errorMessage, divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "block"; - errorDiv.innerHTML = errorMessage; -} - -function removeErrorMessage(divName) { - const errorDiv = document.getElementById(divName); - errorDiv.style.display = "none"; -} - -function attachValidationListener(config) { - const button = document.getElementById(config.buttonId); - - button.addEventListener("click", function (event) { - for (const key in config.validations) { - if (config.validations.hasOwnProperty(key)) { - const validationFunc = config.validations[key]; - const elementValue = document.getElementById(key).value; - validationFunc(elementValue, config.errorDivs[key], event); - } - } - }); -} - -// Form subsmission functions -// config parameters are: formId, url, alertDiv, modal -function submitCreateForm(config) { - const form = document.getElementById(config.formId); - form.addEventListener("submit", function (event) { - event.preventDefault(); - const formData = new FormData(form); - - fetch(config.url, { - method: "POST", - body: formData, - }) - .then(function (response) { - switch (response.status) { - case 409: - showAlert("entity already exists!", config.alertDiv); - break; - case 400: - showAlert("invalid file contents!", config.alertDiv); - break; - case 415: - showAlert("invalid file type!", config.alertDiv); - break; - default: - form.reset(); - config.modal.hide(); - window.location.reload(); - } - }) - .catch((error) => { - console.error("error submitting form: ", error); - }); - }); -} - -function showAlert(errorMessage, alertDiv) { - const alert = document.getElementById(alertDiv); - alert.innerHTML = ` - <div class="alert alert-danger alert-dismissable fade show d-flex flex-row justify-content-between" role="alert"> - <div>${errorMessage}</div> - <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> - </div> `; -} - -// Functions to make a row editable. - -// make a cell editable. -function makeEditable(cell) { - cell.setAttribute("contenteditable", "true"); - cell.dataset.originalContent = cell.innerHTML; -} - -// make cell uneditable. -function makeUneditable(cell) { - const originalContent = cell.dataset.originalContent; - cell.innerHTML = originalContent; - cell.setAttribute("contenteditable", "false"); -} - -// function show the save/cancel buttons and hide the edit button. -function showSaveCancelButtons(editBtn, saveCancelBtn) { - editBtn.style.display = "none"; - saveCancelBtn.style.display = "inline-block"; -} - -// function to show the edit button anf hide the save/cancel buttons. -function showEditButton(editBtn, saveCancelBtn) { - editBtn.style.display = "inline-block"; - saveCancelBtn.style.display = "none"; -} - -// config parameters are: button, field -function editRow(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - makeEditable(config.cell); - showSaveCancelButtons(config.editBtn, config.saveCancelBtn); - }); -} - -function cancelEditRow(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - makeUneditable(config.cell); - showEditButton(config.editBtn, config.saveCancelBtn); - removeErrorMessage(config.alertDiv); - }); -} - -function submitUpdateForm(config) { - fetch(config.url, { - method: "POST", - body: JSON.stringify(config.data), - headers: { - "Content-Type": "application/json", - }, - }).then((response) => { - switch (response.status) { - case 409: - showAlert("entity already exists!", config.alertDiv); - break; - default: - window.location.reload(); - } - }); -} - -function updateName(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateName(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function updateIdentity(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateEmail(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/identity`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function updateMetadata(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateJSON(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}`; - const data = { [config.field]: JSON.parse(updatedValue) }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function updateTags(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validateStringArray(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/tags`; - const data = { [config.field]: JSON.parse(updatedValue) }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function updateSecret(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - if (validatePassword(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/secret`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function updateOwner(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}/owner`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); -} - -function updateDescription(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); -} - -// Bootstrap update functions -function updateContent(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); -} - -function updateClientCerts(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function () { - const updatedValue = config.cell.textContent.trim(); - const url = `/${config.entity}/${config.id}/certs`; - const data = { [config.field]: updatedValue }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - }); -} - -function updateConnections(config) { - const button = document.getElementById(config.button); - - button.addEventListener("click", function (event) { - const updatedValue = config.cell.textContent.trim(); - - if (validateStringArray(updatedValue, config.alertDiv, event)) { - const url = `/${config.entity}/${config.id}/connections`; - const data = { [config.field]: JSON.parse(updatedValue) }; - - submitUpdateForm({ - url: url, - data: data, - alertDiv: config.alertDiv, - }); - } - }); -} - -function attachEditRowListener(config) { - for (const key in config.rows) { - if (config.rows.hasOwnProperty(key)) { - const cell = document.querySelector(`td[data-field="${key}"]`); - const editBtn = cell.parentNode.querySelector(".edit-btn"); - const saveCancelBtn = cell.parentNode.querySelector(".save-cancel-buttons"); - editRow({ - button: `edit-${key}`, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - }); - cancelEditRow({ - button: `cancel-${key}`, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - alertDiv: config.errorDiv, - }); - const saveRow = config.rows[key]; - saveRow({ - button: `save-${key}`, - field: key, - cell: cell, - editBtn: editBtn, - saveCancelBtn: saveCancelBtn, - id: config.id, - entity: config.entity, - alertDiv: config.errorDiv, - }); - } - } -} - -function fetchIndividualEntity(config) { - document.addEventListener("DOMContentLoaded", function () { - getEntities(config.item, ""); - infiniteScroll(config.item); - }); - - const input = document.getElementById(config.input); - - input.addEventListener("input", function (event) { - const itemSelect = document.getElementById(config.itemSelect); - if (event.target.value === "") { - itemSelect.innerHTML = `<option disabled>select a ${config.type}</option>`; - getEntities(config.item, ""); - infiniteScroll(config.item); - } else { - itemSelect.innerHTML = ""; - getEntities(config.item, event.target.value); - } - }); -} - -function getEntities(item, name) { - fetchData(item, name, 1); -} - -function infiniteScroll(item) { - var selectElement = document.getElementById("infiniteScroll"); - var singleOptionHeight = selectElement.querySelector("option").offsetHeight; - var selectBoxHeight = selectElement.offsetHeight; - var numOptionsBeforeLoad = 2; - var lastScrollTop = 0; - var currentPageNo = 1; - var currentScroll = 0; - - selectElement.addEventListener("scroll", function () { - var st = selectElement.scrollTop; - var totalHeight = selectElement.querySelectorAll("option").length * singleOptionHeight; - - if (st > lastScrollTop) { - currentScroll = st + selectBoxHeight; - if (currentScroll + numOptionsBeforeLoad * singleOptionHeight >= totalHeight) { - currentPageNo++; - fetchData(item, "", currentPageNo); - } - } - - lastScrollTop = st; - }); -} - -let limit = 5; -function fetchData(item, name, page) { - fetch(`/entities?item=${item}&limit=${limit}&name=${name}&page=${page}`, { - method: "GET", - }) - .then((response) => response.json()) - .then((data) => { - const selectElement = document.getElementById("infiniteScroll"); - data.data.forEach((entity) => { - const option = document.createElement("option"); - option.value = entity.id; - option.text = entity.name; - selectElement.appendChild(option); - }); - }) - .catch((error) => console.error("Error:", error)); -} diff --git a/ui/web/static/js/update.js b/ui/web/static/js/update.js new file mode 100644 index 000000000..21d9f5a46 --- /dev/null +++ b/ui/web/static/js/update.js @@ -0,0 +1,278 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +import { submitUpdateForm } from "./forms.js"; +import { + validateName, + validateEmail, + validateJSON, + validateStringArray, + validatePassword, +} from "./validation.js"; + +function updateName(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateName(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateIdentity(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateEmail(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/identity`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateMetadata(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateJSON(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}`; + const data = { [config.field]: JSON.parse(updatedValue) }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateTags(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validateStringArray(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/tags`; + const data = { [config.field]: JSON.parse(updatedValue) }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateSecret(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + if (validatePassword(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/secret`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +function updateOwner(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}/owner`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateDescription(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +// Bootstrap update functions +function updateContent(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateClientCerts(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + const updatedValue = config.cell.textContent.trim(); + const url = `/${config.entity}/${config.id}/certs`; + const data = { [config.field]: updatedValue }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + }); +} + +function updateConnections(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function (event) { + const updatedValue = config.cell.textContent.trim(); + + if (validateStringArray(updatedValue, config.alertDiv, event)) { + const url = `/${config.entity}/${config.id}/connections`; + const data = { [config.field]: JSON.parse(updatedValue) }; + + submitUpdateForm({ + url: url, + data: data, + alertDiv: config.alertDiv, + }); + } + }); +} + +// make a cell editable. +function makeEditable(cell) { + cell.setAttribute("contenteditable", "true"); + cell.dataset.originalContent = cell.innerHTML; +} + +// make cell uneditable. +function makeUneditable(cell) { + const originalContent = cell.dataset.originalContent; + cell.innerHTML = originalContent; + cell.setAttribute("contenteditable", "false"); +} + +// function show the save/cancel buttons and hide the edit button. +function showSaveCancelButtons(editBtn, saveCancelBtn) { + editBtn.style.display = "none"; + saveCancelBtn.style.display = "inline-block"; +} + +// function to show the edit button anf hide the save/cancel buttons. +function showEditButton(editBtn, saveCancelBtn) { + editBtn.style.display = "inline-block"; + saveCancelBtn.style.display = "none"; +} + +// config parameters are: button, field +function editRow(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + makeEditable(config.cell); + showSaveCancelButtons(config.editBtn, config.saveCancelBtn); + }); +} + +function cancelEditRow(config) { + const button = document.getElementById(config.button); + + button.addEventListener("click", function () { + makeUneditable(config.cell); + showEditButton(config.editBtn, config.saveCancelBtn); + removeErrorMessage(config.alertDiv); + }); +} + +function attachEditRowListener(config) { + for (const key in config.rows) { + if (config.rows.hasOwnProperty(key)) { + const cell = document.querySelector(`td[data-field="${key}"]`); + const editBtn = cell.parentNode.querySelector(".edit-btn"); + const saveCancelBtn = cell.parentNode.querySelector(".save-cancel-buttons"); + editRow({ + button: `edit-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + }); + cancelEditRow({ + button: `cancel-${key}`, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + alertDiv: config.errorDiv, + }); + const saveRow = config.rows[key]; + saveRow({ + button: `save-${key}`, + field: key, + cell: cell, + editBtn: editBtn, + saveCancelBtn: saveCancelBtn, + id: config.id, + entity: config.entity, + alertDiv: config.errorDiv, + }); + } + } +} + +export { + updateName, + updateIdentity, + updateMetadata, + updateTags, + updateSecret, + updateOwner, + updateDescription, + updateContent, + updateClientCerts, + updateConnections, + attachEditRowListener, +}; diff --git a/ui/web/static/js/validation.js b/ui/web/static/js/validation.js new file mode 100644 index 000000000..8d83f2450 --- /dev/null +++ b/ui/web/static/js/validation.js @@ -0,0 +1,105 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +import { displayErrorMessage, removeErrorMessage } from "./errors.js"; + +const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; +const minLength = 8; + +function validateName(name, errorDiv, event) { + removeErrorMessage(errorDiv); + if (name.trim() === "") { + event.preventDefault(); + displayErrorMessage("Name is Required", errorDiv); + return false; + } + return true; +} + +function validateEmail(email, errorDiv, event) { + removeErrorMessage(errorDiv); + if (email.trim() === "") { + event.preventDefault(); + displayErrorMessage("Email is Required", errorDiv); + return false; + } else if (!email.match(emailRegex)) { + event.preventDefault(); + displayErrorMessage("Invalid email format", errorDiv); + return false; + } + return true; +} + +function validatePassword(password, errorDiv, event) { + removeErrorMessage(errorDiv); + if (password.trim().length < minLength) { + event.preventDefault(); + var errorMessage = `Password must be at least ${minLength} characters long`; + displayErrorMessage(errorMessage, errorDiv); + return false; + } + return true; +} + +function validateJSON(data, errorDiv, event) { + removeErrorMessage(errorDiv); + try { + if (data.trim() !== "") { + JSON.parse(data); + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("not a valid JSON object", errorDiv); + return false; + } + return true; +} + +function validateStringArray(tags, errorDiv, event) { + removeErrorMessage(errorDiv); + var tagsArray; + try { + if (tags.trim() !== "") { + tagsArray = JSON.parse(tags); + } + if ( + !Array.isArray(tagsArray) || + !tagsArray.every(function (tag) { + return typeof tag === "string"; + }) + ) { + event.preventDefault(); + displayErrorMessage("must be strings in an array", errorDiv); + return false; + } + } catch (error) { + event.preventDefault(); + displayErrorMessage("must be a string array", errorDiv); + return false; + } + + return true; +} + +function attachValidationListener(config) { + const button = document.getElementById(config.buttonId); + + button.addEventListener("click", function (event) { + for (const key in config.validations) { + if (config.validations.hasOwnProperty(key)) { + const validationFunc = config.validations[key]; + const elementValue = document.getElementById(key).value; + validationFunc(elementValue, config.errorDivs[key], event); + } + } + }); +} + +export { + validateName, + validateEmail, + validatePassword, + validateJSON, + validateStringArray, + attachValidationListener, +}; diff --git a/ui/web/template/bootstrap.html b/ui/web/template/bootstrap.html index a377c346b..a821146e5 100644 --- a/ui/web/template/bootstrap.html +++ b/ui/web/template/bootstrap.html @@ -4,7 +4,11 @@ {{ define "bootstrap" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Bootstrap</title> + {{ template "header" }} + <script src="/js/update.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -129,7 +133,8 @@ </div> </div> {{ template "footer" }} - <script> + <script type="module"> + import { attachEditRowListener, updateName, updateContent, updateConnections, updateClientCerts } from "/js/update.js"; attachEditRowListener({ entity: "bootstraps", id: "{{ .Bootstrap.ThingID }}", diff --git a/ui/web/template/bootstraps.html b/ui/web/template/bootstraps.html index 1e0f7e70e..f728a18fe 100644 --- a/ui/web/template/bootstraps.html +++ b/ui/web/template/bootstraps.html @@ -4,7 +4,13 @@ {{ define "bootstraps" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Bootstraps</title> + {{ template "header" }} + <script src="/js/forms.js" type="module"></script> + <script src="/js/validation.js" type="module"></script> + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -32,7 +38,7 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> </h5> </div> <div class="modal-body"> - <form method="post" onsubmit="return validateForm()"> + <form method="post"> <div class="mb-3"> <label for="name" class="form-label">Bootstrap Name</label> <input @@ -97,7 +103,7 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> <div id="channelsHelp" class="form-text"> Enter channels as a string slice. </div> - <div id="channelsError" class="error-message"></div> + <div id="channelsError" class="text-danger"></div> </div> <div class="mb-3"> @@ -112,7 +118,7 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> <div id="contentHelp" class="form-text"> Enter content in JSON format. </div> - <div id="contentError" class="error-message"></div> + <div id="contentError" class="text-danger"></div> </div> <div class="mb-3"> <label for="clientCert" class="form-label">Client Cert</label> @@ -147,7 +153,13 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> value="CACert" /> </div> - <button type="submit" class="btn body-button">Submit</button> + <button + type="submit" + id="create-bootstrap-button" + class="btn body-button" + > + Submit + </button> </form> </div> </div> @@ -194,54 +206,31 @@ <h5 class="modal-title" id="addBootstrapModalLabel"> </div> {{ template "footer" }} <script> - function validateForm() { - var channelsInput = document.getElementById("channels").value; - var contentInput = document.getElementById("content").value; - - var channelsError = document.getElementById("channelsError"); - var contentError = document.getElementById("contentError"); - - channelsError.innerHTML = ""; - contentError.innerHTML = ""; - - var content; - var channels; - - var isValid = true; - - try { - channels = JSON.parse(channelsInput); - } catch (error) { - channelsError.innerHTML = "Please enter valid channels as a string array!"; - isValid = false; - } - - if ( - !Array.isArray(channels) || - !channels.every(function (channels) { - return typeof channels === "string"; - }) - ) { - channelsError.innerHTML = "Channels must be a string array!"; - isValid = false; - } - - try { - content = JSON.parse(contentInput); - content = contentInput; - } catch (error) { - contentError.innerHTML = "Please enter valid content in JSON format!"; - isValid = false; - } - - return isValid; - } - const bootstrapsModal = new bootstrap.Modal(document.getElementById("addBootstrapModal")); function openModal() { bootstrapsModal.show(); } + </script> + <script type="module"> + import { + attachValidationListener, + validateStringArray, + validateJSON, + } from "/js/validation.js"; + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + attachValidationListener({ + buttonId: "create-bootstrap-button", + errorDivs: { + channels: "channelsError", + content: "contentError", + }, + validations: { + channels: validateStringArray, + content: validateJSON, + }, + }); fetchIndividualEntity({ input: "thingFilter", diff --git a/ui/web/template/channel.html b/ui/web/template/channel.html index a9d7a6c2d..b8c43aebb 100644 --- a/ui/web/template/channel.html +++ b/ui/web/template/channel.html @@ -4,7 +4,11 @@ {{ define "channel" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Channel</title> + {{ template "header" }} + <script src="/js/update.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -129,20 +133,21 @@ </div> </div> {{ template "footer" }} - <script> - attachEditRowListener( - { - entity: "channels", - id: "{{ .Channel.ID }}", - rows: { - name:updateName, - description: updateDescription, - metadata:updateMetadata, - }, - errorDiv: "error-message", - } - ); - </script> + <script type="module"> + import { attachEditRowListener, updateName, updateDescription, updateMetadata} from "/js/update.js"; + + attachEditRowListener( + { + entity: "channels", + id: "{{ .Channel.ID }}", + rows: { + name:updateName, + description: updateDescription, + metadata:updateMetadata, + }, + errorDiv: "error-message", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/channelgroups.html b/ui/web/template/channelgroups.html index 62d8f9fc6..69757abfe 100644 --- a/ui/web/template/channelgroups.html +++ b/ui/web/template/channelgroups.html @@ -4,7 +4,11 @@ {{ define "channelgroups" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Channel Groups</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -161,6 +165,9 @@ <h1 class="modal-title fs-5" id="addGroupModalLabel">Add Group</h1> function openGroupModal() { groupModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; fetchIndividualEntity({ input: "groupFilter", diff --git a/ui/web/template/channels.html b/ui/web/template/channels.html index 6a67066e6..1dc7faf30 100644 --- a/ui/web/template/channels.html +++ b/ui/web/template/channels.html @@ -4,7 +4,13 @@ {{ define "channels" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Channels</title> + {{ template "header" }} + <script src="/js/forms.js" type="module"></script> + <script src="/js/validation.js" type="module"></script> + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -106,144 +112,118 @@ <h5 class="modal-title" id="addChannelModalLabel">Add Channel</h5> Add Channels </button> - <!-- Modal --> - <div - class="modal fade" - id="addChannelsModal" - tabindex="-1" - role="dialog" - aria-labelledby="addChannelsModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addChannelsModalLabel"> - Add Channels - </h5> - </div> - <div class="modal-body"> - <div id="alertBulkMessage"></div> - <form - method="post" - enctype="multipart/form-data" - id="bulkchannelsform" - > - <div class="form-group"> - <label for="channelsFile"> - Add csv file containing channels names with - metadata. The metadata field can be empty. Find - a sample csv file - <a - href="https://github.com/absmach/magistrala-ui/blob/main/samples/channels.csv" - target="_blank" - > - here - </a> - </label> - <input - type="file" - class="form-control-file" - id="channelsFile" - name="channelsFile" - required - /> - <button - type="submit" - value="upload" - class="btn body-button" - > - Submit - </button> - </div> - </form> - </div> - </div> - </div> - </div> - </div> - <div class="table-responsive table-container"> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $c := .Channels }} - {{ $disableButton := false }} - <tr> - <td>{{ $c.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/channels/%s" $c.ID }}"> - {{ $c.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $c.Description }}</td> - <td class="meta-col">{{ toJSON $c.Metadata }}</td> - <td class="created-col">{{ $c.CreatedAt }}</td> - <td class="text-center"> - <form action="/channels/disabled" method="post"> - <input - type="hidden" - name="channelID" - id="channelID" - value="{{ $c.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - attachValidationListener({ - buttonId: "create-channel-button", - errorDivs: { - name: "nameError", - metadata: "metadataError", - }, - validations: { - name: validateName, - metadata: validateJSON, - }, - }); - + <!-- Modal --> + <div + class="modal fade" + id="addChannelsModal" + tabindex="-1" + role="dialog" + aria-labelledby="addChannelsModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addChannelsModalLabel">Add Channels</h5> + </div> + <div class="modal-body"> + <div id="alertBulkMessage"></div> + <form method="post" enctype="multipart/form-data" id="bulkchannelsform"> + <div class="form-group"> + <label for="channelsFile"> + Add csv file containing channels names with metadata. The metadata + field can be empty. Find a sample csv file + <a + href="https://github.com/absmach/magistrala-ui/blob/main/samples/channels.csv" + target="_blank" + > + here + </a> + </label> + <input + type="file" + class="form-control-file" + id="channelsFile" + name="channelsFile" + required + /> + <button type="submit" value="upload" class="btn body-button"> + Submit + </button> + </div> + </form> + </div> + </div> + </div> + </div> + </div> + <div class="table-responsive table-container"> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $c := .Channels }} + {{ $disableButton := false }} + <tr> + <td>{{ $c.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/channels/%s" $c.ID }}"> + {{ $c.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $c.Description }}</td> + <td class="meta-col">{{ toJSON $c.Metadata }}</td> + <td class="created-col">{{ $c.CreatedAt }}</td> + <td class="text-center"> + <form action="/channels/disabled" method="post"> + <input + type="hidden" + name="channelID" + id="channelID" + value="{{ $c.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> const channelModal = new bootstrap.Modal(document.getElementById("addChannelModal")); const channelsModal = new bootstrap.Modal(document.getElementById("addChannelsModal")); @@ -254,6 +234,23 @@ <h5 class="modal-title" id="addChannelsModalLabel"> channelsModal.show(); } } + </script> + <script type="module"> + import { attachValidationListener, validateName, validateJSON } from "/js/validation.js"; + import { submitCreateForm } from "/js/forms.js"; + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + attachValidationListener({ + buttonId: "create-channel-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + }, + validations: { + name: validateName, + metadata: validateJSON, + }, + }); submitCreateForm({ url: "/channels", diff --git a/ui/web/template/channelthings.html b/ui/web/template/channelthings.html index a0e6c32c8..2be7ff125 100644 --- a/ui/web/template/channelthings.html +++ b/ui/web/template/channelthings.html @@ -4,7 +4,11 @@ {{ define "channelthings" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Channel Things</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -161,6 +165,9 @@ <h1 class="modal-title fs-5" id="addThingModalLabel">Add Thing</h1> function openThingModal() { thingModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; fetchIndividualEntity({ input: "thingFilter", diff --git a/ui/web/template/channelusers.html b/ui/web/template/channelusers.html index 12d383c14..845907f62 100644 --- a/ui/web/template/channelusers.html +++ b/ui/web/template/channelusers.html @@ -4,7 +4,11 @@ {{ define "channelusers" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Channel Users</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -475,12 +479,6 @@ <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> userModal.show(); } - fetchIndividualEntity({ - input: "userFilter", - itemSelect: "infiniteScroll", - item: "users", - }); - function openTab(relation) { event.preventDefault(); var channelID = '{{.ChannelID}}'; @@ -493,6 +491,15 @@ <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> .catch((error) => console.error("Error:", error)); } </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/error.html b/ui/web/template/error.html index 63c0ad3d9..bd83d8150 100644 --- a/ui/web/template/error.html +++ b/ui/web/template/error.html @@ -4,7 +4,10 @@ {{ define "error" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Error</title> + {{ template "header" }} + </head> <body> {{ template "navbar" }} <div class="main-content"> diff --git a/ui/web/template/group.html b/ui/web/template/group.html index e7ae42567..ab1f59c47 100644 --- a/ui/web/template/group.html +++ b/ui/web/template/group.html @@ -4,7 +4,11 @@ {{ define "group" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Group</title> + {{ template "header" }} + <script src="/js/update.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -113,7 +117,9 @@ </div> </div> {{ template "footer" }} - <script> + <script type="module"> + import { attachEditRowListener, updateName, updateDescription, updateMetadata} from "/js/update.js"; + attachEditRowListener( { entity: "groups", diff --git a/ui/web/template/groupchannels.html b/ui/web/template/groupchannels.html index 26b5af2fa..daf8eaee7 100644 --- a/ui/web/template/groupchannels.html +++ b/ui/web/template/groupchannels.html @@ -4,7 +4,11 @@ {{ define "groupchannels" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Group Channels</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -156,6 +160,9 @@ <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> function openChannelModal() { channelModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; fetchIndividualEntity({ input: "channelFilter", diff --git a/ui/web/template/groups.html b/ui/web/template/groups.html index 33a82d047..ffc641fb0 100644 --- a/ui/web/template/groups.html +++ b/ui/web/template/groups.html @@ -4,7 +4,13 @@ {{ define "groups" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Groups</title> + {{ template "header" }} + <script src="/js/forms.js" type="module"></script> + <script src="/js/validation.js" type="module"></script> + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -100,131 +106,134 @@ <h5 class="modal-title" id="addGroupModalLabel">Add Group</h5> Add Groups </button> - <!-- Modal --> - <div - class="modal fade" - id="addGroupsModal" - tabindex="-1" - role="dialog" - aria-labelledby="addGroupsModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addGroupsModalLabel"> - Add Groups - </h5> - </div> - <div class="modal-body"> - <div id="alertBulkMessage"></div> - <form - enctype="multipart/form-data" - id="bulkgroupsform" - > - <div class="form-group"> - <label for="groupsFile"> - Add csv file containing groups names. Find a - sample csv file - <a - href="https://github.com/absmach/magistrala-ui/blob/main/samples/groups.csv" - target="_blank" - > - here - </a> - </label> - <input - type="file" - class="form-control-file" - id="groupsFile" - name="groupsFile" - required - /> - <button - type="submit" - value="upload" - class="btn body-button" - > - Submit - </button> - </div> - </form> - </div> - </div> - </div> - </div> - </div> - <div class="table-responsive table-container"> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="desc-col" scope="col">Description</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $g := .Groups }} - {{ $disableButton := false }} - <tr> - <td>{{ $g.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/groups/%s" $g.ID }}"> - {{ $g.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="desc-col">{{ $g.Description }}</td> - <td class="meta-col">{{ toJSON $g.Metadata }}</td> - <td class="created-col">{{ $g.CreatedAt }}</td> - <td class="text-center"> - <form action="/groups/disabled" method="post"> - <input - type="hidden" - name="groupID" - id="groupID" - value="{{ $g.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> + <!-- Modal --> + <div + class="modal fade" + id="addGroupsModal" + tabindex="-1" + role="dialog" + aria-labelledby="addGroupsModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addGroupsModalLabel">Add Groups</h5> + </div> + <div class="modal-body"> + <div id="alertBulkMessage"></div> + <form enctype="multipart/form-data" id="bulkgroupsform"> + <div class="form-group"> + <label for="groupsFile"> + Add csv file containing groups names. Find a sample csv file + <a + href="https://github.com/absmach/magistrala-ui/blob/main/samples/groups.csv" + target="_blank" + > + here + </a> + </label> + <input + type="file" + class="form-control-file" + id="groupsFile" + name="groupsFile" + required + /> + <button type="submit" value="upload" class="btn body-button"> + Submit + </button> + </div> + </form> + </div> + </div> + </div> + </div> + </div> + <div class="table-responsive table-container"> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="desc-col" scope="col">Description</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $g := .Groups }} + {{ $disableButton := false }} + <tr> + <td>{{ $g.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/groups/%s" $g.ID }}"> + {{ $g.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="desc-col">{{ $g.Description }}</td> + <td class="meta-col">{{ toJSON $g.Metadata }}</td> + <td class="created-col">{{ $g.CreatedAt }}</td> + <td class="text-center"> + <form action="/groups/disabled" method="post"> + <input + type="hidden" + name="groupID" + id="groupID" + value="{{ $g.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> {{ template "footer" }} <script> + const groupModal = new bootstrap.Modal(document.getElementById("addGroupModal")); + const groupsModal = new bootstrap.Modal(document.getElementById("addGroupsModal")); + + function openModal(modal) { + if (modal === "single") { + groupModal.show(); + } else if (modal === "bulk") { + groupsModal.show(); + } + } + </script> + <script type="module"> + import { attachValidationListener, validateName, validateJSON } from "/js/validation.js"; + import { submitCreateForm } from "/js/forms.js"; + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + attachValidationListener({ buttonId: "create-group-button", errorDivs: { @@ -237,17 +246,6 @@ <h5 class="modal-title" id="addGroupsModalLabel"> }, }); - const groupModal = new bootstrap.Modal(document.getElementById("addGroupModal")); - const groupsModal = new bootstrap.Modal(document.getElementById("addGroupsModal")); - - function openModal(modal) { - if (modal === "single") { - groupModal.show(); - } else if (modal === "bulk") { - groupsModal.show(); - } - } - submitCreateForm({ url: "/groups", formId: "groupform", diff --git a/ui/web/template/groupusers.html b/ui/web/template/groupusers.html index ae60dc189..270af2de5 100644 --- a/ui/web/template/groupusers.html +++ b/ui/web/template/groupusers.html @@ -4,7 +4,11 @@ {{ define "groupusers" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Group Users</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -471,12 +475,6 @@ <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> userModal.show(); } - fetchIndividualEntity({ - input: "userFilter", - itemSelect: "infiniteScroll", - item: "users", - }); - function openTab(relation) { event.preventDefault(); var groupID = '{{.GroupID}}'; @@ -489,6 +487,15 @@ <h1 class="modal-title fs-5" id="addUserModalLabel">Add User</h1> .catch((error) => console.error("Error:", error)); } </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + fetchIndividualEntity({ + input: "userFilter", + itemSelect: "infiniteScroll", + item: "users", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/header.html b/ui/web/template/header.html index e906b381b..8f7c8d5c4 100644 --- a/ui/web/template/header.html +++ b/ui/web/template/header.html @@ -2,25 +2,20 @@ SPDX-License-Identifier: Apache-2.0 --> {{ define "header" }} - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="description" content="" /> - <meta name="author" content="" /> - - <title>Magistrala</title> - <link rel="stylesheet" href="/css/styles.css" /> - <link rel="icon" type="image/x-icon" href="/images/favicon.ico" /> - <link - rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" - /> - <link - href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" - rel="stylesheet" - integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" - crossorigin="anonymous" - /> - <script src="/js/main.js"></script> - </head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="description" content="" /> + <meta name="author" content="" /> + <link rel="stylesheet" href="/css/styles.css" /> + <link rel="icon" type="image/x-icon" href="/images/favicon.ico" /> + <link + rel="stylesheet" + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" + /> + <link + href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" + rel="stylesheet" + integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" + crossorigin="anonymous" + /> {{ end }} diff --git a/ui/web/template/index.html b/ui/web/template/index.html index ad5a656e6..7b731d0e3 100644 --- a/ui/web/template/index.html +++ b/ui/web/template/index.html @@ -4,7 +4,10 @@ {{ define "index" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Magistrala</title> + {{ template "header" }} + </head> <body> {{ template "navbar" . }} <div class="main-content mt-5 pt-5"> diff --git a/ui/web/template/login.html b/ui/web/template/login.html index 1866189f4..67aadcf0a 100644 --- a/ui/web/template/login.html +++ b/ui/web/template/login.html @@ -4,7 +4,10 @@ {{ define "login" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Login</title> + {{ template "header" }} + </head> <body class="login-body"> <div class="container mt-5 pt-5"> <div class="row p-5"> diff --git a/ui/web/template/messagesread.html b/ui/web/template/messagesread.html index 5f060ca77..4b008b119 100644 --- a/ui/web/template/messagesread.html +++ b/ui/web/template/messagesread.html @@ -4,13 +4,13 @@ {{ define "messagesread" }} <!doctype html> <html lang="en"> - {{ template "header" }} - + <head> + <title>Messages</title> + {{ template "header" }} + </head> <body> {{ template "navbar" . }} - - <div class="main-content pt-3"> <div class="container"> <div class="row"> @@ -128,6 +128,7 @@ <h5 class="modal-title" id="sendMessageModalLabel">Read Message</h5> {{ template "footer" }} + <script> const messageModal = new bootstrap.Modal(document.getElementById("sendMessageModal")); diff --git a/ui/web/template/resetpassword.html b/ui/web/template/resetpassword.html index 380896f7b..6152b138d 100644 --- a/ui/web/template/resetpassword.html +++ b/ui/web/template/resetpassword.html @@ -4,7 +4,10 @@ {{ define "resetPassword" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Password Reset</title> + {{ template "header" }} + </head> <body class="login-body"> <div class="container mt-5 pt-5"> <div class="row p-5"> diff --git a/ui/web/template/terminal.html b/ui/web/template/terminal.html index bbc15138c..4973d4575 100644 --- a/ui/web/template/terminal.html +++ b/ui/web/template/terminal.html @@ -4,7 +4,10 @@ {{ define "remoteTerminal" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Terminal</title> + {{ template "header" }} + </head> <style> .terminal { color: #fff; diff --git a/ui/web/template/thing.html b/ui/web/template/thing.html index 11efc99dc..9a6499b61 100644 --- a/ui/web/template/thing.html +++ b/ui/web/template/thing.html @@ -4,7 +4,11 @@ {{ define "thing" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Thing</title> + {{ template "header" }} + <script src="/js/update.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -159,21 +163,23 @@ </div> </div> {{ template "footer" }} - <script> - attachEditRowListener( - { - entity: "things", - id: "{{ .Thing.ID }}", - rows: { - name:updateName, - secret: updateSecret, - tags:updateTags, - owner: updateOwner, - metadata:updateMetadata, - }, - errorDiv: "error-message", - } - ); + <script type="module"> + import { attachEditRowListener, updateName, updateSecret, updateTags,updateOwner, updateMetadata} from "/js/update.js"; + + attachEditRowListener( + { + entity: "things", + id: "{{ .Thing.ID }}", + rows: { + name:updateName, + secret: updateSecret, + tags:updateTags, + owner: updateOwner, + metadata:updateMetadata, + }, + errorDiv: "error-message", + } + ); </script> </body> </html> diff --git a/ui/web/template/thingchannels.html b/ui/web/template/thingchannels.html index 186b991e4..f937fdc5f 100644 --- a/ui/web/template/thingchannels.html +++ b/ui/web/template/thingchannels.html @@ -4,7 +4,11 @@ {{ define "thingchannels" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Thing Channels</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -227,6 +231,9 @@ <h1 class="modal-title fs-5" id="sendMesageModalLabel{{ $i }}"> function openChannelModal() { channelModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; fetchIndividualEntity({ input: "channelFilter", diff --git a/ui/web/template/things.html b/ui/web/template/things.html index dfd06a71b..b8c32e8f9 100644 --- a/ui/web/template/things.html +++ b/ui/web/template/things.html @@ -4,7 +4,12 @@ {{ define "things" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Things</title> + {{ template "header" }} + <script src="/js/forms.js" type="module"></script> + <script src="/js/validation.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -120,146 +125,118 @@ <h5 class="modal-title" id="addThingModalLabel">Add Thing</h5> Add Things </button> - <!-- Modal --> - <div - class="modal fade" - id="addThingsModal" - tabindex="-1" - role="dialog" - aria-labelledby="addThingsModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addThingsModalLabel"> - Add Things - </h5> - </div> - <div class="modal-body"> - <div id="alertBulkMessage"></div> - <form - method="post" - enctype="multipart/form-data" - id="bulkThingsForm" - > - <div class="form-group"> - <label for="thingsFile"> - Add csv file containing thing names with - metadata. The metadata field can be empty. Find - a sample csv file - <a - href="https://github.com/absmach/magistrala-ui/blob/main/samples/things.csv" - target="_blank" - > - here - </a> - </label> - <input - type="file" - class="form-control-file" - id="thingsFile" - name="thingsFile" - required - /> - <button - type="submit" - value="upload" - class="btn body-button" - > - Submit - </button> - </div> - </form> - </div> - </div> - </div> - </div> - </div> - <div class="table-responsive table-container"> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $t := .Things }} - {{ $disableButton := false }} - <tr> - <td>{{ $t.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/things/%s" $t.ID }}"> - {{ $t.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $t.Tags }}</td> - <td class="meta-col">{{ toJSON $t.Metadata }}</td> - <td class="created-col">{{ $t.CreatedAt }}</td> - <td class="text-center"> - <form action="/things/disabled" method="post"> - <input - type="hidden" - name="thingID" - id="thingID" - value="{{ $t.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - attachValidationListener({ - buttonId: "create-thing-button", - errorDivs: { - name: "nameError", - metadata: "metadataError", - tags: "tagsError", - }, - validations: { - name: validateName, - metadata: validateJSON, - tags: validateStringArray, - }, - }); - + <!-- Modal --> + <div + class="modal fade" + id="addThingsModal" + tabindex="-1" + role="dialog" + aria-labelledby="addThingsModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addThingsModalLabel">Add Things</h5> + </div> + <div class="modal-body"> + <div id="alertBulkMessage"></div> + <form method="post" enctype="multipart/form-data" id="bulkThingsForm"> + <div class="form-group"> + <label for="thingsFile"> + Add csv file containing thing names with metadata. The metadata + field can be empty. Find a sample csv file + <a + href="https://github.com/absmach/magistrala-ui/blob/main/samples/things.csv" + target="_blank" + > + here + </a> + </label> + <input + type="file" + class="form-control-file" + id="thingsFile" + name="thingsFile" + required + /> + <button type="submit" value="upload" class="btn body-button"> + Submit + </button> + </div> + </form> + </div> + </div> + </div> + </div> + </div> + <div class="table-responsive table-container"> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $t := .Things }} + {{ $disableButton := false }} + <tr> + <td>{{ $t.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/things/%s" $t.ID }}"> + {{ $t.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $t.Tags }}</td> + <td class="meta-col">{{ toJSON $t.Metadata }}</td> + <td class="created-col">{{ $t.CreatedAt }}</td> + <td class="text-center"> + <form action="/things/disabled" method="post"> + <input + type="hidden" + name="thingID" + id="thingID" + value="{{ $t.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> const thingModal = new bootstrap.Modal(document.getElementById("addThingModal")); const thingsModal = new bootstrap.Modal(document.getElementById("addThingsModal")); @@ -270,6 +247,29 @@ <h5 class="modal-title" id="addThingsModalLabel"> thingsModal.show(); } } + </script> + <script type="module"> + import { + attachValidationListener, + validateName, + validateJSON, + validateStringArray, + } from "/js/validation.js"; + import { submitCreateForm } from "/js/forms.js"; + + attachValidationListener({ + buttonId: "create-thing-button", + errorDivs: { + name: "nameError", + metadata: "metadataError", + tags: "tagsError", + }, + validations: { + name: validateName, + metadata: validateJSON, + tags: validateStringArray, + }, + }); submitCreateForm({ url: "/things", diff --git a/ui/web/template/thingusers.html b/ui/web/template/thingusers.html index 9c460b7f6..1f8db06f8 100644 --- a/ui/web/template/thingusers.html +++ b/ui/web/template/thingusers.html @@ -4,7 +4,11 @@ {{ define "thingusers" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Thing Users</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -172,6 +176,10 @@ <h1 class="modal-title fs-5" id="addUserModalLabel">Share Thing</h1> function openUserModal() { userModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + fetchIndividualEntity({ input: "userFilter", itemSelect: "infiniteScroll", diff --git a/ui/web/template/updatepassword.html b/ui/web/template/updatepassword.html index 858654b79..c30459be1 100644 --- a/ui/web/template/updatepassword.html +++ b/ui/web/template/updatepassword.html @@ -4,7 +4,11 @@ {{ define "updatePassword" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Password Update</title> + {{ template "header" }} + <script src="/js/main.js" type= "text/javascript"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> diff --git a/ui/web/template/user.html b/ui/web/template/user.html index e4ed0c3df..e3a86f14a 100644 --- a/ui/web/template/user.html +++ b/ui/web/template/user.html @@ -4,7 +4,11 @@ {{ define "user" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>User</title> + {{ template "header" }} + <script src="/js/update.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -146,21 +150,22 @@ </div> </div> {{ template "footer" }} - <script> - attachEditRowListener( - { - entity: "users", - id: "{{ .UserID }}", - rows: { - name:updateName, - identity:updateIdentity, - tags:updateTags, - metadata:updateMetadata, - }, - errorDiv: "error-message", - } - ); - </script> + <script type="module"> + import { attachEditRowListener, updateName, updateIdentity, updateTags, updateMetadata} from "/js/update.js"; + + attachEditRowListener( + { + entity: "users", + id: "{{ .UserID }}", + rows: { + name:updateName, + identity:updateIdentity, + tags:updateTags, + metadata:updateMetadata, + }, + errorDiv: "error-message", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/userchannels.html b/ui/web/template/userchannels.html index 53a20ef23..8b0739098 100644 --- a/ui/web/template/userchannels.html +++ b/ui/web/template/userchannels.html @@ -4,7 +4,11 @@ {{ define "userchannels" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>User Channels</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -473,12 +477,6 @@ <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> channelModal.show(); } - fetchIndividualEntity({ - input: "channelFilter", - itemSelect: "infiniteScroll", - item: "channels", - }); - function openTab(relation) { event.preventDefault(); var userID = '{{.UserID}}'; @@ -491,6 +489,15 @@ <h1 class="modal-title fs-5" id="addChannelModalLabel">Add Channel</h1> .catch((error) => console.error("Error:", error)); } </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + fetchIndividualEntity({ + input: "channelFilter", + itemSelect: "infiniteScroll", + item: "channels", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/usergroups.html b/ui/web/template/usergroups.html index 30979fdb5..b52153c11 100644 --- a/ui/web/template/usergroups.html +++ b/ui/web/template/usergroups.html @@ -4,7 +4,11 @@ {{ define "usergroups" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>User Groups</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -474,12 +478,6 @@ <h1 class="modal-title fs-5" id="addGroupModalLabel">Add Group</h1> groupModal.show(); } - fetchIndividualEntity({ - input: "groupFilter", - itemSelect: "infiniteScroll", - item: "groups", - }); - function openTab(relation) { event.preventDefault(); var userID = '{{.UserID}}'; @@ -492,6 +490,15 @@ <h1 class="modal-title fs-5" id="addGroupModalLabel">Add Group</h1> .catch((error) => console.error("Error:", error)); } </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + + fetchIndividualEntity({ + input: "groupFilter", + itemSelect: "infiniteScroll", + item: "groups", + }); + </script> </body> </html> {{ end }} diff --git a/ui/web/template/users.html b/ui/web/template/users.html index 6c9b1c018..ba5b180bb 100644 --- a/ui/web/template/users.html +++ b/ui/web/template/users.html @@ -4,7 +4,12 @@ {{ define "users" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>Users</title> + {{ template "header" }} + <script src="/js/forms.js" type="module"></script> + <script src="/js/validation.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -108,149 +113,119 @@ <h5 class="modal-title" id="addUserModalLabel">Add User</h5> </div> </div> - <!-- add users modal --> - <div - class="modal fade" - id="addUsersModal" - tabindex="-1" - role="dialog" - aria-labelledby="addUsersModalLabel" - aria-hidden="true" - > - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="addUsersModalLabel"> - Add Users - </h5> - </div> - <div class="modal-body"> - <div id="alertBulkMessage"></div> - <form - enctype="multipart/form-data" - id="bulkusersform" - > - <div class="form-group"> - <label for="usersFile"> - Add csv file containing users names and - passwords. Find a sample csv file - <a - href="https://github.com/absmach/magistrala-ui/blob/main/samples/users.csv" - target="_blank" - > - here - </a> - </label> - <input - type="file" - class="form-control-file" - id="usersFile" - name="usersFile" - required - /> - <button - type="submit" - value="upload" - class="btn body-button" - > - Submit - </button> - </div> - </form> - </div> - </div> - </div> - </div> - <!-- modals end --> - </div> - <div class="table-responsive table-container"> - {{ template "tableheader" . }} - <div class="itemsTable"> - <table id="itemsTable" class="table"> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">ID</th> - <th class="tags-col" scope="col">Tags</th> - <th class="meta-col" scope="col">Metadata</th> - <th class="created-col" scope="col">Created At</th> - <th class="text-center" scope="col"></th> - </tr> - </thead> - <tbody> - {{ range $i, $u := .Users }} - {{ $disableButton := false }} - <tr> - <td>{{ $u.Name }}</td> - <td> - <div class="copy-con-container"> - <a href="{{ printf "/users/%s" $u.ID }}"> - {{ $u.ID }} - </a> - <button - class="copy-icon" - onclick="copyToClipboard(this)" - > - <i class="far fa-copy"></i> - </button> - </div> - </td> - <td class="tags-col">{{ toSlice $u.Tags }}</td> - <td class="meta-col">{{ toJSON $u.Metadata }}</td> - <td class="created-col">{{ $u.CreatedAt }}</td> - <td class="text-center"> - <form action="/users/disabled" method="post"> - <input - type="hidden" - name="userID" - id="userID" - value="{{ $u.ID }}" - /> - <button - type="submit" - class="btn btn-sm" - {{ if - $disableButton - }} - disabled - {{ end }} - > - <i class="fas fa-trash-alt"></i> - </button> - </form> - </td> - </tr> - {{ end }} - </tbody> - </table> - </div> - {{ template "tablefooter" . }} - </div> - </div> - </div> - </div> - </div> - </div> - {{ template "footer" }} - <script> - attachValidationListener({ - buttonId: "create-user-button", - errorDivs: { - name: "nameError", - identity: "identityError", - secret: "secretError", - metadata: "metadataError", - tags: "tagsError", - }, - validations: { - name: validateName, - identity: validateEmail, - secret: validatePassword, - metadata: validateJSON, - tags: validateStringArray, - }, - }); - + <!-- add users modal --> + <div + class="modal fade" + id="addUsersModal" + tabindex="-1" + role="dialog" + aria-labelledby="addUsersModalLabel" + aria-hidden="true" + > + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addUsersModalLabel">Add Users</h5> + </div> + <div class="modal-body"> + <div id="alertBulkMessage"></div> + <form enctype="multipart/form-data" id="bulkusersform"> + <div class="form-group"> + <label for="usersFile"> + Add csv file containing users names and passwords. Find a sample csv + file + <a + href="https://github.com/absmach/magistrala-ui/blob/main/samples/users.csv" + target="_blank" + > + here + </a> + </label> + <input + type="file" + class="form-control-file" + id="usersFile" + name="usersFile" + required + /> + <button type="submit" value="upload" class="btn body-button"> + Submit + </button> + </div> + </form> + </div> + </div> + </div> + </div> + <!-- modals end --> + </div> + <div class="table-responsive table-container"> + {{ template "tableheader" . }} + <div class="itemsTable"> + <table id="itemsTable" class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">ID</th> + <th class="tags-col" scope="col">Tags</th> + <th class="meta-col" scope="col">Metadata</th> + <th class="created-col" scope="col">Created At</th> + <th class="text-center" scope="col"></th> + </tr> + </thead> + <tbody> + {{ range $i, $u := .Users }} + {{ $disableButton := false }} + <tr> + <td>{{ $u.Name }}</td> + <td> + <div class="copy-con-container"> + <a href="{{ printf "/users/%s" $u.ID }}"> + {{ $u.ID }} + </a> + <button class="copy-icon" onclick="copyToClipboard(this)"> + <i class="far fa-copy"></i> + </button> + </div> + </td> + <td class="tags-col">{{ toSlice $u.Tags }}</td> + <td class="meta-col">{{ toJSON $u.Metadata }}</td> + <td class="created-col">{{ $u.CreatedAt }}</td> + <td class="text-center"> + <form action="/users/disabled" method="post"> + <input + type="hidden" + name="userID" + id="userID" + value="{{ $u.ID }}" + /> + <button + type="submit" + class="btn btn-sm" + {{ if + $disableButton + }} + disabled + {{ end }} + > + <i class="fas fa-trash-alt"></i> + </button> + </form> + </td> + </tr> + {{ end }} + </tbody> + </table> + </div> + {{ template "tablefooter" . }} + </div> + </div> + </div> + </div> + </div> + </div> + {{ template "footer" }} + <script> const userModal = new bootstrap.Modal(document.getElementById("addUserModal")); const usersModal = new bootstrap.Modal(document.getElementById("addUsersModal")); @@ -261,6 +236,35 @@ <h5 class="modal-title" id="addUsersModalLabel"> usersModal.show(); } } + </script> + <script type="module"> + import { + attachValidationListener, + validateName, + validateEmail, + validatePassword, + validateJSON, + validateStringArray, + } from "/js/validation.js"; + import { submitCreateForm } from "/js/forms.js"; + + attachValidationListener({ + buttonId: "create-user-button", + errorDivs: { + name: "nameError", + identity: "identityError", + secret: "secretError", + metadata: "metadataError", + tags: "tagsError", + }, + validations: { + name: validateName, + identity: validateEmail, + secret: validatePassword, + metadata: validateJSON, + tags: validateStringArray, + }, + }); submitCreateForm({ url: "/users", diff --git a/ui/web/template/userthings.html b/ui/web/template/userthings.html index 0a3a85f1d..df0591252 100644 --- a/ui/web/template/userthings.html +++ b/ui/web/template/userthings.html @@ -4,7 +4,11 @@ {{ define "userthings" }} <!doctype html> <html lang="en"> - {{ template "header" }} + <head> + <title>User Things</title> + {{ template "header" }} + <script src="/js/infinitescroll.js" type="module"></script> + </head> <body> {{ template "navbar" . }} <div class="main-content pt-3"> @@ -175,6 +179,10 @@ <h1 class="modal-title fs-5" id="addThingModalLabel">Add Thing</h1> function openThingModal() { thingModal.show(); } + </script> + <script type="module"> + import { fetchIndividualEntity } from "/js/infinitescroll.js"; + fetchIndividualEntity({ input: "thingFilter", itemSelect: "things", From 344dd21fa3309db1ee3f2a4b4063bb741b207125 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Tue, 28 Nov 2023 17:40:48 +0300 Subject: [PATCH 09/11] update endpoints returns Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/api/endpoint.go | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/ui/api/endpoint.go b/ui/api/endpoint.go index 327bff60e..9b364dcc5 100644 --- a/ui/api/endpoint.go +++ b/ui/api/endpoint.go @@ -992,8 +992,7 @@ func createChannelEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": channelsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -1010,8 +1009,7 @@ func createChannelsEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": channelsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -1073,8 +1071,7 @@ func updateChannelEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": channelsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -1246,8 +1243,7 @@ func createGroupEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": groupsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -1264,8 +1260,7 @@ func createGroupsEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": groupsAPIEndpoint}, + code: http.StatusCreated, }, nil } } @@ -1327,8 +1322,7 @@ func updateGroupEndpoint(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": groupsAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -1536,8 +1530,7 @@ func updateBootstrap(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": bootstrapAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -1558,8 +1551,7 @@ func updateBootstrapConnections(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": bootstrapAPIEndpoint + "/" + req.id}, + code: http.StatusOK, }, nil } } @@ -1582,8 +1574,7 @@ func updateBootstrapCerts(svc ui.Service) endpoint.Endpoint { } return uiRes{ - code: http.StatusSeeOther, - headers: map[string]string{"Location": bootstrapAPIEndpoint + "/" + req.thingID}, + code: http.StatusOK, }, nil } } From 2f37a0cf1762dc3df27a1e9648cb134722bbfc26 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Wed, 29 Nov 2023 11:01:02 +0300 Subject: [PATCH 10/11] add clipboard js file Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- ui/web/template/footer.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/web/template/footer.html b/ui/web/template/footer.html index 0c6e2e736..ab6583678 100644 --- a/ui/web/template/footer.html +++ b/ui/web/template/footer.html @@ -16,4 +16,5 @@ crossorigin="anonymous" ></script> <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> + <script src="/js/clipboard.js" type="text/javascript"></script> {{ end }} From c6dbe42e192b7ba33298d7d1be7c169a9be908f8 Mon Sep 17 00:00:00 2001 From: ianmuchyri <ianmuchiri8@gmail.com> Date: Wed, 29 Nov 2023 12:00:35 +0300 Subject: [PATCH 11/11] remove unused param Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --- .prettierrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.prettierrc b/.prettierrc index 6ceed361a..b641d2f1e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,7 +9,6 @@ } ], "goTemplateBracketSpacing": true, - "useTabs": false, "printWidth": 100, "semi": true, "tabWidth": 2,