Skip to content

Commit e13c3d4

Browse files
committed
feat(sidebar): show loading state when initially loading connections
1 parent 59f70e7 commit e13c3d4

File tree

10 files changed

+127
-48
lines changed

10 files changed

+127
-48
lines changed

configs/testing-library-compass/src/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,11 @@ type TestConnectionsOptions = {
8181
*/
8282
preferences?: Partial<AllPreferences>;
8383
/**
84-
* Initial list of connections to be "loaded" to the application
84+
* Initial list of connections to be "loaded" to the application. Empty list
85+
* by default. You can explicitly pass `null` to disable initial preloading of
86+
* the connections
8587
*/
86-
connections?: ConnectionInfo[];
88+
connections?: ConnectionInfo[] | null;
8789
/**
8890
* Connection function that returns DataService when connecting to a
8991
* connection with the connections store. Second argument is a constructor
@@ -266,6 +268,9 @@ function createWrapper(
266268
TestingLibraryWrapper: ComponentWithChildren = EmptyWrapper,
267269
container?: HTMLElement
268270
) {
271+
const connections =
272+
options.connections === null ? undefined : options.connections ?? [];
273+
269274
const wrapperState = {
270275
globalAppRegistry: new AppRegistry(),
271276
localAppRegistry: new AppRegistry(),
@@ -274,7 +279,7 @@ function createWrapper(
274279
logger: createNoopLogger(),
275280
connectionStorage:
276281
options.connectionStorage ??
277-
(new InMemoryConnectionStorage(options.connections) as ConnectionStorage),
282+
(new InMemoryConnectionStorage(connections) as ConnectionStorage),
278283
connectionsStore: {
279284
getState: undefined as unknown as () => State,
280285
actions: {} as ReturnType<typeof useConnectionActions>,
@@ -359,7 +364,7 @@ function createWrapper(
359364
onAutoconnectInfoRequest={
360365
options.onAutoconnectInfoRequest
361366
}
362-
preloadStorageConnectionInfos={options.connections}
367+
preloadStorageConnectionInfos={connections}
363368
>
364369
<StoreGetter>
365370
<TestEnvCurrentConnectionContext.Provider

packages/compass-connections/src/index.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,20 @@ const CompassConnectionsPlugin = registerHadronPlugin(
8585
{ logger, preferences, connectionStorage, track, globalAppRegistry },
8686
{ addCleanup, cleanup }
8787
) {
88-
const store = configureStore(initialProps.preloadStorageConnectionInfos, {
89-
logger,
90-
preferences,
91-
connectionStorage,
92-
track,
93-
getExtraConnectionData: initialProps.onExtraConnectionDataRequest,
94-
appName: initialProps.appName,
95-
connectFn: initialProps.connectFn,
96-
globalAppRegistry,
97-
onFailToLoadConnections: initialProps.onFailToLoadConnections,
98-
});
88+
const store = configureStore(
89+
initialProps.preloadStorageConnectionInfos ?? null,
90+
{
91+
logger,
92+
preferences,
93+
connectionStorage,
94+
track,
95+
getExtraConnectionData: initialProps.onExtraConnectionDataRequest,
96+
appName: initialProps.appName,
97+
connectFn: initialProps.connectFn,
98+
globalAppRegistry,
99+
onFailToLoadConnections: initialProps.onFailToLoadConnections,
100+
}
101+
);
99102

100103
setTimeout(() => {
101104
void store.dispatch(loadConnections());

packages/compass-connections/src/provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export {
6262
useConnectionsList,
6363
useConnectionsListRef,
6464
connectionsLocator,
65+
useConnectionsListLoadingStatus,
6566
} from './stores/store-context';
6667

6768
export type { ConnectionsService } from './stores/store-context';

packages/compass-connections/src/stores/connections-store-redux.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('CompassConnections store', function () {
6767
.rejects(new Error('loadAll failed'));
6868

6969
renderCompassConnections({
70+
connections: null,
7071
connectionStorage,
7172
onFailToLoadConnections: onFailToLoadConnectionsSpy,
7273
});

packages/compass-connections/src/stores/connections-store-redux.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,17 @@ const INITIAL_STATE: State = {
469469
};
470470

471471
export function getInitialConnectionsStateForConnectionInfos(
472-
connectionInfos: ConnectionInfo[] = []
472+
connectionInfos: ConnectionInfo[] | null
473473
): State['connections'] {
474+
if (!connectionInfos) {
475+
// Keep initial state if we're not preloading any connections
476+
return {
477+
byId: {},
478+
ids: [],
479+
status: 'initial',
480+
error: null,
481+
};
482+
}
474483
const byId = Object.fromEntries<ConnectionState>(
475484
connectionInfos.map((info) => {
476485
return [info.id, createDefaultConnectionState(info)];
@@ -479,8 +488,7 @@ export function getInitialConnectionsStateForConnectionInfos(
479488
return {
480489
byId,
481490
ids: getSortedIdsForConnections(Object.values(byId)),
482-
// Keep initial state if we're not preloading any connections
483-
status: connectionInfos.length > 0 ? 'ready' : 'initial',
491+
status: 'ready',
484492
error: null,
485493
};
486494
}
@@ -2126,7 +2134,7 @@ export const openSettingsModal = (
21262134
};
21272135

21282136
export function configureStore(
2129-
preloadConnectionInfos: ConnectionInfo[] = [],
2137+
preloadConnectionInfos: ConnectionInfo[] | null,
21302138
thunkArg: ThunkExtraArg
21312139
) {
21322140
return createStore(

packages/compass-connections/src/stores/store-context.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,14 @@ export function useConnectionsColorList(): {
364364
});
365365
}, isEqual);
366366
}
367+
368+
export function useConnectionsListLoadingStatus() {
369+
return useSelector((state) => {
370+
const status = state.connections.status;
371+
return {
372+
status,
373+
error: state.connections.error?.message ?? null,
374+
isInitialLoad: status === 'initial' || status === 'loading',
375+
};
376+
}, isEqual);
377+
}

packages/compass-sidebar/src/components/connections-filter-popover.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ type ConnectionsFilterPopoverProps = PropsWithChildren<{
4343
onFilterChange(
4444
updater: (filter: ConnectionsFilter) => ConnectionsFilter
4545
): void;
46+
disabled?: boolean;
4647
}>;
4748

4849
export default function ConnectionsFilterPopover({
4950
open,
5051
setOpen,
5152
filter,
5253
onFilterChange,
54+
disabled = false,
5355
}: ConnectionsFilterPopoverProps) {
5456
const onExcludeInactiveChange = useCallback(
5557
(excludeInactive: boolean) => {
@@ -103,6 +105,7 @@ export default function ConnectionsFilterPopover({
103105
active={open}
104106
aria-label="Filter connections"
105107
ref={ref}
108+
disabled={disabled}
106109
>
107110
<Icon glyph="Filter" />
108111
{isActivated && (

packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
Button,
1919
Icon,
2020
ButtonVariant,
21+
cx,
22+
Placeholder,
2123
} from '@mongodb-js/compass-components';
2224
import { ConnectionsNavigationTree } from '@mongodb-js/compass-connections-navigation';
2325
import type { MapDispatchToProps, MapStateToProps } from 'react-redux';
@@ -38,6 +40,7 @@ import type { RootState, SidebarThunkAction } from '../../modules';
3840
import {
3941
type useConnectionsWithStatus,
4042
ConnectionStatus,
43+
useConnectionsListLoadingStatus,
4144
} from '@mongodb-js/compass-connections/provider';
4245
import {
4346
useOpenWorkspace,
@@ -89,6 +92,10 @@ const connectionCountStyles = css({
8992
marginLeft: spacing[100],
9093
});
9194

95+
const connectionCountDisabledStyles = css({
96+
opacity: 0.6,
97+
});
98+
9299
const noDeploymentStyles = css({
93100
paddingLeft: spacing[400],
94101
paddingRight: spacing[400],
@@ -305,7 +312,7 @@ const ConnectionsNavigation: React.FC<ConnectionsNavigationProps> = ({
305312
}
306313

307314
return actions;
308-
}, [supportsConnectionImportExport]);
315+
}, [supportsConnectionImportExport, enableCreatingNewConnections]);
309316

310317
const onConnectionItemAction = useCallback(
311318
(
@@ -491,6 +498,17 @@ const ConnectionsNavigation: React.FC<ConnectionsNavigationProps> = ({
491498

492499
const isAtlasConnectionStorage = useContext(AtlasClusterConnectionsOnly);
493500

501+
const { isInitialLoad: isInitialConnectionsLoad } =
502+
useConnectionsListLoadingStatus();
503+
504+
const connectionsCount = isInitialConnectionsLoad ? (
505+
<span className={cx(connectionCountStyles, connectionCountDisabledStyles)}>
506+
(…)
507+
</span>
508+
) : connections.length !== 0 ? (
509+
<span className={connectionCountStyles}>({connections.length})</span>
510+
) : undefined;
511+
494512
return (
495513
<div className={connectionsContainerStyles}>
496514
<div
@@ -499,11 +517,7 @@ const ConnectionsNavigation: React.FC<ConnectionsNavigationProps> = ({
499517
>
500518
<Subtitle className={connectionListHeaderTitleStyles}>
501519
{isAtlasConnectionStorage ? 'Clusters' : 'Connections'}
502-
{connections.length !== 0 && (
503-
<span className={connectionCountStyles}>
504-
({connections.length})
505-
</span>
506-
)}
520+
{connectionsCount}
507521
</Subtitle>
508522
<ItemActionControls<ConnectionListTitleActions>
509523
iconSize="xsmall"
@@ -514,27 +528,25 @@ const ConnectionsNavigation: React.FC<ConnectionsNavigationProps> = ({
514528
collapseAfter={2}
515529
></ItemActionControls>
516530
</div>
517-
{connections.length > 0 && (
518-
<>
519-
<NavigationItemsFilter
520-
placeholder={
521-
isAtlasConnectionStorage
522-
? 'Search clusters'
523-
: 'Search connections'
524-
}
525-
filter={filter}
526-
onFilterChange={onFilterChange}
527-
/>
528-
<ConnectionsNavigationTree
529-
connections={filtered || connections}
530-
activeWorkspace={activeWorkspace}
531-
onItemAction={onItemAction}
532-
onItemExpand={onItemExpand}
533-
expanded={expanded}
534-
/>
535-
</>
536-
)}
537-
{connections.length === 0 && (
531+
<NavigationItemsFilter
532+
placeholder={
533+
isAtlasConnectionStorage ? 'Search clusters' : 'Search connections'
534+
}
535+
filter={filter}
536+
onFilterChange={onFilterChange}
537+
disabled={isInitialConnectionsLoad || connections.length === 0}
538+
/>
539+
{isInitialConnectionsLoad ? (
540+
<ConnectionsPlaceholder></ConnectionsPlaceholder>
541+
) : connections.length > 0 ? (
542+
<ConnectionsNavigationTree
543+
connections={filtered || connections}
544+
activeWorkspace={activeWorkspace}
545+
onItemAction={onItemAction}
546+
onItemExpand={onItemExpand}
547+
expanded={expanded}
548+
/>
549+
) : connections.length === 0 ? (
538550
<div className={noDeploymentStyles}>
539551
<Body data-testid="no-deployments-text">
540552
You have not connected to any deployments.
@@ -550,11 +562,37 @@ const ConnectionsNavigation: React.FC<ConnectionsNavigationProps> = ({
550562
</Button>
551563
)}
552564
</div>
553-
)}
565+
) : null}
554566
</div>
555567
);
556568
};
557569

570+
const placeholderListStyles = css({
571+
display: 'grid',
572+
gridTemplateColumns: '1fr',
573+
// placeholder height that visually matches font size (16px) + vertical
574+
// spacing (12px) to align it visually with real items
575+
gridAutoRows: spacing[400] + spacing[300],
576+
alignItems: 'center',
577+
// navigation list padding + "empty" caret icon space (4px) to align it
578+
// visually with real items
579+
paddingLeft: spacing[400] + spacing[100],
580+
paddingRight: spacing[400],
581+
});
582+
583+
function ConnectionsPlaceholder() {
584+
return (
585+
<div
586+
data-testid="connections-placeholder"
587+
className={placeholderListStyles}
588+
>
589+
{Array.from({ length: 3 }, (_, index) => (
590+
<Placeholder key={index} height={spacing[400]}></Placeholder>
591+
))}
592+
</div>
593+
);
594+
}
595+
558596
const onRefreshDatabases = (connectionId: string): SidebarThunkAction<void> => {
559597
return (_dispatch, getState, { globalAppRegistry }) => {
560598
globalAppRegistry.emit('refresh-databases', { connectionId });

packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('Multiple Connections Sidebar Component', function () {
9494

9595
function doRender(
9696
activeWorkspace: WorkspaceTab | null = null,
97-
connections: ConnectionInfo[] = [savedFavoriteConnection],
97+
connections: ConnectionInfo[] | null = [savedFavoriteConnection],
9898
atlasClusterConnectionsOnly: boolean | undefined = undefined
9999
) {
100100
workspace = sinon.spy({
@@ -235,6 +235,11 @@ describe('Multiple Connections Sidebar Component', function () {
235235
});
236236

237237
describe('connections list', function () {
238+
it('should display a loading state while connections are not loaded yet', function () {
239+
doRender(null, null);
240+
expect(screen.getByTestId('connections-placeholder')).to.be.visible;
241+
});
242+
238243
context('when there are no connections', function () {
239244
it('should display an empty state with a CTA to add new connection', function () {
240245
doRender(undefined, []);

packages/compass-sidebar/src/components/navigation-items-filter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function NavigationItemsFilter({
3030
title = 'Search',
3131
filter,
3232
onFilterChange,
33+
disabled = false,
3334
}: {
3435
placeholder?: string;
3536
ariaLabel?: string;
@@ -38,6 +39,7 @@ export default function NavigationItemsFilter({
3839
onFilterChange(
3940
updater: (filter: ConnectionsFilter) => ConnectionsFilter
4041
): void;
42+
disabled?: boolean;
4143
}): React.ReactElement {
4244
const onChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
4345
(event) => {
@@ -66,12 +68,14 @@ export default function NavigationItemsFilter({
6668
title={title}
6769
onChange={onChange}
6870
className={textInputStyles}
71+
disabled={disabled}
6972
/>
7073
<ConnectionsFilterPopover
7174
open={isPopoverOpen}
7275
setOpen={setPopoverOpen}
7376
filter={filter}
7477
onFilterChange={onFilterChange}
78+
disabled={disabled}
7579
/>
7680
</form>
7781
);

0 commit comments

Comments
 (0)