Skip to content

Commit 3797a5f

Browse files
authored
feat(databases-collections): handle non-existent namespaces COMPASS-5750 (#6664)
* setup data for the feature * update sidebar * update database-collection * correct icon * update workspace tabs * rename property * fetch collstats only if db exists * clean up * checks and lint * fix log id and message * tests * correcct comment * correct color on grid * rename prop * also handle non-existent collections * fix check * react to changes * do mix with adapt ns info * border on hover * use spacing nums * text change * install
1 parent f5ccf44 commit 3797a5f

File tree

27 files changed

+704
-175
lines changed

27 files changed

+704
-175
lines changed

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/collection-model/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ interface CollectionProps {
7878
index_size: number;
7979
isTimeSeries: boolean;
8080
isView: boolean;
81+
/** Only relevant for a view and identifies collection/view from which this view was created. */
8182
sourceName: string | null;
8283
source: Collection;
83-
properties: { id: string; options?: unknown }[];
84+
properties: { id: string; options?: Record<string, unknown> }[];
85+
is_non_existent: boolean;
8486
}
8587

8688
type CollectionDataService = Pick<DataService, 'collectionStats' | 'collectionInfo' | 'listCollections' | 'isListSearchIndexesSupported'>;

packages/collection-model/lib/model.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ function pickCollectionInfo({
102102
validation,
103103
clustered,
104104
fle2,
105+
is_non_existent,
105106
}) {
106-
return { type, readonly, view_on, collation, pipeline, validation, clustered, fle2 };
107+
return { type, readonly, view_on, collation, pipeline, validation, clustered, fle2, is_non_existent };
107108
}
108109

109110
/**
@@ -124,6 +125,7 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), {
124125
statusError: { type: 'string', default: null },
125126

126127
// Normalized values from collectionInfo command
128+
is_non_existent: 'boolean',
127129
readonly: 'boolean',
128130
clustered: 'boolean',
129131
fle2: 'boolean',
@@ -250,6 +252,16 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), {
250252
...collStats,
251253
...(collectionInfo && pickCollectionInfo(collectionInfo)),
252254
});
255+
// If the collection is not unprovisioned `is_non_existent` anymore,
256+
// let's update the parent database model to reflect the change.
257+
// This happens when a user tries to insert first document into a
258+
// collection that doesn't exist yet or creates a new collection
259+
// for an unprovisioned database.
260+
if (!this.is_non_existent) {
261+
getParentByType(this, 'Database').set({
262+
is_non_existent: false,
263+
});
264+
}
253265
} catch (err) {
254266
this.set({ status: 'error', statusError: err.message });
255267
throw err;

packages/compass-components/src/components/workspace-tabs/tab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ function Tab({
206206
tabContentId,
207207
iconGlyph,
208208
tabTheme,
209+
className: tabClassName,
209210
...props
210211
}: TabProps & React.HTMLProps<HTMLDivElement>) {
211212
const darkMode = useDarkMode();
@@ -250,7 +251,8 @@ function Tab({
250251
themeClass,
251252
isSelected && selectedTabStyles,
252253
isSelected && tabTheme && selectedThemedTabStyles,
253-
isDragging && draggingTabStyles
254+
isDragging && draggingTabStyles,
255+
tabClassName
254256
)}
255257
aria-selected={isSelected}
256258
role="tab"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react';
2+
import type { SidebarTreeItem } from './tree-data';
3+
import { css, Icon, ServerIcon, Tooltip } from '@mongodb-js/compass-components';
4+
import type { GlyphName } from '@mongodb-js/compass-components';
5+
import { WithStatusMarker } from './with-status-marker';
6+
import { isLocalhost } from 'mongodb-build-info';
7+
8+
const NON_EXISTANT_NAMESPACE_TEXT =
9+
'Your privileges grant you access to this namespace, but it does not currently exist';
10+
11+
const tooltipTriggerStyles = css({
12+
display: 'flex',
13+
});
14+
const IconWithTooltip = ({
15+
text,
16+
glyph,
17+
}: {
18+
text: string;
19+
glyph: GlyphName;
20+
}) => {
21+
return (
22+
<Tooltip
23+
align="bottom"
24+
justify="start"
25+
trigger={
26+
<div className={tooltipTriggerStyles}>
27+
<Icon glyph={glyph} />
28+
</div>
29+
}
30+
>
31+
{text}
32+
</Tooltip>
33+
);
34+
};
35+
36+
export const NavigationItemIcon = ({ item }: { item: SidebarTreeItem }) => {
37+
if (item.type === 'database') {
38+
if (item.isNonExistent) {
39+
return (
40+
<IconWithTooltip
41+
text={NON_EXISTANT_NAMESPACE_TEXT}
42+
glyph="EmptyDatabase"
43+
/>
44+
);
45+
}
46+
return <Icon glyph="Database" />;
47+
}
48+
if (item.type === 'collection') {
49+
if (item.isNonExistent) {
50+
return (
51+
<IconWithTooltip
52+
text={NON_EXISTANT_NAMESPACE_TEXT}
53+
glyph="EmptyFolder"
54+
/>
55+
);
56+
}
57+
return <Icon glyph="Folder" />;
58+
}
59+
if (item.type === 'view') {
60+
return <Icon glyph="Visibility" />;
61+
}
62+
if (item.type === 'timeseries') {
63+
return <Icon glyph="TimeSeries" />;
64+
}
65+
if (item.type === 'connection') {
66+
const isFavorite = item.connectionInfo.savedConnectionType === 'favorite';
67+
if (isFavorite) {
68+
return (
69+
<WithStatusMarker status={item.connectionStatus}>
70+
<Icon glyph="Favorite" />
71+
</WithStatusMarker>
72+
);
73+
}
74+
if (isLocalhost(item.connectionInfo.connectionOptions.connectionString)) {
75+
return (
76+
<WithStatusMarker status={item.connectionStatus}>
77+
<Icon glyph="Laptop" />
78+
</WithStatusMarker>
79+
);
80+
}
81+
return (
82+
<WithStatusMarker status={item.connectionStatus}>
83+
<ServerIcon />
84+
</WithStatusMarker>
85+
);
86+
}
87+
return null;
88+
};

packages/compass-connections-navigation/src/navigation-item.tsx

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import React, { useCallback, useMemo } from 'react';
2-
import { isLocalhost } from 'mongodb-build-info';
32
import {
4-
Icon,
5-
ServerIcon,
63
cx,
74
css,
85
palette,
@@ -17,8 +14,8 @@ import type { NavigationItemActions } from './item-actions';
1714
import type { SidebarTreeItem, SidebarActionableItem } from './tree-data';
1815
import { getTreeItemStyles } from './utils';
1916
import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
20-
import { WithStatusMarker } from './with-status-marker';
2117
import type { Actions } from './constants';
18+
import { NavigationItemIcon } from './navigation-item-icon';
2219

2320
const nonGenuineBtnStyles = css({
2421
color: palette.yellow.dark2,
@@ -115,43 +112,6 @@ export function NavigationItem({
115112
getItemActions,
116113
}: NavigationItemProps) {
117114
const isDarkMode = useDarkMode();
118-
const itemIcon = useMemo(() => {
119-
if (item.type === 'database') {
120-
return <Icon glyph="Database" />;
121-
}
122-
if (item.type === 'collection') {
123-
return <Icon glyph="Folder" />;
124-
}
125-
if (item.type === 'view') {
126-
return <Icon glyph="Visibility" />;
127-
}
128-
if (item.type === 'timeseries') {
129-
return <Icon glyph="TimeSeries" />;
130-
}
131-
if (item.type === 'connection') {
132-
const isFavorite = item.connectionInfo.savedConnectionType === 'favorite';
133-
if (isFavorite) {
134-
return (
135-
<WithStatusMarker status={item.connectionStatus}>
136-
<Icon glyph="Favorite" />
137-
</WithStatusMarker>
138-
);
139-
}
140-
if (isLocalhost(item.connectionInfo.connectionOptions.connectionString)) {
141-
return (
142-
<WithStatusMarker status={item.connectionStatus}>
143-
<Icon glyph="Laptop" />
144-
</WithStatusMarker>
145-
);
146-
}
147-
return (
148-
<WithStatusMarker status={item.connectionStatus}>
149-
<ServerIcon />
150-
</WithStatusMarker>
151-
);
152-
}
153-
}, [item]);
154-
155115
const onAction = useCallback(
156116
(action: Actions) => {
157117
if (item.type !== 'placeholder') {
@@ -258,7 +218,7 @@ export function NavigationItem({
258218
hasDefaultAction={
259219
item.type !== 'connection' || item.connectionStatus === 'connected'
260220
}
261-
icon={itemIcon}
221+
icon={<NavigationItemIcon item={item} />}
262222
name={item.name}
263223
style={style}
264224
dataAttributes={itemDataProps}

packages/compass-connections-navigation/src/styled-navigation-item.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import {
55
} from '@mongodb-js/connection-form';
66
import { palette, useDarkMode } from '@mongodb-js/compass-components';
77
import type { SidebarTreeItem } from './tree-data';
8-
import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
98

109
type AcceptedStyles = {
1110
'--item-bg-color'?: string;
1211
'--item-bg-color-hover'?: string;
1312
'--item-bg-color-active'?: string;
1413
'--item-color'?: string;
14+
'--item-color-active'?: string;
1515
};
1616

1717
export default function StyledNavigationItem({
@@ -25,28 +25,37 @@ export default function StyledNavigationItem({
2525
const { connectionColorToHex, connectionColorToHexActive } =
2626
useConnectionColor();
2727
const { colorCode } = item;
28-
const isDisconnectedConnection =
29-
item.type === 'connection' &&
30-
item.connectionStatus !== ConnectionStatus.Connected;
28+
const inactiveColor = useMemo(
29+
() => (isDarkMode ? palette.gray.light1 : palette.gray.dark1),
30+
[isDarkMode]
31+
);
3132

3233
const style: React.CSSProperties & AcceptedStyles = useMemo(() => {
3334
const style: AcceptedStyles = {};
35+
const isDisconnectedConnection =
36+
item.type === 'connection' && item.connectionStatus !== 'connected';
37+
const isNonExistentNamespace =
38+
(item.type === 'database' || item.type === 'collection') &&
39+
item.isNonExistent;
3440

3541
if (colorCode && colorCode !== DefaultColorCode) {
3642
style['--item-bg-color'] = connectionColorToHex(colorCode);
3743
style['--item-bg-color-hover'] = connectionColorToHexActive(colorCode);
3844
style['--item-bg-color-active'] = connectionColorToHexActive(colorCode);
3945
}
4046

41-
if (isDisconnectedConnection) {
42-
style['--item-color'] = isDarkMode
43-
? palette.gray.light1
44-
: palette.gray.dark1;
47+
if (isDisconnectedConnection || isNonExistentNamespace) {
48+
style['--item-color'] = inactiveColor;
49+
}
50+
51+
// For a non-existent namespace, even if its active, we show it as inactive
52+
if (isNonExistentNamespace) {
53+
style['--item-color-active'] = inactiveColor;
4554
}
4655
return style;
4756
}, [
48-
isDarkMode,
49-
isDisconnectedConnection,
57+
inactiveColor,
58+
item,
5059
colorCode,
5160
connectionColorToHex,
5261
connectionColorToHexActive,

packages/compass-connections-navigation/src/tree-data.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type Database = {
5454
collectionsStatus: DatabaseOrCollectionStatus;
5555
collectionsLength: number;
5656
collections: Collection[];
57+
isNonExistent: boolean;
5758
};
5859

5960
type PlaceholderTreeItem = VirtualPlaceholderItem & {
@@ -67,6 +68,7 @@ export type Collection = {
6768
type: 'view' | 'collection' | 'timeseries';
6869
sourceName: string | null;
6970
pipeline: unknown[];
71+
isNonExistent: boolean;
7072
};
7173

7274
export type NotConnectedConnectionTreeItem = VirtualTreeItem & {
@@ -100,6 +102,7 @@ export type DatabaseTreeItem = VirtualTreeItem & {
100102
connectionId: string;
101103
dbName: string;
102104
hasWriteActionsDisabled: boolean;
105+
isNonExistent: boolean;
103106
};
104107

105108
export type CollectionTreeItem = VirtualTreeItem & {
@@ -110,6 +113,7 @@ export type CollectionTreeItem = VirtualTreeItem & {
110113
connectionId: string;
111114
namespace: string;
112115
hasWriteActionsDisabled: boolean;
116+
isNonExistent: boolean;
113117
};
114118

115119
export type SidebarActionableItem =
@@ -245,6 +249,7 @@ const databaseToItems = ({
245249
collections,
246250
collectionsLength,
247251
collectionsStatus,
252+
isNonExistent,
248253
},
249254
connectionId,
250255
expandedItems = {},
@@ -277,6 +282,7 @@ const databaseToItems = ({
277282
dbName: id,
278283
isExpandable: true,
279284
hasWriteActionsDisabled,
285+
isNonExistent,
280286
};
281287

282288
const sidebarData: SidebarTreeItem[] = [databaseTI];
@@ -304,19 +310,22 @@ const databaseToItems = ({
304310
}
305311

306312
return sidebarData.concat(
307-
collections.map(({ _id: id, name, type }, collectionIndex) => ({
308-
id: `${connectionId}.${id}`, // id is the namespace of the collection, so includes db as well
309-
level: level + 1,
310-
name,
311-
type,
312-
setSize: collectionsLength,
313-
posInSet: collectionIndex + 1,
314-
colorCode,
315-
connectionId,
316-
namespace: id,
317-
hasWriteActionsDisabled,
318-
isExpandable: false,
319-
}))
313+
collections.map(
314+
({ _id: id, name, type, isNonExistent }, collectionIndex) => ({
315+
id: `${connectionId}.${id}`, // id is the namespace of the collection, so includes db as well
316+
level: level + 1,
317+
name,
318+
type,
319+
setSize: collectionsLength,
320+
posInSet: collectionIndex + 1,
321+
colorCode,
322+
connectionId,
323+
namespace: id,
324+
hasWriteActionsDisabled,
325+
isExpandable: false,
326+
isNonExistent,
327+
})
328+
)
320329
);
321330
};
322331

0 commit comments

Comments
 (0)