Juggernaut1397 commited on
Commit
efb09de
·
verified ·
1 Parent(s): c2627ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +590 -651
app.py CHANGED
@@ -14,10 +14,7 @@ script_dir = Path(__file__).resolve().parent
14
  env_path = script_dir / '.env'
15
  load_dotenv(dotenv_path=env_path)
16
 
17
- script_dir = Path(__file__).resolve().parent
18
- env_path = script_dir / '.env'
19
- load_dotenv(dotenv_path=env_path)
20
-
21
  def fetch_api_endpoints_yaml(spec_url):
22
  try:
23
  response = requests.get(spec_url)
@@ -39,21 +36,17 @@ def fetch_api_endpoints_yaml(spec_url):
39
  if not methods or not isinstance(methods, dict):
40
  continue
41
 
42
- # Get common parameters defined at path level
43
  common_params = methods.get("parameters", [])
44
-
45
  for method, details in methods.items():
46
  if method.lower() not in valid_methods:
47
  continue
48
 
49
- # Combine path-level and method-level parameters
50
  method_params = details.get("parameters", [])
51
  all_params = common_params + method_params
52
-
53
  endpoint_info = {
54
  "summary": details.get("summary", ""),
55
  "description": details.get("description", ""),
56
- "parameters": all_params # Include all parameters
57
  }
58
  endpoints[path][method.lower()] = endpoint_info
59
  return endpoints
@@ -78,21 +71,17 @@ def fetch_api_endpoints_json(spec_url):
78
  if not methods or not isinstance(methods, dict):
79
  continue
80
 
81
- # Get common parameters defined at path level
82
  common_params = methods.get("parameters", [])
83
-
84
  for method, details in methods.items():
85
  if method.lower() not in valid_methods:
86
  continue
87
 
88
- # Combine path-level and method-level parameters
89
  method_params = details.get("parameters", [])
90
  all_params = common_params + method_params
91
-
92
  endpoint_info = {
93
  "summary": details.get("summary", ""),
94
  "description": details.get("description", ""),
95
- "parameters": all_params # Include all parameters
96
  }
97
  endpoints[path][method.lower()] = endpoint_info
98
  return endpoints
@@ -111,790 +100,740 @@ def get_endpoints(spec_choice):
111
  return fetch_api_endpoints_yaml(spec_url)
112
 
113
  def group_endpoints(endpoints, spec_choice):
114
- """Group endpoints with special handling for Okta API"""
115
  groups = {}
116
-
117
- # Special handling for Okta endpoints
118
  if spec_choice == "Okta (JSON)":
119
  for path, methods in endpoints.items():
120
- # Remove /api/v1/ prefix and get the first segment
121
  clean_path = path.replace('/api/v1/', '')
122
  segments = clean_path.strip("/").split("/")
123
  group_key = segments[0] if segments else "other"
124
-
125
  if group_key not in groups:
126
  groups[group_key] = {}
127
  groups[group_key][path] = methods
128
  else:
129
- # Original grouping logic for other APIs
130
  for path, methods in endpoints.items():
131
  segments = path.strip("/").split("/")
132
  group_key = segments[0] if segments[0] != "" else "other"
133
  if group_key not in groups:
134
  groups[group_key] = {}
135
  groups[group_key][path] = methods
136
-
137
  return groups
138
 
139
  def verify_credentials(spec_choice, api_base_url, grant_type=None, client_id=None,
140
  client_secret=None, api_token=None, iiq_username=None, iiq_password=None):
141
- """Verify API credentials using existing handlers"""
142
-
143
- # Base URL validation
144
  if not api_base_url or not api_base_url.strip():
145
  return (
146
- gr.update(value=" API Base URL is required", visible=True),
147
- False, # endpoints_vis
148
- False # fetch_vis
149
  )
150
 
151
  try:
152
- # Credential validation based on API type
153
  if spec_choice == "Okta (JSON)":
154
  if not api_token or not api_token.strip():
155
  return (
156
- gr.update(value=" API Token is required", visible=True),
157
  False,
158
  False
159
  )
