Skip to content

Commit

Permalink
feat(databases-collections): handle non-existent namespaces COMPASS-5750
Browse files Browse the repository at this point in the history
 (#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
  • Loading branch information
mabaasit authored Jan 31, 2025
1 parent f5ccf44 commit 3797a5f
Show file tree
Hide file tree
Showing 27 changed files with 704 additions and 175 deletions.
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/collection-model/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ interface CollectionProps {
index_size: number;
isTimeSeries: boolean;
isView: boolean;
/** Only relevant for a view and identifies collection/view from which this view was created. */
sourceName: string | null;
source: Collection;
properties: { id: string; options?: unknown }[];
properties: { id: string; options?: Record<string, unknown> }[];
is_non_existent: boolean;
}

type CollectionDataService = Pick<DataService, 'collectionStats' | 'collectionInfo' | 'listCollections' | 'isListSearchIndexesSupported'>;
Expand Down
14 changes: 13 additions & 1 deletion packages/collection-model/lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ function pickCollectionInfo({
validation,
clustered,
fle2,
is_non_existent,
}) {
return { type, readonly, view_on, collation, pipeline, validation, clustered, fle2 };
return { type, readonly, view_on, collation, pipeline, validation, clustered, fle2, is_non_existent };
}

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

// Normalized values from collectionInfo command
is_non_existent: 'boolean',
readonly: 'boolean',
clustered: 'boolean',
fle2: 'boolean',
Expand Down Expand Up @@ -250,6 +252,16 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), {
...collStats,
...(collectionInfo && pickCollectionInfo(collectionInfo)),
});
// If the collection is not unprovisioned `is_non_existent` anymore,
// let's update the parent database model to reflect the change.
// This happens when a user tries to insert first document into a
// collection that doesn't exist yet or creates a new collection
// for an unprovisioned database.
if (!this.is_non_existent) {
getParentByType(this, 'Database').set({
is_non_existent: false,
});
}
} catch (err) {
this.set({ status: 'error', statusError: err.message });
throw err;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function Tab({
tabContentId,
iconGlyph,
tabTheme,
className: tabClassName,
...props
}: TabProps & React.HTMLProps<HTMLDivElement>) {
const darkMode = useDarkMode();
Expand Down Expand Up @@ -250,7 +251,8 @@ function Tab({
themeClass,
isSelected && selectedTabStyles,
isSelected && tabTheme && selectedThemedTabStyles,
isDragging && draggingTabStyles
isDragging && draggingTabStyles,
tabClassName
)}
aria-selected={isSelected}
role="tab"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import type { SidebarTreeItem } from './tree-data';
import { css, Icon, ServerIcon, Tooltip } from '@mongodb-js/compass-components';
import type { GlyphName } from '@mongodb-js/compass-components';
import { WithStatusMarker } from './with-status-marker';
import { isLocalhost } from 'mongodb-build-info';

const NON_EXISTANT_NAMESPACE_TEXT =
'Your privileges grant you access to this namespace, but it does not currently exist';

const tooltipTriggerStyles = css({
display: 'flex',
});
const IconWithTooltip = ({
text,
glyph,
}: {
text: string;
glyph: GlyphName;
}) => {
return (
<Tooltip
align="bottom"
justify="start"
trigger={
<div className={tooltipTriggerStyles}>
<Icon glyph={glyph} />
</div>
}
>
{text}
</Tooltip>
);
};

export const NavigationItemIcon = ({ item }: { item: SidebarTreeItem }) => {
if (item.type === 'database') {
if (item.isNonExistent) {
return (
<IconWithTooltip
text={NON_EXISTANT_NAMESPACE_TEXT}
glyph="EmptyDatabase"
/>
);
}
return <Icon glyph="Database" />;
}
if (item.type === 'collection') {
if (item.isNonExistent) {
return (
<IconWithTooltip
text={NON_EXISTANT_NAMESPACE_TEXT}
glyph="EmptyFolder"
/>
);
}
return <Icon glyph="Folder" />;
}
if (item.type === 'view') {
return <Icon glyph="Visibility" />;
}
if (item.type === 'timeseries') {
return <Icon glyph="TimeSeries" />;
}
if (item.type === 'connection') {
const isFavorite = item.connectionInfo.savedConnectionType === 'favorite';
if (isFavorite) {
return (
<WithStatusMarker status={item.connectionStatus}>
<Icon glyph="Favorite" />
</WithStatusMarker>
);
}
if (isLocalhost(item.connectionInfo.connectionOptions.connectionString)) {
return (
<WithStatusMarker status={item.connectionStatus}>
<Icon glyph="Laptop" />
</WithStatusMarker>
);
}
return (
<WithStatusMarker status={item.connectionStatus}>
<ServerIcon />
</WithStatusMarker>
);
}
return null;
};
44 changes: 2 additions & 42 deletions packages/compass-connections-navigation/src/navigation-item.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { isLocalhost } from 'mongodb-build-info';
import {
Icon,
ServerIcon,
cx,
css,
palette,
Expand All @@ -17,8 +14,8 @@ import type { NavigationItemActions } from './item-actions';
import type { SidebarTreeItem, SidebarActionableItem } from './tree-data';
import { getTreeItemStyles } from './utils';
import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
import { WithStatusMarker } from './with-status-marker';
import type { Actions } from './constants';
import { NavigationItemIcon } from './navigation-item-icon';

const nonGenuineBtnStyles = css({
color: palette.yellow.dark2,
Expand Down Expand Up @@ -115,43 +112,6 @@ export function NavigationItem({
getItemActions,
}: NavigationItemProps) {
const isDarkMode = useDarkMode();
const itemIcon = useMemo(() => {
if (item.type === 'database') {
return <Icon glyph="Database" />;
}
if (item.type === 'collection') {
return <Icon glyph="Folder" />;
}
if (item.type === 'view') {
return <Icon glyph="Visibility" />;
}
if (item.type === 'timeseries') {
return <Icon glyph="TimeSeries" />;
}
if (item.type === 'connection') {
const isFavorite = item.connectionInfo.savedConnectionType === 'favorite';
if (isFavorite) {
return (
<WithStatusMarker status={item.connectionStatus}>
<Icon glyph="Favorite" />
</WithStatusMarker>
);
}
if (isLocalhost(item.connectionInfo.connectionOptions.connectionString)) {
return (
<WithStatusMarker status={item.connectionStatus}>
<Icon glyph="Laptop" />
</WithStatusMarker>
);
}
return (
<WithStatusMarker status={item.connectionStatus}>
<ServerIcon />
</WithStatusMarker>
);
}
}, [item]);

const onAction = useCallback(
(action: Actions) => {
if (item.type !== 'placeholder') {
Expand Down Expand Up @@ -258,7 +218,7 @@ export function NavigationItem({
hasDefaultAction={
item.type !== 'connection' || item.connectionStatus === 'connected'
}
icon={itemIcon}
icon={<NavigationItemIcon item={item} />}
name={item.name}
style={style}
dataAttributes={itemDataProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
} from '@mongodb-js/connection-form';
import { palette, useDarkMode } from '@mongodb-js/compass-components';
import type { SidebarTreeItem } from './tree-data';
import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';

type AcceptedStyles = {
'--item-bg-color'?: string;
'--item-bg-color-hover'?: string;
'--item-bg-color-active'?: string;
'--item-color'?: string;
'--item-color-active'?: string;
};

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

const style: React.CSSProperties & AcceptedStyles = useMemo(() => {
const style: AcceptedStyles = {};
const isDisconnectedConnection =
item.type === 'connection' && item.connectionStatus !== 'connected';
const isNonExistentNamespace =
(item.type === 'database' || item.type === 'collection') &&
item.isNonExistent;

if (colorCode && colorCode !== DefaultColorCode) {
style['--item-bg-color'] = connectionColorToHex(colorCode);
style['--item-bg-color-hover'] = connectionColorToHexActive(colorCode);
style['--item-bg-color-active'] = connectionColorToHexActive(colorCode);
}

if (isDisconnectedConnection) {
style['--item-color'] = isDarkMode
? palette.gray.light1
: palette.gray.dark1;
if (isDisconnectedConnection || isNonExistentNamespace) {
style['--item-color'] = inactiveColor;
}

// For a non-existent namespace, even if its active, we show it as inactive
if (isNonExistentNamespace) {
style['--item-color-active'] = inactiveColor;
}
return style;
}, [
isDarkMode,
isDisconnectedConnection,
inactiveColor,
item,
colorCode,
connectionColorToHex,
connectionColorToHexActive,
Expand Down
35 changes: 22 additions & 13 deletions packages/compass-connections-navigation/src/tree-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type Database = {
collectionsStatus: DatabaseOrCollectionStatus;
collectionsLength: number;
collections: Collection[];
isNonExistent: boolean;
};

type PlaceholderTreeItem = VirtualPlaceholderItem & {
Expand All @@ -67,6 +68,7 @@ export type Collection = {
type: 'view' | 'collection' | 'timeseries';
sourceName: string | null;
pipeline: unknown[];
isNonExistent: boolean;
};

export type NotConnectedConnectionTreeItem = VirtualTreeItem & {
Expand Down Expand Up @@ -100,6 +102,7 @@ export type DatabaseTreeItem = VirtualTreeItem & {
connectionId: string;
dbName: string;
hasWriteActionsDisabled: boolean;
isNonExistent: boolean;
};

export type CollectionTreeItem = VirtualTreeItem & {
Expand All @@ -110,6 +113,7 @@ export type CollectionTreeItem = VirtualTreeItem & {
connectionId: string;
namespace: string;
hasWriteActionsDisabled: boolean;
isNonExistent: boolean;
};

export type SidebarActionableItem =
Expand Down Expand Up @@ -245,6 +249,7 @@ const databaseToItems = ({
collections,
collectionsLength,
collectionsStatus,
isNonExistent,
},
connectionId,
expandedItems = {},
Expand Down Expand Up @@ -277,6 +282,7 @@ const databaseToItems = ({
dbName: id,
isExpandable: true,
hasWriteActionsDisabled,
isNonExistent,
};

const sidebarData: SidebarTreeItem[] = [databaseTI];
Expand Down Expand Up @@ -304,19 +310,22 @@ const databaseToItems = ({
}

return sidebarData.concat(
collections.map(({ _id: id, name, type }, collectionIndex) => ({
id: `${connectionId}.${id}`, // id is the namespace of the collection, so includes db as well
level: level + 1,
name,
type,
setSize: collectionsLength,
posInSet: collectionIndex + 1,
colorCode,
connectionId,
namespace: id,
hasWriteActionsDisabled,
isExpandable: false,
}))
collections.map(
({ _id: id, name, type, isNonExistent }, collectionIndex) => ({
id: `${connectionId}.${id}`, // id is the namespace of the collection, so includes db as well
level: level + 1,
name,
type,
setSize: collectionsLength,
posInSet: collectionIndex + 1,
colorCode,
connectionId,
namespace: id,
hasWriteActionsDisabled,
isExpandable: false,
isNonExistent,
})
)
);
};

Expand Down
Loading

0 comments on commit 3797a5f

Please sign in to comment.