CreateUserAdvancedAuthModal.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { X, Plus, Loader2, Users as UsersIcon } from 'lucide-react';
  4. import { Card, CardContent, CardHeader } from './Card';
  5. import { Button } from './Button';
  6. import type { Group, UserCreate } from '../api/client';
  7. interface AdvancedAuthFormData extends UserCreate {
  8. group_ids: number[];
  9. confirmPassword: string;
  10. email?: string;
  11. }
  12. interface CreateUserAdvancedAuthModalProps {
  13. formData: AdvancedAuthFormData;
  14. setFormData: (data: AdvancedAuthFormData) => void;
  15. groups: Group[];
  16. onClose: () => void;
  17. onCreate: () => void;
  18. isCreating: boolean;
  19. isCreateButtonDisabled: boolean;
  20. }
  21. export function CreateUserAdvancedAuthModal({
  22. formData,
  23. setFormData,
  24. groups,
  25. onClose,
  26. onCreate,
  27. isCreating,
  28. isCreateButtonDisabled,
  29. }: CreateUserAdvancedAuthModalProps) {
  30. const { t } = useTranslation();
  31. // Close modal on Escape key
  32. useEffect(() => {
  33. const handleKeyDown = (e: KeyboardEvent) => {
  34. if (e.key === 'Escape') {
  35. onClose();
  36. }
  37. };
  38. window.addEventListener('keydown', handleKeyDown);
  39. return () => window.removeEventListener('keydown', handleKeyDown);
  40. }, [onClose]);
  41. const toggleGroup = (groupId: number) => {
  42. setFormData({
  43. ...formData,
  44. group_ids: formData.group_ids.includes(groupId)
  45. ? formData.group_ids.filter(id => id !== groupId)
  46. : [...formData.group_ids, groupId],
  47. });
  48. };
  49. return (
  50. <div
  51. className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
  52. onClick={onClose}
  53. >
  54. <Card
  55. className="w-full max-w-md"
  56. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  57. >
  58. <CardHeader>
  59. <div className="flex items-center justify-between">
  60. <div className="flex flex-col gap-1">
  61. <div className="flex items-center gap-2">
  62. <UsersIcon className="w-5 h-5 text-bambu-green" />
  63. <h2 className="text-lg font-semibold text-white">{t('users.modal.createUser')}</h2>
  64. </div>
  65. <p className="text-sm text-bambu-gray ml-7">{t('users.modal.advancedAuthSubtitle') || 'with Advanced Authentication'}</p>
  66. </div>
  67. <Button
  68. variant="ghost"
  69. size="sm"
  70. onClick={onClose}
  71. >
  72. <X className="w-5 h-5" />
  73. </Button>
  74. </div>
  75. </CardHeader>
  76. <CardContent>
  77. <div className="space-y-4">
  78. {/* Username Field */}
  79. <div>
  80. <label className="block text-sm font-medium text-white mb-2">
  81. {t('users.form.username')} <span className="text-red-400">*</span>
  82. </label>
  83. <input
  84. type="text"
  85. value={formData.username}
  86. onChange={(e) => setFormData({ ...formData, username: e.target.value })}
  87. className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  88. placeholder={t('users.form.usernamePlaceholder')}
  89. autoComplete="username"
  90. required
  91. />
  92. </div>
  93. {/* Email Field */}
  94. <div>
  95. <label className="block text-sm font-medium text-white mb-2">
  96. {t('users.form.email') || 'Email'} <span className="text-red-400">*</span>
  97. </label>
  98. <input
  99. type="email"
  100. value={formData.email}
  101. onChange={(e) => setFormData({ ...formData, email: e.target.value })}
  102. className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  103. placeholder={t('users.form.emailPlaceholder') || 'user@example.com'}
  104. required
  105. />
  106. </div>
  107. {/* Info box about auto-generated password */}
  108. <div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3">
  109. <p className="text-sm text-bambu-gray">
  110. {t('users.form.autoGeneratedPassword') || 'A secure password will be automatically generated and emailed to the user.'}
  111. </p>
  112. </div>
  113. {/* Groups Field */}
  114. <div>
  115. <label className="block text-sm font-medium text-white mb-2">
  116. {t('users.form.groups')}
  117. </label>
  118. <div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
  119. {groups.map(group => (
  120. <label
  121. key={group.id}
  122. className="flex items-center gap-3 px-2 py-1.5 rounded hover:bg-bambu-dark-tertiary cursor-pointer"
  123. >
  124. <input
  125. type="checkbox"
  126. checked={formData.group_ids.includes(group.id)}
  127. onChange={() => toggleGroup(group.id)}
  128. className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark"
  129. />
  130. <span className="text-sm text-white">{group.name}</span>
  131. {group.is_system && (
  132. <span className="text-xs text-yellow-400">({t('users.system')})</span>
  133. )}
  134. </label>
  135. ))}
  136. {groups.length === 0 && (
  137. <p className="text-sm text-bambu-gray">{t('users.noGroupsAvailable')}</p>
  138. )}
  139. </div>
  140. </div>
  141. </div>
  142. {/* Action Buttons */}
  143. <div className="mt-6 flex justify-end gap-3">
  144. <Button
  145. variant="secondary"
  146. onClick={onClose}
  147. >
  148. {t('users.modal.cancel')}
  149. </Button>
  150. <Button
  151. onClick={onCreate}
  152. disabled={isCreateButtonDisabled}
  153. >
  154. {isCreating ? (
  155. <>
  156. <Loader2 className="w-4 h-4 animate-spin" />
  157. {t('users.modal.creating')}
  158. </>
  159. ) : (
  160. <>
  161. <Plus className="w-4 h-4" />
  162. {t('users.modal.createUser')}
  163. </>
  164. )}
  165. </Button>
  166. </div>
  167. </CardContent>
  168. </Card>
  169. </div>
  170. );
  171. }