160
-
161
- # Test Okta connection
162
  result = handle_okta_call(api_base_url, api_token, "", {}, ["/api/v1/users/me"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  elif spec_choice == "SailPoint IdentityNow (YAML)":
165
- # Validate all required IdentityNow credentials
166
- if not all([
167
- grant_type and grant_type.strip(),
168
- client_id and client_id.strip(),
169
- client_secret and client_secret.strip()
170
- ]):
171
  return (
172
- gr.update(value=" All IdentityNow credentials (Grant Type, Client ID, Client Secret) are required", visible=True),
173
  False,
174
  False
175
  )
 
176
 
177
- # Test IdentityNow connection
178
- result = handle_identitynow_call(
179
- api_base_url,
180
- grant_type,
181
- client_id,
182
- client_secret,
183
- "", # session_id
184
- {}, # param_values
185
- [] # endpoints
186
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  elif spec_choice == "Sailpoint IIQ (YAML)":
189
- # Validate IIQ credentials
190
- if not all([
191
- iiq_username and iiq_username.strip(),
192
- iiq_password and iiq_password.strip()
193
- ]):
194
  return (
195
- gr.update(value=" Both IIQ username and password are required", visible=True),
196
  False,
197
  False
198
  )
 
199
 
200
- # Test IIQ connection
201
- result = handle_iiq_call(
202
- api_base_url,
203
- iiq_username,
204
- iiq_password,
205
- "", # session_id
206
- {}, # param_values
207
- [] # endpoints
208
- )
209
- else:
210
- return (
211
- gr.update(value=" Invalid API type selected", visible=True),
212
- False,
213
- False
214
- )
215
-
216
- # Validate API response
217
- if not isinstance(result, tuple) or len(result) < 4:
218
- return (
219
- gr.update(value="❌ Invalid API response format", visible=True),
220
- False,
221
- False
222
- )
223
-
224
- if "error" in result[0]:
225
- return (
226
- gr.update(value=f"❌ Connection failed: {result[0]['error']}", visible=True),
227
- False,
228
- False
229
- )
 
230
 
231
- # Success case
232
  return (
233
- gr.update(value=" API Connection successful! Credentials verified.", visible=True),
234
- True, # Show endpoints section
235
- True # Show fetch button
236
  )
237
 
238
  except requests.exceptions.ConnectionError:
239
  return (
240
- gr.update(value=" Connection Error: Could not reach the API. Please check the URL and your network connection.", visible=True),
241
  False,
242
  False
243
  )
244
  except Exception as e:
245
  return (
246
- gr.update(value=f" API Error: {str(e)}", visible=True),
247
  False,
248
  False
249
  )
250
 
251
- # CSS to match dummy.html
252
  custom_css = """
253
- body, .gradio-container {
254
- font-family: Arial, sans-serif;
255
- background: #f3faf7;
256
- color: #333
257
- }
258
- .container {
259
- max-width: 800px;
260
- margin: auto;
261
- padding: 20px;
262
- border-radius: 4px;
263
- background: black;
264
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
265
- }
266
- .container h1 {
267
- color: White !important;
268
- }
269
- .title h1 {
270
- color: #ff6347 !important;
271
- }
272
- .section {
273
- margin-bottom: 20px;
274
- padding-bottom: 10px;
275
- border-bottom: 1px solid #eee;
276
- }
277
- .section h2 {
278
- margin-top: 0;
279
- }
280
- .section #api_choice {
281
- background: #ff6347;
282
- }
283
- .section-text #api_base_url {
284
- background: #ff6347;
285
- }
286
- .section-acc #acc {
287
- background: #ff6347;
288
- }
289
- .section-acc #lock {
290
- background: gray;
291
- }
292
- .section-acc #unlock {
293
- background: gray;
294
- }
295
- input[type="text"], select, .gr-textbox, .gr-dropdown {
296
- width: 100%;
297
- padding: 8px;
298
- margin-bottom: 10px;
299
- border: 1px solid #ccc;
300
- border-radius: 4px;
301
- }
302
- .gr-button {
303
- background-color: #007BFF;
304
- color: #fff;
305
- border: none;
306
- padding: 10px 20px;
307
- border-radius: 4px;
308
- cursor: pointer;
309
- }
310
- .gr-textbox label {
311
- color: black !important;
312
- }
313
- .gradio-container .label-wrap {
314
- color: black !important;
315
- }
316
- .gr-button:disabled {
317
- background-color: #ccc;
318
- cursor: not-allowed;
319
- }
320
- .message {
321
- margin-top: 10px;
322
- padding: 8px;
323
- border-radius: 4px;
324
- font-size: 0.9em;
325
- }
326
- .success { background-color: #d4edda; color: #155724; }
327
- .error { background-color: #f8d7da; color: #721c24; }
328
- .endpoint { padding: 10px 0; border-bottom: 1px solid #eee; }
329
- .endpoint:last-child { border-bottom: none; }
330
- .endpoint-params { margin-left: 20px; margin-top: 5px; }
331
- .results {
332
- border: 1px solid #eee;
333
- padding: 10px;
334
- background: #fafafa;
335
- margin-bottom: 10px;
336
- white-space: pre;
337
- font-family: monospace;
338
- }
339
- .download-btn {
340
- margin-top: 10px;
341
- padding: 5px 10px;
342
- font-size: 0.9em;
343
- background-color: #007BFF;
344
- color: #fff;
345
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  """
347
- text_box_css = """
348
- #api_base_url {
349
- background: gr.themes.colors.red;
350
- }
351
- """
352
 
353
  with gr.Blocks(css=custom_css) as demo:
354
- gr.Markdown("# Connect to Your Data Source", elem_classes="title")
355
 
356
- # Session state
357
  session_id_state = gr.State("")
358
- confirmed_endpoints_state = gr.State([])
359
- display_values_state = gr.State([])
360
  locked_endpoints_state = gr.State([])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- # API Selection Section
363
- with gr.Group(elem_classes="section"):
364
- gr.Markdown("### Select an API")
365
- spec_choice = gr.Dropdown(
366
- choices=["Okta (JSON)", "SailPoint IdentityNow (YAML)", "Sailpoint IIQ (YAML)"],
367
- value="SailPoint IdentityNow (YAML)",
368
- label="Select an API",
369
- filterable=True,
370
- elem_id="api_choice"
371
- )
372
 
373
- # Credentials Section
374
- with gr.Group(elem_classes="section-text"):
375
- gr.Markdown("### Enter Credentials")
376
- api_base_url = gr.Textbox(label="API Base URL", placeholder="Enter your API Base URL", elem_id="api_base_url")
377
-
378
- # Authentication inputs (dynamically shown)
379
- with gr.Column(visible=True) as identitynow_auth:
380
- grant_type = gr.Textbox(label="Grant Type", value="client_credentials", elem_id="api_base_url")
381
- client_id = gr.Textbox(label="Client ID", placeholder="Enter your Client ID", elem_id="api_base_url")
382
- client_secret = gr.Textbox(label="Client Secret", placeholder="Enter your Client Secret", type="password", elem_id="api_base_url")
383
- with gr.Column(visible=False) as okta_auth:
384
- api_token = gr.Textbox(label="API Token", placeholder="Enter your API Token", type="password", elem_id="api_base_url")
385
- with gr.Column(visible=False) as iiq_auth:
386
- iiq_username = gr.Textbox(label="Username", placeholder="Enter your IIQ Username", elem_id="api_base_url")
387
- iiq_password = gr.Textbox(label="Password", placeholder="Enter your IIQ Password", type="password", elem_id="api_base_url")
388
-
389
- verify_btn = gr.Button("Verify Credentials")
390
- loading_indicator = gr.Markdown("", visible=False) # Add this
391
- credentials_message = gr.Markdown("")
392
 
 
 
 
 
 
393
  max_groups = 100
394
- # Endpoint Selection Section (hidden until verified)
395
- with gr.Group(elem_classes="section-acc", visible=False) as endpoints_section:
396
- gr.Markdown("### Choose Endpoints")
397
  accordion_placeholders = []
398
- with gr.Column():
399
- for i in range(max_groups):
400
- with gr.Accordion(label="", open=False, visible=False, elem_id= "acc") as acc:
401
- cb = gr.CheckboxGroup(label="", choices=[], value=[])
402
- accordion_placeholders.append((acc, cb))
403
-
404
- with gr.Row():
405
- lock_endpoints_btn = gr.Button("Lock Endpoints", variant="primary", elem_id= "lock")
406
- unlock_endpoints_btn = gr.Button("Unlock Endpoints", variant="secondary", elem_id="unlock", interactive=False)
407
- lock_loading_indicator = gr.Markdown("", visible=False) # Add loading indicator for lock operation
408
-
409
- # Parameter inputs (dynamically populated)
410
- with gr.Group(visible=False) as param_group:
411
- param_header = gr.Markdown("### Parameters Required")
412
- param_components = []
413
- for i in range(5):
414
- with gr.Group(visible=False) as group:
415
- param_display = gr.Markdown(visible=False)
416
- param_input = gr.Textbox(label="Parameter Value", visible=False)
417
- param_components.append((group, param_display, param_input))
418
-
419
- # Fetch Data Section (hidden until verified)
420
- with gr.Group(elem_classes="section", visible=False) as fetch_section:
421
- fetch_btn = gr.Button("Fetch Data", interactive=False)
422
- fetch_loading_indicator = gr.Markdown("", visible=False) # Add loading indicator for fetch operation
423
- # Results Section (hidden until data fetched)
424
- with gr.Group(elem_classes="section", visible=False) as results_section:
425
  gr.Markdown("### Results")
426
- results_out = gr.JSON(label="API Responses")
427
- download_out = gr.File(label="Download Session Data (ZIP)")
428
-
429
- # Event handlers
430
- def update_auth_fields(api_choice):
431
- """Update authentication fields and reset UI when API selection changes"""
432
- # Update auth field visibility
433
- auth_updates = {
434
- "Okta (JSON)": [False, True, False],
435
- "SailPoint IdentityNow (YAML)": [True, False, False],
436
- "Sailpoint IIQ (YAML)": [False, False, True]
437
- }
438
- visibilities = auth_updates.get(api_choice, [False, False, False])
439
-
440
  return [
441
- *[gr.update(visible=v) for v in visibilities], # auth fields (3)
442
- gr.update(visible=False), # endpoints_section
443
- gr.update(visible=False), # fetch_section
444
- gr.update(visible=False), # results_section
445
- gr.update(value=None), # results_out
446
- gr.update(value=None), # download_out
447
- gr.update(value="") # credentials_message
 
 
448
  ]
449
 
450
- def verify_and_load_endpoints(spec_choice, api_base_url, grant_type, client_id, client_secret,
451
- api_token, iiq_username, iiq_password):
452
- """Verify credentials and load endpoints into accordions"""
453
- # Show loading message first
454
  yield (
455
- gr.update(value="⏳ Verifying credentials and loading endpoints...", visible=True), # loading indicator
456
- gr.update(value="", visible=False), # credentials message
457
- gr.update(visible=False), # endpoints_section
458
- gr.update(visible=False), # fetch_section
459
- *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)] # accordion updates
460
  )
461
 
462
- # Verify credentials
463
- status, endpoints_vis, fetch_vis = verify_credentials(
464
- spec_choice, api_base_url, grant_type, client_id, client_secret,
465
- api_token, iiq_username, iiq_password
466
- )
467
-
 
 
 
468
  if not endpoints_vis:
469
  yield (
470
- gr.update(visible=False), # hide loading
471
- status, # show error in credentials message
472
- gr.update(visible=False), # endpoints_section
473
- gr.update(visible=False), # fetch_section
474
  *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
475
  )
476
  return
477
-
478
- try:
479
- endpoints = get_endpoints(spec_choice)
480
- if not endpoints:
481
- yield (
482
- gr.update(visible=False),
483
- gr.update(value="No endpoints found", visible=True),
484
- gr.update(visible=False),
485
- gr.update(visible=False),
486
- *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
487
- )
488
- return
489
-
490
- groups = group_endpoints(endpoints, spec_choice)
491
- group_keys = list(groups.keys())
492
- updates = []
493
-
494
- # Process each group
495
- for i in range(max_groups):
496
- if i < len(group_keys):
497
- group = group_keys[i]
498
- choices = []
499
- # Collect GET endpoints for this group
500
- for ep, methods in groups[group].items():
501
- if 'get' in methods:
502
- summary = methods['get'].get('summary', 'No summary')
503
- choices.append(f"{ep} | GET - {summary}")
504
-
505
- # Add updates for this accordion
506
- if choices:
507
- updates.extend([
508
- gr.update(
509
- label=f"Group: {group}",
510
- visible=True,
511
- open=False # Don't auto-open any accordion
512
- ),
513
- gr.update(
514
- choices=choices,
515
- value=[],
516
- visible=True,
517
- interactive=True # Make sure checkboxes are interactive
518
- )
519
- ])
520
- else:
521
- updates.extend([
522
- gr.update(visible=False, label=""),
523
- gr.update(visible=False, choices=[], value=[])
524
- ])
525
- else:
526
- # Hide unused accordions
527
- updates.extend([
528
- gr.update(visible=False, label=""),
529
- gr.update(visible=False, choices=[], value=[])
530
- ])
531
-
532
- yield (
533
- gr.update(visible=False), # hide loading
534
- gr.update(value="✅ Endpoints loaded successfully", visible=True),
535
- gr.update(visible=True),
536
- gr.update(visible=True),
537
- *updates
538
- )
539
-
540
- except Exception as e:
541
  yield (
542
- gr.update(visible=False), # hide loading
543
- gr.update(value=f"Error loading endpoints: {str(e)}", visible=True),
544
  gr.update(visible=False),
545
  gr.update(visible=False),
546
  *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
547
  )
 
548
 
549
- def unlock_selected_endpoints():
550
- """Unlock endpoints and reset parameter section"""
551
- return [
552
- gr.update(value="✅ Endpoints unlocked", visible=True), # message
553
- [], # locked_endpoints_state
554
- gr.update(interactive=True), # lock button
555
- gr.update(interactive=False), # unlock button - disable when unlocked
556
- *[gr.update(interactive=True) for _ in range(max_groups)], # checkboxes
557
- gr.update(visible=False), # param_group
558
- gr.update(visible=False), # param_header
559
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)], # parameter components
560
- gr.update(interactive=False) # fetch button
561
- ]
562
 
