Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
dbb8f4e
Split Admin Settings template into per-tab partials
paullizer Aug 19, 2026
36496dc
Move Latest Features to the end of Admin Settings navigation
paullizer Aug 19, 2026
f8147d5
Give Global Identities and File Sync proper Admin Settings surfaces
paullizer Aug 19, 2026
236b732
Fix classification banner preview and document Admin Settings changes
paullizer Aug 19, 2026
6663efa
Prune the admin sidebar sectionMap to its real aliases
paullizer Aug 19, 2026
23bb4d5
Fix CodeQL alerts in the Home Page Text preview and composition test
paullizer Aug 19, 2026
5884e76
Sanitize the Home Page Text preview at the sink
paullizer Aug 19, 2026
a2694c6
Run the quality gates on PRs into the IA staging branch
paullizer Aug 19, 2026
08c61b6
Merge pull request #1297 from paullizer/paullizer-admin-settings-ia-r…
paullizer Aug 19, 2026
7e3163e
Point Admin Settings cross-tab links at cards instead of tabs
paullizer Aug 19, 2026
8c85fd9
Announce Admin Settings options that require another option
paullizer Aug 19, 2026
0726aca
Merge pull request #1304 from paullizer/admin-ia-stage-a
paullizer Aug 19, 2026
4546ce7
Enforce the Admin Settings form field contract in CI
paullizer Aug 19, 2026
ae46771
Document Admin Settings IA rework status and handoff
paullizer Aug 19, 2026
2422d86
Merge pull request #1306 from paullizer/admin-ia-field-contract
paullizer Aug 19, 2026
c1bfca2
Group the Admin Settings navigation and render it from one definition
paullizer Aug 19, 2026
a809a71
Update IA rework status for stage C
paullizer Aug 19, 2026
d08d842
Merge pull request #1307 from paullizer/admin-ia-stage-c
paullizer Aug 19, 2026
02412b0
Split Governance into Feature Governance, Policies and MCP Governance
paullizer Aug 19, 2026
98b7cd7
Split Scale into Redis & Caching, Cosmos, and move Front Door to Secu…
paullizer Aug 19, 2026
08f5a99
Bump version and document the stage D card-move pattern
paullizer Aug 19, 2026
40c5fb0
Merge pull request #1309 from paullizer/admin-ia-stage-d
paullizer Aug 19, 2026
e6cd257
Stage D part 2: Data Lifecycle and Chat groups
paullizer Aug 19, 2026
87ad47f
Merge pull request #1310 from microsoft/admin-ia-stage-d2
paullizer Aug 19, 2026
b433565
Stage D part 3: dismantle General and Security
paullizer Aug 19, 2026
6c682bf
Stage D part 4: Knowledge, Workspaces and Workflow
paullizer Aug 19, 2026
ee569c9
Merge pull request #1311 from microsoft/admin-ia-stage-d3
paullizer Aug 19, 2026
a937b82
Merge pull request #1313 from microsoft/admin-ia-stage-d4
paullizer Aug 19, 2026
6343cfa
Stage D part 5: AI Models and Agents & Actions
paullizer Aug 19, 2026
434f574
Merge pull request #1314 from microsoft/admin-ia-stage-d5
paullizer Aug 19, 2026
9bbd7bd
Stage D part 6: Backup & Recovery, completing Stage D
paullizer Aug 19, 2026
17202e3
Stage E: split the mixed System Settings card, add the role roster
paullizer Aug 19, 2026
f69ceae
Merge pull request #1315 from microsoft/admin-ia-stage-d6
paullizer Aug 19, 2026
6750ca5
Merge pull request #1316 from microsoft/admin-ia-stage-e
paullizer Aug 19, 2026
1aad05e
Revert the temporary CI branch filters
paullizer Aug 19, 2026
5013d5f
Merge pull request #1318 from microsoft/admin-ia-ci-revert
paullizer Aug 19, 2026
69e5965
Merge Development into the Admin Settings staging branch
paullizer Aug 19, 2026
a36dcd9
Stage F: stop naming tabs, name the setting
paullizer Aug 19, 2026
3c7f523
Merge pull request #1320 from microsoft/admin-ia-sync-dev
paullizer Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
586 changes: 586 additions & 0 deletions application/single_app/admin_settings_nav.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from functions_content import *
from functions_documents import *
from functions_latest_features_nav import should_hide_latest_features_nav
from admin_settings_nav import ADMIN_NAV, get_landing_tab_id
from functions_search import *
from functions_settings import *
from functions_mcp_server_config import is_mcp_ui_enabled
Expand Down Expand Up @@ -598,6 +599,8 @@ def inject_settings():
app_settings=public_settings,
user_settings=user_settings,
custom_pages_nav=custom_pages_nav,
admin_nav=ADMIN_NAV,
admin_landing_tab=get_landing_tab_id(),
latest_features_current_version=VERSION,
latest_features_nav_hidden=latest_features_nav_hidden,
latest_features_nav_hidden_by_development=IS_DEVELOPMENT,
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.260.011"
VERSION = "0.260.018"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
148 changes: 148 additions & 0 deletions application/single_app/static/js/admin/admin_access_roles_roster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// admin_access_roles_roster.js
//
// Access & Roles shows every "require an Entra app role" switch in one place.
//
// The switches themselves stay on the tabs that own them, because that is where
// they make sense in context. Duplicating the real inputs here would submit
// each setting twice, so this builds a roster of mirrors instead: each row
// carries no name attribute and simply drives the canonical input.
//
// The roster is built from the page rather than from a hand-written list, so a
// new role requirement anywhere in Admin Settings appears here on its own and
// this list cannot fall out of step with reality.
//
// Roster links carry data-admin-link, which admin_card_links.js already handles
// through a delegated listener, so no wiring is needed here.

