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 components
  • lucide-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
  • SharingStatus - Displays configured access permissions
  • TagAccessDialog - Uses AccessSelector for tag permissions
  • Group management and administration interfaces

Best Practices

  1. Validate Input: Always validate emails and domains before adding
  2. Prevent Duplicates: Check for existing entries before adding
  3. Handle Edge Cases: Empty states and invalid selections
  4. Responsive Design: Ensure usability across device sizes
  5. Clear State Management: Avoid circular update loops
  6. Accessibility: Support keyboard navigation and screen readers