563
- def lock_selected_endpoints(*checkbox_values):
564
- """Collect and lock all selected endpoints"""
565
- # Show loading message first
566
  yield (
567
- gr.update(value="⏳ Locking endpoints and processing parameters...", visible=True), # loading indicator
568
- [], # locked_endpoints_state - clear state initially
569
- gr.update(interactive=False), # lock button
570
- gr.update(interactive=False), # unlock button
571
- *[gr.update(interactive=False) for _ in range(len(checkbox_values))], # checkboxes
572
- gr.update(visible=False), # param_group
573
- gr.update(visible=False), # param_header
574
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)], # parameter components
575
- gr.update(interactive=False) # fetch button
576
  )
577
 
578
- all_selected = []
579
- for checkbox_group in checkbox_values:
580
- if isinstance(checkbox_group, list) and checkbox_group:
581
- all_selected.extend(checkbox_group)
582
 
 
583
  if not all_selected:
584
- base_updates = [
585
- gr.update(visible=False), # loading indicator
586
- [], # locked_endpoints_state - keep empty
587
- gr.update(interactive=True), # lock button
588
- gr.update(interactive=False), # unlock button
589
- *[gr.update(interactive=True) for _ in range(len(checkbox_values))] # checkboxes
590
- ]
591
- param_updates = [
592
- gr.update(visible=False), # param_group
593
- gr.update(visible=False), # param_header
594
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)] # parameter components
595
  ]
596
- yield base_updates + param_updates + [gr.update(interactive=False)] # fetch button
597
- return
598
 
599
- base_updates = [
600
- gr.update(visible=False), # loading indicator
601
- all_selected, # locked_endpoints_state - update with selected endpoints
602
- gr.update(interactive=False), # lock button
603
- gr.update(interactive=True), # unlock button
604
- *[gr.update(interactive=False) for _ in range(len(checkbox_values))] # checkboxes
605
- ]
606
-
607
- param_updates = update_params(spec_choice.value, all_selected)
608
- fetch_update = [gr.update(interactive=True)]
609
 
610
- yield base_updates + param_updates + fetch_update
 
 
 
 
 
 
 
 
 
 
611
 
612
- def update_params(spec_choice_value, locked_endpoints):
613
- """Update parameter inputs based on selected endpoints"""
614
- if not locked_endpoints:
615
- return [gr.update(visible=False), gr.update(visible=False)] + [gr.update(visible=False)] * (len(param_components) * 3)
 
 
 
 
 
 
 
 
616
 
617
- # Get the API spec
618
- endpoints = get_endpoints(spec_choice_value)
619
-
620
- # First pass: collect all parameters from all selected endpoints
621
- all_parameters = [] # This will store ALL parameters from ALL endpoints
622
- required_params_count = 0
623
-
624
- # Collect parameters from all endpoints first
625
- for ep in locked_endpoints:
626
  endpoint = ep.split(" | GET")[0].strip()
627
  endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
628
-
629
- # Collect path parameters
630
  path_params = extract_path_params(endpoint)
631
- for param in path_params:
632
- all_parameters.append({
633
- 'endpoint': endpoint,
634
- 'type': 'path',
635
- 'name': param,
636
- 'required': True,
637
- 'description': f"Required path parameter for {endpoint}"
638
- })
639
- required_params_count += 1
640
-
641
- # Collect required query parameters
642
  query_params = extract_query_params(endpoint_spec)
643
- for param_type, name, required, description in query_params:
 
 
644
  if required:
645
- all_parameters.append({
646
- 'endpoint': endpoint,
647
- 'type': 'query',
648
- 'name': name,
649
- 'required': True,
650
- 'description': description
651
- })
652
- required_params_count += 1
653
-
654
- # Create updates for components
655
- updates = []
656
 
657
- if not required_params_count:
 
 
 
