Skip to content

Commit 8dfa4af

Browse files
sregan1claude
andcommitted
Add admin user filters and increase profile photo sizes
User filters (web part property pane — User Filters group): - Exclude accounts: comma-separated patterns matched against display name, UPN, and email. Use for conference rooms, service accounts, noreply@, etc. - Only show tenant users: auto-detects the site's email domain from the current user's login and hides anyone with a different domain (gmail, hotmail, etc.) - Hide guest accounts: removes Azure AD Guest users entirely (they are still shown with a badge when this is off) - Hide disabled accounts: removes blocked sign-in accounts entirely Filtering applied inside GraphService._applyUserFilters() after all data sources, so excluded users don't appear in stats, org tree, or directory. Changing any filter property re-initialises the service. Photo size increases for more prominence: - Directory card avatars: 48px → 64px + soft drop shadow - Directory list avatars: 32px → 40px + soft shadow - Org chart node photos/initials: 62px → 78px + drop shadow - Org chart node card height: 185px → 200px (to fit larger photo) - Compact card photos: 40px → 52px; card height 140px → 158px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4666fa1 commit 8dfa4af

5 files changed

Lines changed: 149 additions & 26 deletions

File tree

src/services/GraphService.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,19 @@ const BATCH_SIZE = 500;
3232

3333
export type DataSource = 'auto' | 'graph' | 'search';
3434

