@mdguggenbichler/slugbase-core 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/index.tsx +3 -3
- package/frontend/public/favicon.svg +1 -0
- package/frontend/public/slugbase_icon_blue.svg +1 -0
- package/frontend/public/slugbase_icon_white.png +0 -0
- package/frontend/public/slugbase_icon_white.svg +1 -0
- package/frontend/src/App.tsx +179 -0
- package/frontend/src/api/client.ts +134 -0
- package/frontend/src/components/AppSidebar.tsx +214 -0
- package/frontend/src/components/EmptyState.tsx +33 -0
- package/frontend/src/components/Favicon.tsx +76 -0
- package/frontend/src/components/FilterChips.tsx +60 -0
- package/frontend/src/components/FolderIcon.tsx +207 -0
- package/frontend/src/components/GlobalSearch.tsx +275 -0
- package/frontend/src/components/Layout.tsx +60 -0
- package/frontend/src/components/PageHeader.tsx +31 -0
- package/frontend/src/components/ScopeSegmentedControl.tsx +42 -0
- package/frontend/src/components/SentryDebug.tsx +32 -0
- package/frontend/src/components/StatCard.tsx +66 -0
- package/frontend/src/components/TopBar.tsx +63 -0
- package/frontend/src/components/UserDropdown.tsx +86 -0
- package/frontend/src/components/admin/AdminAI.tsx +207 -0
- package/frontend/src/components/admin/AdminOIDCProviders.tsx +183 -0
- package/frontend/src/components/admin/AdminSettings.tsx +413 -0
- package/frontend/src/components/admin/AdminTeams.tsx +177 -0
- package/frontend/src/components/admin/AdminUsers.tsx +225 -0
- package/frontend/src/components/bookmarks/BookmarkCard.tsx +312 -0
- package/frontend/src/components/bookmarks/BookmarkListItem.tsx +159 -0
- package/frontend/src/components/bookmarks/BookmarkTableView.tsx +419 -0
- package/frontend/src/components/bookmarks/BulkActionModals.tsx +162 -0
- package/frontend/src/components/bookmarks/FilterChips.tsx +5 -0
- package/frontend/src/components/modals/BookmarkModal.tsx +493 -0
- package/frontend/src/components/modals/FolderModal.tsx +306 -0
- package/frontend/src/components/modals/ImportModal.tsx +232 -0
- package/frontend/src/components/modals/OIDCProviderModal.tsx +284 -0
- package/frontend/src/components/modals/SharingModal.tsx +96 -0
- package/frontend/src/components/modals/TagModal.tsx +101 -0
- package/frontend/src/components/modals/TeamAssignmentModal.tsx +354 -0
- package/frontend/src/components/modals/TeamModal.tsx +117 -0
- package/frontend/src/components/modals/UserModal.tsx +225 -0
- package/frontend/src/components/profile/CreateTokenModal.tsx +172 -0
- package/frontend/src/components/sharing/ShareResourceDialog.tsx +422 -0
- package/frontend/src/components/ui/Autocomplete.tsx +155 -0
- package/frontend/src/components/ui/Button.tsx +68 -0
- package/frontend/src/components/ui/ConfirmDialog.tsx +79 -0
- package/frontend/src/components/ui/FormFieldWrapper.tsx +36 -0
- package/frontend/src/components/ui/ModalFooterActions.tsx +49 -0
- package/frontend/src/components/ui/ModalSection.tsx +34 -0
- package/frontend/src/components/ui/PageLoadingSkeleton.tsx +24 -0
- package/frontend/src/components/ui/Select.tsx +61 -0
- package/frontend/src/components/ui/SharingField.tsx +298 -0
- package/frontend/src/components/ui/Toast.tsx +47 -0
- package/frontend/src/components/ui/Tooltip.tsx +21 -0
- package/frontend/src/components/ui/alert-dialog.tsx +139 -0
- package/frontend/src/components/ui/badge.tsx +36 -0
- package/frontend/src/components/ui/button-base.tsx +57 -0
- package/frontend/src/components/ui/card.tsx +76 -0
- package/frontend/src/components/ui/command.tsx +161 -0
- package/frontend/src/components/ui/dialog.tsx +120 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +199 -0
- package/frontend/src/components/ui/input.tsx +22 -0
- package/frontend/src/components/ui/label.tsx +24 -0
- package/frontend/src/components/ui/popover.tsx +33 -0
- package/frontend/src/components/ui/progress.tsx +26 -0
- package/frontend/src/components/ui/scroll-area.tsx +48 -0
- package/frontend/src/components/ui/select-base.tsx +159 -0
- package/frontend/src/components/ui/separator.tsx +29 -0
- package/frontend/src/components/ui/sheet.tsx +140 -0
- package/frontend/src/components/ui/sidebar.tsx +783 -0
- package/frontend/src/components/ui/skeleton.tsx +15 -0
- package/frontend/src/components/ui/sonner.tsx +46 -0
- package/frontend/src/components/ui/switch.tsx +28 -0
- package/frontend/src/components/ui/table.tsx +120 -0
- package/frontend/src/components/ui/tooltip-base.tsx +30 -0
- package/frontend/src/config/api.ts +16 -0
- package/frontend/src/config/mode.ts +6 -0
- package/frontend/src/contexts/AppConfigContext.tsx +39 -0
- package/frontend/src/contexts/AuthContext.tsx +137 -0
- package/frontend/src/contexts/SearchCommandContext.tsx +28 -0
- package/frontend/src/hooks/use-mobile.tsx +19 -0
- package/frontend/src/hooks/useConfirmDialog.ts +63 -0
- package/frontend/src/hooks/useMarketingTheme.ts +47 -0
- package/frontend/src/i18n.ts +39 -0
- package/frontend/src/index.css +117 -0
- package/frontend/src/instrument.ts +20 -0
- package/frontend/src/lib/utils.ts +6 -0
- package/frontend/src/locales/de.json +899 -0
- package/frontend/src/locales/en.json +937 -0
- package/frontend/src/locales/es.json +884 -0
- package/frontend/src/locales/fr.json +550 -0
- package/frontend/src/locales/it.json +535 -0
- package/frontend/src/locales/ja.json +535 -0
- package/frontend/src/locales/nl.json +550 -0
- package/frontend/src/locales/pl.json +535 -0
- package/frontend/src/locales/pt.json +535 -0
- package/frontend/src/locales/ru.json +535 -0
- package/frontend/src/locales/zh.json +535 -0
- package/frontend/src/main.tsx +44 -0
- package/frontend/src/pages/Bookmarks.tsx +1004 -0
- package/frontend/src/pages/Dashboard.tsx +427 -0
- package/frontend/src/pages/Folders.tsx +578 -0
- package/frontend/src/pages/GoPreferences.tsx +134 -0
- package/frontend/src/pages/Login.tsx +196 -0
- package/frontend/src/pages/PasswordReset.tsx +242 -0
- package/frontend/src/pages/Profile.tsx +593 -0
- package/frontend/src/pages/SearchEngineGuide.tsx +135 -0
- package/frontend/src/pages/Setup.tsx +210 -0
- package/frontend/src/pages/Signup.tsx +199 -0
- package/frontend/src/pages/Tags.tsx +421 -0
- package/frontend/src/pages/VerifyEmail.tsx +254 -0
- package/frontend/src/pages/admin/AdminAIPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminLayout.tsx +40 -0
- package/frontend/src/pages/admin/AdminMembersPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminOIDCPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminSettingsPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminTeamsPage.tsx +5 -0
- package/frontend/src/utils/favicon.ts +36 -0
- package/frontend/src/utils/formatRelativeTime.ts +37 -0
- package/frontend/src/utils/safeHref.ts +31 -0
- package/frontend/src/vite-env.d.ts +10 -0
- package/package.json +9 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import api from '../../api/client';
|
|
4
|
+
import { useToast } from '../ui/Toast';
|
|
5
|
+
import { Save, Sparkles } from 'lucide-react';
|
|
6
|
+
import Button from '../ui/Button';
|
|
7
|
+
import { PageLoadingSkeleton } from '../ui/PageLoadingSkeleton';
|
|
8
|
+
import { Switch } from '../ui/switch';
|
|
9
|
+
import { Label } from '../ui/label';
|
|
10
|
+
import { Input } from '../ui/input';
|
|
11
|
+
import Select from '../ui/Select';
|
|
12
|
+
|
|
13
|
+
const PROVIDER_OPTIONS = [
|
|
14
|
+
{ value: 'openai', label: 'OpenAI' },
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export default function AdminAI() {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const { showToast } = useToast();
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [saving, setSaving] = useState(false);
|
|
22
|
+
const [settings, setSettings] = useState({
|
|
23
|
+
ai_enabled: false,
|
|
24
|
+
ai_provider: 'openai',
|
|
25
|
+
ai_model: 'gpt-4o-mini',
|
|
26
|
+
ai_api_key: '',
|
|
27
|
+
ai_api_key_set: false,
|
|
28
|
+
});
|
|
29
|
+
const [models, setModels] = useState<{ id: string }[]>([]);
|
|
30
|
+
const [modelsLoading, setModelsLoading] = useState(false);
|
|
31
|
+
|
|
32
|
+
const loadSettings = useCallback(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const res = await api.get('/admin/settings/ai');
|
|
35
|
+
setSettings({
|
|
36
|
+
ai_enabled: res.data.ai_enabled ?? false,
|
|
37
|
+
ai_provider: res.data.ai_provider || 'openai',
|
|
38
|
+
ai_model: res.data.ai_model || 'gpt-4o-mini',
|
|
39
|
+
ai_api_key: '',
|
|
40
|
+
ai_api_key_set: res.data.ai_api_key_set ?? false,
|
|
41
|
+
});
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
const e = err as { response?: { status?: number; data?: { error?: string } } };
|
|
44
|
+
if (e?.response?.status === 403) {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
showToast(e?.response?.data?.error || t('common.error'), 'error');
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [showToast, t]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
loadSettings();
|
|
56
|
+
}, [loadSettings]);
|
|
57
|
+
|
|
58
|
+
const loadModels = useCallback(async () => {
|
|
59
|
+
setModelsLoading(true);
|
|
60
|
+
try {
|
|
61
|
+
const res = await api.get<{ models: { id: string }[] }>('/admin/settings/ai/models');
|
|
62
|
+
setModels(Array.isArray(res.data?.models) ? res.data.models : []);
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
const e = err as { response?: { data?: { error?: string } } };
|
|
65
|
+
showToast(e?.response?.data?.error || t('admin.ai.modelsLoadError'), 'error');
|
|
66
|
+
setModels([]);
|
|
67
|
+
} finally {
|
|
68
|
+
setModelsLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, [showToast, t]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (settings.ai_api_key_set && !loading) {
|
|
74
|
+
loadModels();
|
|
75
|
+
} else {
|
|
76
|
+
setModels([]);
|
|
77
|
+
}
|
|
78
|
+
}, [settings.ai_api_key_set, loading, loadModels]);
|
|
79
|
+
|
|
80
|
+
const providerOptions = PROVIDER_OPTIONS.map((p) => ({
|
|
81
|
+
value: p.value,
|
|
82
|
+
label: p.value === 'openai' ? t('admin.ai.providerOpenAI') : p.label,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const modelOptions = models.map((m) => ({ value: m.id, label: m.id }));
|
|
86
|
+
const currentModelInList = modelOptions.some((o) => o.value === settings.ai_model);
|
|
87
|
+
const modelOptionsWithCurrent =
|
|
88
|
+
currentModelInList || !settings.ai_model
|
|
89
|
+
? modelOptions
|
|
90
|
+
: [{ value: settings.ai_model, label: settings.ai_model }, ...modelOptions];
|
|
91
|
+
|
|
92
|
+
const handleSave = async (e: React.FormEvent) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setSaving(true);
|
|
95
|
+
try {
|
|
96
|
+
await api.post('/admin/settings/ai', {
|
|
97
|
+
ai_enabled: settings.ai_enabled,
|
|
98
|
+
ai_provider: settings.ai_provider,
|
|
99
|
+
ai_model: settings.ai_model,
|
|
100
|
+
ai_api_key: settings.ai_api_key || undefined,
|
|
101
|
+
});
|
|
102
|
+
showToast(t('common.success'), 'success');
|
|
103
|
+
await loadSettings();
|
|
104
|
+
} catch (err: unknown) {
|
|
105
|
+
const e = err as { response?: { data?: { error?: string } } };
|
|
106
|
+
showToast(e?.response?.data?.error || t('common.error'), 'error');
|
|
107
|
+
} finally {
|
|
108
|
+
setSaving(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (loading) {
|
|
113
|
+
return <PageLoadingSkeleton lines={8} />;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="space-y-6">
|
|
118
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm p-4">
|
|
119
|
+
<div className="flex items-center gap-2 mb-6">
|
|
120
|
+
<Sparkles className="h-5 w-5 text-violet-500 dark:text-violet-400" />
|
|
121
|
+
<div>
|
|
122
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
123
|
+
{t('admin.ai.title')}
|
|
124
|
+
</h2>
|
|
125
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
126
|
+
{t('admin.ai.description')}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<form onSubmit={handleSave} className="space-y-4">
|
|
132
|
+
<div className="flex items-center gap-3">
|
|
133
|
+
<Switch
|
|
134
|
+
id="ai-enabled"
|
|
135
|
+
checked={settings.ai_enabled}
|
|
136
|
+
onCheckedChange={(checked) =>
|
|
137
|
+
setSettings({ ...settings, ai_enabled: checked })
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
<Label htmlFor="ai-enabled" className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer">
|
|
141
|
+
{t('admin.ai.enabled')}
|
|
142
|
+
</Label>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div>
|
|
146
|
+
<Label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
147
|
+
{t('admin.ai.provider')}
|
|
148
|
+
</Label>
|
|
149
|
+
<Select
|
|
150
|
+
value={settings.ai_provider}
|
|
151
|
+
onChange={(value) => setSettings({ ...settings, ai_provider: value })}
|
|
152
|
+
options={providerOptions}
|
|
153
|
+
className="max-w-xs"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div>
|
|
158
|
+
<Label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
159
|
+
{t('admin.ai.apiKey')}
|
|
160
|
+
</Label>
|
|
161
|
+
<Input
|
|
162
|
+
type="password"
|
|
163
|
+
value={settings.ai_api_key}
|
|
164
|
+
onChange={(e) => setSettings({ ...settings, ai_api_key: e.target.value })}
|
|
165
|
+
placeholder={settings.ai_api_key_set ? t('admin.ai.apiKeyPlaceholder') : 'sk-...'}
|
|
166
|
+
className="max-w-md font-mono text-sm"
|
|
167
|
+
/>
|
|
168
|
+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
169
|
+
{settings.ai_api_key_set ? t('admin.ai.apiKeyChangeHint') : t('admin.ai.apiKeyHint')}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div>
|
|
174
|
+
<Label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
175
|
+
{t('admin.ai.model')}
|
|
176
|
+
</Label>
|
|
177
|
+
<Select
|
|
178
|
+
value={settings.ai_model}
|
|
179
|
+
onChange={(value) => setSettings({ ...settings, ai_model: value })}
|
|
180
|
+
options={modelOptionsWithCurrent}
|
|
181
|
+
placeholder={
|
|
182
|
+
!settings.ai_api_key_set
|
|
183
|
+
? t('admin.ai.modelPlaceholderNoKey')
|
|
184
|
+
: modelsLoading
|
|
185
|
+
? t('admin.ai.modelPlaceholderLoading')
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
188
|
+
disabled={!settings.ai_api_key_set || modelsLoading}
|
|
189
|
+
className="max-w-xs"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div className="pt-2">
|
|
194
|
+
<Button type="submit" variant="primary" disabled={saving}>
|
|
195
|
+
{saving ? t('common.loading') : (
|
|
196
|
+
<>
|
|
197
|
+
<Save className="h-4 w-4 mr-2 inline" />
|
|
198
|
+
{t('common.save')}
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</form>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import api from '../../api/client';
|
|
4
|
+
import ConfirmDialog from '../ui/ConfirmDialog';
|
|
5
|
+
import { useConfirmDialog } from '../../hooks/useConfirmDialog';
|
|
6
|
+
import { Plus, Edit, Trash2, Key, Globe } from 'lucide-react';
|
|
7
|
+
import OIDCProviderModal from '../modals/OIDCProviderModal';
|
|
8
|
+
import Button from '../ui/Button';
|
|
9
|
+
import { PageLoadingSkeleton } from '../ui/PageLoadingSkeleton';
|
|
10
|
+
|
|
11
|
+
interface OIDCProvider {
|
|
12
|
+
id: string;
|
|
13
|
+
provider_key: string;
|
|
14
|
+
issuer_url: string;
|
|
15
|
+
authorization_url?: string;
|
|
16
|
+
token_url?: string;
|
|
17
|
+
userinfo_url?: string;
|
|
18
|
+
scopes: string;
|
|
19
|
+
auto_create_users: boolean;
|
|
20
|
+
default_role: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
callback_url?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function AdminOIDCProviders() {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const { showConfirm, dialogState } = useConfirmDialog();
|
|
28
|
+
const [providers, setProviders] = useState<OIDCProvider[]>([]);
|
|
29
|
+
const [loading, setLoading] = useState(true);
|
|
30
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
31
|
+
const [editingProvider, setEditingProvider] = useState<OIDCProvider | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
loadProviders();
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const loadProviders = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const response = await api.get('/oidc-providers');
|
|
40
|
+
setProviders(response.data);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Failed to load providers:', error);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleEdit = (provider: OIDCProvider) => {
|
|
49
|
+
setEditingProvider(provider);
|
|
50
|
+
setModalOpen(true);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleDelete = (id: string) => {
|
|
54
|
+
showConfirm(
|
|
55
|
+
t('admin.confirmDeleteProvider'),
|
|
56
|
+
t('admin.confirmDeleteProvider'),
|
|
57
|
+
async () => {
|
|
58
|
+
try {
|
|
59
|
+
await api.delete(`/oidc-providers/${id}`);
|
|
60
|
+
await loadProviders();
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
console.error('Failed to delete provider:', error);
|
|
63
|
+
alert(error.response?.data?.error || t('common.error'));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{ variant: 'danger', confirmText: t('common.delete'), cancelText: t('common.cancel') }
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleModalClose = () => {
|
|
71
|
+
setModalOpen(false);
|
|
72
|
+
setEditingProvider(null);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (loading) {
|
|
76
|
+
return <PageLoadingSkeleton lines={6} />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="space-y-6">
|
|
81
|
+
<div className="flex items-center justify-between">
|
|
82
|
+
<div>
|
|
83
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
84
|
+
{t('admin.oidcProviders')}
|
|
85
|
+
</h2>
|
|
86
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
87
|
+
{providers.length} {providers.length === 1 ? t('common.provider') : t('common.providers')}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
<Button onClick={() => setModalOpen(true)} icon={Plus}>
|
|
91
|
+
{t('admin.addProvider')}
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{providers.length === 0 ? (
|
|
96
|
+
<div className="flex flex-col items-center justify-center py-16 px-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
97
|
+
<Key className="h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
|
|
98
|
+
<p className="text-gray-500 dark:text-gray-400 text-lg mb-4">{t('auth.noProviders')}</p>
|
|
99
|
+
<Button onClick={() => setModalOpen(true)} variant="primary" size="sm" icon={Plus}>
|
|
100
|
+
{t('admin.addProvider')}
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
105
|
+
{providers.map((provider) => (
|
|
106
|
+
<div
|
|
107
|
+
key={provider.id}
|
|
108
|
+
className="group bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-all overflow-hidden"
|
|
109
|
+
>
|
|
110
|
+
<div className="p-4 space-y-4">
|
|
111
|
+
<div className="flex items-start justify-between gap-3">
|
|
112
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
113
|
+
<Key className="h-5 w-5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
|
|
114
|
+
<h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate">
|
|
115
|
+
{provider.provider_key}
|
|
116
|
+
</h3>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
|
120
|
+
<Globe className="h-4 w-4" />
|
|
121
|
+
<span className="truncate">{provider.issuer_url}</span>
|
|
122
|
+
</div>
|
|
123
|
+
{(provider.authorization_url || provider.token_url || provider.userinfo_url) && (
|
|
124
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
|
125
|
+
{provider.token_url && (
|
|
126
|
+
<div className="truncate">
|
|
127
|
+
<span className="font-medium">Token:</span> {provider.token_url}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
{provider.callback_url && (
|
|
133
|
+
<div className="p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
134
|
+
<div className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
|
|
135
|
+
{t('admin.callbackUrl')}:
|
|
136
|
+
</div>
|
|
137
|
+
<code className="text-xs text-gray-900 dark:text-gray-100 break-all">
|
|
138
|
+
{provider.callback_url}
|
|
139
|
+
</code>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
<div className="flex flex-wrap gap-2">
|
|
143
|
+
<span className="px-2.5 py-1 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 rounded-lg">
|
|
144
|
+
{t('admin.autoCreate')}: {provider.auto_create_users ? t('common.yes') : t('common.no')}
|
|
145
|
+
</span>
|
|
146
|
+
<span className="px-2.5 py-1 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 rounded-lg">
|
|
147
|
+
{t('admin.defaultRole')}: {provider.default_role}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
151
|
+
<Button
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="sm"
|
|
154
|
+
icon={Edit}
|
|
155
|
+
onClick={() => handleEdit(provider)}
|
|
156
|
+
className="flex-1"
|
|
157
|
+
>
|
|
158
|
+
{t('common.edit')}
|
|
159
|
+
</Button>
|
|
160
|
+
<Button
|
|
161
|
+
variant="danger"
|
|
162
|
+
size="sm"
|
|
163
|
+
icon={Trash2}
|
|
164
|
+
onClick={() => handleDelete(provider.id)}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<OIDCProviderModal
|
|
174
|
+
provider={editingProvider}
|
|
175
|
+
isOpen={modalOpen}
|
|
176
|
+
onClose={handleModalClose}
|
|
177
|
+
onSuccess={loadProviders}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<ConfirmDialog {...dialogState} />
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|