mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-02 17:08:32 +00:00
Fix: Loading state while collection mount (#5138)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user