35+
export interface IUserFilterOptions {
36+
tenantDomain?: string; // only show users whose email domain matches (e.g. 'contoso.com')
37+
excludedPatterns?: string[]; // lower-case substrings — hide any user whose name/UPN/mail contains one
38+
hideGuestUsers?: boolean; // hide userType === 'Guest'
39+
hideDisabledAccounts?: boolean; // hide accountEnabled === false
40+
}
41+
3542
export class GraphService {
3643
private _client: SPHttpClient;
3744
private _graphClient: MSGraphClientV3 | undefined;
3845
private _webUrl: string;
3946
private _dataSource: DataSource;
47+
private _filterOptions: IUserFilterOptions;
4048
private _photoCache: Map<string, string | null> = new Map();
4149
private _allUsersCache: IGraphUser[] | null = null;
4250
private _childrenMap: Map<string, IGraphUser[]> = new Map();
@@ -48,11 +56,18 @@ export class GraphService {
4856
private _presenceExpiry = 0;
4957
private readonly _PRESENCE_TTL = 60_000;
5058

51-
constructor(client: SPHttpClient, webUrl: string, graphClient?: MSGraphClientV3, dataSource: DataSource = 'auto') {
52-
this._client = client;
53-
this._webUrl = webUrl.replace(/\/$/, '');
54-
this._graphClient = graphClient;
55-
this._dataSource = dataSource;
59+
constructor(
60+
client: SPHttpClient,
61+
webUrl: string,
62+
graphClient?: MSGraphClientV3,
63+
dataSource: DataSource = 'auto',
64+
filterOptions: IUserFilterOptions = {}
65+
) {
66+
this._client = client;
67+
this._webUrl = webUrl.replace(/\/$/, '');
68+
this._graphClient = graphClient;
69+
this._dataSource = dataSource;
70+
this._filterOptions = filterOptions;
5671
}
5772

5873
/* ── Public API ──────────────────────────────────────────────────── */
@@ -91,7 +106,7 @@ export class GraphService {
91106
try {
92107
const users = await this._fetchAllUsersFromGraph();
93108
if (users.length === 0) throw new Error('Graph API returned 0 users');
94-
return users;
109+
return this._applyUserFilters(users);
95110
} catch (err) {
96111
if (this._dataSource === 'graph') throw err;
97112
// 'auto' mode: fall back to SharePoint Search
@@ -102,7 +117,32 @@ export class GraphService {
102117

103118
// SharePoint Search path (primary or fallback)
104119
const users = await this._fetchAllUsers();
105-
return this._supplementUnlicensedReports(users);
120+
return this._applyUserFilters(await this._supplementUnlicensedReports(users));
121+
}
122+
123+
private _applyUserFilters(users: IGraphUser[]): IGraphUser[] {
124+
const { tenantDomain, excludedPatterns, hideGuestUsers, hideDisabledAccounts } = this._filterOptions;
125+
if (!tenantDomain && (!excludedPatterns || excludedPatterns.length === 0) && !hideGuestUsers && !hideDisabledAccounts) {
126+
return users;
127+
}
128+
return users.filter(user => {
129+
if (hideDisabledAccounts && user.accountEnabled === false) return false;
130+
if (hideGuestUsers && user.userType === 'Guest') return false;
131+
if (tenantDomain) {
132+
const emailDomain = ((user.mail || user.id || '').split('@')[1] || '').toLowerCase();
133+
if (emailDomain && emailDomain !== tenantDomain) return false;
134+
}
135+
if (excludedPatterns && excludedPatterns.length > 0) {
136+
const haystack = [
137+
user.displayName || '',
138+
user.id || '',
139+
user.mail || '',
140+
user.userPrincipalName || '',
141+
].join('\0').toLowerCase();
142+
if (excludedPatterns.some(p => haystack.includes(p))) return false;
143+
}
144+
return true;
145+
});
106146
}
107147

108148
public async getUserPhoto(userId: string): Promise<string | null> {

src/webparts/smartOrgChart/SmartOrgChartWebPart.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export interface ISmartOrgChartWebPartProps {
3434
enableUserFilter: boolean;
3535
// Data
3636
dataSource: 'auto' | 'graph' | 'search';
37+
// User filters
38+
excludedAccounts: string;
39+
restrictToTenantDomain: boolean;
40+
hideGuestUsers: boolean;
41+
hideDisabledAccounts: boolean;
3742
}
3843

3944
export default class SmartOrgChartWebPart extends BaseClientSideWebPart<ISmartOrgChartWebPartProps> {
@@ -53,6 +58,11 @@ export default class SmartOrgChartWebPart extends BaseClientSideWebPart<ISmartOr
5358
if (p.logoUrl === undefined) p.logoUrl = '';
5459
// Data source default
5560
if (p.dataSource === undefined) p.dataSource = 'auto';
61+
// User filter defaults
62+
if (p.excludedAccounts === undefined) p.excludedAccounts = '';
63+
if (p.restrictToTenantDomain === undefined) p.restrictToTenantDomain = false;
64+
if (p.hideGuestUsers === undefined) p.hideGuestUsers = false;
65+
if (p.hideDisabledAccounts === undefined) p.hideDisabledAccounts = false;
5666
return super.onInit();
5767
}
5868

@@ -74,6 +84,10 @@ export default class SmartOrgChartWebPart extends BaseClientSideWebPart<ISmartOr
7484
enableDeptFilter: this.properties.enableDeptFilter !== false,
7585
enableUserFilter: this.properties.enableUserFilter !== false,
7686
dataSource: this.properties.dataSource || 'auto',
87+
excludedAccounts: this.properties.excludedAccounts || '',
88+
restrictToTenantDomain: this.properties.restrictToTenantDomain || false,
89+
hideGuestUsers: this.properties.hideGuestUsers || false,
90+
hideDisabledAccounts: this.properties.hideDisabledAccounts || false,
7791
onSettingsSaved: (newProps: Partial<ISmartOrgChartWebPartProps>) => {
7892
Object.assign(this.properties, newProps);
7993
}
@@ -179,6 +193,33 @@ export default class SmartOrgChartWebPart extends BaseClientSideWebPart<ISmartOr
179193
})
180194
]
181195
},
196+
{
197+
groupName: 'User Filters',
198+
groupFields: [
199+
PropertyPaneTextField('excludedAccounts', {
200+
label: 'Exclude accounts',
201+
placeholder: 'conf-room, noreply, admin@, Service Account',
202+
description: 'Comma-separated words or patterns (case-insensitive). Any user whose display name, email, or UPN contains one of these will be hidden from all views.',
203+
multiline: true,
204+
rows: 3
205+
}),
206+
PropertyPaneToggle('restrictToTenantDomain', {
207+
label: 'Only show tenant users',
208+
onText: 'On — hides accounts with external email domains (e.g. gmail.com, hotmail.com)',
209+
offText: 'Off — all users shown regardless of email domain'
210+
}),
211+
PropertyPaneToggle('hideGuestUsers', {
212+
label: 'Hide Azure AD guest accounts',
213+
onText: 'On — guest accounts hidden',
214+
offText: 'Off — guest accounts visible (shown with Guest badge)'
215+
}),
216+
PropertyPaneToggle('hideDisabledAccounts', {
217+
label: 'Hide disabled accounts',
218+
onText: 'On — blocked sign-in accounts hidden',
219+
offText: 'Off — disabled accounts visible (shown with Disabled badge)'
220+
}),
221+
]
222+
},
182223
{
183224
groupName: 'Org Chart',
184225
groupFields: [

src/webparts/smartOrgChart/components/EmployeeDirectory/EmployeeDirectory.module.scss

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@
151151
overflow-y: auto;
152152
}
153153

154-
.size_small { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-auto-rows: 185px; }
155-
.size_medium { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-auto-rows: 200px; }
156-
.size_large { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); grid-auto-rows: 215px; }
154+
.size_small { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-auto-rows: 205px; }
155+
.size_medium { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-auto-rows: 222px; }
156+
.size_large { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); grid-auto-rows: 238px; }
157157

158158
/* ── Card ────────────────────────────────── */
159159

@@ -193,17 +193,19 @@
193193
}
194194

