diff --git a/internal/data/events.go b/internal/data/events.go index 75643d0e..55c17a96 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -43,7 +43,7 @@ type ( AclChangesFunc func(TargettedEvent[acls.Acl], int) GroupChangesFunc func(TargettedEvent[[]string], int) - ClusterHealthFunc func(state string, dead int) + ClusterHealthFunc func(state string, serverID int) ) var ( @@ -210,6 +210,7 @@ func checkClusterHealth() { select { case <-etcdServer.Server.LeaderChangedNotify(): + execWatchers(clusterHealthWatchers, "changed", 0) leader := etcdServer.Server.Leader() if leader == 0 { @@ -219,7 +220,7 @@ func checkClusterHealth() { } if leader != 0 { - execWatchers(clusterHealthWatchers, "healthy", 0) + notfyHealthy() } else { execWatchers(clusterHealthWatchers, "dead", 0) } @@ -228,3 +229,11 @@ func checkClusterHealth() { } } + +func notfyHealthy() { + if etcdServer.Server.IsLearner() { + execWatchers(clusterHealthWatchers, "learner", 0) + } else { + execWatchers(clusterHealthWatchers, "healthy", 0) + } +} diff --git a/ui/src/js/clustering.js b/ui/src/js/clustering.js new file mode 100755 index 00000000..21c4c9c0 --- /dev/null +++ b/ui/src/js/clustering.js @@ -0,0 +1,200 @@ + + +// Call the dataTables jQuery plugin +function getIdSelections(table) { + return $.map(table.bootstrapTable('getSelections'), function (row) { + return row.internal_ip + }) +} + +function responseHandler(res) { + $.each(res.rows, function (i, row) { + row.state = $.inArray(row.internal_ip, selections) !== -1 + }) + return res +} + +function ownersFormatter(values, row) { + let a = document.createElement('a') + a.href = '/management/users/?username=' + encodeURIComponent(row.owner) + a.innerText = row.owner + + return a.outerHTML +} + +function lockedFormatter(value) { + let p = document.createElement('p') + if (value === true) { + p.className = "badge badge-danger" + } + p.innerText = value + return p.outerHTML +} + + +$(function () { + let table = createTable('#devicesTable', [ + { + field: 'state', + checkbox: true, + align: 'center', + escape: "true" + }, { + title: 'Owner', + field: 'owner', + align: 'center', + sortable: true, + formatter: ownersFormatter + }, { + field: 'active', + title: 'Active', + sortable: true, + align: 'center', + escape: "true" + }, { + field: 'is_locked', + title: 'Locked', + sortable: true, + align: 'center', + formatter: lockedFormatter + }, { + field: 'internal_ip', + title: 'Address', + sortable: true, + align: 'center', + escape: "true" + }, { + field: 'public_key', + title: 'Public Key', + sortable: true, + align: 'center', + escape: "true" + }, { + field: 'last_endpoint', + title: 'Last Endpoint Address', + sortable: true, + align: 'center', + escape: "true" + } + ]) + + var $remove = $('#remove') + var $lock = $('#lock') + var $unlock = $('#unlock') + + + table.on('check.bs.table uncheck.bs.table ' + + 'check-all.bs.table uncheck-all.bs.table', + function () { + let enableModifications = !table.bootstrapTable('getSelections').length; + + $("#removeStart").prop('disabled', enableModifications) + $lock.prop('disabled', enableModifications) + $unlock.prop('disabled', enableModifications) + + // save your data, here just save the current page + selections = getIdSelections(table) + // push or splice the selections if you want to save all data selections + }) + $lock.on("click", function () { + var ids = getIdSelections(table) + action(ids, "lock", table) + }) + + $unlock.on("click", function () { + var ids = getIdSelections(table) + action(ids, "unlock", table) + }) + + $remove.on("click", function () { + var ids = getIdSelections(table) + table.bootstrapTable('remove', { + field: 'internal_ip', + values: ids + }) + + + fetch("/management/devices/data", { + method: 'DELETE', + mode: 'same-origin', + cache: 'no-cache', + credentials: 'same-origin', + redirect: 'follow', + headers: { + 'Content-Type': 'application/json', + 'WAG-CSRF': $("#csrf_token").val() + }, + body: JSON.stringify(ids) + }).then((response) => { + if (response.status == 200) { + table.bootstrapTable('refresh') + $("#issue").hide() + return + } + + response.text().then(txt => { + + $("#issue").text(txt) + $("#issue").show() + }) + }) + }) + + $('#clearFilter').on("click", function () { + table.bootstrapTable('filterBy', {}) + $('#clearFilter').hide() + }) + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.toString().length > 0) { + $('#clearFilter').show() + + let filter = {} + + if (urlParams.has('owner')) { + filter.owner = urlParams.get('owner') + } + + if (urlParams.has('is_locked')) { + filter.is_locked = urlParams.get('is_locked') == "true" + } + + if (urlParams.has('active')) { + filter.active = urlParams.get('active') == "true" + } + + table.bootstrapTable('filterBy', filter) + } + +}); + +function action(onDevices, action, table) { + let data = { + "action": action, + "addresses": onDevices, + } + + fetch("/management/devices/data", { + method: 'PUT', + mode: 'same-origin', + cache: 'no-cache', + credentials: 'same-origin', + redirect: 'follow', + headers: { + 'Content-Type': 'application/json', + 'WAG-CSRF': $("#csrf_token").val() + }, + body: JSON.stringify(data) + }).then((response) => { + if (response.status == 200) { + table.bootstrapTable('refresh') + $("#issue").hide() + return + } + + response.text().then(txt => { + $("#issue").text(txt) + $("#issue").show() + }) + }) +} \ No newline at end of file diff --git a/ui/src/scss/_utilities.scss b/ui/src/scss/_utilities.scss index bf50ba20..f94e7692 100755 --- a/ui/src/scss/_utilities.scss +++ b/ui/src/scss/_utilities.scss @@ -5,3 +5,33 @@ @import "utilities/border.scss"; @import "utilities/progress.scss"; @import "utilities/rotate.scss"; + +.status { + &.open:before { + background-color: #94E185; + border-color: #78D965; + box-shadow: 0px 0px 4px 1px #94E185; + } + + &.in-progress:before { + background-color: #FFC182; + border-color: #FFB161; + box-shadow: 0px 0px 4px 1px #FFC182; + } + + &.dead:before { + background-color: #C9404D; + border-color: #C42C3B; + box-shadow: 0px 0px 4px 1px #C9404D; + } + + &:before { + content: ' '; + display: inline-block; + width: 7px; + height: 7px; + margin-right: 10px; + border: 1px solid #000; + border-radius: 7px; + } +} \ No newline at end of file diff --git a/ui/statemanager.go b/ui/statemanager.go new file mode 100644 index 00000000..62df0417 --- /dev/null +++ b/ui/statemanager.go @@ -0,0 +1,13 @@ +package ui + +import "github.com/NHAS/wag/internal/data" + +var ( + clusterState string + serverID string +) + +func watchClusterHealth(state string, _ int) { + clusterState = state + serverID = data.GetServerID() +} diff --git a/ui/structs.go b/ui/structs.go index 3aa642f2..83311ce9 100644 --- a/ui/structs.go +++ b/ui/structs.go @@ -2,10 +2,12 @@ package ui type Page struct { Update - Description string - Title string - User string - WagVersion string + Description string + Title string + User string + WagVersion string + ClusterState string + ServerID string } type Dashboard struct { diff --git a/ui/templates/management/clustering.html b/ui/templates/management/clustering.html new file mode 100755 index 00000000..e493151c --- /dev/null +++ b/ui/templates/management/clustering.html @@ -0,0 +1,38 @@ +{{define "Content"}} + + + + +
+ Wag ETCd cluster nodes +
+