diff --git a/api/src/main/java/org/pmiops/workbench/api/AppsController.java b/api/src/main/java/org/pmiops/workbench/api/AppsController.java index e98fafa1870..b00a6343df8 100644 --- a/api/src/main/java/org/pmiops/workbench/api/AppsController.java +++ b/api/src/main/java/org/pmiops/workbench/api/AppsController.java @@ -97,8 +97,12 @@ public ResponseEntity listAppsInWorkspace(String workspaceName return ResponseEntity.ok(response); } + @Override public ResponseEntity localizeApp( - String workspaceNamespace, String appName, AppLocalizeRequest body) { + String workspaceNamespace, + String appName, + Boolean localizeAllFiles, + AppLocalizeRequest body) { DbUser user = userProvider.get(); leonardoApiHelper.enforceComputeSecuritySuspension(user); @@ -111,6 +115,7 @@ public ResponseEntity localizeApp( body.getAppType(), body.getFileNames(), body.isPlaygroundMode(), - false))); + false, + localizeAllFiles))); } } diff --git a/api/src/main/java/org/pmiops/workbench/api/RuntimeController.java b/api/src/main/java/org/pmiops/workbench/api/RuntimeController.java index 86b9da7c1ad..73a937e91ae 100644 --- a/api/src/main/java/org/pmiops/workbench/api/RuntimeController.java +++ b/api/src/main/java/org/pmiops/workbench/api/RuntimeController.java @@ -274,7 +274,7 @@ public ResponseEntity deleteRuntime( @Override public ResponseEntity localize( - String workspaceNamespace, RuntimeLocalizeRequest body) { + String workspaceNamespace, Boolean localizeAllFiles, RuntimeLocalizeRequest body) { DbUser user = userProvider.get(); leonardoApiHelper.enforceComputeSecuritySuspension(user); DbWorkspace dbWorkspace = workspaceService.lookupWorkspaceByNamespace(workspaceNamespace); @@ -295,6 +295,7 @@ public ResponseEntity localize( appType, body.getNotebookNames(), body.isPlaygroundMode(), - true))); + true, + localizeAllFiles))); } } diff --git a/api/src/main/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisService.java b/api/src/main/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisService.java index f45f1b807f6..9a002d46bf4 100644 --- a/api/src/main/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisService.java +++ b/api/src/main/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisService.java @@ -23,6 +23,8 @@ import org.pmiops.workbench.firecloud.FireCloudService; import org.pmiops.workbench.leonardo.LeonardoApiClient; import org.pmiops.workbench.model.AppType; +import org.pmiops.workbench.model.FileDetail; +import org.pmiops.workbench.notebooks.NotebooksService; import org.pmiops.workbench.notebooks.model.StorageLink; import org.pmiops.workbench.rawls.model.RawlsWorkspaceDetails; import org.pmiops.workbench.workspaces.WorkspaceService; @@ -43,6 +45,7 @@ public class InteractiveAnalysisService { private final UserRecentResourceService userRecentResourceService; private final LeonardoApiClient leonardoNotebooksClient; + private final NotebooksService notebooksService; private static final String AOU_CONFIG_FILENAME = ".all_of_us_config.json"; private static final String WORKSPACE_NAMESPACE_KEY = "WORKSPACE_NAMESPACE"; @@ -69,13 +72,15 @@ public InteractiveAnalysisService( Provider workbenchConfigProvider, Provider userProvider, UserRecentResourceService userRecentResourceService, - LeonardoApiClient leonardoNotebooksClient) { + LeonardoApiClient leonardoNotebooksClient, + NotebooksService notebooksService) { this.workspaceService = workspaceService; this.fireCloudService = fireCloudService; this.workbenchConfigProvider = workbenchConfigProvider; this.userProvider = userProvider; this.userRecentResourceService = userRecentResourceService; this.leonardoNotebooksClient = leonardoNotebooksClient; + this.notebooksService = notebooksService; } public String localize( @@ -84,7 +89,8 @@ public String localize( @Nullable AppType appType, List fileNames, boolean isPlayground, - boolean isGceRuntime) { + boolean isGceRuntime, + boolean localizeAllFiles) { DbWorkspace dbWorkspace = workspaceService.lookupWorkspaceByNamespace(workspaceNamespace); final RawlsWorkspaceDetails firecloudWorkspace; try { @@ -161,12 +167,35 @@ public String localize( localizeMap.put(aouConfigEditDir, aouConfigUri); - // Localize the requested notebooks, if any. - localizeMap.putAll( - fileNames.stream() - .collect( - Collectors.toMap( - name -> localizeTargetDir + name, name -> gcsNotebooksDir + "/" + name))); + // Localize all files if localizeAllFiles is true, otherwise, localize the requested notebooks. + if (localizeAllFiles) { + List notebooks; + if (isGceRuntime) { + notebooks = + notebooksService.getAllJupyterNotebooks( + firecloudWorkspace.getBucketName(), + workspaceNamespace, + firecloudWorkspace.getName()); + } else { + notebooks = + notebooksService.getAllNotebooksByAppType( + firecloudWorkspace.getBucketName(), + workspaceNamespace, + firecloudWorkspace.getName(), + appType); + } + localizeMap.putAll( + notebooks.stream() + .collect( + Collectors.toMap( + notebook -> localizeTargetDir + notebook.getName(), FileDetail::getPath))); + } else { + localizeMap.putAll( + fileNames.stream() + .collect( + Collectors.toMap( + name -> localizeTargetDir + name, name -> gcsNotebooksDir + "/" + name))); + } if (isGceRuntime) { localizeMap.put(playgroundDir + "/" + AOU_CONFIG_FILENAME, aouConfigUri); diff --git a/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksService.java b/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksService.java index a987df4359d..db76d2a52a0 100644 --- a/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksService.java +++ b/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksService.java @@ -3,6 +3,7 @@ import com.google.cloud.storage.Blob; import java.util.List; import org.json.JSONObject; +import org.pmiops.workbench.model.AppType; import org.pmiops.workbench.model.FileDetail; import org.pmiops.workbench.model.KernelTypeEnum; @@ -18,6 +19,12 @@ public interface NotebooksService { List getNotebooksAsService( String bucketName, String workspaceNamespace, String workspaceName); + List getAllNotebooksByAppType( + String bucketName, String workspaceNamespace, String workspaceName, AppType appType); + + List getAllJupyterNotebooks( + String bucketName, String workspaceNamespace, String workspaceName); + /** * Is this a notebook file which is managed (localized and delocalized) by the Workbench? * diff --git a/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksServiceImpl.java b/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksServiceImpl.java index 9c489ba5b2e..52adae632b9 100644 --- a/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksServiceImpl.java +++ b/api/src/main/java/org/pmiops/workbench/notebooks/NotebooksServiceImpl.java @@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.Clock; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -28,6 +29,7 @@ import org.pmiops.workbench.firecloud.FireCloudService; import org.pmiops.workbench.google.CloudStorageClient; import org.pmiops.workbench.google.GoogleCloudLocators; +import org.pmiops.workbench.model.AppType; import org.pmiops.workbench.model.FileDetail; import org.pmiops.workbench.model.KernelTypeEnum; import org.pmiops.workbench.model.WorkspaceAccessLevel; @@ -124,6 +126,32 @@ public List getNotebooksAsService( .collect(Collectors.toList()); } + @Override + public List getAllNotebooksByAppType( + String bucketName, String workspaceNamespace, String workspaceName, AppType appType) { + List allNotebooks = + getNotebooksAsService(bucketName, workspaceNamespace, workspaceName); + if (appType.equals(AppType.RSTUDIO)) { + return allNotebooks.stream() + .filter(fileDetail -> NotebookUtils.isRStudioFile(fileDetail.getName())) + .collect(Collectors.toList()); + } else if (appType.equals(AppType.SAS)) { + return allNotebooks.stream() + .filter(fileDetail -> NotebookUtils.isSasFile(fileDetail.getName())) + .collect(Collectors.toList()); + } else { + return new ArrayList<>(); + } + } + + @Override + public List getAllJupyterNotebooks( + String bucketName, String workspaceNamespace, String workspaceName) { + return getNotebooksAsService(bucketName, workspaceNamespace, workspaceName).stream() + .filter(fileDetail -> NotebookUtils.isJupyterNotebook(fileDetail.getName())) + .collect(Collectors.toList()); + } + @Override public boolean isManagedNotebookBlob(Blob blob) { // Blobs have notebooks/ directory diff --git a/api/src/main/resources/workbench-api.yaml b/api/src/main/resources/workbench-api.yaml index adb88cdea4d..c6dfd5ce3a8 100644 --- a/api/src/main/resources/workbench-api.yaml +++ b/api/src/main/resources/workbench-api.yaml @@ -832,6 +832,15 @@ paths: required: true schema: type: string + - in: query + name: localizeAllFiles + description: | + Optional query that will localize all files associate with this app. + If true, it will ignore fileNames in body and localize all files instead. + required: false + schema: + type: boolean + default: false requestBody: description: Localization request. content: @@ -1102,6 +1111,15 @@ paths: required: true schema: type: string + - in: query + name: localizeAllFiles + description: | + Optional query that will localize all files associate with this app. + If true, it will ignore fileNames in body and localize all files instead. + required: false + schema: + type: boolean + default: false requestBody: description: Localization request. content: diff --git a/api/src/test/java/org/pmiops/workbench/api/RuntimeControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/RuntimeControllerTest.java index 5ad352ee0ae..84ab6584c37 100644 --- a/api/src/test/java/org/pmiops/workbench/api/RuntimeControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/RuntimeControllerTest.java @@ -1321,7 +1321,7 @@ public void testLocalize_securitySuspended() { assertThrows( FailedPreconditionException.class, - () -> runtimeController.localize(WORKSPACE_NS, new RuntimeLocalizeRequest())); + () -> runtimeController.localize(WORKSPACE_NS, false, new RuntimeLocalizeRequest())); } @Test @@ -1331,7 +1331,8 @@ public void localize_validateActiveBilling() { .validateActiveBilling(WORKSPACE_NS, WORKSPACE_ID); RuntimeLocalizeRequest req = new RuntimeLocalizeRequest(); - assertThrows(ForbiddenException.class, () -> runtimeController.localize(WORKSPACE_NS, req)); + assertThrows( + ForbiddenException.class, () -> runtimeController.localize(WORKSPACE_NS, false, req)); } @Test @@ -1342,7 +1343,8 @@ public void localize_validateActiveBilling_checkAccessFirst() { RuntimeLocalizeRequest req = new RuntimeLocalizeRequest(); - assertThrows(ForbiddenException.class, () -> runtimeController.localize(WORKSPACE_NS, req)); + assertThrows( + ForbiddenException.class, () -> runtimeController.localize(WORKSPACE_NS, false, req)); verify(mockWorkspaceAuthService, never()).validateActiveBilling(anyString(), anyString()); } diff --git a/api/src/test/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisServiceTest.java b/api/src/test/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisServiceTest.java index dfb5890c215..9b09ff15aee 100644 --- a/api/src/test/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisServiceTest.java +++ b/api/src/test/java/org/pmiops/workbench/interactiveanalysis/InteractiveAnalysisServiceTest.java @@ -8,6 +8,7 @@ import static org.pmiops.workbench.interactiveanalysis.InteractiveAnalysisService.SAS_DELOC_PATTERN; import static org.pmiops.workbench.interactiveanalysis.InteractiveAnalysisService.aouConfigDataUri; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -22,6 +23,7 @@ import org.pmiops.workbench.firecloud.FireCloudService; import org.pmiops.workbench.leonardo.LeonardoApiClient; import org.pmiops.workbench.model.AppType; +import org.pmiops.workbench.notebooks.NotebooksService; import org.pmiops.workbench.notebooks.model.StorageLink; import org.pmiops.workbench.rawls.model.RawlsWorkspaceDetails; import org.pmiops.workbench.rawls.model.RawlsWorkspaceResponse; @@ -74,6 +76,7 @@ DbUser user() { @MockBean WorkspaceService mockWorkspaceService; @MockBean LeonardoApiClient mockLeonardoApiClient; @MockBean FireCloudService mockFireCloudService; + @MockBean NotebooksService mockNotebooksService; @Autowired InteractiveAnalysisService interactiveAnalysisService; @@ -96,7 +99,7 @@ public void setUp() { .setFirecloudName(FIRECLOUD_WS_NAME); when(mockWorkspaceService.lookupWorkspaceByNamespace(WORKSPACE_NS)).thenReturn(dbWorkspace); RawlsWorkspaceDetails rawlsWorkspaceDetails = - new RawlsWorkspaceDetails().bucketName(BUCKET_NAME); + new RawlsWorkspaceDetails().bucketName(BUCKET_NAME).name(FIRECLOUD_WS_NAME); when(mockFireCloudService.getWorkspace(WORKSPACE_NS, FIRECLOUD_WS_NAME)) .thenReturn(new RawlsWorkspaceResponse().workspace(rawlsWorkspaceDetails)); @@ -122,7 +125,7 @@ public void testLocalize_jupyter_editMode() { AppType appType = null; // Jupyter uses GCE, so it doesn't have a GKE App Type interactiveAnalysisService.localize( - WORKSPACE_NS, APP_NAME, appType, notebookLists, false, true); + WORKSPACE_NS, APP_NAME, appType, notebookLists, false, true, false); verify(mockLeonardoApiClient) .createStorageLinkForRuntime(GOOGLE_PROJECT_ID, APP_NAME, expectedStorageLink); verify(mockLeonardoApiClient) @@ -146,7 +149,8 @@ public void testLocalize_jupyter_playground() { expectedLocalizeMap.put(playgroundDir + "/foo.ipynb", NOTEBOOK_DIR + "/foo.ipynb"); AppType appType = null; // Jupyter uses GCE, so it doesn't have a GKE App Type - interactiveAnalysisService.localize(WORKSPACE_NS, APP_NAME, appType, notebookLists, true, true); + interactiveAnalysisService.localize( + WORKSPACE_NS, APP_NAME, appType, notebookLists, true, true, false); verify(mockLeonardoApiClient) .createStorageLinkForRuntime(GOOGLE_PROJECT_ID, APP_NAME, expectedStorageLink); verify(mockLeonardoApiClient) @@ -167,12 +171,36 @@ public void testLocalize_RStudio_editMode() { expectedLocalizeMap.put("foo.Rmd", NOTEBOOK_DIR + "/foo.Rmd"); interactiveAnalysisService.localize( - WORKSPACE_NS, APP_NAME, AppType.RSTUDIO, notebookLists, false, false); + WORKSPACE_NS, APP_NAME, AppType.RSTUDIO, notebookLists, false, false, false); verify(mockLeonardoApiClient) .createStorageLinkForApp(GOOGLE_PROJECT_ID, APP_NAME, expectedStorageLink); verify(mockLeonardoApiClient).localizeForApp(GOOGLE_PROJECT_ID, APP_NAME, expectedLocalizeMap); } + @Test + public void testLocalize_allFiles_rstudio() { + interactiveAnalysisService.localize( + WORKSPACE_NS, APP_NAME, AppType.RSTUDIO, new ArrayList<>(), false, false, true); + verify(mockNotebooksService) + .getAllNotebooksByAppType(BUCKET_NAME, WORKSPACE_NS, FIRECLOUD_WS_NAME, AppType.RSTUDIO); + } + + @Test + public void testLocalize_allFiles_sas() { + interactiveAnalysisService.localize( + WORKSPACE_NS, APP_NAME, AppType.SAS, new ArrayList<>(), false, false, true); + verify(mockNotebooksService) + .getAllNotebooksByAppType(BUCKET_NAME, WORKSPACE_NS, FIRECLOUD_WS_NAME, AppType.SAS); + } + + @Test + public void testLocalize_allFiles_jupyter() { + interactiveAnalysisService.localize( + WORKSPACE_NS, APP_NAME, null, new ArrayList<>(), false, true, true); + verify(mockNotebooksService) + .getAllJupyterNotebooks(BUCKET_NAME, WORKSPACE_NS, FIRECLOUD_WS_NAME); + } + @Test public void testLocalize_SAS_editMode() { String editDir = ""; @@ -187,7 +215,7 @@ public void testLocalize_SAS_editMode() { expectedLocalizeMap.put("foo.sas", NOTEBOOK_DIR + "/foo.sas"); interactiveAnalysisService.localize( - WORKSPACE_NS, APP_NAME, AppType.SAS, notebookLists, false, false); + WORKSPACE_NS, APP_NAME, AppType.SAS, notebookLists, false, false, false); verify(mockLeonardoApiClient) .createStorageLinkForApp(GOOGLE_PROJECT_ID, APP_NAME, expectedStorageLink); verify(mockLeonardoApiClient).localizeForApp(GOOGLE_PROJECT_ID, APP_NAME, expectedLocalizeMap); @@ -200,6 +228,12 @@ public void testLocalize_appType_not_supported() { NotImplementedException.class, () -> interactiveAnalysisService.localize( - WORKSPACE_NS, APP_NAME, unsupportedAppType, Collections.emptyList(), false, false)); + WORKSPACE_NS, + APP_NAME, + unsupportedAppType, + Collections.emptyList(), + false, + false, + false)); } } diff --git a/api/src/test/java/org/pmiops/workbench/notebooks/NotebooksServiceTest.java b/api/src/test/java/org/pmiops/workbench/notebooks/NotebooksServiceTest.java index c1a9254548a..8b5b95262a1 100644 --- a/api/src/test/java/org/pmiops/workbench/notebooks/NotebooksServiceTest.java +++ b/api/src/test/java/org/pmiops/workbench/notebooks/NotebooksServiceTest.java @@ -42,6 +42,7 @@ import org.pmiops.workbench.exceptions.NotImplementedException; import org.pmiops.workbench.firecloud.FireCloudService; import org.pmiops.workbench.google.CloudStorageClient; +import org.pmiops.workbench.model.AppType; import org.pmiops.workbench.model.FileDetail; import org.pmiops.workbench.model.KernelTypeEnum; import org.pmiops.workbench.model.WorkspaceAccessLevel; @@ -520,6 +521,81 @@ public void testGetNotebooks_mixedFileTypes() { assertThat(gotNames).isEqualTo(ImmutableList.of("f1.ipynb", "f2.Rmd")); } + @Test + public void testGetNotebooks_getAllFilesByAppType() { + Blob mockBlob1 = mock(Blob.class); + Blob mockBlob2 = mock(Blob.class); + Blob mockBlob3 = mock(Blob.class); + Blob mockBlob4 = mock(Blob.class); + FileDetail fileDetail1 = mock(FileDetail.class); + FileDetail fileDetail2 = mock(FileDetail.class); + FileDetail fileDetail3 = mock(FileDetail.class); + FileDetail fileDetail4 = mock(FileDetail.class); + Set workspaceUsersSet = new HashSet(); + + stubGetWorkspace( + dbWorkspace.getWorkspaceNamespace(), + dbWorkspace.getFirecloudName(), + BUCKET_NAME, + WorkspaceAccessLevel.OWNER); + when(mockBlob1.getName()).thenReturn(NotebookUtils.withNotebookPath("f1.ipynb")); + when(mockBlob2.getName()).thenReturn(NotebookUtils.withNotebookPath("f2.Rmd")); + when(mockBlob3.getName()).thenReturn(NotebookUtils.withNotebookPath("f3.sas")); + when(mockBlob4.getName()).thenReturn(NotebookUtils.withNotebookPath("f4.random")); + when(mockCloudStorageClient.getBlobPageForPrefix( + BUCKET_NAME, NotebookUtils.NOTEBOOKS_WORKSPACE_DIRECTORY)) + .thenReturn(ImmutableList.of(mockBlob1, mockBlob2, mockBlob3)); + when(mockCloudStorageClient.blobToFileDetail(mockBlob1, BUCKET_NAME, workspaceUsersSet)) + .thenReturn(fileDetail1); + when(mockCloudStorageClient.blobToFileDetail(mockBlob2, BUCKET_NAME, workspaceUsersSet)) + .thenReturn(fileDetail2); + when(mockCloudStorageClient.blobToFileDetail(mockBlob3, BUCKET_NAME, workspaceUsersSet)) + .thenReturn(fileDetail3); + when(fileDetail1.getName()).thenReturn("f1.ipynb"); + when(fileDetail2.getName()).thenReturn("f2.Rmd"); + when(fileDetail3.getName()).thenReturn("f3.sas"); + when(fileDetail4.getName()).thenReturn("f4.random"); + + List jupyterNotebooks = + notebooksService + .getAllJupyterNotebooks( + BUCKET_NAME, dbWorkspace.getWorkspaceNamespace(), dbWorkspace.getFirecloudName()) + .stream() + .map(FileDetail::getName) + .collect(Collectors.toList()); + List rstudioNotebooks = + notebooksService + .getAllNotebooksByAppType( + BUCKET_NAME, + dbWorkspace.getWorkspaceNamespace(), + dbWorkspace.getFirecloudName(), + AppType.RSTUDIO) + .stream() + .map(FileDetail::getName) + .collect(Collectors.toList()); + List sasNotebooks = + notebooksService + .getAllNotebooksByAppType( + BUCKET_NAME, + dbWorkspace.getWorkspaceNamespace(), + dbWorkspace.getFirecloudName(), + AppType.SAS) + .stream() + .map(FileDetail::getName) + .collect(Collectors.toList()); + + assertThat(jupyterNotebooks).containsExactly("f1.ipynb"); + assertThat(rstudioNotebooks).containsExactly("f2.Rmd"); + assertThat(sasNotebooks).containsExactly("f3.sas"); + assertThat( + notebooksService.getAllNotebooksByAppType( + BUCKET_NAME, + dbWorkspace.getWorkspaceNamespace(), + dbWorkspace.getFirecloudName(), + AppType.CROMWELL)) + .isEmpty(); + } + @Test public void testGetNotebooks_omitsExtraDirectories() { Blob mockBlob1 = mock(Blob.class); diff --git a/ui/src/app/components/apps-panel/expanded-app.spec.tsx b/ui/src/app/components/apps-panel/expanded-app.spec.tsx index 9860b5676b6..177826f5794 100644 --- a/ui/src/app/components/apps-panel/expanded-app.spec.tsx +++ b/ui/src/app/components/apps-panel/expanded-app.spec.tsx @@ -394,6 +394,7 @@ describe('ExpandedApp', () => { expect(localizeSpy).toHaveBeenCalledWith( WorkspaceStubVariables.DEFAULT_WORKSPACE_NS, appName, + false, { appType: toAppType[appType], fileNames: [], diff --git a/ui/src/app/pages/analysis/leonardo-app-launcher.tsx b/ui/src/app/pages/analysis/leonardo-app-launcher.tsx index 1efd3916ea4..3fb3562086c 100644 --- a/ui/src/app/pages/analysis/leonardo-app-launcher.tsx +++ b/ui/src/app/pages/analysis/leonardo-app-launcher.tsx @@ -657,6 +657,7 @@ export const LeonardoAppLauncher = fp.flow( const resp = await this.runtimeRetry(() => runtimeApi().localize( workspace.namespace, + false, { notebookNames, playgroundMode: this.isPlaygroundMode(), diff --git a/ui/src/app/utils/user-apps-utils.tsx b/ui/src/app/utils/user-apps-utils.tsx index 8d367d34a69..3f44f5e285b 100644 --- a/ui/src/app/utils/user-apps-utils.tsx +++ b/ui/src/app/utils/user-apps-utils.tsx @@ -20,6 +20,7 @@ import { userAppsStore } from 'app/utils/stores'; import { fetchWithErrorModal } from './errors'; import { getLastActiveEpochMillis, setLastActive } from './inactivity'; +import { currentWorkspaceStore } from './navigation'; // the polling timeout to use when waiting for a transition (e.g. from Running to Paused) const transitionPollingTimeoutMs = 10e3; // 10 sec @@ -57,6 +58,71 @@ export const updateLastActive = (userApps: ListAppsResponse) => { } }; +const localizeUserApp = ( + namespace: string, + appName: string, + appType: AppType, + fileNames: Array, + playgroundMode: boolean, + localizeAllFile: boolean +) => + appsApi().localizeApp(namespace, appName, localizeAllFile, { + fileNames, + playgroundMode, + appType, + }); + +const appJustTurnedRunningFromProvisioning = (listAppsResponse) => { + // Note: We do not call localize for CROMWELL + // We want app that are transitioning from PROVISIONING to RUNNING + const appsJustStartedRunning = userAppsStore + .get() + .userApps.filter((userApp) => { + if ( + userApp.status === AppStatus.PROVISIONING && + userApp.appType !== AppType.CROMWELL + ) { + const runningAppFromApi = listAppsResponse.filter( + (app) => + app.appType === userApp.appType && app.status === AppStatus.RUNNING + ); + return !!runningAppFromApi && runningAppFromApi.length > 0; + } + return false; + }); + return appsJustStartedRunning; +}; + +const callLocalizeIfApplicable = (listAppsResponse) => { + // If userAppsStore is not updated lets wait for it to be updated before checking + if (!!userAppsStore.get() && userAppsStore.get().userApps === undefined) { + return null; + } + + // Get the list of Apps that are in PROVISIONING state in store but RUNNING in list of Apps from api response + // We want to call Localize only ONCE just as soon as they are running + const appsTransitionToRunningNow = + appJustTurnedRunningFromProvisioning(listAppsResponse); + + if (appsTransitionToRunningNow.length === 0) { + return null; + } + + appsTransitionToRunningNow.forEach((app) => { + fetchWithErrorModal( + async () => + await localizeUserApp( + currentWorkspaceStore.getValue().namespace, + app.appName, + app.appType, + [], + false, + true + ) + ); + }); +}; + export const maybeStartPollingForUserApps = (namespace: string) => { const { updating } = userAppsStore.get(); // Prevents multiple update processes from running concurrently. @@ -81,6 +147,8 @@ export const maybeStartPollingForUserApps = (namespace: string) => { : activityPollingTimeoutMs ); + callLocalizeIfApplicable(listAppsResponse); + userAppsStore.set({ userApps: listAppsResponse, updating: false, @@ -133,19 +201,6 @@ export function unattachedDiskExists( return !app && disk !== undefined; } -const localizeUserApp = ( - namespace: string, - appName: string, - appType: AppType, - fileNames: Array, - playgroundMode: boolean -) => - appsApi().localizeApp(namespace, appName, { - fileNames, - playgroundMode, - appType, - }); - // does this app have a UI that the user can interact with? export const isInteractiveUIApp = (appType: UIAppType) => ( @@ -161,12 +216,20 @@ export const openAppInIframe = ( userApp: UserAppEnvironment, navigate: (commands: any, extras?: any) => void ) => { + const url = window.location.href; + const urlRoute = url.substring(url.lastIndexOf('/') + 1); + const fileNameRegex = /[^\\]*\.(\w+)$/; + const fileName = urlRoute.match(fileNameRegex); + const localizeFileList = + !!fileName && fileName.length > 0 ? [fileName[0]] : []; + fetchWithErrorModal(() => localizeUserApp( workspaceNamespace, userApp.appName, userApp.appType, - [], + localizeFileList, + false, false ) ); diff --git a/ui/src/testing/stubs/apps-api-stub.ts b/ui/src/testing/stubs/apps-api-stub.ts index d1b825f4e97..1ee37689318 100644 --- a/ui/src/testing/stubs/apps-api-stub.ts +++ b/ui/src/testing/stubs/apps-api-stub.ts @@ -109,6 +109,7 @@ export class AppsApiStub extends AppsApi { public localizeApp( _workspaceNamespace: string, _appName: string, + _localizeAllFiles: boolean, _body: AppLocalizeRequest, _options: any ): Promise {