195195
.avatar {
196-
width: 48px; height: 48px;
196+
width: 64px; height: 64px;
197197
border-radius: 50%; object-fit: cover;
198+
box-shadow: 0 2px 8px rgba(0,0,0,0.14);
198199
}
199200

200201
.initials {
201-
width: 48px; height: 48px;
202+
width: 64px; height: 64px;
202203
border-radius: 50%;
203204
background: $ms-color-themePrimary;
204205
color: #ffffff;
205206
display: flex; align-items: center; justify-content: center;
206-
font-size: 15px; font-weight: 700;
207+
font-size: 20px; font-weight: 700;
208+
box-shadow: 0 2px 8px rgba(0,0,0,0.14);
207209
}
208210

209211
.cardBody { flex: 1; min-width: 0; }
@@ -323,15 +325,17 @@
323325
.listAvatarWrap { position: relative; display: inline-block; flex-shrink: 0; }
324326

325327
.listAvatar {
326-
width: 32px; height: 32px;
328+
width: 40px; height: 40px;
327329
border-radius: 50%; object-fit: cover; flex-shrink: 0;
330+
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
328331
}
329332

330333
.listInitials {
331-
width: 32px; height: 32px; border-radius: 50%;
334+
width: 40px; height: 40px; border-radius: 50%;
332335
background: $ms-color-themePrimary; color: #fff;
333336
display: flex; align-items: center; justify-content: center;
334-
font-size: 11px; font-weight: 700; flex-shrink: 0;
337+
font-size: 13px; font-weight: 700; flex-shrink: 0;
338+
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
335339
}
336340

337341
.listName { font-weight: 600; color: #1a1a1a; white-space: nowrap; }

src/webparts/smartOrgChart/components/OrgChart/OrgChart.module.scss

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
padding: 16px 10px 18px;
326326
width: 180px;
327327
min-width: 180px;
328-
height: 185px;
328+
height: 200px;
329329
overflow: visible;
330330
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07), 0 1px 2px rgba(0, 0, 0, 0.04);
331331
transition: box-shadow 0.18s ease, transform 0.15s ease;
@@ -363,11 +363,15 @@
363363
pointer-events: none;
364364
}
365365

366-
.photo { width: 62px; height: 62px; border-radius: 50%; object-fit: cover; display: block; }
366+
.photo {
367+
width: 78px; height: 78px; border-radius: 50%; object-fit: cover; display: block;
368+
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
369+
}
367370
.initials {
368-
width: 62px; height: 62px; border-radius: 50%;
371+
width: 78px; height: 78px; border-radius: 50%;
369372
color: #ffffff; display: flex; align-items: center; justify-content: center;
370-
font-size: 17px; font-weight: 700; letter-spacing: 0.3px;
373+
font-size: 22px; font-weight: 700; letter-spacing: 0.3px;
374+
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
371375
}
372376