const ROLE_INPUT_SELECTOR = 'input[type="checkbox"][name^="require_member_of_"]';
const LIST_ID = 'app-role-requirements-list';
const EMPTY_ID = 'app-role-requirements-empty';
const ROSTER_CARD_ID = 'app-role-requirements-section';

/**
* Read the visible label for a control, falling back to its field name.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {string} Human readable label.
*/
function labelFor(input) {
const explicit = input.id ? document.querySelector(`label[for="${input.id}"]`) : null;
if (explicit && explicit.textContent.trim()) {
return explicit.textContent.trim();
}

const wrapping = input.closest('label');
if (wrapping && wrapping.textContent.trim()) {
return wrapping.textContent.trim();
}

return input.name;
}

/**
* Find the card a control belongs to, so the roster can link back to it.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {HTMLElement|null} The owning card, when it has an id.
*/
function owningCard(input) {
let card = input.closest('.card[id]');
while (card && card.id === ROSTER_CARD_ID) {
card = card.parentElement ? card.parentElement.closest('.card[id]') : null;
}
return card;
}

/**
* Read the heading of a card, used as the "where does this live" hint.
* @param {HTMLElement} card Owning card.
* @returns {string} Card title, or an empty string when it has none.
*/
function cardTitle(card) {
const heading = card ? card.querySelector('h5, h4, h6, .card-title') : null;
return heading ? heading.textContent.trim() : '';
}

/**
* Build one roster row: a mirror switch, its label, and a link to the setting.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {HTMLElement} The row element.
*/
function buildRow(input) {
const row = document.createElement('div');
row.className = 'd-flex flex-wrap align-items-center gap-2';
row.setAttribute('data-role-requirement-row', input.name);

const wrapper = document.createElement('div');
wrapper.className = 'form-check form-switch mb-0 flex-grow-1';

// No name attribute: only the canonical input is submitted with the form.
const mirror = document.createElement('input');
mirror.type = 'checkbox';
mirror.className = 'form-check-input';
mirror.id = `${input.name}-roster-mirror`;
mirror.checked = input.checked;
mirror.disabled = input.disabled;
mirror.setAttribute('data-role-mirror-for', input.id || input.name);
mirror.setAttribute('data-ignore-settings-change', 'true');

const label = document.createElement('label');
label.className = 'form-check-label ms-2';
label.setAttribute('for', mirror.id);
label.textContent = labelFor(input);

wrapper.append(mirror, label);
row.appendChild(wrapper);

const card = owningCard(input);
if (card) {
const title = cardTitle(card);
const link = document.createElement('a');
link.href = `#${card.id}`;
link.className = 'small text-nowrap';
link.setAttribute('data-admin-link', card.id);
link.textContent = title ? `In ${title}` : 'Go to setting';
row.appendChild(link);
}

// Two-way: the mirror drives the real input, and the real input keeps the
// mirror honest when it is changed on its own tab.
mirror.addEventListener('change', () => {
if (input.checked === mirror.checked) {
return;
}
input.checked = mirror.checked;
input.dispatchEvent(new Event('change', { bubbles: true }));
});

input.addEventListener('change', () => {
mirror.checked = input.checked;
mirror.disabled = input.disabled;
});

return row;
}

/**
* Populate the Access & Roles roster from the role switches on the page.
*/
export function initAdminAccessRolesRoster() {
const list = document.getElementById(LIST_ID);
if (!list) {
return;
}

const empty = document.getElementById(EMPTY_ID);
const inputs = Array.from(document.querySelectorAll(ROLE_INPUT_SELECTOR))
.filter(input => !list.contains(input));

list.replaceChildren();
inputs
.map(input => ({ input, label: labelFor(input) }))
.sort((a, b) => a.label.localeCompare(b.label))
.forEach(({ input }) => list.appendChild(buildRow(input)));

if (empty) {
empty.classList.toggle('d-none', inputs.length > 0);
}
}

