systemlink-cli 1.3.1__py3-none-any.whl
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.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
slcli/dff_click.py
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
"""CLI commands for managing SystemLink Custom Fields."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import questionary
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .platform import require_feature
|
|
13
|
+
from .universal_handlers import FilteredResponse, UniversalResponseHandler
|
|
14
|
+
from .utils import (
|
|
15
|
+
ExitCodes,
|
|
16
|
+
get_base_url,
|
|
17
|
+
get_workspace_map,
|
|
18
|
+
handle_api_error,
|
|
19
|
+
load_json_file,
|
|
20
|
+
make_api_request,
|
|
21
|
+
sanitize_filename,
|
|
22
|
+
save_json_file,
|
|
23
|
+
)
|
|
24
|
+
from .web_editor import launch_dff_editor
|
|
25
|
+
from .workspace_utils import (
|
|
26
|
+
WorkspaceFormatter,
|
|
27
|
+
filter_by_workspace,
|
|
28
|
+
get_effective_workspace,
|
|
29
|
+
resolve_workspace_filter,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Valid resource types for Custom Fields
|
|
33
|
+
VALID_RESOURCE_TYPES = [
|
|
34
|
+
"workorder:workorder",
|
|
35
|
+
"workitem:workitem",
|
|
36
|
+
"asset:asset",
|
|
37
|
+
"system:system",
|
|
38
|
+
"testmonitor:product",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Valid field types for Custom Fields
|
|
42
|
+
VALID_FIELD_TYPES = [
|
|
43
|
+
"Text",
|
|
44
|
+
"Number",
|
|
45
|
+
"Boolean",
|
|
46
|
+
"Enum",
|
|
47
|
+
"DateTime",
|
|
48
|
+
"Table",
|
|
49
|
+
"LinkedResource",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Help text for resource type parameter
|
|
53
|
+
RESOURCE_TYPE_HELP = f"Resource type. Valid values: {', '.join(VALID_RESOURCE_TYPES)}"
|
|
54
|
+
|
|
55
|
+
# Help text for field type parameter
|
|
56
|
+
FIELD_TYPE_HELP = f"Field type. Valid values: {', '.join(VALID_FIELD_TYPES)}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _handle_dff_error_response(error_data: Dict[str, Any], operation: str = "operation") -> None:
|
|
60
|
+
"""Parse and display DFF-specific error responses."""
|
|
61
|
+
# Check for DFF-specific error structure with failedConfigurations, failedGroups, etc.
|
|
62
|
+
if any(key in error_data for key in ["failedConfigurations", "failedGroups", "failedFields"]):
|
|
63
|
+
_handle_dff_creation_errors(error_data, operation)
|
|
64
|
+
|
|
65
|
+
elif "error" in error_data and "innerErrors" in error_data["error"]:
|
|
66
|
+
# Handle nested error structure
|
|
67
|
+
_handle_dff_nested_errors(error_data["error"])
|
|
68
|
+
|
|
69
|
+
elif "errors" in error_data:
|
|
70
|
+
# Handle simple validation errors structure
|
|
71
|
+
_handle_simple_validation_errors(error_data)
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
# Fallback for unknown error structure
|
|
75
|
+
click.echo("✗ Request failed with validation errors:", err=True)
|
|
76
|
+
if "message" in error_data:
|
|
77
|
+
click.echo(f" {error_data['message']}", err=True)
|
|
78
|
+
else:
|
|
79
|
+
click.echo(f" {error_data}", err=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_dff_creation_errors(error_data: Dict[str, Any], operation: str = "operation") -> None:
|
|
83
|
+
"""Handle DFF creation/update response with failed configurations/groups/fields."""
|
|
84
|
+
click.echo(f"✗ Configuration {operation} failed with the following issues:", err=True)
|
|
85
|
+
|
|
86
|
+
# Show successful operations if any
|
|
87
|
+
successful_configs = error_data.get("configurations", [])
|
|
88
|
+
if successful_configs:
|
|
89
|
+
click.echo(f"\n✓ Successfully {operation}d configurations:")
|
|
90
|
+
for config in successful_configs:
|
|
91
|
+
click.echo(f" - {config.get('name', config.get('key', 'Unknown'))}")
|
|
92
|
+
|
|
93
|
+
# Show failed configurations
|
|
94
|
+
failed_configs = error_data.get("failedConfigurations", [])
|
|
95
|
+
if failed_configs:
|
|
96
|
+
click.echo("\n✗ Failed configurations:")
|
|
97
|
+
for config in failed_configs:
|
|
98
|
+
click.echo(f" - {config.get('name', config.get('key', 'Unknown'))}")
|
|
99
|
+
|
|
100
|
+
# Show failed groups
|
|
101
|
+
failed_groups = error_data.get("failedGroups", [])
|
|
102
|
+
if failed_groups:
|
|
103
|
+
click.echo("\n✗ Failed groups:")
|
|
104
|
+
for group in failed_groups:
|
|
105
|
+
click.echo(f" - {group.get('displayText', group.get('key', 'Unknown'))}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _handle_dff_nested_errors(error: Dict[str, Any]) -> None:
|
|
109
|
+
"""Handle nested error structure with innerErrors."""
|
|
110
|
+
click.echo("✗ Request failed with validation errors:", err=True)
|
|
111
|
+
|
|
112
|
+
if "message" in error:
|
|
113
|
+
click.echo(f" {error['message']}")
|
|
114
|
+
|
|
115
|
+
inner_errors = error.get("innerErrors", [])
|
|
116
|
+
if inner_errors:
|
|
117
|
+
click.echo("\nDetailed errors:")
|
|
118
|
+
for inner_error in inner_errors:
|
|
119
|
+
message = inner_error.get("message", "Unknown error")
|
|
120
|
+
click.echo(f" • {message}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handle_simple_validation_errors(error_data: Dict[str, Any]) -> None:
|
|
124
|
+
"""Handle simple validation errors structure."""
|
|
125
|
+
click.echo("✗ Validation errors occurred:", err=True)
|
|
126
|
+
errors = error_data.get("errors", {})
|
|
127
|
+
|
|
128
|
+
for field, field_errors in errors.items():
|
|
129
|
+
if isinstance(field_errors, list):
|
|
130
|
+
for error in field_errors:
|
|
131
|
+
click.echo(f" - {field}: {error}", err=True)
|
|
132
|
+
else:
|
|
133
|
+
click.echo(f" - {field}: {field_errors}", err=True)
|
|
134
|
+
|
|
135
|
+
# Show title if available
|
|
136
|
+
if "title" in error_data:
|
|
137
|
+
click.echo(f" Summary: {error_data['title']}", err=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_resource_type(resource_type: str) -> None:
|
|
141
|
+
"""Validate that the resource type is one of the supported values.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
resource_type: The resource type to validate
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
click.ClickException: If the resource type is not valid
|
|
148
|
+
"""
|
|
149
|
+
if resource_type not in VALID_RESOURCE_TYPES:
|
|
150
|
+
valid_types_str = ", ".join(VALID_RESOURCE_TYPES)
|
|
151
|
+
raise click.ClickException(
|
|
152
|
+
f"Invalid resource type: '{resource_type}'. " f"Valid types are: {valid_types_str}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def validate_field_type(field_type: str) -> None:
|
|
157
|
+
"""Validate that the field type is one of the supported values.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
field_type: The field type to validate
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
click.ClickException: If the field type is not valid
|
|
164
|
+
"""
|
|
165
|
+
if field_type not in VALID_FIELD_TYPES:
|
|
166
|
+
valid_types_str = ", ".join(VALID_FIELD_TYPES)
|
|
167
|
+
raise click.ClickException(
|
|
168
|
+
f"Invalid field type: '{field_type}'. " f"Valid types are: {valid_types_str}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _query_all_groups(
|
|
173
|
+
workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None
|
|
174
|
+
) -> List[Dict[str, Any]]:
|
|
175
|
+
"""Query all DFF groups using continuation token pagination.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
workspace_filter: Optional workspace ID or name to filter by
|
|
179
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of all groups, optionally filtered by workspace
|
|
183
|
+
"""
|
|
184
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/groups"
|
|
185
|
+
all_groups = []
|
|
186
|
+
continuation_token = None
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
# Build parameters for the request
|
|
190
|
+
params = {"Take": 100} # Use smaller page size for efficient pagination
|
|
191
|
+
if continuation_token:
|
|
192
|
+
params["ContinuationToken"] = continuation_token
|
|
193
|
+
|
|
194
|
+
# Build query string
|
|
195
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
196
|
+
full_url = f"{url}?{query_string}"
|
|
197
|
+
|
|
198
|
+
resp = make_api_request("GET", full_url)
|
|
199
|
+
data = resp.json()
|
|
200
|
+
|
|
201
|
+
# Extract groups from this page
|
|
202
|
+
groups = data.get("groups", [])
|
|
203
|
+
all_groups.extend(groups)
|
|
204
|
+
|
|
205
|
+
# Check if there are more pages
|
|
206
|
+
continuation_token = data.get("continuationToken")
|
|
207
|
+
if not continuation_token:
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# Filter by workspace if specified
|
|
211
|
+
if workspace_filter and workspace_map:
|
|
212
|
+
all_groups = filter_by_workspace(all_groups, workspace_filter, workspace_map)
|
|
213
|
+
|
|
214
|
+
return all_groups
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _query_all_fields(
|
|
218
|
+
workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None
|
|
219
|
+
) -> List[Dict[str, Any]]:
|
|
220
|
+
"""Query all DFF fields using continuation token pagination.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
workspace_filter: Optional workspace ID or name to filter by
|
|
224
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of all fields, optionally filtered by workspace
|
|
228
|
+
"""
|
|
229
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/fields"
|
|
230
|
+
all_fields = []
|
|
231
|
+
continuation_token = None
|
|
232
|
+
|
|
233
|
+
while True:
|
|
234
|
+
# Build parameters for the request
|
|
235
|
+
params = {"Take": 500} # Use smaller page size for efficient pagination
|
|
236
|
+
if continuation_token:
|
|
237
|
+
params["ContinuationToken"] = continuation_token
|
|
238
|
+
|
|
239
|
+
# Build query string
|
|
240
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
241
|
+
full_url = f"{url}?{query_string}"
|
|
242
|
+
|
|
243
|
+
resp = make_api_request("GET", full_url)
|
|
244
|
+
data = resp.json()
|
|
245
|
+
|
|
246
|
+
# Extract fields from this page
|
|
247
|
+
fields = data.get("fields", [])
|
|
248
|
+
all_fields.extend(fields)
|
|
249
|
+
|
|
250
|
+
# Check if there are more pages
|
|
251
|
+
continuation_token = data.get("continuationToken")
|
|
252
|
+
if not continuation_token:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
# Filter by workspace if specified
|
|
256
|
+
if workspace_filter and workspace_map:
|
|
257
|
+
all_fields = filter_by_workspace(all_fields, workspace_filter, workspace_map)
|
|
258
|
+
|
|
259
|
+
return all_fields
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _query_all_configurations(
|
|
263
|
+
workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None
|
|
264
|
+
) -> List[Dict[str, Any]]:
|
|
265
|
+
"""Query all configurations using continuation token pagination.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
workspace_filter: Optional workspace ID or name to filter by
|
|
269
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of all configurations, optionally filtered by workspace
|
|
273
|
+
"""
|
|
274
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/configurations"
|
|
275
|
+
all_configurations = []
|
|
276
|
+
continuation_token = None
|
|
277
|
+
|
|
278
|
+
while True:
|
|
279
|
+
# Build parameters for the request
|
|
280
|
+
params = {"Take": 100} # Use smaller page size for efficient pagination
|
|
281
|
+
if continuation_token:
|
|
282
|
+
params["ContinuationToken"] = continuation_token
|
|
283
|
+
|
|
284
|
+
# Build query string
|
|
285
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
286
|
+
full_url = f"{url}?{query_string}"
|
|
287
|
+
|
|
288
|
+
resp = make_api_request("GET", full_url)
|
|
289
|
+
data = resp.json()
|
|
290
|
+
|
|
291
|
+
# Extract configurations from this page
|
|
292
|
+
configurations = data.get("configurations", [])
|
|
293
|
+
all_configurations.extend(configurations)
|
|
294
|
+
|
|
295
|
+
# Check if there are more pages
|
|
296
|
+
continuation_token = data.get("continuationToken")
|
|
297
|
+
if not continuation_token:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
# Filter by workspace if specified
|
|
301
|
+
if workspace_filter and workspace_map:
|
|
302
|
+
all_configurations = filter_by_workspace(
|
|
303
|
+
all_configurations, workspace_filter, workspace_map
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return all_configurations
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def register_dff_commands(cli: Any) -> None:
|
|
310
|
+
"""Register the 'customfield' command group and its subcommands."""
|
|
311
|
+
|
|
312
|
+
@cli.group(name="customfield")
|
|
313
|
+
@click.pass_context
|
|
314
|
+
def dff(ctx: click.Context) -> None:
|
|
315
|
+
"""Manage custom field (DFF) configurations."""
|
|
316
|
+
# Check for platform feature availability
|
|
317
|
+
# Only check if a subcommand is being invoked (not just --help)
|
|
318
|
+
if ctx.invoked_subcommand is not None:
|
|
319
|
+
require_feature("dynamic_form_fields")
|
|
320
|
+
|
|
321
|
+
# Configuration commands (now at top level under dff)
|
|
322
|
+
@dff.command(name="list")
|
|
323
|
+
@click.option("--workspace", "-w", help="Filter by workspace name or ID")
|
|
324
|
+
@click.option(
|
|
325
|
+
"--take",
|
|
326
|
+
default=25,
|
|
327
|
+
show_default=True,
|
|
328
|
+
help="Maximum number of configurations to return",
|
|
329
|
+
)
|
|
330
|
+
@click.option(
|
|
331
|
+
"--format",
|
|
332
|
+
"-f",
|
|
333
|
+
type=click.Choice(["table", "json"], case_sensitive=False),
|
|
334
|
+
default="table",
|
|
335
|
+
show_default=True,
|
|
336
|
+
help="Output format: table or json",
|
|
337
|
+
)
|
|
338
|
+
def list_configurations(
|
|
339
|
+
workspace: Optional[str] = None, take: int = 25, format: str = "table"
|
|
340
|
+
) -> None:
|
|
341
|
+
"""List custom field configurations."""
|
|
342
|
+
try:
|
|
343
|
+
# Get workspace map once and reuse it
|
|
344
|
+
workspace_map = get_workspace_map()
|
|
345
|
+
|
|
346
|
+
# Use the workspace formatter for consistent formatting
|
|
347
|
+
format_config_row = WorkspaceFormatter.create_config_row_formatter(workspace_map)
|
|
348
|
+
|
|
349
|
+
# Use continuation token pagination following user_click.py pattern
|
|
350
|
+
all_configurations = _query_all_configurations(workspace, workspace_map)
|
|
351
|
+
|
|
352
|
+
# Use UniversalResponseHandler for consistent pagination
|
|
353
|
+
from typing import Any
|
|
354
|
+
|
|
355
|
+
# Create a mock response with all data
|
|
356
|
+
filtered_resp: Any = FilteredResponse({"configurations": all_configurations})
|
|
357
|
+
|
|
358
|
+
handler = UniversalResponseHandler()
|
|
359
|
+
handler.handle_list_response(
|
|
360
|
+
filtered_resp,
|
|
361
|
+
"configurations",
|
|
362
|
+
"configuration",
|
|
363
|
+
format,
|
|
364
|
+
format_config_row,
|
|
365
|
+
["Workspace", "Name", "Configuration ID"],
|
|
366
|
+
[36, 40, 36],
|
|
367
|
+
"No custom field configurations found.",
|
|
368
|
+
enable_pagination=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
handle_api_error(exc)
|
|
373
|
+
|
|
374
|
+
@dff.command(name="get")
|
|
375
|
+
@click.option(
|
|
376
|
+
"--id",
|
|
377
|
+
"-i",
|
|
378
|
+
"config_id",
|
|
379
|
+
required=True,
|
|
380
|
+
help="Configuration ID to retrieve",
|
|
381
|
+
)
|
|
382
|
+
@click.option(
|
|
383
|
+
"--format",
|
|
384
|
+
"-f",
|
|
385
|
+
type=click.Choice(["table", "json"], case_sensitive=False),
|
|
386
|
+
default="json",
|
|
387
|
+
show_default=True,
|
|
388
|
+
help="Output format: table or json",
|
|
389
|
+
)
|
|
390
|
+
def get_configuration(config_id: str, format: str = "json") -> None:
|
|
391
|
+
"""Get a specific custom field configuration by ID."""
|
|
392
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/resolved-configuration"
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
params = {"configurationId": config_id}
|
|
396
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
397
|
+
full_url = f"{url}?{query_string}"
|
|
398
|
+
|
|
399
|
+
resp = make_api_request("GET", full_url)
|
|
400
|
+
data = resp.json()
|
|
401
|
+
|
|
402
|
+
if format == "json":
|
|
403
|
+
click.echo(json.dumps(data, indent=2))
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
# Table format - show basic info
|
|
407
|
+
configuration = data.get("configuration", {})
|
|
408
|
+
workspace_map = get_workspace_map()
|
|
409
|
+
workspace_id = configuration.get("workspace", "")
|
|
410
|
+
workspace_name = workspace_map.get(workspace_id, workspace_id)
|
|
411
|
+
|
|
412
|
+
click.echo("Configuration Details")
|
|
413
|
+
click.echo("=" * 50)
|
|
414
|
+
click.echo(f"ID: {configuration.get('id', '')}")
|
|
415
|
+
click.echo(f"Name: {configuration.get('name', '')}")
|
|
416
|
+
click.echo(f"Workspace: {workspace_name}")
|
|
417
|
+
click.echo(f"Resource Type: {configuration.get('resourceType', '')}")
|
|
418
|
+
|
|
419
|
+
groups = data.get("groups", [])
|
|
420
|
+
fields = data.get("fields", [])
|
|
421
|
+
click.echo(f"Groups: {len(groups)}")
|
|
422
|
+
click.echo(f"Fields: {len(fields)}")
|
|
423
|
+
|
|
424
|
+
except Exception as exc:
|
|
425
|
+
handle_api_error(exc)
|
|
426
|
+
|
|
427
|
+
@dff.command(name="create")
|
|
428
|
+
@click.option(
|
|
429
|
+
"--file",
|
|
430
|
+
"-f",
|
|
431
|
+
"input_file",
|
|
432
|
+
required=True,
|
|
433
|
+
help="Input JSON file with configuration data",
|
|
434
|
+
)
|
|
435
|
+
def create_configuration(input_file: str) -> None:
|
|
436
|
+
"""Create custom field configurations from a JSON file."""
|
|
437
|
+
from .utils import check_readonly_mode
|
|
438
|
+
|
|
439
|
+
check_readonly_mode("create a DataFlow Definition")
|
|
440
|
+
|
|
441
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/configurations"
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
data = load_json_file(input_file)
|
|
445
|
+
|
|
446
|
+
# Ensure data is in the expected format
|
|
447
|
+
if isinstance(data, dict) and "configurations" not in data:
|
|
448
|
+
# Wrap single configuration
|
|
449
|
+
data = {"configurations": [data]}
|
|
450
|
+
elif isinstance(data, list):
|
|
451
|
+
# Wrap list of configurations
|
|
452
|
+
data = {"configurations": data}
|
|
453
|
+
|
|
454
|
+
# Validate resource types in configurations
|
|
455
|
+
configurations = data.get("configurations", [])
|
|
456
|
+
for i, config in enumerate(configurations):
|
|
457
|
+
if isinstance(config, dict):
|
|
458
|
+
resource_type = config.get("resourceType")
|
|
459
|
+
if resource_type:
|
|
460
|
+
try:
|
|
461
|
+
validate_resource_type(resource_type)
|
|
462
|
+
except click.ClickException as e:
|
|
463
|
+
raise click.ClickException(
|
|
464
|
+
f"Invalid resource type in configuration {i + 1}: {e.message}"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Validate field types in fields
|
|
468
|
+
fields = data.get("fields", [])
|
|
469
|
+
for i, field in enumerate(fields):
|
|
470
|
+
if isinstance(field, dict):
|
|
471
|
+
field_type = field.get("type")
|
|
472
|
+
if field_type:
|
|
473
|
+
try:
|
|
474
|
+
validate_field_type(field_type)
|
|
475
|
+
except click.ClickException as e:
|
|
476
|
+
raise click.ClickException(
|
|
477
|
+
f"Invalid field type in field {i + 1}: {e.message}"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Make API request without automatic error handling to parse validation errors
|
|
481
|
+
resp = make_api_request("POST", url, data, handle_errors=False)
|
|
482
|
+
|
|
483
|
+
# Check for partial success response
|
|
484
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
485
|
+
|
|
486
|
+
if resp.status_code == 201:
|
|
487
|
+
# Full success
|
|
488
|
+
click.echo("✓ Custom field configurations created successfully.")
|
|
489
|
+
created_configs = response_data.get("configurations", [])
|
|
490
|
+
for config in created_configs:
|
|
491
|
+
click.echo(f" - {config.get('name', 'Unknown')}: {config.get('id', '')}")
|
|
492
|
+
elif resp.status_code == 200:
|
|
493
|
+
# Partial success - may contain DFF-specific error structure
|
|
494
|
+
if any(
|
|
495
|
+
key in response_data
|
|
496
|
+
for key in ["failedConfigurations", "failedGroups", "failedFields"]
|
|
497
|
+
):
|
|
498
|
+
# Use DFF-specific error handling for partial failures
|
|
499
|
+
_handle_dff_error_response(response_data, "creation")
|
|
500
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
501
|
+
else:
|
|
502
|
+
# Handle legacy partial success format
|
|
503
|
+
click.echo("⚠ Some configurations were created, but some failed:", err=True)
|
|
504
|
+
|
|
505
|
+
# Show successful creations
|
|
506
|
+
successful = response_data.get("created", [])
|
|
507
|
+
if successful:
|
|
508
|
+
click.echo("Created:")
|
|
509
|
+
for config in successful:
|
|
510
|
+
click.echo(
|
|
511
|
+
f" ✓ {config.get('name', 'Unknown')}: {config.get('id', '')}"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Show failures
|
|
515
|
+
failed = response_data.get("failed", [])
|
|
516
|
+
if failed:
|
|
517
|
+
click.echo("Failed:")
|
|
518
|
+
for failure in failed:
|
|
519
|
+
name = failure.get("name", "Unknown")
|
|
520
|
+
error = failure.get("error", {})
|
|
521
|
+
error_msg = error.get("message", "Unknown error")
|
|
522
|
+
click.echo(f" ✗ {name}: {error_msg}", err=True)
|
|
523
|
+
|
|
524
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
525
|
+
|
|
526
|
+
except requests.RequestException as exc:
|
|
527
|
+
# Handle HTTP errors with detailed validation error parsing
|
|
528
|
+
if hasattr(exc, "response") and exc.response is not None:
|
|
529
|
+
try:
|
|
530
|
+
error_data = exc.response.json()
|
|
531
|
+
status_code = exc.response.status_code
|
|
532
|
+
|
|
533
|
+
if status_code == 400:
|
|
534
|
+
# Parse DFF-specific error structure
|
|
535
|
+
_handle_dff_error_response(error_data, "creation")
|
|
536
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
537
|
+
else:
|
|
538
|
+
# Fallback to general error handling for other HTTP errors
|
|
539
|
+
handle_api_error(exc)
|
|
540
|
+
except (ValueError, KeyError):
|
|
541
|
+
# If JSON parsing fails, fall back to general error handling
|
|
542
|
+
handle_api_error(exc)
|
|
543
|
+
else:
|
|
544
|
+
# For non-HTTP errors, use general error handling
|
|
545
|
+
handle_api_error(exc)
|
|
546
|
+
except Exception as exc:
|
|
547
|
+
handle_api_error(exc)
|
|
548
|
+
|
|
549
|
+
@dff.command(name="update")
|
|
550
|
+
@click.option(
|
|
551
|
+
"--file",
|
|
552
|
+
"-f",
|
|
553
|
+
"input_file",
|
|
554
|
+
required=True,
|
|
555
|
+
help="Input JSON file with updated configuration data",
|
|
556
|
+
)
|
|
557
|
+
def update_configuration(input_file: str) -> None:
|
|
558
|
+
"""Update custom field configurations from a JSON file."""
|
|
559
|
+
from .utils import check_readonly_mode
|
|
560
|
+
|
|
561
|
+
check_readonly_mode("update a DataFlow Definition")
|
|
562
|
+
|
|
563
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/update-configurations"
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
data = load_json_file(input_file)
|
|
567
|
+
|
|
568
|
+
# Ensure data is in the expected format
|
|
569
|
+
if isinstance(data, dict) and "configurations" not in data:
|
|
570
|
+
data = {"configurations": [data]}
|
|
571
|
+
elif isinstance(data, list):
|
|
572
|
+
data = {"configurations": data}
|
|
573
|
+
|
|
574
|
+
resp = make_api_request("POST", url, data, handle_errors=False)
|
|
575
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
576
|
+
|
|
577
|
+
if resp.status_code == 200:
|
|
578
|
+
# Check if it's a DFF-specific partial success response
|
|
579
|
+
if any(
|
|
580
|
+
key in response_data
|
|
581
|
+
for key in ["failedConfigurations", "failedGroups", "failedFields"]
|
|
582
|
+
):
|
|
583
|
+
# Use DFF-specific error handling for partial failures
|
|
584
|
+
_handle_dff_error_response(response_data, "update")
|
|
585
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
586
|
+
else:
|
|
587
|
+
# Handle legacy partial success format
|
|
588
|
+
updated_configs = response_data.get("configurations", [])
|
|
589
|
+
failed_updates = response_data.get("failed", [])
|
|
590
|
+
|
|
591
|
+
if failed_updates:
|
|
592
|
+
click.echo("⚠ Some configurations were updated, but some failed:", err=True)
|
|
593
|
+
|
|
594
|
+
if updated_configs:
|
|
595
|
+
click.echo("Updated:")
|
|
596
|
+
for config in updated_configs:
|
|
597
|
+
click.echo(
|
|
598
|
+
f" ✓ {config.get('name', 'Unknown')}: {config.get('id', '')}"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
click.echo("Failed:")
|
|
602
|
+
for failure in failed_updates:
|
|
603
|
+
name = failure.get("name", "Unknown")
|
|
604
|
+
error = failure.get("error", {})
|
|
605
|
+
error_msg = error.get("message", "Unknown error")
|
|
606
|
+
click.echo(f" ✗ {name}: {error_msg}", err=True)
|
|
607
|
+
|
|
608
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
609
|
+
else:
|
|
610
|
+
click.echo("✓ Custom field configurations updated successfully.")
|
|
611
|
+
for config in updated_configs:
|
|
612
|
+
click.echo(
|
|
613
|
+
f" - {config.get('name', 'Unknown')}: {config.get('id', '')}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
except requests.RequestException as exc:
|
|
617
|
+
# Handle HTTP errors with detailed validation error parsing
|
|
618
|
+
if hasattr(exc, "response") and exc.response is not None:
|
|
619
|
+
try:
|
|
620
|
+
error_data = exc.response.json()
|
|
621
|
+
status_code = exc.response.status_code
|
|
622
|
+
|
|
623
|
+
if status_code == 400:
|
|
624
|
+
# Parse DFF-specific error structure
|
|
625
|
+
_handle_dff_error_response(error_data, "update")
|
|
626
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
627
|
+
else:
|
|
628
|
+
# Fallback to general error handling for other HTTP errors
|
|
629
|
+
handle_api_error(exc)
|
|
630
|
+
except (ValueError, KeyError):
|
|
631
|
+
# If JSON parsing fails, fall back to general error handling
|
|
632
|
+
handle_api_error(exc)
|
|
633
|
+
else:
|
|
634
|
+
# For non-HTTP errors, use general error handling
|
|
635
|
+
handle_api_error(exc)
|
|
636
|
+
except Exception as exc:
|
|
637
|
+
handle_api_error(exc)
|
|
638
|
+
|
|
639
|
+
@dff.command(name="delete")
|
|
640
|
+
@click.option(
|
|
641
|
+
"--id",
|
|
642
|
+
"-i",
|
|
643
|
+
"config_ids",
|
|
644
|
+
multiple=True,
|
|
645
|
+
help="Configuration ID(s) to delete (can be specified multiple times)",
|
|
646
|
+
)
|
|
647
|
+
@click.option(
|
|
648
|
+
"--group-id",
|
|
649
|
+
"-g",
|
|
650
|
+
"group_ids",
|
|
651
|
+
multiple=True,
|
|
652
|
+
help="Group ID(s) to delete (can be specified multiple times)",
|
|
653
|
+
)
|
|
654
|
+
@click.option(
|
|
655
|
+
"--field-id",
|
|
656
|
+
"--fid",
|
|
657
|
+
"field_ids",
|
|
658
|
+
multiple=True,
|
|
659
|
+
help="Field ID(s) to delete (can be specified multiple times)",
|
|
660
|
+
)
|
|
661
|
+
@click.option(
|
|
662
|
+
"--no-recursive",
|
|
663
|
+
"recursive",
|
|
664
|
+
is_flag=True,
|
|
665
|
+
flag_value=False,
|
|
666
|
+
default=True,
|
|
667
|
+
help="Do not recursively delete dependent items (groups/fields when deleting configs)",
|
|
668
|
+
)
|
|
669
|
+
@click.confirmation_option(prompt="Are you sure you want to delete these items?")
|
|
670
|
+
def delete_configuration(
|
|
671
|
+
config_ids: tuple[str, ...],
|
|
672
|
+
group_ids: tuple[str, ...],
|
|
673
|
+
field_ids: tuple[str, ...],
|
|
674
|
+
recursive: bool = True,
|
|
675
|
+
) -> None:
|
|
676
|
+
"""Delete custom field configurations, groups, and fields."""
|
|
677
|
+
from .utils import check_readonly_mode
|
|
678
|
+
|
|
679
|
+
check_readonly_mode("delete a custom field configuration")
|
|
680
|
+
|
|
681
|
+
if not config_ids and not group_ids and not field_ids:
|
|
682
|
+
click.echo("✗ Must provide at least one of: --id, --group-id, or --field-id", err=True)
|
|
683
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
684
|
+
|
|
685
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/delete"
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
ids_to_delete = {
|
|
689
|
+
"configurationIds": list(config_ids),
|
|
690
|
+
"groupIds": list(group_ids),
|
|
691
|
+
"fieldIds": list(field_ids),
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
# Build payload with only non-empty ID lists
|
|
695
|
+
payload: dict[str, Any] = {k: v for k, v in ids_to_delete.items() if v}
|
|
696
|
+
payload["recursive"] = recursive
|
|
697
|
+
|
|
698
|
+
if not payload or all(not v for k, v in payload.items() if k != "recursive"):
|
|
699
|
+
click.echo("✗ No IDs found to delete", err=True)
|
|
700
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
701
|
+
|
|
702
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
703
|
+
|
|
704
|
+
if resp.status_code in (200, 204):
|
|
705
|
+
# Build summary
|
|
706
|
+
summary_parts = []
|
|
707
|
+
if ids_to_delete.get("configurationIds"):
|
|
708
|
+
summary_parts.append(
|
|
709
|
+
f"{len(ids_to_delete['configurationIds'])} configuration(s)"
|
|
710
|
+
)
|
|
711
|
+
if ids_to_delete.get("groupIds"):
|
|
712
|
+
summary_parts.append(f"{len(ids_to_delete['groupIds'])} group(s)")
|
|
713
|
+
if ids_to_delete.get("fieldIds"):
|
|
714
|
+
summary_parts.append(f"{len(ids_to_delete['fieldIds'])} field(s)")
|
|
715
|
+
|
|
716
|
+
summary = " and ".join(summary_parts) if summary_parts else "item(s)"
|
|
717
|
+
click.echo(f"✓ {summary} deleted successfully.")
|
|
718
|
+
|
|
719
|
+
# File-based delete removed; no input file updates
|
|
720
|
+
else:
|
|
721
|
+
# Handle partial success if needed
|
|
722
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
723
|
+
failed_deletes = response_data.get("failed", [])
|
|
724
|
+
|
|
725
|
+
if failed_deletes:
|
|
726
|
+
click.echo("⚠ Some items were deleted, but some failed:", err=True)
|
|
727
|
+
for failure in failed_deletes:
|
|
728
|
+
item_id = failure.get("id", "Unknown")
|
|
729
|
+
error = failure.get("error", {})
|
|
730
|
+
error_msg = error.get("message", "Unknown error")
|
|
731
|
+
click.echo(f" ✗ {item_id}: {error_msg}", err=True)
|
|
732
|
+
|
|
733
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
734
|
+
|
|
735
|
+
except requests.RequestException as exc:
|
|
736
|
+
# Handle HTTP errors with detailed validation error parsing
|
|
737
|
+
if hasattr(exc, "response") and exc.response is not None:
|
|
738
|
+
try:
|
|
739
|
+
error_data = exc.response.json()
|
|
740
|
+
status_code = exc.response.status_code
|
|
741
|
+
|
|
742
|
+
if status_code == 400:
|
|
743
|
+
# Parse DFF-specific error structure
|
|
744
|
+
_handle_dff_error_response(error_data, "deletion")
|
|
745
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
746
|
+
else:
|
|
747
|
+
# Fallback to general error handling for other HTTP errors
|
|
748
|
+
handle_api_error(exc)
|
|
749
|
+
except (ValueError, KeyError):
|
|
750
|
+
# If JSON parsing fails, fall back to general error handling
|
|
751
|
+
handle_api_error(exc)
|
|
752
|
+
else:
|
|
753
|
+
# For non-HTTP errors, use general error handling
|
|
754
|
+
handle_api_error(exc)
|
|
755
|
+
except Exception as exc:
|
|
756
|
+
handle_api_error(exc)
|
|
757
|
+
|
|
758
|
+
@dff.command(name="export")
|
|
759
|
+
@click.option(
|
|
760
|
+
"--id",
|
|
761
|
+
"-i",
|
|
762
|
+
"config_id",
|
|
763
|
+
required=True,
|
|
764
|
+
help="Configuration ID to export",
|
|
765
|
+
)
|
|
766
|
+
@click.option("--output", "-o", help="Output JSON file (default: <config-name>.json)")
|
|
767
|
+
def export_configuration(config_id: str, output: Optional[str] = None) -> None:
|
|
768
|
+
"""Export a custom field configuration to a JSON file."""
|
|
769
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/resolved-configuration"
|
|
770
|
+
|
|
771
|
+
try:
|
|
772
|
+
params = {"configurationId": config_id}
|
|
773
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
774
|
+
full_url = f"{url}?{query_string}"
|
|
775
|
+
|
|
776
|
+
resp = make_api_request("GET", full_url)
|
|
777
|
+
data = resp.json()
|
|
778
|
+
|
|
779
|
+
# Generate output filename if not provided
|
|
780
|
+
if not output:
|
|
781
|
+
config_name = data.get("configuration", {}).get("name", f"config-{config_id}")
|
|
782
|
+
safe_name = sanitize_filename(config_name, f"config-{config_id}")
|
|
783
|
+
output = f"{safe_name}.json"
|
|
784
|
+
|
|
785
|
+
save_json_file(data, output)
|
|
786
|
+
click.echo(f"✓ Configuration exported to {output}")
|
|
787
|
+
|
|
788
|
+
except Exception as exc:
|
|
789
|
+
handle_api_error(exc)
|
|
790
|
+
|
|
791
|
+
@dff.command(name="init")
|
|
792
|
+
@click.option(
|
|
793
|
+
"--name",
|
|
794
|
+
"-n",
|
|
795
|
+
help="Configuration name (will prompt if not provided)",
|
|
796
|
+
)
|
|
797
|
+
@click.option(
|
|
798
|
+
"--workspace",
|
|
799
|
+
"-w",
|
|
800
|
+
help="Workspace name or ID (will prompt if not provided)",
|
|
801
|
+
)
|
|
802
|
+
@click.option(
|
|
803
|
+
"--resource-type",
|
|
804
|
+
"-r",
|
|
805
|
+
type=click.Choice(VALID_RESOURCE_TYPES, case_sensitive=False),
|
|
806
|
+
help=RESOURCE_TYPE_HELP,
|
|
807
|
+
)
|
|
808
|
+
@click.option(
|
|
809
|
+
"--output",
|
|
810
|
+
"-o",
|
|
811
|
+
help="Output file path (default: <name>-config.json)",
|
|
812
|
+
)
|
|
813
|
+
def init_configuration(
|
|
814
|
+
name: Optional[str] = None,
|
|
815
|
+
workspace: Optional[str] = None,
|
|
816
|
+
resource_type: Optional[str] = None,
|
|
817
|
+
output: Optional[str] = None,
|
|
818
|
+
) -> None:
|
|
819
|
+
"""Create a template configuration file for custom fields."""
|
|
820
|
+
try:
|
|
821
|
+
# Prompt for required fields if not provided
|
|
822
|
+
if not name:
|
|
823
|
+
name = click.prompt("Configuration name")
|
|
824
|
+
|
|
825
|
+
if not workspace:
|
|
826
|
+
workspace = click.prompt("Workspace name or ID")
|
|
827
|
+
|
|
828
|
+
if not resource_type:
|
|
829
|
+
resource_type = questionary.select(
|
|
830
|
+
"Resource type?",
|
|
831
|
+
choices=VALID_RESOURCE_TYPES,
|
|
832
|
+
).ask()
|
|
833
|
+
if resource_type is None:
|
|
834
|
+
raise click.Abort()
|
|
835
|
+
|
|
836
|
+
# Validate resource type (resource_type is guaranteed to be str at this point)
|
|
837
|
+
if resource_type:
|
|
838
|
+
validate_resource_type(resource_type)
|
|
839
|
+
|
|
840
|
+
# Generate output filename if not provided
|
|
841
|
+
if not output:
|
|
842
|
+
safe_name = sanitize_filename(name or "config", "config")
|
|
843
|
+
output = f"{safe_name}-config.json"
|
|
844
|
+
|
|
845
|
+
# Try to resolve workspace name to ID
|
|
846
|
+
try:
|
|
847
|
+
workspace_map = get_workspace_map()
|
|
848
|
+
workspace_id = resolve_workspace_filter(
|
|
849
|
+
get_effective_workspace(workspace) or "", workspace_map
|
|
850
|
+
)
|
|
851
|
+
except Exception:
|
|
852
|
+
workspace_id = get_effective_workspace(workspace) or ""
|
|
853
|
+
|
|
854
|
+
# Create template configuration
|
|
855
|
+
safe_name = sanitize_filename(name or "config", "config")
|
|
856
|
+
import uuid
|
|
857
|
+
|
|
858
|
+
unique_suffix = str(uuid.uuid4())[:8] # Use first 8 chars of UUID for uniqueness
|
|
859
|
+
|
|
860
|
+
template_config = {
|
|
861
|
+
"configurations": [
|
|
862
|
+
{
|
|
863
|
+
"name": name,
|
|
864
|
+
"key": f"{safe_name}-config-{unique_suffix}",
|
|
865
|
+
"workspace": workspace_id,
|
|
866
|
+
"resourceType": resource_type,
|
|
867
|
+
"views": [
|
|
868
|
+
{
|
|
869
|
+
"key": f"default-view-{unique_suffix}",
|
|
870
|
+
"displayText": "Default View",
|
|
871
|
+
"groups": [f"group1-{unique_suffix}"],
|
|
872
|
+
}
|
|
873
|
+
],
|
|
874
|
+
}
|
|
875
|
+
],
|
|
876
|
+
"groups": [
|
|
877
|
+
{
|
|
878
|
+
"key": f"group1-{unique_suffix}",
|
|
879
|
+
"workspace": workspace_id,
|
|
880
|
+
"displayText": "Example Group",
|
|
881
|
+
"fields": [f"field1-{unique_suffix}", f"field2-{unique_suffix}"],
|
|
882
|
+
}
|
|
883
|
+
],
|
|
884
|
+
"fields": [
|
|
885
|
+
{
|
|
886
|
+
"key": f"field1-{unique_suffix}",
|
|
887
|
+
"workspace": workspace_id,
|
|
888
|
+
"displayText": "Example Field",
|
|
889
|
+
"type": "Text",
|
|
890
|
+
"mandatory": False,
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"key": f"field2-{unique_suffix}",
|
|
894
|
+
"workspace": workspace_id,
|
|
895
|
+
"displayText": "Example Field 2",
|
|
896
|
+
"type": "Text",
|
|
897
|
+
"mandatory": False,
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
save_json_file(template_config, output)
|
|
903
|
+
click.echo(f"✓ Configuration template created: {output}")
|
|
904
|
+
click.echo("Edit the file to customize:")
|
|
905
|
+
click.echo(" - Add/modify groups and fields")
|
|
906
|
+
click.echo(
|
|
907
|
+
" - Set field types (Text, Number, Boolean, Enum, DateTime, Table, LinkedResource)"
|
|
908
|
+
)
|
|
909
|
+
click.echo(" - Configure mandatory/optional fields")
|
|
910
|
+
click.echo(" - Add validation rules and properties as needed")
|
|
911
|
+
|
|
912
|
+
except Exception as exc:
|
|
913
|
+
click.echo(f"✗ Error creating configuration template: {exc}", err=True)
|
|
914
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
915
|
+
|
|
916
|
+
# Editor command
|
|
917
|
+
@dff.command(name="edit")
|
|
918
|
+
@click.option(
|
|
919
|
+
"--file",
|
|
920
|
+
"-f",
|
|
921
|
+
help="JSON file to edit (will create new if not exists)",
|
|
922
|
+
)
|
|
923
|
+
@click.option(
|
|
924
|
+
"--id",
|
|
925
|
+
"-i",
|
|
926
|
+
"config_id",
|
|
927
|
+
help="Configuration ID to load in the editor",
|
|
928
|
+
)
|
|
929
|
+
@click.option(
|
|
930
|
+
"--port",
|
|
931
|
+
"-p",
|
|
932
|
+
default=8080,
|
|
933
|
+
show_default=True,
|
|
934
|
+
help="Port for local HTTP server",
|
|
935
|
+
)
|
|
936
|
+
@click.option(
|
|
937
|
+
"--no-browser",
|
|
938
|
+
is_flag=True,
|
|
939
|
+
help="Don't automatically open browser",
|
|
940
|
+
)
|
|
941
|
+
def edit_configuration(
|
|
942
|
+
file: Optional[str] = None,
|
|
943
|
+
config_id: Optional[str] = None,
|
|
944
|
+
port: int = 8080,
|
|
945
|
+
no_browser: bool = False,
|
|
946
|
+
) -> None:
|
|
947
|
+
"""Launch a local web editor for custom field configurations.
|
|
948
|
+
|
|
949
|
+
This command starts a web editor for editing custom field configurations.
|
|
950
|
+
You can provide a JSON file to edit, or load a configuration by ID from the server.
|
|
951
|
+
"""
|
|
952
|
+
from .utils import check_readonly_mode
|
|
953
|
+
|
|
954
|
+
check_readonly_mode("edit custom field configurations")
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
# If config_id is provided, fetch and save it to a temporary file
|
|
958
|
+
if config_id:
|
|
959
|
+
url = f"{get_base_url()}/nidynamicformfields/v1/resolved-configuration"
|
|
960
|
+
params = {"configurationId": config_id}
|
|
961
|
+
query_string = urllib.parse.urlencode(params)
|
|
962
|
+
full_url = f"{url}?{query_string}"
|
|
963
|
+
|
|
964
|
+
resp = make_api_request("GET", full_url)
|
|
965
|
+
data = resp.json()
|
|
966
|
+
|
|
967
|
+
# Generate a temporary file for the configuration
|
|
968
|
+
if not file:
|
|
969
|
+
config_name = data.get("configuration", {}).get("name", f"config-{config_id}")
|
|
970
|
+
safe_name = sanitize_filename(config_name, f"config-{config_id}")
|
|
971
|
+
file = f"{safe_name}.json"
|
|
972
|
+
|
|
973
|
+
# Save the fetched configuration to file
|
|
974
|
+
save_json_file(data, file)
|
|
975
|
+
click.echo(f"✓ Configuration loaded from server: {file}")
|
|
976
|
+
|
|
977
|
+
launch_dff_editor(file=file, port=port, open_browser=not no_browser)
|
|
978
|
+
except Exception as exc:
|
|
979
|
+
handle_api_error(exc)
|