From 2bd5a123610e3d706c8b00d241d10b67456bbf14 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 23:33:36 -0500 Subject: [PATCH 1/2] Add license assignment states to user and group pages Extended user and group pages to display additional license-related fields, including 'licenseAssignmentStates' for users and several license and sync fields for groups. Updated getCippFormatting to handle and format 'licenseAssignmentStates' for display, including transforming and rendering the data as a table. --- .../identity/administration/groups/index.js | 7 +- .../identity/administration/users/index.js | 4 +- src/utils/get-cipp-formatting.js | 73 +++++++++++++------ 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index af5182224a12..4289bb9d01e6 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -287,6 +287,11 @@ const Page = () => { "mailEnabled", "securityEnabled", "visibility", + "assignedLicenses", + "licenseProcessingState.state", + "onPremisesSamAccountName", + "membershipRule", + "onPremisesSyncEnabled", ], actions: actions, }; @@ -320,11 +325,11 @@ const Page = () => { "mailNickname", "groupType", "assignedLicenses", + "licenseProcessingState.state", "visibility", "onPremisesSamAccountName", "membershipRule", "onPremisesSyncEnabled", - "userPrincipalName", ]} /> ); diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index 27148701979b..8f5d613095d1 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -50,6 +50,7 @@ const Page = () => { "onPremisesLastSyncDateTime", // OnPrem Last Sync "onPremisesDistinguishedName", // OnPrem DN "otherMails", // Alternate Email Addresses + "licenseAssignmentStates", // License Assignment States ], actions: userActions, }; @@ -85,7 +86,7 @@ const Page = () => { Endpoint: "users", manualPagination: true, $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,licenseAssignmentStates,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", $count: true, $orderby: "displayName", $top: 999, @@ -101,6 +102,7 @@ const Page = () => { "businessPhones", "proxyAddresses", "assignedLicenses", + "licenseAssignmentStates", ]} filters={filters} /> diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index f47559d34499..68521b0781df 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -249,14 +249,14 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr unit === "d" ? "day" : unit === "h" - ? "hour" - : unit === "w" - ? "week" - : unit === "m" - ? "minutes" - : unit === "y" - ? "year" - : unit; + ? "hour" + : unit === "w" + ? "week" + : unit === "m" + ? "minutes" + : unit === "y" + ? "year" + : unit; return isText ? `Every ${value} ${unitText}` : `Every ${value} ${unitText}`; } } @@ -352,7 +352,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr icon: icon, key: key, }; - }) + }), ); } else { // Handle null/undefined single element @@ -459,7 +459,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr : renderChipList( data .filter((item) => item) - .map((item) => (typeof item === "object" && item?.label ? item.label : item)) + .map((item) => (typeof item === "object" && item?.label ? item.label : item)), ); } } @@ -500,12 +500,12 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr normalized === "enabled" ? "Enabled" : normalized === "disabled" - ? "Disabled" - : normalized === "enabledforreportingbutnotenforced" || - normalized === "report-only" || - normalized === "reportonly" - ? "Report Only" - : data.charAt(0).toUpperCase() + data.slice(1); + ? "Disabled" + : normalized === "enabledforreportingbutnotenforced" || + normalized === "report-only" || + normalized === "reportonly" + ? "Report Only" + : data.charAt(0).toUpperCase() + data.slice(1); if (isText) { return label; @@ -561,8 +561,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr const accessRights = Array.isArray(data) ? data.flatMap((item) => (typeof item === "string" ? item.split(", ") : [])) : typeof data === "string" - ? data.split(", ") - : []; + ? data.split(", ") + : []; return isText ? accessRights.join(", ") : renderChipList(accessRights); } @@ -643,8 +643,37 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ? translatedLicenses.join(", ") : translatedLicenses : Array.isArray(translatedLicenses) - ? renderChipList(translatedLicenses) - : translatedLicenses; + ? renderChipList(translatedLicenses) + : translatedLicenses; + } + + // Handle license assignment states + if (cellName === "licenseAssignmentStates") { + if (!Array.isArray(data) || data.length === 0) { + return []; + } + + // Transform the array to replace skuId with translated name and remove disabledPlans + const transformedData = data.map((license) => { + const translatedLicense = getCippLicenseTranslation([license]); + const licenseName = Array.isArray(translatedLicense) + ? translatedLicense[0] + : translatedLicense; + + // Return new object with skuId replaced by License and without disabledPlans + const { skuId, disabledPlans, ...rest } = license; + return { + License: licenseName, + ...rest, + }; + }); + + // Render as a table + return isText ? ( + JSON.stringify(transformedData) + ) : ( + + ); } if (cellName === "unifiedRoles") { @@ -849,7 +878,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr }, { fallbackLocale: "en", - } + }, ); const duration = isoDuration(data); return duration.humanize("en"); @@ -904,7 +933,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return { label: item.label, }; - }) + }), ); } From a546b734ac7628f55fb249bf8a0e089e56798617 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 00:28:13 -0500 Subject: [PATCH 2/2] Add 'Reprocess License Assignments' user action Introduces a new user action for reprocessing license assignments, including confirmation text and conditional availability based on write permissions. implements #5163 --- src/components/CippComponents/CippUserActions.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 9d3e284e3678..9b79b58fd32c 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -333,7 +333,7 @@ export const useCippUserActions = () => { labelField: (option) => option?.calculatedGroupType ? `${option.displayName} (${option.calculatedGroupType})` - : option?.displayName ?? "", + : (option?.displayName ?? ""), valueField: "id", addedField: { groupType: "groupType", @@ -549,6 +549,17 @@ export const useCippUserActions = () => { "Are you sure you want to change the source of authority for [userPrincipalName]? Setting it to On-Premises Managed will take until the next sync cycle to show the change.", multiPost: false, }, + { + label: "Reprocess License Assignments", + type: "POST", + icon: , + url: "/api/ExecReprocessUserLicenses", + data: { ID: "id", userPrincipalName: "userPrincipalName" }, + confirmText: + "Are you sure you want to reprocess license assignments for [userPrincipalName]?", + multiPost: false, + condition: (row) => canWriteUser, + }, { label: "Revoke all user sessions", type: "POST",