658
  updates.extend([
659
- gr.update(visible=False), # param_group
660
- gr.update(value="✅ No parameters required. You can proceed to call the API.", visible=True) # param_header
661
  ])
662
- updates.extend([gr.update(visible=False)] * (len(param_components) * 3))
663
  else:
664
- # Show parameter section
665
  updates.extend([
666
- gr.update(visible=True), # param_group
667
- gr.update(value=f"⚠️ Required Parameters ({required_params_count})", visible=True) # param_header
668
  ])
669
-
670
- # Update parameter components (limited to first 5 parameters)
671
  for i in range(5):
672
  if i < len(all_parameters):
673
- param = all_parameters[i] # Get parameter from the collected list
674
  emoji = "🔑" if param['type'] == 'path' else "🔍"
675
  updates.extend([
676
- gr.update(visible=True), # group
677
- gr.update(
678
- visible=True,
679
- value=f"Endpoint: {param['endpoint']} - {param['type'].title()} Parameter"
680
- ), # display
681
- gr.update(
682
- visible=True,
683
- label=f"{emoji} Enter {param['type']} parameter: {param['name']}",
684
- placeholder=param['description']
685
- ) # input
686
  ])
687
  else:
688
- # Hide unused parameter components
689
- updates.extend([
690
- gr.update(visible=False),
691
- gr.update(visible=False, value=""),
692
- gr.update(visible=False, label="")
693
- ])
694
-
695
  return updates
696
-
697
- def update_fetch_button(*param_values):
698
- """Enable fetch button if all required parameters are filled"""
699
- if all(val.strip() for val in param_values if val is not None):
700
  return gr.update(interactive=True)
701
- return gr.update(interactive=False)
702
-
703
- for _, _, input_box in param_components:
704
- input_box.change(
705
- fn=update_fetch_button,
706
- inputs=[input_box for _, _, input_box in param_components],
707
- outputs=[fetch_btn]
708
- )
709
 
710
- def handle_api_call(spec_choice, api_base_url, grant_type, client_id, client_secret, api_token,
711
- iiq_username, iiq_password, locked_endpoints, *param_values):
712
- """Handle API calls for selected endpoints with parameters"""
713
-
714
  yield (
715
- gr.update(value="⏳ Fetching data from API...", visible=True), # loading indicator
716
- gr.update(visible=False), # results_section
717
- gr.update(value=None), # results_out
718
- gr.update(value=None) # download_out
719
  )
720
 
721
- if not locked_endpoints:
722
- yield (
723
- gr.update(visible=False), # hide loading
724
- gr.update(visible=True),
725
- {"error": "No endpoints selected"},
726
- None
727
- )
728
- return
729
-
730
- # Initialize parameter dictionaries
731
- path_params = {}
732
- query_params = {}
733
  param_idx = 0
734
- endpoints = get_endpoints(spec_choice)
735
-
736
- # Process each endpoint and map parameters
737
  for ep in locked_endpoints:
738
- endpoint = ep.split(" | GET")[0].strip() # Extract endpoint path
739
  endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
740
-
741
- # Get path parameters
742
- path_params_list = extract_path_params(endpoint)
743
- for param in path_params_list:
744
- if param_idx < len(param_values) and param_values[param_idx]:
745
- path_params[param] = param_values[param_idx]
746
  param_idx += 1
747
-
748
- # Get query parameters
749
- query_params_list = extract_query_params(endpoint_spec)
750
- for _, name, required, _ in query_params_list:
751
- if required and param_idx < len(param_values) and param_values[param_idx]:
752
- query_params[name] = param_values[param_idx]
753
  param_idx += 1
754
 
755
- try:
756
- # Call appropriate API handler based on selection
757
- if spec_choice == "Okta (JSON)":
758
- result = handle_okta_call(
759
- api_base_url,
760
- api_token,
761
- "", # session_id
762
- path_params,
763
- query_params,
764
- locked_endpoints
765
- )
766
- elif spec_choice == "SailPoint IdentityNow (YAML)":
767
- result = handle_identitynow_call(
768
- api_base_url,
769
- grant_type,
770
- client_id,
771
- client_secret,
772
- "", # session_id
773
- path_params,
774
- query_params,
775
- locked_endpoints
776
- )
777
- else: # Sailpoint IIQ
778
- result = handle_iiq_call(
779
- api_base_url,
780
- iiq_username,
781
- iiq_password,
782
- "", # session_id
783
- path_params,
784
- query_params,
785
- locked_endpoints
786
- )
787
 
788
- # Show results section and return API response
789
- yield (
790
- gr.update(visible=False), # hide loading
791
- gr.update(visible=True), # Show results section
792
- result[0], # API response data
793
- result[1] # Download file if any
794
- )
795
 
796
- except Exception as e:
797
- yield (
798
- gr.update(visible=False), # hide loading
799
- gr.update(visible=True),
800
- {"error": f"API call failed: {str(e)}"},
801
- None
802
- )
 
 
 
 
 
 
 
 
 
803
 
804
- # Wire events
805
- spec_choice.change(fn=update_auth_fields, inputs=[spec_choice], outputs=[identitynow_auth, okta_auth, iiq_auth])
806
- verify_btn.click(
807
- fn=verify_and_load_endpoints,
808
- inputs=[
809
- spec_choice,
810
- api_base_url,
811
- grant_type,
812
- client_id,
813
- client_secret,
814
- api_token,
815
- iiq_username,
816
- iiq_password
817
- ],
818
- outputs=[
819
- loading_indicator, # Add loading indicator to outputs
820
- credentials_message,
821
- endpoints_section,
822
- fetch_section,
823
- *[comp for acc_cb in accordion_placeholders for comp in acc_cb]
824
- ]
825
- )
826
 
827
- spec_choice.change(
828
- fn=update_auth_fields,
829
- inputs=[spec_choice],
830
- outputs=[
831
- identitynow_auth, # auth columns
832
- okta_auth,
833
- iiq_auth,
834
- endpoints_section, # sections
835
- fetch_section,
836
- results_section,
837
- results_out, # results
838
- download_out,
839
- credentials_message # message
840
- ]
841
  )
842
 
 
 
 
 
 
843
 
844
- lock_endpoints_btn.click(
845
  fn=lock_selected_endpoints,
846
  inputs=[cb for _, cb in accordion_placeholders],
847
- outputs=[
848
- lock_loading_indicator, # Add loading indicator
849
- locked_endpoints_state, # Add state to outputs
850
- lock_endpoints_btn,
851
- unlock_endpoints_btn,
852
- *[cb for _, cb in accordion_placeholders],
853
- param_group,
854
- param_header,
855
- *[comp for group, display, input_box in param_components
856
- for comp in (group, display, input_box)],
857
- fetch_btn
858
- ]
859
  )
860
 
861
- unlock_endpoints_btn.click(
862
  fn=unlock_selected_endpoints,
863
  inputs=[],
864
- outputs=[
865
- credentials_message,
866
- locked_endpoints_state,
867
- lock_endpoints_btn,
868
- unlock_endpoints_btn,
869
- *[cb for _, cb in accordion_placeholders],
870
- param_group,
871
- param_header,
872
- *[comp for group, display, input_box in param_components
873
- for comp in (group, display, input_box)],
874
- fetch_btn
875
- ]
876
  )
877
-
 
 
 
 
 
 
 
