Fix: Loading state while collection mount (#5138)

This commit is contained in:
naman-bruno
2025-07-29 17:15:30 +05:30
committed by GitHub
parent 780beb832e
commit 62151330f2
8 changed files with 185 additions and 31 deletions

View File

@@ -2,10 +2,10 @@ import React, { useState, useRef, useEffect } from 'react';
import path from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { get, cloneDeep } from 'lodash';
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
import { runCollectionFolder, cancelRunnerExecution, mountCollection } from 'providers/ReduxStore/slices/collections/actions';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
@@ -103,11 +103,24 @@ export default function RunnerResults({ collection }) {
})
.filter(Boolean);
const ensureCollectionIsMounted = () => {
if(collection.mountStatus === 'mounted'){
return;
}
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
};
const runCollection = () => {
ensureCollectionIsMounted();
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
};
const runAgain = () => {
ensureCollectionIsMounted();
dispatch(
runCollectionFolder(
collection.uid,
@@ -149,8 +162,12 @@ export default function RunnerResults({ collection }) {
</div>
<div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
{isCollectionLoading && (
<span className="ml-2 text-sm text-gray-500">
(Loading...)
</span>
)}
</div>
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
<div className="mt-6">
<label>Delay (in ms)</label>
<input
@@ -168,13 +185,16 @@ export default function RunnerResults({ collection }) {
{/* Tags for the collection run */}
<RunnerTags collectionUid={collection.uid} className='mb-6' />
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" disabled={shouldDisableCollectionRun} onClick={runCollection}>
Run Collection
</button>
<div className='flex flex-row gap-2'>
<button type="submit" className="submit btn btn-sm btn-secondary flex items-center gap-2" disabled={shouldDisableCollectionRun || isCollectionLoading} onClick={runCollection}>
{isCollectionLoading && <IconLoader2 size={16} className="animate-spin" />}
Run Collection
</button>
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
Reset
</button>
<button className="submit btn btn-sm btn-close" onClick={resetRunner}>
Reset
</button>
</div>
</StyledWrapper>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Modal from 'components/Modal';
import { IconDownload } from '@tabler/icons';
import { IconDownload, IconLoader2 } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import Bruno from 'components/Bruno';
import exportBrunoCollection from 'utils/collections/export';
@@ -8,10 +8,12 @@ import exportPostmanCollection from 'utils/exporters/postman-collection';
import { cloneDeep } from 'lodash';
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
import { useSelector } from 'react-redux';
import { findCollectionByUid } from 'utils/collections/index';
import { findCollectionByUid, areItemsLoading } from 'utils/collections/index';
const ShareCollection = ({ onClose, collectionUid }) => {
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
const isCollectionLoading = areItemsLoading(collection);
const handleExportBrunoCollection = () => {
const collectionCopy = cloneDeep(collection);
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
@@ -35,23 +37,49 @@ const ShareCollection = ({ onClose, collectionUid }) => {
>
<StyledWrapper className="flex flex-col h-full w-[500px]">
<div className="space-y-2">
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportBrunoCollection}>
<div
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportBrunoCollection}
>
<div className="mr-3 p-1 rounded-full">
<Bruno width={28} />
{isCollectionLoading ? (
<IconLoader2 size={28} className="animate-spin" />
) : (
<Bruno width={28} />
)}
</div>
<div className="flex-1">
<div className="font-medium">Bruno Collection</div>
<div className="text-xs">Export in Bruno format</div>
<div className="text-xs">
{isCollectionLoading ? 'Loading collection...' : 'Export in Bruno format'}
</div>
</div>
</div>
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportPostmanCollection}>
<div
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
>
<div className="mr-3 p-1 rounded-full">
<IconDownload size={28} strokeWidth={1} className="" />
{isCollectionLoading ? (
<IconLoader2 size={28} className="animate-spin" />
) : (
<IconDownload size={28} strokeWidth={1} className="" />
)}
</div>
<div className="flex-1">
<div className="font-medium">Postman Collection</div>
<div className="text-xs">Export in Postman format</div>
<div className="text-xs">
{isCollectionLoading ? 'Loading collection...' : 'Export in Postman format'}
</div>
</div>
</div>
</div>

View File

@@ -61,13 +61,14 @@ const Collection = ({ collection, searchText }) => {
};
const ensureCollectionIsMounted = () => {
if (collection.mountStatus === 'unmounted') {
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
if(collection.mountStatus === 'mounted'){
return;
}
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
}
const hasSearchText = searchText && searchText?.trim()?.length;
@@ -269,6 +270,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
ensureCollectionIsMounted();
handleRun();
}}
>
@@ -287,6 +289,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
ensureCollectionIsMounted();
setShowShareCollectionModal(true);
}}
>

View File

@@ -24,7 +24,7 @@ import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
import { addLog } from 'providers/ReduxStore/slices/logs';
const useIpcEvents = () => {
@@ -179,6 +179,10 @@ const useIpcEvents = () => {
dispatch(collectionAddOauth2CredentialsByUrl(payload));
});
const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
dispatch(updateCollectionLoadingState(val));
});
return () => {
removeCollectionTreeUpdateListener();
removeOpenCollectionListener();
@@ -199,6 +203,7 @@ const useIpcEvents = () => {
removeGlobalEnvironmentsUpdatesListener();
removeSnapshotHydrationListener();
removeCollectionOauth2CredentialsUpdatesListener();
removeCollectionLoadingStateListener();
};
}, [isElectron]);
};

