diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index fe757d0..c4a5ea0 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "t42-sso-manager", - "version": "1.7.0", + "version": "1.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "t42-sso-manager", - "version": "1.7.0", + "version": "1.8.3", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", @@ -14,7 +14,7 @@ "@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/directory-schema": "^1.0.0", - "@simpleworkjs/frontend": "^0.2.6", + "@simpleworkjs/frontend": "^0.2.7", "@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/orm": "^0.2.8", "bcrypt": "^6.0.0", @@ -1280,9 +1280,9 @@ } }, "node_modules/@simpleworkjs/frontend": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@simpleworkjs/frontend/-/frontend-0.2.6.tgz", - "integrity": "sha512-2uqvEjxyZ2LE+sfhP6rJcEMmqdViazJ3ZkitWJXInPMWF6DiEZuP5MYqBqJvfDko63CCHEt1/ChFQd7Ry85Pzg==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@simpleworkjs/frontend/-/frontend-0.2.7.tgz", + "integrity": "sha512-s5oBc9dKLjd1bVhOQWR6+97faqQsbVKi0QYn5sNqOP6pGkUYUg2mY88ruHHg4Fp710owrzO/F3of/7tteFiGCw==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/nodejs/package.json b/nodejs/package.json index e0c929e..4cbbd55 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -26,7 +26,7 @@ "@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/directory-schema": "^1.0.0", - "@simpleworkjs/frontend": "^0.2.6", + "@simpleworkjs/frontend": "^0.2.7", "@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/orm": "^0.2.8", "bcrypt": "^6.0.0", diff --git a/nodejs/public/css/styles.css b/nodejs/public/css/styles.css index 1640742..7528f11 100755 --- a/nodejs/public/css/styles.css +++ b/nodejs/public/css/styles.css @@ -7,6 +7,12 @@ body { display: flex; flex-direction: column; min-height: 100vh; + /* Height of the fixed navbar (plus the update banner, while shown -- + see top.ejs's showUpdateBanner/dismissUpdateBanner). Lets an in-page + sticky element offset itself below both fixed elements via + `top: var(--sw-content-offset)` instead of colliding with them at the + viewport's true top:0. */ + --sw-content-offset: 4.5rem; } #spa-shell { diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 3d4398d..88f4290 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -584,10 +584,31 @@ app.util = (function(app){ document.body.removeChild(element); } + // Scroll a just-added/-edited element into view and flash its + // background, so the user's eye lands on the row that changed instead of + // it silently appearing/updating somewhere off-screen. Takes a jQuery + // object or a raw DOM node (e.g. jq-repeat's `item.__jq_$el`). + function revealItem(el){ + var node = el && el.jquery ? el[0] : el; + if (!node) return; + if (typeof node.scrollIntoView === 'function') { + node.scrollIntoView({behavior: 'smooth', block: 'center'}); + } + var prevTransition = node.style.transition; + var prevBg = node.style.backgroundColor; + node.style.transition = 'background-color 1.5s ease'; + node.style.backgroundColor = 'var(--bs-success-bg-subtle, #d1e7dd)'; + setTimeout(function(){ + node.style.backgroundColor = prevBg; + setTimeout(function(){ node.style.transition = prevTransition; }, 1500); + }, 300); + } + return { downloadFile: downloadFile, getUrlParameter: getUrlParameter, escapeHtml: escapeHtml, + revealItem: revealItem, } })(app); diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index c2fd2dc..6d49cb2 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -29,31 +29,25 @@
| Kind | -Name | -Env | -Host | +Resource | IP / Address | Actions |
|---|---|---|---|---|---|---|
| + | ||||||
| {{{indentHtml}}} {{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}} - | -
+ {{#metadata.isProduction}}Prod{{/metadata.isProduction}}
+ {{^metadata.isProduction}}Dev{{/metadata.isProduction}}
+ {{hostName}}
+ {{name}} - {{slug}} + {{slug}} |
- - {{#metadata.isProduction}}Prod{{/metadata.isProduction}} - {{^metadata.isProduction}}Dev{{/metadata.isProduction}} - | -{{hostName}} |
{{#metadata.ip}} IP: {{metadata.ip}} {{/metadata.ip}}
{{#metadata.address}}URL: {{metadata.address}} {{/metadata.address}}
@@ -259,7 +253,8 @@
`;
var groupsTabHtml = `
-
+
+
@@ -606,7 +601,94 @@
// destroyed and recreated runs asynchronously -- populating it
// synchronously right after open() (as this function is called) would race
// that and silently lose the pushed data on the second and later opens.
- function refreshGroupsUI(resourceId) {
+ // uid from a member/owner DN, e.g. "cn=jdoe,ou=people,..." -> "jdoe".
+ function uidFromDn(dn) {
+ const m = dn.match(/cn=[a-zA-Z0-9_\-@.]+/);
+ return m ? m[0].replace('cn=', '') : dn;
+ }
+
+ var directoryUserCache = null;
+ async function loadDirectoryUsers() {
+ if (!directoryUserCache) {
+ const data = await app.user.list();
+ directoryUserCache = data.results;
+ }
+ return directoryUserCache;
+ }
+
+ // Renders one associated group's member/owner management block: a
+ // collapsible member list (remove button per member) plus an "add member"
+ // dropdown of users not already in the group. Fetches the live LDAP group
+ // detail (member/owner DN arrays) rather than relying on `allGroups`, which
+ // only holds the resource<->group association record, not membership.
+ async function renderGroupMembership(g, resourceId) {
+ const $li = $('');
+ const $head = $('');
+ const $span = $('');
+ $span.append('');
+ $('').text(g.groupCn).appendTo($span);
+ $('').text(g.accessLevel).appendTo($span);
+ $head.append($span);
+
+ const collapseId = 'group-members-' + g.id;
+ const $actions = $('');
+ $('')
+ .attr('data-bs-target', '#' + collapseId)
+ .appendTo($actions);
+ $('')
+ .on('click', function(){ removeGroup(g.id); })
+ .appendTo($actions);
+ $head.append($actions);
+ $li.append($head);
+
+ const $collapse = $('').attr('id', collapseId);
+ $li.append($collapse);
+
+ let detail, userlist;
+ try {
+ [detail, userlist] = await Promise.all([
+ app.group.get(g.groupCn).then(d => d.results),
+ loadDirectoryUsers(),
+ ]);
+ } catch (err) {
+ console.error(err);
+ $collapse.append(' Failed to load group membership. '); + return $li; + } + + const memberDns = Array.isArray(detail.member) ? detail.member : (detail.member ? [detail.member] : []); + const $memberList = $('
-
+
@@ -173,7 +172,7 @@
- |