878
  fetch_btn.click(
879
  fn=handle_api_call,
880
- inputs=[
881
- spec_choice,
882
- api_base_url,
883
- grant_type,
884
- client_id,
885
- client_secret,
886
- api_token,
887
- iiq_username,
888
- iiq_password,
889
- locked_endpoints_state,
890
- *[input_box for _, _, input_box in param_components]
891
- ],
892
- outputs=[
893
- fetch_loading_indicator, # Add loading indicator
894
- results_section,
895
- results_out,
896
- download_out
897
- ]
898
  )
899
 
900
  if __name__ == "__main__":
 
14
  env_path = script_dir / '.env'
15
  load_dotenv(dotenv_path=env_path)
16
 
17
+ # Helper functions (unchanged)
 
 
 
18
  def fetch_api_endpoints_yaml(spec_url):
19
  try:
20
  response = requests.get(spec_url)
 
36
  if not methods or not isinstance(methods, dict):
37
  continue
38
 
 
39
  common_params = methods.get("parameters", [])
 
40
  for method, details in methods.items():
41
  if method.lower() not in valid_methods:
42
  continue
43
 
 
44
  method_params = details.get("parameters", [])
45
  all_params = common_params + method_params
 
46
  endpoint_info = {
47
  "summary": details.get("summary", ""),
48
  "description": details.get("description", ""),
49
+ "parameters": all_params
50
  }
51
  endpoints[path][method.lower()] = endpoint_info
52
  return endpoints
 
71
  if not methods or not isinstance(methods, dict):
72
  continue
73
 
 
74
  common_params = methods.get("parameters", [])
 
75
  for method, details in methods.items():
76
  if method.lower() not in valid_methods:
77
  continue
78
 
 
79
  method_params = details.get("parameters", [])
80
  all_params = common_params + method_params
 
81
  endpoint_info = {
82
  "summary": details.get("summary", ""),
83
  "description": details.get("description", ""),
84
+ "parameters": all_params
85
  }
86
  endpoints[path][method.lower()] = endpoint_info
87
  return endpoints
 
100
  return fetch_api_endpoints_yaml(spec_url)
101
 
102
  def group_endpoints(endpoints, spec_choice):
 
103
  groups = {}
 
 
104
  if spec_choice == "Okta (JSON)":
105
  for path, methods in endpoints.items():
 
106
  clean_path = path.replace('/api/v1/', '')
107
  segments = clean_path.strip("/").split("/")
108
  group_key = segments[0] if segments else "other"
 
109
  if group_key not in groups:
110
  groups[group_key] = {}
111
  groups[group_key][path] = methods
112
  else:
 
113
  for path, methods in endpoints.items():
114
  segments = path.strip("/").split("/")
115
  group_key = segments[0] if segments[0] != "" else "other"
116
  if group_key not in groups:
117
  groups[group_key] = {}
118
  groups[group_key][path] = methods
 
119
  return groups
120
 
121
  def verify_credentials(spec_choice, api_base_url, grant_type=None, client_id=None,
122
  client_secret=None, api_token=None, iiq_username=None, iiq_password=None):
 
 
 
123
  if not api_base_url or not api_base_url.strip():
124
  return (
125
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Base URL is required</div>", visible=True),
126
+ False,
127
+ False
128
  )
129
 
130
  try:
 
131
  if spec_choice == "Okta (JSON)":
132
  if not api_token or not api_token.strip():
133
  return (
134
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Token is required</div>", visible=True),
135
  False,
136
  False
137
  )
 
 
138
  result = handle_okta_call(api_base_url, api_token, "", {}, ["/api/v1/users/me"])
139
+
140
+ # Check the response for the specific endpoint
141
+ if isinstance(result[0], dict) and "/api/v1/users/me" in result[0]:
142
+ response = result[0]["/api/v1/users/me"]
143
+
144
+ # Check if response is an error string
145
+ if isinstance(response, str) and response.startswith("Error:"):
146
+ return (
147
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
148
+ False,
149
+ False
150
+ )
151
+
152
+ # Check if response is an error dict
153
+ if isinstance(response, dict) and "error" in response:
154
+ return (
155
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
156
+ False,
157
+ False
158
+ )
159
+ else:
160
+ # Unexpected response format
161
+ return (
162
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from Okta</div>", visible=True),
163
+ False,
164
+ False
165
+ )
166
 
167
  elif spec_choice == "SailPoint IdentityNow (YAML)":
168
+ if not all([grant_type and grant_type.strip(), client_id and client_id.strip(), client_secret and client_secret.strip()]):
 
 
 
 
 
169
  return (
170
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> All IdentityNow credentials are required</div>", visible=True),
171
  False,
172
  False
173
  )
174
+ result = handle_identitynow_call(api_base_url, grant_type, client_id, client_secret, "", {}, ["/beta/test-connection"])
175
 
176
+ # Check the response
177
+ if isinstance(result[0], dict):
178
+ # Check for direct error in response
179
+ if "error" in result[0]:
180
+ return (
181
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {result[0]['error']}</div>", visible=True),
182
+ False,
183
+ False
184
+ )
185
+
186
+ # Check for endpoint-specific error
187
+ if "/beta/test-connection" in result[0]:
188
+ response = result[0]["/beta/test-connection"]
189
+ if isinstance(response, str) and response.startswith("Error:"):
190
+ return (
191
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
192
+ False,
193
+ False
194
+ )
195
+ if isinstance(response, dict) and "error" in response:
196
+ return (
197
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
198
+ False,
199
+ False
200
+ )
201
+ else:
202
+ return (
203
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from IdentityNow</div>", visible=True),
204
+ False,
205
+ False
206
+ )
207
 
208
  elif spec_choice == "Sailpoint IIQ (YAML)":
209
+ if not all([iiq_username and iiq_username.strip(), iiq_password and iiq_password.strip()]):
 
 
 
 
210
  return (
211
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Both IIQ username and password are required</div>", visible=True),
212
  False,
213
  False
214
  )
215
+ result = handle_iiq_call(api_base_url, iiq_username, iiq_password, "", {}, ["/ping"])
216
 
217
+ # Check the response
218
+ if isinstance(result[0], dict):
219
+ # Check for direct error in response
220
+ if "error" in result[0]:
221
+ return (
222
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {result[0]['error']}</div>", visible=True),
223
+ False,
224
+ False
225
+ )
226
+
227
+ # Check for endpoint-specific error
228
+ if "/ping" in result[0]:
229
+ response = result[0]["/ping"]
230
+ if isinstance(response, str) and response.startswith("Error:"):
231
+ return (
232
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
233
+ False,
234
+ False
235
+ )
236
+ if isinstance(response, dict) and "error" in response:
237
+ return (
238
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
239
+ False,
240
+ False
241
+ )
242
+ else:
243
+ return (
244
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from IIQ</div>", visible=True),
245
+ False,
246
+ False
247
+ )
248
 
249
+ # If no error is detected and response is valid, assume success
250
  return (
251
+ gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Credentials verified successfully!</div>", visible=True),
252
+ True,
253
+ True
254
  )
255
 
256
  except requests.exceptions.ConnectionError:
257
  return (
258
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Could not reach the API. Check URL and network.</div>", visible=True),
259
  False,
260
  False
261
  )
262
  except Exception as e:
263
  return (
264
+ gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Error: {str(e)}</div>", visible=True),
265
  False,
266
  False
267
  )
268
 
