SED-4859 Support Automation Package file resources in the Frontend - #1440
SED-4859 Support Automation Package file resources in the Frontend#1440dvladir wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new file picker modal and service to support selecting and downloading resources directly from automation packages, integrating this capability into the dynamic resource and resource input components. The review feedback highlights several important issues: extracting the automation package resource path by splitting on colons can truncate paths containing colons (such as Windows paths); the location input signal can get out of sync when the form control value is set programmatically without emitting events; the method name downloadAutomationPackageResourceUrl is misleading as it triggers a download rather than returning a URL; and using a non-null assertion on preview.resourceId is risky and should be replaced with a defensive check.
| export const extractApResourcePath = (id: string): string => { | ||
| if (!isApResourceId(id)) { | ||
| return id; | ||
| } | ||
| const [prefix, apId, path] = id.split(':'); | ||
| return path; | ||
| }; |
There was a problem hiding this comment.
Splitting the ID by : and destructuring it into [prefix, apId, path] will fail if the path itself contains colons (such as absolute Windows paths like C:/path/to/file). In such cases, the path will be truncated. Using slice(2).join(':') ensures that any colons within the path are preserved correctly.
| export const extractApResourcePath = (id: string): string => { | |
| if (!isApResourceId(id)) { | |
| return id; | |
| } | |
| const [prefix, apId, path] = id.split(':'); | |
| return path; | |
| }; | |
| export const extractApResourcePath = (id: string): string => { | |
| if (!isApResourceId(id)) { | |
| return id; | |
| } | |
| const parts = id.split(':'); | |
| return parts.slice(2).join(':'); | |
| }; |
There was a problem hiding this comment.
Path format is known
| protected readonly location = signalFromFormControl<string>(this.locationControl); | ||
| protected readonly isLocationButtonDisabled = computed(() => { | ||
| const location = this.location(); | ||
| return !location.trim(); | ||
| }); |
There was a problem hiding this comment.
Since location is a signal wrapping locationControl.valueChanges, it will not emit or update when this.locationControl.setValue is called with { emitEvent: false } (which happens in showRoots() and updateStateFromResult()). This causes isLocationButtonDisabled to become out of sync with the actual input value. Replacing the signal and computed property with a simple getter resolves this synchronization issue cleanly.
protected get isLocationButtonDisabled(): boolean {
return !this.locationControl.value?.trim();
}There was a problem hiding this comment.
Use get in bindings is not a good practice.
Fixed in other way. isLocationButtonDisabled was switched to linked signal
| downloadAutomationPackageResourceUrl(apId: string, path: string, fileName: string): void { | ||
| const url = this.getDownloadAutomationPackageResourceUrl(apId, path); | ||
| downloadFile(url, fileName); | ||
| } |
There was a problem hiding this comment.
The method name downloadAutomationPackageResourceUrl is misleading because it actually triggers the file download (by calling downloadFile) rather than returning a URL. Renaming it to downloadAutomationPackageResource better reflects its behavior.
| downloadAutomationPackageResourceUrl(apId: string, path: string, fileName: string): void { | |
| const url = this.getDownloadAutomationPackageResourceUrl(apId, path); | |
| downloadFile(url, fileName); | |
| } | |
| downloadAutomationPackageResource(apId: string, path: string, fileName: string): void { | |
| const url = this.getDownloadAutomationPackageResourceUrl(apId, path); | |
| downloadFile(url, fileName); | |
| } |
| } | ||
| const list = path.split('/'); | ||
| const fileName = list[list.length - 1]; | ||
| this._apApiService.downloadAutomationPackageResourceUrl(this.apId, path, fileName); |
There was a problem hiding this comment.
Update the method call to match the renamed downloadAutomationPackageResource method in AugmentedAutomationPackagesService.
| this._apApiService.downloadAutomationPackageResourceUrl(this.apId, path, fileName); | |
| this._apApiService.downloadAutomationPackageResource(this.apId, path, fileName); |
| for (const preview of previews) { | ||
| this.setPreviewContent(preview.resourceId, preview.success ? preview.content : undefined); | ||
| this.setPreviewContent(preview.resourceId!, preview.success ? preview.content : undefined); | ||
| } |
There was a problem hiding this comment.
Using the non-null assertion operator preview.resourceId! is risky here because resourceId is defined as optional on the ResourcePreview type. It is safer to add a defensive check to ensure resourceId is defined before calling setPreviewContent.
| for (const preview of previews) { | |
| this.setPreviewContent(preview.resourceId, preview.success ? preview.content : undefined); | |
| this.setPreviewContent(preview.resourceId!, preview.success ? preview.content : undefined); | |
| } | |
| for (const preview of previews) { | |
| if (preview.resourceId) { | |
| this.setPreviewContent(preview.resourceId, preview.success ? preview.content : undefined); | |
| } | |
| } |
| <div class="directory-list"> | ||
| @for (item of directories(); track item.absolutePath) { | ||
| @if (item.isDirectory && (showingRoots() || !item.isHidden)) { | ||
| <div class="file-list"> |
There was a problem hiding this comment.
We have a function scrollDirectoryIntoView() which is used to scroll to newly created folders. But this one still uses directory-list so it is not working anymore.
…841-automation-package-file-system-resource-resolver # Conflicts: # projects/step-core/src/lib/client/generated/models/FetchBucketsRequest.ts # projects/step-core/src/lib/client/generated/models/TwoStageAggregation.ts
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for selecting, browsing, and downloading resources from automation packages. It refactors the file picker into a generic, reusable modal component with a data provider abstraction, integrates the new AutomationPackageResourceComponent into the standard resource input, and adds the RESOURCE_AP_ID token to dynamically resolve package IDs. Feedback highlights several improvement opportunities: ensuring extractApResourcePath correctly handles paths containing colons, making the IDE_MODE injection optional to prevent runtime injector errors in non-IDE contexts, allowing the clear button to be displayed for legacy standard resources even when an automation package is configured, and fixing a typo in a script file tooltip.
| export const extractApResourcePath = (id: string): string => { | ||
| if (!isApResourceId(id)) { | ||
| return id; | ||
| } | ||
| const [prefix, apId, path] = id.split(':'); | ||
| return path; | ||
| }; |
There was a problem hiding this comment.
The current implementation of extractApResourcePath splits the entire ID string by : and only takes the third element. If the resource path itself contains one or more colons (which is possible in some paths or filenames), the path will be truncated and parts of it will be lost.
Using parts.slice(2).join(':') is a more robust approach that correctly reconstructs the full path even if it contains colons.
| export const extractApResourcePath = (id: string): string => { | |
| if (!isApResourceId(id)) { | |
| return id; | |
| } | |
| const [prefix, apId, path] = id.split(':'); | |
| return path; | |
| }; | |
| export const extractApResourcePath = (id: string): string => { | |
| if (!isApResourceId(id)) { | |
| return id; | |
| } | |
| const parts = id.split(':'); | |
| return parts.slice(2).join(':'); | |
| }; |
There was a problem hiding this comment.
Non issue as we don't have collons in paths
| const _injector = inject(Injector); | ||
| const _ideMode = inject(IDE_MODE); |
There was a problem hiding this comment.
Injecting IDE_MODE without making it optional can cause a NullInjectorError at runtime in non-IDE contexts (such as the standard keyword configuration or plan editor) if the token is not provided globally.
To ensure defensive programming and prevent runtime crashes, inject IDE_MODE with { optional: true } and default to false.
| const _injector = inject(Injector); | |
| const _ideMode = inject(IDE_MODE); | |
| const _injector = inject(Injector); | |
| const _ideMode = inject(IDE_MODE, { optional: true }) ?? false; |
There was a problem hiding this comment.
IDE_MODE is safely defined
|
|
||
| <ng-template #clearButtonTemplate> | ||
| @if (_config.withClearButton()) { | ||
| @if (conf.withClearButton) { |
There was a problem hiding this comment.
When automationPackageId is defined, conf.withClearButton is forced to false by the configuration directive. However, if the current resource is a legacy standard resource (i.e., isApResource() is false), the user will be unable to clear it to switch to the automation package resource mode.
We should allow showing the clear button in this scenario so that users can clear standard resources and select an AP resource instead.
@if (conf.withClearButton || (automationPackageId() && !isApResource())) {
There was a problem hiding this comment.
This would enable the removal of non-ap resources which can never be recreated, therefor it should not be allowed.
| <step-dynamic-resource | ||
| label="Script file" | ||
| type="functions" | ||
| tooltip="Leave empty if you wan\'t step to generate it for you otherwise specify a file accessible to the controller. This file will be transferred automatically to the agents." | ||
| [formControl]="formGroup.controls.scriptFile" | ||
| ></step-dynamic-resource> | ||
| </ng-container> | ||
| </ng-container> | ||
| /> |
There was a problem hiding this comment.
The tooltip contains a typo and unnecessary backslash escaping: wan\'t. This should be corrected to want for proper English and to avoid rendering the backslash literally in the UI.
| <step-dynamic-resource | |
| label="Script file" | |
| type="functions" | |
| tooltip="Leave empty if you wan\'t step to generate it for you otherwise specify a file accessible to the controller. This file will be transferred automatically to the agents." | |
| [formControl]="formGroup.controls.scriptFile" | |
| ></step-dynamic-resource> | |
| </ng-container> | |
| </ng-container> | |
| /> | |
| <step-dynamic-resource | |
| label="Script file" | |
| type="functions" | |
| tooltip="Leave empty if you want step to generate it for you otherwise specify a file accessible to the controller. This file will be transferred automatically to the agents." | |
| [formControl]="formGroup.controls.scriptFile" | |
| /> |
No description provided.