-
Select a group
-
+
+
Select a group
+
+
+
@@ -367,9 +381,24 @@
const OVERVIEW_API = "/api/admin/access-overview";
const GROUPS_API = "/api/admin/groups";
const GRANTS_API = "/api/admin/grants";
+// Server-injected so the sidebar can derive a friendly display name from
+// google-sync groups whose `name` is the raw Workspace email — same trick
+// /admin/groups uses; keeping the surface identical here.
+const GOOGLE_GROUP_PREFIX = {{ config.AGNES_GOOGLE_GROUP_PREFIX | tojson }};
function esc(s) { const d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; }
+function deriveDisplayName(fullEmail) {
+ if (!fullEmail) return "";
+ const local = String(fullEmail).split("@")[0] || String(fullEmail);
+ const px = (GOOGLE_GROUP_PREFIX || "").toLowerCase();
+ let s = local;
+ if (px && s.toLowerCase().startsWith(px)) s = s.slice(px.length);
+ s = s.replace(/^[_\-\s]+/, "");
+ if (!s) return local;
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
function toast(msg, kind = "") {
const el = document.createElement("div");
el.className = "toast " + kind;
@@ -432,12 +461,15 @@ async function selectGroup(gid) {
function renderDetail() {
const title = document.getElementById("detail-title");
+ const mapped = document.getElementById("detail-mapped");
const sub = document.getElementById("detail-sub");
const empty = document.getElementById("detail-empty");
const resourcesPane = document.querySelector('[data-pane="resources"]');
if (!state.activeGroupId) {
title.textContent = "Select a group";
+ mapped.style.display = "none";
+ mapped.textContent = "";
sub.textContent = "";
resourcesPane.style.display = "none";
empty.style.display = "block";
@@ -447,7 +479,32 @@ function renderDetail() {
resourcesPane.style.display = "block";
const group = state.groups.find(g => g.id === state.activeGroupId);
- title.textContent = group ? group.name : "Group";
+ if (group) {
+ // Mirror the sidebar's title rules: mapped_email present → big name
+ // stays canonical, email goes to the subtitle line. Plain google-sync
+ // group → derive a friendly name and put the raw email below.
+ let bigName = group.name;
+ let subtitleText = "";
+ if (group.mapped_email) {
+ subtitleText = group.mapped_email;
+ } else if (group.is_google_managed) {
+ bigName = deriveDisplayName(group.name);
+ subtitleText = group.name;
+ }
+ const origin = group.origin || (group.is_system ? "system" : "custom");
+ title.innerHTML = `
${esc(bigName)}${esc(origin.replace("_"," "))}`;
+ if (subtitleText) {
+ mapped.textContent = subtitleText;
+ mapped.style.display = "block";
+ } else {
+ mapped.style.display = "none";
+ mapped.textContent = "";
+ }
+ } else {
+ title.textContent = "Group";
+ mapped.style.display = "none";
+ mapped.textContent = "";
+ }
const grantedCount = state.grants.filter(g => g.group_id === state.activeGroupId).length;
sub.textContent = `${grantedCount} resource${grantedCount === 1 ? "" : "s"} granted`;
@@ -467,10 +524,29 @@ function renderGroups() {
for (const g of state.groups) {
const li = document.createElement("li");
li.className = "group-item"
- + (state.activeGroupId === g.id ? " is-active" : "")
- + (g.is_system ? " is-system" : "");
+ + (state.activeGroupId === g.id ? " is-active" : "");
li.dataset.id = g.id;
- const sysTag = g.is_system ? '
system' : '';
+ // Origin pill — single chip mirroring /admin/groups treatment. Mapped
+ // Admin/Everyone report origin='google_sync' so the chip color matches
+ // their actual source of truth (Workspace), not the seed mechanism.
+ const origin = g.origin || (g.is_system ? "system" : "custom");
+ const originPill = `
${esc(origin.replace("_"," "))}`;
+ // Big-title / subtitle rules — same logic as the /admin/groups list:
+ // - mapped_email present → big = canonical name, subtitle = mapped_email
+ // - google_managed user-created group → big = derived friendly name,
+ // subtitle = full Workspace email stored as `name`
+ // - everything else → big = name, subtitle = description (or none)
+ let bigName, subtitle;
+ if (g.mapped_email) {
+ bigName = esc(g.name);
+ subtitle = `
${esc(g.mapped_email)}`;
+ } else if (g.is_google_managed) {
+ bigName = esc(deriveDisplayName(g.name));
+ subtitle = `
${esc(g.name)}`;
+ } else {
+ bigName = esc(g.name);
+ subtitle = g.description ? `
${esc(g.description)}` : "";
+ }
// Compute live from state.grants — g.grant_count is a server-side
// snapshot from /access-overview that goes stale as soon as the user
// toggles a checkbox; reading it here would clobber refreshCounts()
@@ -479,8 +555,8 @@ function renderGroups() {
li.innerHTML = `
- ${esc(g.name)}${sysTag}
- ${g.description ? `${esc(g.description)}` : ""}
+ ${bigName}${originPill}
+ ${subtitle}
${liveCount}
`;
diff --git a/app/web/templates/admin_group_detail.html b/app/web/templates/admin_group_detail.html
index 9fe10ff..3fa2699 100644
--- a/app/web/templates/admin_group_detail.html
+++ b/app/web/templates/admin_group_detail.html
@@ -42,7 +42,7 @@
vertical-align: middle; margin-left: 8px;
}
.origin-system { background: #fef3c7; color: #92400e; }
- .origin-admin { background: #ede9fe; color: #6d28d9; }
+ .origin-custom { background: #ede9fe; color: #6d28d9; }
.origin-google_sync { background: #dcfce7; color: #166534; }
.gd-section {
@@ -154,19 +154,31 @@
data-group-name="{{ target_group.name }}"
data-is-system="{{ 'true' if target_group.is_system else 'false' }}"
data-is-google-managed="{{ 'true' if target_group.is_google_managed else 'false' }}"
+ data-mapped-email="{{ target_group.mapped_email or '' }}"
data-google-prefix="{{ config.AGNES_GOOGLE_GROUP_PREFIX }}">