View File

@@ -67,6 +67,12 @@ export const collectionsSlice = createSlice({
}
}
},
updateCollectionLoadingState: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
collection.isLoading = action.payload.isLoading;
}
},
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
@@ -2413,6 +2419,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
updateCollectionMountStatus,
updateCollectionLoadingState,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,

View File

@@ -137,6 +137,10 @@ export const findEnvironmentInCollectionByName = (collection, name) => {
};
export const areItemsLoading = (folder) => {
if (!folder || folder.isLoading) {
return true;
}
let flattenedItems = flattenItems(folder.items);
return flattenedItems?.reduce((isLoading, i) => {
if (i?.loading) {

View File

@@ -166,7 +166,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
}
};
const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => {
const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread, watcher) => {
console.log(`watcher add: ${pathname}`);
if (isBrunoConfigFile(pathname, collectionPath)) {
@@ -251,6 +251,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
}
if (hasBruExtension(pathname)) {
watcher.addFileToProcessing(collectionUid, pathname);
const file = {
meta: {
collectionUid,
@@ -270,8 +272,11 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (error) {
console.error(error);
} finally {
watcher.markFileAsProcessed(win, collectionUid, pathname);
}
return;
}
@@ -320,6 +325,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} finally {
watcher.markFileAsProcessed(win, collectionUid, pathname);
}
}
};
@@ -510,7 +517,10 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
};
const onWatcherSetupComplete = (win, watchPath) => {
const onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => {
// Mark discovery as complete
watcher.completeCollectionDiscovery(win, collectionUid);
const UiStateSnapshotStore = new UiStateSnapshot();
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath);
@@ -520,6 +530,75 @@ const onWatcherSetupComplete = (win, watchPath) => {
class CollectionWatcher {
constructor() {
this.watchers = {};
this.loadingStates = {};
}
// Initialize loading state tracking for a collection
initializeLoadingState(collectionUid) {
if (!this.loadingStates[collectionUid]) {
this.loadingStates[collectionUid] = {
isDiscovering: false, // Initial discovery phase
isProcessing: false, // Processing discovered files
pendingFiles: new Set(), // Files that need processing
};
}
}
startCollectionDiscovery(win, collectionUid) {
this.initializeLoadingState(collectionUid);
const state = this.loadingStates[collectionUid];
state.isDiscovering = true;
state.pendingFiles.clear();
win.webContents.send('main:collection-loading-state-updated', {
collectionUid,
isLoading: true
});
}
addFileToProcessing(collectionUid, filepath) {
this.initializeLoadingState(collectionUid);
const state = this.loadingStates[collectionUid];
state.pendingFiles.add(filepath);
}
markFileAsProcessed(win, collectionUid, filepath) {
if (!this.loadingStates[collectionUid]) return;
const state = this.loadingStates[collectionUid];
state.pendingFiles.delete(filepath);
// If discovery is complete and no pending files, mark as not loading
if (!state.isDiscovering && state.pendingFiles.size === 0 && state.isProcessing) {
state.isProcessing = false;
win.webContents.send('main:collection-loading-state-updated', {
collectionUid,
isLoading: false
});
}
}
completeCollectionDiscovery(win, collectionUid) {
if (!this.loadingStates[collectionUid]) return;
const state = this.loadingStates[collectionUid];
state.isDiscovering = false;
// If there are pending files, start processing phase
if (state.pendingFiles.size > 0) {
state.isProcessing = true;
} else {
// No pending files, collection is fully loaded
win.webContents.send('main:collection-loading-state-updated', {
collectionUid,
isLoading: false
});
}
}
cleanupLoadingState(collectionUid) {
delete this.loadingStates[collectionUid];
}
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
@@ -527,6 +606,10 @@ class CollectionWatcher {
this.watchers[watchPath].close();
}
this.initializeLoadingState(collectionUid);
this.startCollectionDiscovery(win, collectionUid);
const ignores = brunoConfig?.ignore || [];
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
@@ -552,8 +635,8 @@ class CollectionWatcher {
let startedNewWatcher = false;
watcher
.on('ready', () => onWatcherSetupComplete(win, watchPath))
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread))
.on('ready', () => onWatcherSetupComplete(win, watchPath, collectionUid, this))
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread, this))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
@@ -588,11 +671,15 @@ class CollectionWatcher {
return this.watchers[watchPath];
}
removeWatcher(watchPath, win) {
removeWatcher(watchPath, win, collectionUid) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
this.watchers[watchPath] = null;
}
if (collectionUid) {
this.cleanupLoadingState(collectionUid);
}
}
getWatcherByItemPath(itemPath) {

View File

@@ -593,10 +593,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
ipcMain.handle('renderer:remove-collection', async (event, collectionPath, collectionUid) => {
if (watcher && mainWindow) {
console.log(`watcher stopWatching: ${collectionPath}`);
watcher.removeWatcher(collectionPath, mainWindow);
watcher.removeWatcher(collectionPath, mainWindow, collectionUid);
lastOpenedCollections.remove(collectionPath);
}
});