document.addEventListener('DOMContentLoaded', initAdminAccessRolesRoster);
103 changes: 103 additions & 0 deletions application/single_app/static/js/admin/admin_card_links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// admin_card_links.js
// Resolves links that point at an Admin Settings card rather than at a tab.
//
// Cross-tab links used to name a tab button directly, for example
// switchTab(event, 'workspaces-tab'). That couples the link to a tab id, so a
// tab rename or an information-architecture change silently breaks the link:
// the button no longer exists, no pane is activated, and the URL hash is left
// pointing at nothing.
//
// A link declares the card it wants instead:
//
// <a href="#redis-cache-section" data-admin-link="redis-cache-section">
//
// The owning tab is resolved from the DOM at click time, so card ids are the
// only contract. Cards can move between tabs, and tabs can be renamed or
// regrouped, without touching a single link.

const HIGHLIGHT_CLASS = 'admin-card-link-target';
const HIGHLIGHT_MS = 1600;

/**
* Find the tab pane that contains a card.
* @param {string} cardId Element id of the target card.
* @returns {{card: HTMLElement, paneId: string|null}|null}
*/
export function resolveAdminCard(cardId) {
const card = document.getElementById(cardId);
if (!card) {
return null;
}

const pane = card.closest('.tab-pane');
return { card, paneId: pane ? pane.id : null };
}

/**
* Activate the tab owning a card, scroll to it, and highlight it briefly.
* @param {string} cardId Element id of the target card.
* @returns {boolean} Whether the card was found.
*/
export function openAdminCard(cardId) {
const resolved = resolveAdminCard(cardId);
if (!resolved) {
console.warn(`openAdminCard: no card with id "${cardId}"`);
return false;
}

const { card, paneId } = resolved;

if (paneId && typeof window.showAdminTab === 'function') {
window.showAdminTab(paneId);
syncSidebarActiveState(paneId);
}

// Let the pane become visible before measuring scroll position.
window.setTimeout(() => {
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
card.classList.add(HIGHLIGHT_CLASS);
window.setTimeout(() => card.classList.remove(HIGHLIGHT_CLASS), HIGHLIGHT_MS);
}, 120);

return true;
}

/**
* Mirror the activated tab in the sidebar, when the sidebar layout is in use.
* @param {string} paneId Tab pane id that was activated.
*/
function syncSidebarActiveState(paneId) {
const navLink = document.querySelector(`.admin-nav-tab[data-tab="${paneId}"]`);
if (!navLink) {
return;
}

document.querySelectorAll('.admin-nav-tab, .admin-nav-section').forEach((link) => {
link.classList.remove('active');
});
navLink.classList.add('active');
}

/**
* Delegate clicks so links added after load keep working.
*/
export function initAdminCardLinks() {
document.addEventListener('click', (event) => {
const link = event.target.closest('[data-admin-link]');
if (!link) {
return;
}

const cardId = link.getAttribute('data-admin-link');
if (!cardId) {
return;
}

event.preventDefault();
openAdminCard(cardId);
});
}

window.openAdminCard = openAdminCard;

document.addEventListener('DOMContentLoaded', initAdminCardLinks);
17 changes: 2 additions & 15 deletions application/single_app/static/js/admin/admin_data_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@ function bindEvents() {
pendingDataManagementBackupDelete = null;
});
elements.dataManagementConfirmBackupDeleteBtn?.addEventListener("click", deleteDataManagementBackup);
elements.dataManagementKeyVaultLink?.addEventListener("click", openKeyVaultSettings);
// The Key Vault link carries data-admin-link, so admin_card_links.js
// resolves the owning tab from the DOM and scrolls to the card.
elements.dataManagementCosmosEditorOpenDangerBtn?.addEventListener("click", showCosmosEditorDangerModal);
elements.datamanagementcosmoseditordangeraccept?.addEventListener("change", updateCosmosEditorDangerAcceptState);
elements.dataManagementCosmosEditorAcceptDangerBtn?.addEventListener("click", acceptCosmosEditorDanger);
Expand Down Expand Up @@ -909,20 +910,6 @@ function setKeyStorageAlert(variant, iconClass, title, message, linkText) {
setText(elements.dataManagementKeyVaultLink, linkText);
}

function openKeyVaultSettings(event) {
event?.preventDefault();
const securityTabButton = document.getElementById("security-tab");
if (securityTabButton && window.bootstrap?.Tab) {
window.bootstrap.Tab.getOrCreateInstance(securityTabButton).show();
} else if (securityTabButton) {
securityTabButton.click();
}
window.location.hash = "security";
window.setTimeout(() => {
document.getElementById("keyvault-section")?.scrollIntoView({ block: "start", behavior: "smooth" });
}, 100);
}

function collectSettings() {
const backupStorageAuthenticationType = getValue(elements.datamanagementstorageauth) || backupStorageAuthManagedIdentity;
return {
Expand Down
Loading
Loading