269
+ # Custom CSS (unchanged)
270
  custom_css = """
271
+ @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
272
+
273
+ body, .gradio-container {
274
+ font-family: 'Arial', sans-serif;
275
+ background: #f3f3f3;
276
+ color: #333;
277
+ margin: 0;
278
+ padding: 0;
279
+ }
280
+
281
+ .title {
282
+ text-align: center;
283
+ margin: 30px 0;
284
+ font-size: 2em;
285
+ color: #222;
286
+ animation: fadeIn 1s ease-in;
287
+ }
288
+
289
+ .api-cards {
290
+ display: flex;
291
+ justify-content: center;
292
+ gap: 20px;
293
+ margin-bottom: 30px;
294
+ animation: slideUp 0.5s ease-out;
295
+ }
296
+
297
+ .api-card {
298
+ background: white;
299
+ padding: 20px;
300
+ border-radius: 10px;
301
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
302
+ text-align: center;
303
+ cursor: pointer;
304
+ transition: transform 0.2s, box-shadow 0.2s;
305
+ width: 200px;
306
+ }
307
+
308
+ .api-card:hover {
309
+ transform: translateY(-5px);
310
+ box-shadow: 0 6px 12px rgba(0,0,0,0.15);
311
+ }
312
+
313
+ .api-card i {
314
+ font-size: 32px;
315
+ color: #007BFF;
316
+ margin-bottom: 10px;
317
+ }
318
+
319
+ .api-card h2 {
320
+ font-size: 18px;
321
+ margin: 10px 0;
322
+ }
323
+
324
+ .api-card p {
325
+ font-size: 14px;
326
+ color: #666;
327
+ }
328
+
329
+ .cred-section {
330
+ background: white;
331
+ padding: 20px;
332
+ border-radius: 10px;
333
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
334
+ margin-bottom: 20px;
335
+ animation: fadeIn 0.5s ease-in;
336
+ }
337
+
338
+ .action-btn {
339
+ background-color: #007BFF;
340
+ color: white;
341
+ border: none;
342
+ padding: 10px 20px;
343
+ border-radius: 5px;
344
+ cursor: pointer;
345
+ transition: background-color 0.2s;
346
+ }
347
+
348
+ .action-btn:hover {
349
+ background-color: #0056b3;
350
+ }
351
+
352
+ .loading-spinner {
353
+ border: 4px solid #f3f3f3;
354
+ border-top: 4px solid #3498db;
355
+ border-radius: 50%;
356
+ width: 30px;
357
+ height: 30px;
358
+ animation: spin 1s linear infinite;
359
+ margin: 10px auto;
360
+ }
361
+
362
+ @keyframes spin {
363
+ 0% { transform: rotate(0deg); }
364
+ 100% { transform: rotate(360deg); }
365
+ }
366
+
367
+ .success-message, .error-message {
368
+ padding: 10px;
369
+ border-radius: 5px;
370
+ margin-top: 10px;
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 10px;
374
+ animation: slideIn 0.3s ease-out;
375
+ }
376
+
377
+ .success-message {
378
+ background: #d4edda;
379
+ color: #155724;
380
+ }
381
+
382
+ .error-message {
383
+ background: #f8d7da;
384
+ color: #721c24;
385
+ }
386
+
387
+ .endpoints-section, .param-group, .fetch-section, .results-section {
388
+ background: white;
389
+ padding: 20px;
390
+ border-radius: 10px;
391
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
392
+ margin-bottom: 20px;
393
+ animation: fadeIn 0.5s ease-in;
394
+ }
395
+
396
+ .custom-accordion {
397
+ border: 1px solid #ddd;
398
+ border-radius: 5px;
399
+ margin-bottom: 10px;
400
+ }
401
+
402
+ .custom-accordion .gr-accordion-header {
403
+ background: #f9f9f9;
404
+ padding: 10px;
405
+ cursor: pointer;
406
+ font-weight: bold;
407
+ }
408
+
409
+ .custom-checkbox .gr-checkbox-group {
410
+ display: flex;
411
+ flex-direction: column;
412
+ gap: 5px;
413
+ }
414
+
415
+ .reset-btn {
416
+ background-color: #6c757d;
417
+ color: white;
418
+ border: none;
419
+ padding: 8px 16px;
420
+ border-radius: 5px;
421
+ cursor: pointer;
422
+ transition: background-color 0.2s;
423
+ }
424
+
425
+ .reset-btn:hover:enabled {
426
+ background-color: #5a6268;
427
+ }
428
+
429
+ .gr-json {
430
+ background: #f9f9f9;
431
+ padding: 10px;
432
+ border: 1px solid #ddd;
433
+ border-radius: 5px;
434
+ max-height: 400px;
435
+ overflow: auto;
436
+ font-family: monospace;
437
+ }
438
+
439
+ .download-btn {
440
+ background-color: #28a745;
441
+ color: white;
442
+ border: none;
443
+ padding: 10px 20px;
444
+ border-radius: 5px;
445
+ cursor: pointer;
446
+ transition: background-color 0.2s;
447
+ }
448
+
449
+ .download-btn:hover {
450
+ background-color: #218838;
451
+ }
452
+
453
+ @keyframes fadeIn {
454
+ from { opacity: 0; }
455
+ to { opacity: 1; }
456
+ }
457
+
458
+ @keyframes slideUp {
459
+ from { transform: translateY(20px); opacity: 0; }
460
+ to { transform: translateY(0); opacity: 1; }
461
+ }
462
+
463
+ @keyframes slideIn {
464
+ from { transform: translateX(-20px); opacity: 0; }
465
+ to { transform: translateX(0); opacity: 1; }
466
+ }
467
+
468
+ .banner {
469
+ background-color: #007BFF;
470
+ color: white;
471
+ padding: 20px;
472
+ text-align: center;
473
+ font-size: 24px;
474
+ font-weight: bold;
475
+ margin-bottom: 20px;
476
+ }
477
  """
 
 
 
 
 
478
 
479
  with gr.Blocks(css=custom_css) as demo:
480
+ gr.HTML("<div class='banner'>Data Connector Demo</div>")
481
 
482
+ # Session states
483
  session_id_state = gr.State("")
 
 
484
  locked_endpoints_state = gr.State([])
485
+ required_params_count = gr.State(0)
486
+
487
+ # API Selection
488
+ # Define API options as a list of dictionaries
489
+ api_options = [
490
+ {"icon": "fa-cloud", "title": "Okta", "description": "Identity and Access Management", "value": "Okta (JSON)"},
491
+ {"icon": "fa-user-shield", "title": "SailPoint IdentityNow", "description": "Cloud Identity Governance", "value": "SailPoint IdentityNow (YAML)"},
492
+ {"icon": "fa-network-wired", "title": "Sailpoint IIQ", "description": "On-premise Identity Governance", "value": "Sailpoint IIQ (YAML)"},
493
+ ]
494
+
495
+ # Generate choices with plain text labels and corresponding values
496
+ choices = [(opt["title"], opt["value"]) for opt in api_options]
497
+
498
+ # Define the radio button component
499
+ api_choice = gr.Radio(
500
+ choices=choices,
501
+ label="",
502
+ type="value",
503
+ elem_classes="api-cards"
504
+ )
505
 
506
+ # Credential Sections
507
+ with gr.Column(visible=False, elem_classes="cred-section") as okta_cred:
508
+ api_base_url_okta = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-okta-domain.okta.com")
509
+ api_token = gr.Textbox(label="API Token", type="password", placeholder="Enter your Okta API token")
 
 
 
 
 
 
510
 