373377
/* ── Text ────────────────────────────────── */
@@ -1081,17 +1085,17 @@
10811085
.compactCard {
10821086
width: 148px !important;
10831087
min-width: 148px !important;
1084-
height: 140px !important;
1088+
height: 158px !important;
10851089
padding: 10px 8px 12px !important;
10861090

1087-
.photo, .initials { width: 40px !important; height: 40px !important; font-size: 13px !important; }
1091+
.photo, .initials { width: 52px !important; height: 52px !important; font-size: 15px !important; }
10881092
.nodeName { font-size: 11.5px !important; margin-bottom: 2px !important; }
10891093
.nodeTitle { font-size: 10px !important; }
10901094
.nodeDept { font-size: 9px !important; padding: 1px 6px !important; margin-top: 5px !important; }
10911095
}
10921096

10931097
/* Adjust connector drop for compact cards */
1094-
.compactMode .nodeWrapper.hasChildren::after { top: 140px; }
1098+
.compactMode .nodeWrapper.hasChildren::after { top: 158px; }
10951099

10961100
/* ════════════════════════════════════════════
10971101
STATS BAR

src/webparts/smartOrgChart/components/SmartOrgChart.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { IconButton } from '@fluentui/react/lib/Button';
33
import { Icon } from '@fluentui/react/lib/Icon';
44
import { MSGraphClientV3 } from '@microsoft/sp-http';
5-
import { GraphService } from '../../../services/GraphService';
5+
import { GraphService, IUserFilterOptions } from '../../../services/GraphService';
66
import { MockGraphService, MockCompanySize } from '../../../services/MockGraphService';
77
import { ISmartOrgChartProps, IUserSettings } from './ISmartOrgChartProps';
88
import { EmployeeDirectory } from './EmployeeDirectory/EmployeeDirectory';
@@ -91,7 +91,14 @@ export class SmartOrgChart extends React.Component<ISmartOrgChartProps, ISmartOr
9191
}
9292

9393
public async componentDidUpdate(prev: ISmartOrgChartProps): Promise<void> {
94-
if (prev.useDemoData !== this.props.useDemoData || prev.dataSource !== this.props.dataSource) {
94+
if (
95+
prev.useDemoData !== this.props.useDemoData ||
96+
prev.dataSource !== this.props.dataSource ||
97+
prev.excludedAccounts !== this.props.excludedAccounts ||
98+
prev.hideDisabledAccounts !== this.props.hideDisabledAccounts ||
99+
prev.hideGuestUsers !== this.props.hideGuestUsers ||
100+
prev.restrictToTenantDomain !== this.props.restrictToTenantDomain
101+
) {
95102
await this._initGraphService();
96103
}
97104
}
@@ -112,7 +119,34 @@ export class SmartOrgChart extends React.Component<ISmartOrgChartProps, ISmartOr
112119
} catch {
113120
// Graph client unavailable — fall back to SP Search only
114121
}
115-
this.setState({ graphService: new GraphService(spHttpClient, pageContext.web.absoluteUrl, graphClient, this.props.dataSource || 'auto') });
122+
123+
// Derive tenant domain from the current user's email when the restriction is enabled
124+
let tenantDomain: string | undefined;
125+
if (this.props.restrictToTenantDomain) {
126+
const userEmail = (pageContext.user?.email || '').toLowerCase();
127+
const atIdx = userEmail.lastIndexOf('@');
128+
if (atIdx > 0) tenantDomain = userEmail.substring(atIdx + 1);
129+
}
130+
131+
const filterOptions: IUserFilterOptions = {
132+
tenantDomain,
133+
excludedPatterns: (this.props.excludedAccounts || '')
134+
.split(',')
135+
.map(s => s.trim().toLowerCase())
136+
.filter(s => s.length > 0),
137+
hideGuestUsers: this.props.hideGuestUsers || false,
138+
hideDisabledAccounts: this.props.hideDisabledAccounts || false,
139+
};
140+
141+
this.setState({
142+
graphService: new GraphService(
143+
spHttpClient,
144+
pageContext.web.absoluteUrl,
145+
graphClient,
146+
this.props.dataSource || 'auto',
147+
filterOptions
148+
)
149+
});
116150
}
117151

118152
private _setMockSize = (size: MockCompanySize): void => {

0 commit comments

Comments
 (0)