larxius commited on
Commit
122d3d1
·
verified ·
1 Parent(s): 88361fe

Update frontend/src/pages/AdminPage.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/AdminPage.jsx +124 -4
frontend/src/pages/AdminPage.jsx CHANGED
@@ -235,6 +235,107 @@ const AdminPageContent = () => {
235
  }
236
  };
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  if (loading) {
239
  return (
240
  <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
@@ -384,10 +485,21 @@ const AdminPageContent = () => {
384
  </table>
385
  </div>
386
  <div className="mt-xl border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm">
387
- <h2 className="font-headline-md text-headline-md text-on-surface mb-sm font-bold tracking-tight">Organizations</h2>
388
- <p className="font-body-md text-body-md text-on-surface-variant mb-lg">
389
- View all tenants and use Impersonation to see their Dashboard, Analytics, and Vulnerabilities.
390
- </p>
 
 
 
 
 
 
 
 
 
 
 
391
 
392
  <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm overflow-hidden">
393
  <table className="w-full">
@@ -436,6 +548,14 @@ const AdminPageContent = () => {
436
  {org.created_at ? new Date(org.created_at).toLocaleDateString() : 'N/A'}
437
  </td>
438
  <td className="px-lg py-md text-right">
 
 
 
 
 
 
 
 
439
  <button
440
  onClick={() => handleAssignScans(org)}
441
  className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs mr-2"
 
235
  }
236
  };
237
 
238
+ const handleProvisionTenant = () => {
239
+ setPromptValues({ tier: 'quick', name: '', admin_email: '' });
240
+ setPromptModal({
241
+ isOpen: true,
242
+ title: 'Add New Organization',
243
+ desc: 'Create a new tenant organization.',
244
+ inputs: [
245
+ { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
246
+ {
247
+ key: 'tier',
248
+ label: 'Subscription Tier',
249
+ type: 'select',
250
+ options: [
251
+ { label: 'Quick', value: 'quick' },
252
+ { label: 'Advanced', value: 'advanced' },
253
+ { label: 'Deep', value: 'deep' },
254
+ { label: 'Enterprise(Custom)', value: 'Enterprise(Custom)' }
255
+ ]
256
+ },
257
+ { key: 'admin_email', label: 'Admin Email (Optional)', placeholder: 'admin@company.com' }
258
+ ],
259
+ onConfirm: async (values) => {
260
+ if (!values.name || !values.name.trim()) {
261
+ setError('Organization name is required.');
262
+ closePrompt();
263
+ return;
264
+ }
265
+ try {
266
+ const res = await fetch('/api/auth/organizations', {
267
+ method: 'POST',
268
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ name: values.name, tier: values.tier, admin_email: values.admin_email })
270
+ });
271
+ if (res.ok) {
272
+ setMessage('Organization created successfully.');
273
+ fetchOrganizations();
274
+ } else {
275
+ const data = await res.json();
276
+ setError(data.message || 'Failed to create organization.');
277
+ }
278
+ } catch {
279
+ setError('Could not connect to API to create organization.');
280
+ }
281
+ closePrompt();
282
+ }
283
+ });
284
+ };
285
+
286
+ const handleEditTenant = (org) => {
287
+ const rawTier = (org.tier || org.subscription_tier || 'quick');
288
+ const isCustom = rawTier.toLowerCase().includes('custom') || rawTier.toLowerCase().includes('enterprise');
289
+ const initialTier = isCustom
290
+ ? 'Enterprise(Custom)'
291
+ : ['quick', 'advanced', 'deep'].includes(rawTier.toLowerCase())
292
+ ? rawTier.toLowerCase()
293
+ : 'quick';
294
+
295
+ setPromptValues({
296
+ name: org.name,
297
+ tier: initialTier
298
+ });
299
+ setPromptModal({
300
+ isOpen: true,
301
+ title: 'Edit Organization',
302
+ desc: `Modify settings for ${org.name}`,
303
+ inputs: [
304
+ { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
305
+ {
306
+ key: 'tier',
307
+ label: 'Subscription Tier',
308
+ type: 'select',
309
+ options: [
310
+ { label: 'Quick', value: 'quick' },
311
+ { label: 'Advanced', value: 'advanced' },
312
+ { label: 'Deep', value: 'deep' },
313
+ { label: 'Enterprise(Custom)', value: 'Enterprise(Custom)' }
314
+ ]
315
+ }
316
+ ],
317
+ onConfirm: async (values) => {
318
+ try {
319
+ const res = await fetch(`/api/auth/organizations/${org.id}`, {
320
+ method: 'PUT',
321
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
322
+ body: JSON.stringify({ name: values.name, tier: values.tier })
323
+ });
324
+ if (res.ok) {
325
+ setMessage('Organization updated successfully.');
326
+ fetchOrganizations();
327
+ } else {
328
+ const data = await res.json();
329
+ setError(data.message || 'Failed to update organization.');
330
+ }
331
+ } catch {
332
+ setError('Could not connect to API to update organization.');
333
+ }
334
+ closePrompt();
335
+ }
336
+ });
337
+ };
338
+
339
  if (loading) {
340
  return (
341
  <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
 
485
  </table>
486
  </div>
487
  <div className="mt-xl border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm">
488
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between mb-lg gap-sm">
489
+ <div>
490
+ <h2 className="font-headline-md text-headline-md text-on-surface mb-xs font-bold tracking-tight">Organizations</h2>
491
+ <p className="font-body-md text-body-md text-on-surface-variant">
492
+ Manage tenant organizations, subscription tiers, and scan quotas.
493
+ </p>
494
+ </div>
495
+ <button
496
+ onClick={handleProvisionTenant}
497
+ className="bg-primary text-white px-md py-sm rounded-lg font-label-md text-label-md hover:brightness-110 transition-all font-bold border-0 cursor-pointer flex items-center gap-xs shadow-md shadow-primary/20 self-start sm:self-auto whitespace-nowrap"
498
+ >
499
+ <span className="material-symbols-outlined text-[18px]">add_business</span>
500
+ Add Organization
501
+ </button>
502
+ </div>
503
 
504
  <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm overflow-hidden">
505
  <table className="w-full">
 
548
  {org.created_at ? new Date(org.created_at).toLocaleDateString() : 'N/A'}
549
  </td>
550
  <td className="px-lg py-md text-right">
551
+ <button
552
+ onClick={() => handleEditTenant(org)}
553
+ className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs mr-2"
554
+ title="Edit Organization"
555
+ >
556
+ <span className="material-symbols-outlined text-[18px]">edit</span>
557
+ <span className="font-label-sm">Edit</span>
558
+ </button>
559
  <button
560
  onClick={() => handleAssignScans(org)}
561
  className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs mr-2"