diff --git a/nodejs/models/user_ldap.js b/nodejs/models/user_ldap.js index e7afd01..44cdf3e 100644 --- a/nodejs/models/user_ldap.js +++ b/nodejs/models/user_ldap.js @@ -295,6 +295,9 @@ User.listDetail = async function(){ obj.onboardingRequired = obj.onboardingNeeds.length > 0 ? 'yes' : ''; obj.isServiceAccount = serviceAccountDNs.has(String(obj.dn).toLowerCase()) ? 'yes' : ''; obj.managerUids = obj.manager.map(dn => dnToUid.get(String(dn).toLowerCase()) || dn); + // hasSshKey is a boolean flag for the UI -- sshPublicKey may be an array, + // and Mustache's {{#sshPublicKey}}...{{/sshPublicKey}} iterates over each item. + obj.hasSshKey = obj.sshPublicKey ? 'yes' : ''; return obj; })); diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index 4d109a2..c7646a4 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -49,12 +49,9 @@ {{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}} {{#metadata.isProduction}}Prod{{/metadata.isProduction}} {{^metadata.isProduction}}Dev{{/metadata.isProduction}} - {{hostName}} -
- + {{name}} - {{slug}} {{#metadata.ip}}
IP: {{metadata.ip}}
{{/metadata.ip}} diff --git a/nodejs/views/landing.ejs b/nodejs/views/landing.ejs index 6bf8dcb..319ea3f 100644 --- a/nodejs/views/landing.ejs +++ b/nodejs/views/landing.ejs @@ -1,15 +1,6 @@ <%- include('top') %>
-
-

<%- name %> Portal

-

Everything the lab offers — what you can reach, and how to reach it.

- My Profile -
-
-

My Access

-
+ +
+
+ My Access +
+
+ +
+
+
+
+
+
+
+
+
+
+
+

Discover More

Things you don't have access to yet. Request what you need.

@@ -99,6 +124,17 @@ }); } + // Render icon as either Font Awesome class or for URL + function renderIcon(icon, kind) { + if (!icon) icon = KIND_ICONS[kind] || 'fa-solid fa-cube'; + // If it starts with http, treat it as an image URL + if (/^https?:\/\//i.test(icon)) { + return ''; + } + // Otherwise it's a Font Awesome class + return ''; + } + // "How do I actually use this?" — the question the directory exists to // answer and the one the old portal never did. Everything here is derived // from directory metadata; nothing is hardcoded per-service. @@ -142,7 +178,6 @@ function cardHtml(r, accessible) { var md = r.metadata || {}; - var icon = md.icon || KIND_ICONS[r.kind] || 'fa-solid fa-cube'; var blurb = md.tagline || r.description || 'No description provided'; var href = accessible ? linkFor(r) : null; var lines = accessible ? howTo(r) : []; @@ -166,10 +201,12 @@ + ' Request access'; } + var iconHtml = renderIcon(md.icon, r.kind); + return '
' + '
' + '
' - + '' + + iconHtml + '' + esc(r.name) + '' + '
' + '
' + esc(r.kind) @@ -235,7 +272,12 @@ } function renderAll() { - renderGrid('my-services', state.mine, true); + // Split mine into services and hosts + var myServices = state.mine.filter(function(r) { return r.kind === 'service' || r.kind === 'oauth'; }); + var myHosts = state.mine.filter(function(r) { return r.kind === 'host'; }); + + renderGrid('my-services', myServices, true); + renderGrid('my-hosts', myHosts, true); renderGrid('other-services', state.others, false); renderRequests(); renderApprovals(); diff --git a/nodejs/views/profile.ejs b/nodejs/views/profile.ejs index 0493205..e846086 100644 --- a/nodejs/views/profile.ejs +++ b/nodejs/views/profile.ejs @@ -6,13 +6,10 @@ var isOwnProfile = !location.pathname.includes('/users/'); function renderProfile(user){ - // data.photo = unescape(encodeURIComponent(data.jpegPhoto)); user.createTimestamp = moment(user.createTimestamp, "YYYYMMDDHHmmssZ").fromNow(); user.modifyTimestamp = moment(user.modifyTimestamp, "YYYYMMDDHHmmssZ").fromNow(); user.managerUids = (user.manager || []).map(app.user.dnToUid); - $.scope.user.update(user); - $.scope.passwordReset.update(user); }; async function renderUserGroups(user){ @@ -92,9 +89,6 @@ async function renderMyServices(){ try{ let res = await app.api.get('discovery/me'); - // The server already resolves this by walking the graph, so a service - // with no address of its own inherits its host's. Only fall back - // locally when it genuinely resolved to nothing. res.results.forEach(r => { r.resolvedAddress = r.resolvedAddress || (r.metadata && r.metadata.address) @@ -112,34 +106,32 @@ try{ let uid = currentUser.uid; let res = await app.api.get(`metrics/user/${uid}`); - if (res && res.results) { - const renderList = (items, id) => { - const el = document.getElementById(id); - if(!el) return; - el.innerHTML = ''; - if (!items || items.length === 0) { - el.innerHTML = '
  • No data available
  • '; - return; - } - items.forEach(item => { - el.innerHTML += `
  • - ${item.value} - ${item.score} -
  • `; - }); - }; - renderList(res.results.services, 'metrics-user-services'); + if (res && res.results && res.results.services) { + const el = document.getElementById('metrics-user-services'); + if(!el) return; + el.innerHTML = ''; + const items = res.results.services; + if (!items || items.length === 0) { + el.innerHTML = '
  • No data available
  • '; + return; + } + items.forEach(item => { + el.innerHTML += `
  • + ${item.value} + ${item.score} +
  • `; + }); } }catch(error){ - console.error('renderMyMetrics error:', error) + console.error('renderMyMetrics error:', error); + const el = document.getElementById('metrics-user-services'); + if(el) el.innerHTML = '
  • Failed to load metrics
  • '; } } - async function determinUser(){ if(location.pathname.includes('/users/')){ let uid = location.pathname.replace('/users/', ''); - return (await app.api.get('user/'+uid)).results; }else{ return await app.auth.asyncUser; @@ -148,16 +140,9 @@ function editUser(user){ user = user || currentUser; - var $profileCard = $('#userProfile'); var $editCard = $('#editProfile'); - $.scope.editProfile.update(user); - // jq-repeat's update() is trailing-edge throttled (~50ms) as of 2.1.0 -- - // wait for the throttle tick to land before sliding the updated card - // into view, or it can briefly show stale/empty data. The manager - // picker is a JS widget, not a mustache-bound input, so it also has to - // wait for update() to (re-)render its empty mount div before attaching. setTimeout(function(){ app.ui.userSelect('#edit-manager', { name: 'manager', @@ -172,8 +157,6 @@ function editUserSeccess(data){ currentUser = data.results; renderProfile(currentUser); - // Same throttle-tick wait as editUser() above -- renderProfile() calls - // $.scope.user.update()/passwordReset.update() internally. setTimeout(function(){ $('#editProfile').slideUp(); $('#userProfile').slideDown() @@ -201,6 +184,29 @@ }); } + // Password reset modal + function openPasswordResetModal(){ + app.modal.open({ + title: 'Reset Password for ' + currentUser.uid, + bodyHtml: + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ', + footer: { + buttonsHtml: '' + + '', + }, + }); + } + $(document).ready(async function(){ currentUser = await determinUser(); @@ -210,7 +216,7 @@ renderMyServices(); if(!isOwnProfile) { renderMyMetrics(); - $('#user-metrics-card').show(); + $('#tab-metrics').parent().show(); } $('#personal-group-uid-label').text(currentUser.uid); @@ -221,8 +227,6 @@ name: 'members', values: [], placeholder: 'Type a username…', }); - // API Tokens are self-service only — never shown when an admin is - // viewing someone else's profile via /users/:uid. if(isOwnProfile){ $('#own-api-tokens-section').show(); tableAJAX(); @@ -230,318 +234,275 @@ }); -