511
+ with gr.Column(visible=False, elem_classes="cred-section") as inow_cred:
512
+ api_base_url_inow = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-org.api.identitynow.com")
513
+ grant_type = gr.Textbox(label="Grant Type", value="client_credentials", interactive=False)
514
+ client_id = gr.Textbox(label="Client ID", placeholder="Enter your client ID")
515
+ client_secret = gr.Textbox(label="Client Secret", type="password", placeholder="Enter your client secret")
516
+
517
+ with gr.Column(visible=False, elem_classes="cred-section") as iiq_cred:
518
+ api_base_url_iiq = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-iiq-server.com/identityiq")
519
+ username = gr.Textbox(label="Username", placeholder="Enter your IIQ username")
520
+ password = gr.Textbox(label="Password", type="password", placeholder="Enter your IIQ password")
 
 
 
 
 
 
 
 
 
521
 
522
+ verify_btn = gr.Button("Verify Credentials", elem_classes="action-btn")
523
+ loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
524
+ credentials_message = gr.HTML("", visible=False)
525
+
526
+ # Endpoints Section
527
  max_groups = 100
528
+ with gr.Group(elem_classes="endpoints-section", visible=False) as endpoints_section:
529
+ reset_btn = gr.Button("Reset", interactive=False, elem_classes="reset-btn")
 
530
  accordion_placeholders = []
531
+ for i in range(max_groups):
532
+ with gr.Accordion(label="", open=False, visible=False, elem_classes="custom-accordion") as acc:
533
+ cb = gr.CheckboxGroup(label="", choices=[], value=[], elem_classes="custom-checkbox")
534
+ accordion_placeholders.append((acc, cb))
535
+ continue_btn = gr.Button("Continue", elem_classes="action-btn")
536
+
537
+ # Parameter Section
538
+ with gr.Group(elem_classes="param-group", visible=False) as param_group:
539
+ param_header = gr.Markdown("### Parameters Required")
540
+ param_components = []
541
+ for i in range(5):
542
+ with gr.Group(visible=False) as group:
543
+ param_display = gr.Markdown(visible=False)
544
+ param_input = gr.Textbox(label="Parameter Value", visible=False)
545
+ param_components.append((group, param_display, param_input))
546
+
547
+ # Fetch Section
548
+ with gr.Group(elem_classes="fetch-section", visible=False) as fetch_section:
549
+ fetch_btn = gr.Button("Fetch Data", interactive=False, elem_classes="action-btn")
550
+ fetch_loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
551
+
552
+ # Results Section
553
+ with gr.Group(elem_classes="results-section", visible=False) as results_section:
 
 
 
 
554
  gr.Markdown("### Results")
555
+ results_out = gr.JSON(label="API Responses", elem_classes="gr-json")
556
+ download_out = gr.File(label="Download Session Data (ZIP)", elem_classes="download-btn")
557
+
558
+ # Event Handlers
559
+ def update_cred_sections(api_choice):
 
 
 
 
 
 
 
 
 
560
  return [
561
+ gr.update(visible=api_choice=="Okta (JSON)"),
562
+ gr.update(visible=api_choice=="SailPoint IdentityNow (YAML)"),
563
+ gr.update(visible=api_choice=="Sailpoint IIQ (YAML)"),
564
+ gr.update(visible=False),
565
+ gr.update(visible=False),
566
+ gr.update(visible=False),
567
+ gr.update(value=None),
568
+ gr.update(value=None),
569
+ gr.update(value="")
570
  ]
571
 
572
+ def verify_and_load_endpoints(api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type,
573
+ client_id, client_secret, api_base_url_iiq, username, password):
 
 
574
  yield (
575
+ gr.update(visible=True),
576
+ gr.update(value=""),
577
+ gr.update(visible=False),
578
+ gr.update(visible=False),
579
+ *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
580
  )
581
 
582
+ if api_choice == "Okta (JSON)":
583
+ status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_okta, api_token=api_token)
584
+ elif api_choice == "SailPoint IdentityNow (YAML)":
585
+ status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_inow, grant_type=grant_type,
586
+ client_id=client_id, client_secret=client_secret)
587
+ else:
588
+ status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_iiq, iiq_username=username,
589
+ iiq_password=password)
590
+
591
  if not endpoints_vis:
592
  yield (
593
+ gr.update(visible=False),
594
+ status,
595
+ gr.update(visible=False),
596
+ gr.update(visible=False),
597
  *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
598
  )
599
  return
600
+
601
+ endpoints = get_endpoints(api_choice)
602
+ if not endpoints:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  yield (
604
+ gr.update(visible=False),
605
+ gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> No endpoints found</div>", visible=True),
606
  gr.update(visible=False),
607
  gr.update(visible=False),
608
  *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
609
  )
610
+ return
611
 
612
+ groups = group_endpoints(endpoints, api_choice)
613
+ group_keys = list(groups.keys())
614
+ updates = []
615
+ for i in range(max_groups):
616
+ if i < len(group_keys):
617
+ group = group_keys[i]
618
+ choices = [f"{ep} | GET - {methods['get'].get('summary', 'No summary')}" for ep, methods in groups[group].items() if 'get' in methods]
619
+ updates.extend([
620
+ gr.update(label=group, visible=bool(choices), open=False),
621
+ gr.update(choices=choices, value=[], visible=bool(choices), interactive=True)
622
+ ])
623
+ else:
624
+ updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
625
 
 
 
 
626
  yield (
627
+ gr.update(visible=False),
628
+ gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Endpoints loaded successfully</div>", visible=True),
629
+ gr.update(visible=True),
630
+ gr.update(visible=True),
631
+ *updates
 
 
 
 
632
  )
633
 
634
+ def lock_selected_endpoints(*checkbox_values):
635
+ all_selected = [sel for group in checkbox_values if isinstance(group, list) for sel in group]
636
+ print("Selected endpoints:", all_selected)
 
637
 
638
+ # Handle empty selection case
639
  if not all_selected:
640
+ return [
641
+ [], # Raw empty list for locked_endpoints_state
642
+ gr.update(interactive=True),
643
+ gr.update(interactive=False),
644
+ *[gr.update(interactive=True) for _ in range(max_groups)],
645
+ gr.update(visible=False),
646
+ gr.update(visible=False),
647
+ *[gr.update(visible=False) for _ in range(len(param_components) * 3)],
648
+ gr.update(interactive=False),
649
+ gr.update(value=0)
 
650
  ]
 
 
651
 
652
+ # Process selected endpoints
653
+ all_parameters = get_required_params(api_choice.value, all_selected)
654
+ param_updates = update_params(api_choice.value, all_selected)
655
+ has_params = len(all_parameters) > 0
656
+ fetch_update = gr.update(interactive=not has_params)
 
 
 
 
 
657
 
658
+ # Create return list with raw endpoints list as first item
659
+ print("About to return with value:", all_selected)
660
+ return [
661
+ all_selected, # Raw list for locked_endpoints_state
662
+ gr.update(interactive=False),
663
+ gr.update(interactive=True),
664
+ *[gr.update(interactive=False) for _ in range(max_groups)],
665
+ *param_updates,
666
+ fetch_update,
667
+ gr.update(value=len(all_parameters))
668
+ ]
669
 
670
+ def unlock_selected_endpoints():
671
+ return [
672
+ gr.update(value=[]),
673
+ gr.update(interactive=True),
674
+ gr.update(interactive=False),
675
+ *[gr.update(interactive=True) for _ in range(max_groups)],
676
+ gr.update(visible=False),
677
+ gr.update(visible=False),
678
+ *[gr.update(visible=False) for _ in range(len(param_components) * 3)],
679
+ gr.update(interactive=False),
680
+ gr.update(value=0)
681
+ ]
682
 
