diff --git a/packages/compass-connections-navigation/src/base-navigation-item.tsx b/packages/compass-connections-navigation/src/base-navigation-item.tsx index 0b39b0ca02b..65c5ffa2189 100644 --- a/packages/compass-connections-navigation/src/base-navigation-item.tsx +++ b/packages/compass-connections-navigation/src/base-navigation-item.tsx @@ -5,12 +5,23 @@ import { css, ItemActionControls, cx, + Badge, + BadgeVariant, + Tooltip, + useDarkMode, + Body, } from '@mongodb-js/compass-components'; import { type Actions, ROW_HEIGHT } from './constants'; import { ExpandButton } from './tree-item'; import { type NavigationItemActions } from './item-actions'; +import type { + ConnectedConnectionTreeItem, + NotConnectedConnectionTreeItem, + SidebarTreeItem, +} from './tree-data'; type NavigationBaseItemProps = { + item: SidebarTreeItem; name: string; isActive: boolean; isExpandVisible: boolean; @@ -86,7 +97,66 @@ const actionControlsWrapperStyles = css({ gap: spacing[100], }); +const ClusterStateBadge: React.FunctionComponent<{ + state: string; +}> = ({ state }) => { + const badgeVariant = + state === 'CREATING' + ? BadgeVariant.Blue + : state === 'DELETED' + ? BadgeVariant.Red + : BadgeVariant.LightGray; + const badgeText = + state === 'DELETING' + ? 'TERMINATING' + : state === 'DELETED' + ? 'TERMINATED' + : state; + + return ( + + {badgeText} + + ); +}; + +const ClusterStateBadgeWithTooltip: React.FunctionComponent<{ + item: ConnectedConnectionTreeItem | NotConnectedConnectionTreeItem; +}> = ({ item }) => { + const isDarkMode = useDarkMode(); + + const atlasClusterState = item.connectionInfo.atlasMetadata?.clusterState; + if (atlasClusterState === 'PAUSED') { + return ( + ) => ( +
+ + {tooltipChildren} +
+ )} + > + Unpause your cluster to connect to it +
+ ); + } else if ( + atlasClusterState === 'DELETING' || + atlasClusterState === 'CREATING' || + atlasClusterState === 'DELETED' + ) { + return ; + } + + return null; +}; + export const NavigationBaseItem: React.FC = ({ + item, isActive, actionProps, name, @@ -102,6 +172,7 @@ export const NavigationBaseItem: React.FC = ({ children, }) => { const [hoverProps, isHovered] = useHoverState(); + return (
= ({ {icon} {name}
+ {item.type === 'connection' && ( + + )}
{ return getVirtualTreeItems({ @@ -69,6 +72,13 @@ const ConnectionsNavigationTree: React.FunctionComponent< const onDefaultAction: OnDefaultAction = useCallback( (item, evt) => { + if (showDisabledConnections) { + const connectionId = getConnectionId(item); + if (!getConnectable(connectionId)) { + return; + } + } + if (item.type === 'connection') { if (item.connectionStatus === 'connected') { onItemAction(item, 'select-connection'); @@ -83,7 +93,7 @@ const ConnectionsNavigationTree: React.FunctionComponent< } } }, - [onItemAction] + [onItemAction, getConnectable, showDisabledConnections] ); const activeItemId = useMemo(() => { @@ -144,6 +154,14 @@ const ConnectionsNavigationTree: React.FunctionComponent< const getItemActionsAndConfig = useCallback( (item: SidebarTreeItem) => { + if (showDisabledConnections) { + const connectionId = getConnectionId(item); + if (!getConnectable(connectionId)) { + return { + actions: [], + }; + } + } switch (item.type) { case 'placeholder': return { @@ -192,7 +210,12 @@ const ConnectionsNavigationTree: React.FunctionComponent< }; } }, - [isRenameCollectionEnabled, getCollapseAfterForConnectedItem] + [ + isRenameCollectionEnabled, + getCollapseAfterForConnectedItem, + getConnectable, + showDisabledConnections, + ] ); const isTestEnv = process.env.NODE_ENV === 'test'; diff --git a/packages/compass-connections-navigation/src/navigation-item-icon.tsx b/packages/compass-connections-navigation/src/navigation-item-icon.tsx index 6b5dc4ed9ae..1dd55b8b34b 100644 --- a/packages/compass-connections-navigation/src/navigation-item-icon.tsx +++ b/packages/compass-connections-navigation/src/navigation-item-icon.tsx @@ -63,6 +63,22 @@ export const NavigationItemIcon = ({ item }: { item: SidebarTreeItem }) => { return ; } if (item.type === 'connection') { + const atlasClusterState = item.connectionInfo.atlasMetadata?.clusterState; + if (atlasClusterState === 'DELETING' || atlasClusterState === 'CREATING') { + return ( + + + + ); + } + if (atlasClusterState === 'PAUSED' || atlasClusterState === 'DELETED') { + return ( + + + + ); + } + const isFavorite = item.connectionInfo.savedConnectionType === 'favorite'; if (isFavorite) { return ( diff --git a/packages/compass-connections-navigation/src/navigation-item.tsx b/packages/compass-connections-navigation/src/navigation-item.tsx index 46b576b9576..4ba31889ce7 100644 --- a/packages/compass-connections-navigation/src/navigation-item.tsx +++ b/packages/compass-connections-navigation/src/navigation-item.tsx @@ -212,6 +212,7 @@ export function NavigationItem({ ) : ( (isDarkMode ? palette.gray.light1 : palette.gray.dark1), [isDarkMode] ); + const getConnectable = useConnectable(); const style: React.CSSProperties & AcceptedStyles = useMemo(() => { const style: AcceptedStyles = {}; + const connectionId = getConnectionId(item); + const isConnectable = + !showDisabledConnections || getConnectable(connectionId); const isDisconnectedConnection = item.type === 'connection' && item.connectionStatus !== 'connected'; const isNonExistentNamespace = @@ -44,12 +51,12 @@ export default function StyledNavigationItem({ style['--item-bg-color-active'] = connectionColorToHexActive(colorCode); } - if (isDisconnectedConnection || isNonExistentNamespace) { + if (isDisconnectedConnection || isNonExistentNamespace || !isConnectable) { style['--item-color'] = inactiveColor; } - // For a non-existent namespace, even if its active, we show it as inactive - if (isNonExistentNamespace) { + // We always show these as inactive + if (isNonExistentNamespace || !isConnectable) { style['--item-color-active'] = inactiveColor; } return style; @@ -57,6 +64,8 @@ export default function StyledNavigationItem({ inactiveColor, item, colorCode, + getConnectable, + showDisabledConnections, connectionColorToHex, connectionColorToHexActive, ]); diff --git a/packages/compass-connections-navigation/src/tree-data.ts b/packages/compass-connections-navigation/src/tree-data.ts index 2d3476f2649..3d14d0eeacd 100644 --- a/packages/compass-connections-navigation/src/tree-data.ts +++ b/packages/compass-connections-navigation/src/tree-data.ts @@ -124,6 +124,16 @@ export type SidebarActionableItem = export type SidebarTreeItem = PlaceholderTreeItem | SidebarActionableItem; +export function getConnectionId(item: SidebarTreeItem): string { + if (item.type === 'placeholder') { + return ''; + } else if (item.type === 'connection') { + return item.connectionInfo.id; + } else { + return item.connectionId; + } +} + const notConnectedConnectionToItems = ({ connection: { name, connectionInfo, connectionStatus }, connectionIndex, diff --git a/packages/compass-connections/src/hooks/use-connectable.ts b/packages/compass-connections/src/hooks/use-connectable.ts new file mode 100644 index 00000000000..df2ead4262b --- /dev/null +++ b/packages/compass-connections/src/hooks/use-connectable.ts @@ -0,0 +1,20 @@ +import { useStore } from '../stores/store-context'; +import { useCallback } from 'react'; +import { connectable } from '../utils/connection-supports'; + +export function useConnectable(): (connectionId: string) => boolean { + const store = useStore(); + const getConnectable = useCallback( + (connectionId: string) => { + const conn = store.getState().connections.byId[connectionId]; + if (!conn) { + return false; + } + + return connectable(conn.info); + }, + [store] + ); + + return getConnectable; +} diff --git a/packages/compass-connections/src/index.tsx b/packages/compass-connections/src/index.tsx index e6a20fabea6..aab107b17a2 100644 --- a/packages/compass-connections/src/index.tsx +++ b/packages/compass-connections/src/index.tsx @@ -23,7 +23,7 @@ import { ConnectionActionsProvider, } from './stores/store-context'; export type { ConnectionFeature } from './utils/connection-supports'; -export { connectionSupports } from './utils/connection-supports'; +export { connectionSupports, connectable } from './utils/connection-supports'; const ConnectionsComponent: React.FunctionComponent<{ /** diff --git a/packages/compass-connections/src/provider.ts b/packages/compass-connections/src/provider.ts index c3f97fb6168..e01a522615a 100644 --- a/packages/compass-connections/src/provider.ts +++ b/packages/compass-connections/src/provider.ts @@ -69,6 +69,8 @@ export type { ConnectionsService } from './stores/store-context'; export { useConnectionSupports } from './hooks/use-connection-supports'; +export { useConnectable } from './hooks/use-connectable'; + const ConnectionStatus = { /** * @deprecated use a string literal directly diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts index c8c01277fee..e6b0f8a7c4d 100644 --- a/packages/compass-connections/src/stores/connections-store-redux.ts +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -32,6 +32,7 @@ import EventEmitter from 'events'; import { showNonGenuineMongoDBWarningModal as _showNonGenuineMongoDBWarningModal } from '../components/non-genuine-connection-modal'; import ConnectionString from 'mongodb-connection-string-url'; import type { ExtraConnectionData as ExtraConnectionDataForTelemetry } from '@mongodb-js/compass-telemetry'; +import { connectable } from '../utils/connection-supports'; export type ConnectionsEventMap = { connected: ( @@ -530,9 +531,12 @@ function mergeConnections( ? newConnections : [newConnections]; + const removedConnectionIds = new Set(connectionsState.ids); + let newConnectionsById = connectionsState.byId; for (const connectionInfo of newConnections) { + removedConnectionIds.delete(connectionInfo.id); const existingConnection = newConnectionsById[connectionInfo.id]; // If we got a new connection, just create a default state for this @@ -557,6 +561,30 @@ function mergeConnections( info: connectionInfo, }, }; + + // TODO(COMPASS-9319): if an Atlas connection is going from PAUSED state to unpaused, we should + // reconnect the data service, since it would previously have been disconnected + // due to non-retryable error code. + } + } + + // If an Atlas connection was removed, it means that the cluster was deleted + for (const connectionId of removedConnectionIds) { + const removedConnection = newConnectionsById[connectionId]; + if (removedConnection.info.atlasMetadata) { + newConnectionsById = { + ...newConnectionsById, + [connectionId]: { + ...removedConnection, + info: { + ...removedConnection.info, + atlasMetadata: { + ...removedConnection.info.atlasMetadata, + clusterState: 'DELETED', + }, + }, + }, + }; } } @@ -1399,7 +1427,9 @@ function getDescriptionForNonRetryableError(error: Error): string { // to the generic error description. const reason = error.message.match(/code: \d+, reason: (.*)$/)?.[1]; return reason && reason.length > 0 - ? reason + ? reason.endsWith('.') + ? reason.slice(0, -1) + : reason // Remove trailing period : NonRetryableErrorDescriptionFallbacks[ Number( error.message.match(/code: (\d+),/)?.[1] @@ -1489,6 +1519,10 @@ const connectWithOptions = ( return; } + if (!connectable(connectionInfo)) { + return; + } + const isAutoconnectAttempt = isAutoconnectInfo( getState(), connectionInfo.id diff --git a/packages/compass-connections/src/utils/connection-supports.ts b/packages/compass-connections/src/utils/connection-supports.ts index c70388f5b68..f1767a6f606 100644 --- a/packages/compass-connections/src/utils/connection-supports.ts +++ b/packages/compass-connections/src/utils/connection-supports.ts @@ -11,6 +11,19 @@ function supportsRollingIndexCreation(connectionInfo: ConnectionInfo) { return atlasMetadata.supports.rollingIndexes; } +export function connectable(connectionInfo: ConnectionInfo) { + const atlasClusterState = connectionInfo.atlasMetadata?.clusterState; + if ( + atlasClusterState === 'DELETED' || + atlasClusterState === 'DELETING' || + atlasClusterState === 'CREATING' || + atlasClusterState === 'PAUSED' + ) { + return false; + } + return true; +} + function supportsGlobalWrites(connectionInfo: ConnectionInfo) { const atlasMetadata = connectionInfo.atlasMetadata; diff --git a/packages/compass-preferences-model/src/feature-flags.ts b/packages/compass-preferences-model/src/feature-flags.ts index f16ed69ed35..668556e7286 100644 --- a/packages/compass-preferences-model/src/feature-flags.ts +++ b/packages/compass-preferences-model/src/feature-flags.ts @@ -22,6 +22,7 @@ export type FeatureFlags = { enableQueryHistoryAutocomplete: boolean; enableProxySupport: boolean; enableRollingIndexes: boolean; + showDisabledConnections: boolean; enableGlobalWrites: boolean; enableDataModeling: boolean; enableIndexesGuidanceExp: boolean; @@ -91,6 +92,14 @@ export const featureFlags: Required<{ }, }, + showDisabledConnections: { + stage: 'development', + description: { + short: + 'Show clusters that are not in a "connectable" state in Atlas Cloud', + }, + }, + enableRollingIndexes: { stage: 'development', description: { diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index 1392df7a318..35dc4780dcb 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -135,6 +135,7 @@ const App = () => { enableCreatingNewConnections: !isAtlas, enableGlobalWrites: isAtlas, enableRollingIndexes: isAtlas, + showDisabledConnections: true, enableGenAIFeaturesAtlasProject: isAtlas && !!enableGenAIFeaturesAtlasProject, enableGenAISampleDocumentPassingOnAtlasProject: diff --git a/packages/compass-web/src/connection-storage.tsx b/packages/compass-web/src/connection-storage.tsx index a51889c2275..64265b8cc2a 100644 --- a/packages/compass-web/src/connection-storage.tsx +++ b/packages/compass-web/src/connection-storage.tsx @@ -247,10 +247,14 @@ export function buildConnectionInfoFromClusterDescription( }; } -const CONNECTABLE_CLUSTER_STATES: AtlasClusterMetadata['clusterState'][] = [ +const VISIBLE_CLUSTER_STATES: AtlasClusterMetadata['clusterState'][] = [ 'IDLE', - 'REPARING', + 'REPAIRING', 'UPDATING', + 'PAUSED', + 'CREATING', + 'DELETING', + 'DELETED', ]; /** @@ -291,11 +295,8 @@ export class AtlasCloudConnectionStorage return connectionInfoList .map((connectionInfo: ConnectionInfo): ConnectionInfo | null => { if ( - !connectionInfo.connectionOptions.connectionString || !connectionInfo.atlasMetadata || - // TODO(COMPASS-8228): do not filter out those connections, display - // them in navigation, but in a way that doesn't allow connecting - !CONNECTABLE_CLUSTER_STATES.includes( + !VISIBLE_CLUSTER_STATES.includes( connectionInfo.atlasMetadata.clusterState ) ) { diff --git a/packages/connection-info/src/connection-info.ts b/packages/connection-info/src/connection-info.ts index 310bd8f237f..831bdc31d52 100644 --- a/packages/connection-info/src/connection-info.ts +++ b/packages/connection-info/src/connection-info.ts @@ -48,7 +48,7 @@ export interface AtlasClusterMetadata { | 'UPDATING' | 'PAUSED' | 'IDLE' - | 'REPARING' + | 'REPAIRING' | 'DELETING' | 'DELETED';