AccessSelector Component
The AccessSelector component provides a comprehensive interface for configuring access control permissions with support for various access types including private, public, domain-based, and group-based access.
Overview
This component offers a visual card-based selection interface for setting access permissions on resources like AI assistants. It supports multiple access patterns including specific users, domain restrictions, predefined groups, and public access with dynamic configuration management.
Location
src/components/AccessSelector.tsx
Features
Access Control Types
- Private: Only the owner can access
- Public: Anyone can access
- Domain: Users from owner’s organization
- Domains: Users from multiple specified domains
- Specific Users: Selected email addresses only
- Groups: Predefined team or group access
Dynamic UI Components
- Visual Card Selection: Icon-based cards for each access type
- Conditional Forms: Input fields appear based on selection
- Real-time Validation: Email and domain format validation
- Tag Management: Add/remove emails, domains, and groups
Group Integration
- Predefined Groups: Integration with organization groups
- Checkbox Selection: Multi-select interface for groups
- Group Metadata: Shows member counts and descriptions
- Visual Indicators: Selected groups with removal capability
Type Definitions
export type AccessType = 'public' | 'private' | 'domain' | 'specific' | 'domains' | 'group';
export type AccessConfig = {
type: AccessType;
emails?: string[]; // Used when type is 'specific'
domains?: string[]; // Used when type is 'domains'
groups?: string[]; // Used when type is 'group'
};
interface AccessSelectorProps {
value: AccessType;
emails?: string[];
domains?: string[];
groups?: string[];
userDomain?: string;
onChange: (config: AccessConfig) => void;
}
Usage Example
import { AccessSelector } from '@/components/AccessSelector';
function AssistantSettings() {
const [accessType, setAccessType] = useState('private');
const [emails, setEmails] = useState([]);
const [domains, setDomains] = useState([]);
const [groups, setGroups] = useState([]);
const handleAccessChange = (config) => {
setAccessType(config.type);
setEmails(config.emails || []);
setDomains(config.domains || []);
setGroups(config.groups || []);
};
return (
<AccessSelector
value={accessType}
emails={emails}
domains={domains}
groups={groups}
userDomain="example.com"
onChange={handleAccessChange}
/>
);
}
Access Type Cards
Private Access
<div className="relative p-4 rounded-lg border-2 cursor-pointer">
<div className="flex flex-col space-y-3">
<div className="flex items-center justify-center gap-2">
<Lock className="h-4 w-4" />
<h3 className="font-medium">Private</h3>
</div>
<p className="text-sm text-muted-foreground">Only you can access</p>
</div>
</div>
Domain Access
<div className="relative p-4 rounded-lg border-2 cursor-pointer">
<div className="flex flex-col space-y-3">
<div className="flex items-center justify-center gap-2">
<Building2 className="h-4 w-4" />
<h3 className="font-medium">Organization</h3>
</div>
<p className="text-sm text-muted-foreground">Only @{userDomain} users</p>
</div>
</div>
Email Management
Email Input Interface
<div className="flex gap-2">
<Input
placeholder="Enter email address"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onKeyDown={handleEmailKeyPress}
className="flex-1"
/>
<Button
onClick={handleAddEmail}
disabled={!emailInput.trim() || !validateEmail(emailInput)}
>
<PlusCircle className="h-4 w-4" />
</Button>
</div>
Email Validation
const validateEmail = (email: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
Email Display Tags
{emails.map(email => (
<div key={email} className="flex items-center bg-secondary rounded-full px-3 py-1">
<span className="break-words mr-1">{email}</span>
<button onClick={() => handleRemoveEmail(email)}>
<X className="h-3 w-3" />
</button>
</div>
))}
Domain Management
Domain Validation
const validateDomain = (domain: string) => {
return /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/.test(domain);
};
Domain Input Processing
const handleAddDomain = () => {
if (!domainInput.trim() || !validateDomain(domainInput)) return;
const newDomain = domainInput.trim().toLowerCase();
if (domains.includes(newDomain)) {
setDomainInput('');
return;
}
const updatedDomains = [...domains, newDomain];
setDomainInput('');
onChange({
type: 'domains',
domains: updatedDomains
});
};
Group Integration
Group Selection Interface
{availableGroups.map(group => (
<div
key={group.id}
className="flex items-start gap-3 p-3 rounded-md hover:bg-secondary/30"
onClick={() => handleToggleGroup(group.id)}
>
<input
type="checkbox"
checked={groups.includes(group.id)}
onChange={() => handleToggleGroup(group.id)}
/>
<div className="flex flex-col flex-1">
<label className="text-sm font-medium">{group.name}</label>
{group.description && (
<span className="text-xs text-muted-foreground">{group.description}</span>
)}
<span className="text-xs text-muted-foreground">
{group.emails.length} member{group.emails.length !== 1 ? 's' : ''}
</span>
</div>
</div>
))}
Group Toggle Logic
const handleToggleGroup = (groupId: string) => {
const isSelected = groups.includes(groupId);
let updatedGroups: string[];
if (isSelected) {
updatedGroups = groups.filter(g => g !== groupId);
} else {
updatedGroups = [...groups, groupId];
}
onChange({
type: 'group',
groups: updatedGroups
});
};
State Management
Preventing Infinite Loops
// IMPORTANT: Uses props directly instead of separate state
// This avoids infinite loops caused by state updates triggering re-renders
const handleAccessTypeChange = (type: AccessType) => {
if (type === value) return; // No change
let configToSend: AccessConfig = { type };
if (type === 'specific') {
configToSend.emails = emails;
} else if (type === 'domains') {
configToSend.domains = domains;
} else if (type === 'group') {
configToSend.groups = groups;
}
onChange(configToSend);
};
Input Handling
Keyboard Navigation
const handleEmailKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddEmail();
}
};
const handleDomainKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddDomain();
}
};
Dependencies
@/lib/groups- Group definitions and utilities@/components/ui/*- Label, Input, Button componentslucide-react- Icons for access types and actions
Responsive Design
Grid Layout
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
{/* Access type cards */}
</div>
Mobile Optimization
- Single Column: Mobile displays cards in single column
- Touch Targets: Adequate size for touch interaction
- Scrollable Lists: Long lists become scrollable with custom scrollbars
- Text Wrapping: Email and domain text wraps appropriately
Accessibility
Semantic Structure
- Card Selection: Proper button/interactive role
- Form Labels: Associated labels for all inputs
- Checkbox States: Clear indication of selection state
Keyboard Navigation
- Tab Order: Logical navigation through interface
- Enter Key: Adds emails/domains on Enter press
- Focus Management: Clear focus indicators
Screen Reader Support
- Descriptive Labels: Clear field descriptions
- State Announcements: Selection changes announced
- Group Information: Member counts and descriptions read
Related Components
SharingStatus- Displays configured access permissionsTagAccessDialog- Uses AccessSelector for tag permissions- Group management and administration interfaces
Best Practices
- Validate Input: Always validate emails and domains before adding
- Prevent Duplicates: Check for existing entries before adding
- Handle Edge Cases: Empty states and invalid selections
- Responsive Design: Ensure usability across device sizes
- Clear State Management: Avoid circular update loops
- Accessibility: Support keyboard navigation and screen readers