683
+ def get_required_params(spec_choice, selected_endpoints):
684
+ endpoints = get_endpoints(spec_choice)
685
+ all_params = []
686
+ for ep in selected_endpoints:
 
 
 
 
 
687
  endpoint = ep.split(" | GET")[0].strip()
688
  endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
 
 
689
  path_params = extract_path_params(endpoint)
 
 
 
 
 
 
 
 
 
 
 
690
  query_params = extract_query_params(endpoint_spec)
691
+ for param in path_params:
692
+ all_params.append({"endpoint": endpoint, "type": "path", "name": param, "description": "Path parameter"})
693
+ for _, name, required, desc in query_params:
694
  if required:
695
+ all_params.append({"endpoint": endpoint, "type": "query", "name": name, "description": desc or "Query parameter"})
696
+ return all_params
 
 
 
 
 
 
 
 
 
697
 
698
+ def update_params(spec_choice_value, locked_endpoints):
699
+ all_parameters = get_required_params(spec_choice_value, locked_endpoints)
700
+ updates = []
701
+ if not all_parameters:
702
  updates.extend([
703
+ gr.update(visible=True),
704
+ gr.update(value="✅ No parameters are required. Press 'Fetch Data' to get data.", visible=True)
705
  ])
706
+ updates.extend([gr.update(visible=False) for _ in range(len(param_components) * 3)])
707
  else:
 
708
  updates.extend([
709
+ gr.update(visible=True),
710
+ gr.update(value=f"⚠️ Required Parameters ({len(all_parameters)})", visible=True)
711
  ])
 
 
712
  for i in range(5):
713
  if i < len(all_parameters):
714
+ param = all_parameters[i]
715
  emoji = "🔑" if param['type'] == 'path' else "🔍"
716
  updates.extend([
717
+ gr.update(visible=True),
718
+ gr.update(visible=True, value=f"Endpoint: {param['endpoint']} - {param['type'].title()} Parameter"),
719
+ gr.update(visible=True, label=f"{emoji} {param['name']}", placeholder=param['description'])
 
 
 
 
 
 
 
720
  ])
721
  else:
722
+ updates.extend([gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)])
 
 
 
 
 
 
723
  return updates
724
+
725
+ def update_fetch_button(required_params_count, *param_values):
726
+ if required_params_count == 0:
 
727
  return gr.update(interactive=True)
728
+ filled = all(val and val.strip() for val in param_values[:required_params_count])
729
+ return gr.update(interactive=filled)
 
 
 
 
 
 
730
 
731
+ def handle_api_call(api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id,
732
+ client_secret, api_base_url_iiq, username, password, locked_endpoints, *param_values):
733
+ print("API call received endpoints:", locked_endpoints)
 
734
  yield (
735
+ gr.update(visible=True),
736
+ gr.update(visible=False),
737
+ gr.update(value=None),
738
+ gr.update(value=None)
739
  )
740
 
741
+ param_dict = {}
 
 
 
 
 
 
 
 
 
 
 
742
  param_idx = 0
743
+ endpoints = get_endpoints(api_choice)
 
 
744
  for ep in locked_endpoints:
745
+ endpoint = ep.split(" | GET")[0].strip()
746
  endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
747
+ for param in extract_path_params(endpoint):
748
+ if param_idx < len(param_values):
749
+ param_dict[param] = param_values[param_idx]
 
 
 
750
  param_idx += 1
751
+ for _, name, required, _ in extract_query_params(endpoint_spec):
752
+ if required and param_idx < len(param_values):
753
+ param_dict[name] = param_values[param_idx]
 
 
 
754
  param_idx += 1
755
 
756
+ # Clean the endpoints to pass only the paths to the API call functions
757
+ clean_endpoints = [ep.split(" | GET")[0].strip() for ep in locked_endpoints]
758
+ print("Selected endpoints to call:", clean_endpoints)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
 
760
+ if api_choice == "Okta (JSON)":
761
+ # Pass locked_endpoints as the only item in a list to match expected format
762
+ result = handle_okta_call(api_base_url_okta, api_token, "", param_dict, clean_endpoints)
763
+ elif api_choice == "SailPoint IdentityNow (YAML)":
764
+ result = handle_identitynow_call(api_base_url_inow, grant_type, client_id, client_secret, "", param_dict, clean_endpoints)
765
+ else:
766
+ result = handle_iiq_call(api_base_url_iiq, username, password, "", param_dict, clean_endpoints)
767
 
768
+ # Process the API result for display
769
+ if not result or not isinstance(result, tuple) or len(result) < 2:
770
+ data = {"error": "Invalid API response format"}
771
+ download_file = None
772
+ else:
773
+ api_data = result[0]
774
+ download_file = result[1] if len(result) > 1 else None
775
+
776
+ if isinstance(api_data, dict):
777
+ data = api_data
778
+ elif isinstance(api_data, list):
779
+ data = {"results": api_data}
780
+ elif api_data is None:
781
+ data = {"message": "No data returned from the API"}
782
+ else:
783
+ data = {"raw_response": str(api_data)}
784
 
785
+ yield (
786
+ gr.update(visible=False),
787
+ gr.update(visible=True),
788
+ gr.update(value=data),
789
+ gr.update(value=download_file)
790
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
 
792
+ # Wire Events
793
+ api_choice.change(
794
+ fn=update_cred_sections,
795
+ inputs=[api_choice],
796
+ outputs=[okta_cred, inow_cred, iiq_cred, endpoints_section, fetch_section, results_section, results_out, download_out, credentials_message]
 
 
 
 
 
 
 
 
 
797
  )
798
 
799
+ verify_btn.click(
800
+ fn=verify_and_load_endpoints,
801
+ inputs=[api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id, client_secret, api_base_url_iiq, username, password],
802
+ outputs=[loading_indicator, credentials_message, endpoints_section, fetch_section] + [comp for acc, cb in accordion_placeholders for comp in (acc, cb)]
803
+ )
804
 
805
+ continue_btn.click(
806
  fn=lock_selected_endpoints,
807
  inputs=[cb for _, cb in accordion_placeholders],
808
+ outputs=[locked_endpoints_state, continue_btn, reset_btn] +
809
+ [cb for _, cb in accordion_placeholders] +
810
+ [param_group, param_header] +
811
+ [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
812
+ [fetch_btn, required_params_count]
 
 
 
 
 
 
 
813
  )
814
 
815
+ reset_btn.click(
816
  fn=unlock_selected_endpoints,
817
  inputs=[],
818
+ outputs=[locked_endpoints_state, continue_btn, reset_btn] +
819
+ [cb for _, cb in accordion_placeholders] +
820
+ [param_group, param_header] +
821
+ [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
822
+ [fetch_btn, required_params_count]
 
 
 
 
 
 
 
823
  )
824
+
825
+ for _, _, input_box in param_components:
826
+ input_box.change(
827
+ fn=update_fetch_button,
828
+ inputs=[required_params_count] + [input_box for _, _, input_box in param_components],
829
+ outputs=[fetch_btn]
830
+ )
831
+
832
  fetch_btn.click(
833
  fn=handle_api_call,
834
+ inputs=[api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id, client_secret, api_base_url_iiq, username, password, locked_endpoints_state] +
835
+ [input_box for _, _, input_box in param_components],
836
+ outputs=[fetch_loading_indicator, results_section, results_out, download_out]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  )
838
 
839
  if __name__ == "__main__":