Skip to content

Commit e8339b9

Browse files
authored
feat(cluster): implement Couchbase connection and tree loading (#321)
1 parent f2f6a9c commit e8339b9

File tree

9 files changed

+517
-25
lines changed

9 files changed

+517
-25
lines changed

src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
</Compile>
7878
<Compile Include="Services\ConnectionInfo.cs" />
7979
<Compile Include="Services\ConnectionSettingsService.cs" />
80+
<Compile Include="Services\CouchbaseService.cs" />
8081
<Compile Include="Services\CredentialManagerService.cs" />
8182
<Compile Include="ViewModels\BucketNode.cs" />
8283
<Compile Include="ViewModels\BucketsFolderNode.cs" />

src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
Name="CouchbaseExplorerControl">
1616

1717
<UserControl.Resources>
18+
<!-- Bool to Visibility Converter -->
19+
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
20+
21+
<!-- Inverse Bool to Visibility Converter -->
22+
<vm:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" />
23+
24+
<!-- Count to Visibility Converter (shows when count is 0) -->
25+
<vm:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
26+
1827
<!-- TreeView Item Style for proper selection binding -->
1928
<Style TargetType="TreeViewItem">
2029
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
@@ -166,15 +175,6 @@
166175
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.HighlightKey}}" />
167176
</StackPanel>
168177
</DataTemplate>
169-
170-
<!-- Bool to Visibility Converter -->
171-
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
172-
173-
<!-- Inverse Bool to Visibility Converter -->
174-
<vm:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" />
175-
176-
<!-- Count to Visibility Converter (shows when count is 0) -->
177-
<vm:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
178178
</UserControl.Resources>
179179

180180
<Grid>
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Threading.Tasks;
6+
using Couchbase;
7+
using Couchbase.Management.Buckets;
8+
9+
namespace CodingWithCalvin.CouchbaseExplorer.Services
10+
{
11+
public class ClusterConnection : IDisposable
12+
{
13+
public ICluster Cluster { get; }
14+
public bool HasQueryService { get; private set; }
15+
public bool HasKvService { get; private set; }
16+
public List<string> AvailableServices { get; private set; } = new List<string>();
17+
18+
public ClusterConnection(ICluster cluster)
19+
{
20+
Cluster = cluster;
21+
}
22+
23+
public async Task DetectServicesAsync()
24+
{
25+
try
26+
{
27+
await Cluster.WaitUntilReadyAsync(TimeSpan.FromSeconds(10));
28+
29+
// Try to detect services by attempting operations
30+
HasKvService = true; // KV is always available if connected
31+
32+
// Check for query service by trying to get buckets (uses management API)
33+
try
34+
{
35+
await Cluster.Buckets.GetAllBucketsAsync();
36+
HasQueryService = true;
37+
AvailableServices.Add("Query");
38+
}
39+
catch
40+
{
41+
HasQueryService = false;
42+
}
43+
44+
AvailableServices.Add("KV");
45+
}
46+
catch (Exception)
47+
{
48+
// Service detection failed, assume basic services
49+
HasKvService = true;
50+
AvailableServices.Add("KV");
51+
}
52+
}
53+
54+
public void Dispose()
55+
{
56+
Cluster?.Dispose();
57+
}
58+
}
59+
60+
public class BucketInfo
61+
{
62+
public string Name { get; set; }
63+
public BucketType BucketType { get; set; }
64+
public long RamQuotaMB { get; set; }
65+
public int NumReplicas { get; set; }
66+
}
67+
68+
public class ScopeInfo
69+
{
70+
public string Name { get; set; }
71+
public List<CollectionInfo> Collections { get; set; } = new List<CollectionInfo>();
72+
}
73+
74+
public class CollectionInfo
75+
{
76+
public string Name { get; set; }
77+
public string ScopeName { get; set; }
78+
}
79+
80+
public static class CouchbaseService
81+
{
82+
private static readonly Dictionary<string, ClusterConnection> _connections = new Dictionary<string, ClusterConnection>();
83+
84+
public static async Task<ClusterConnection> ConnectAsync(string connectionId, string connectionString, string username, string password, bool useSsl)
85+
{
86+
// Enable TLS 1.2/1.3 explicitly for .NET Framework
87+
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;
88+
89+
if (_connections.TryGetValue(connectionId, out var existing))
90+
{
91+
return existing;
92+
}
93+
94+
var options = new ClusterOptions
95+
{
96+
UserName = username,
97+
Password = password,
98+
KvTimeout = TimeSpan.FromSeconds(30),
99+
ManagementTimeout = TimeSpan.FromSeconds(30),
100+
QueryTimeout = TimeSpan.FromSeconds(30)
101+
};
102+
103+
// Check if this is a Capella connection
104+
var isCapella = connectionString.Contains(".cloud.couchbase.com");
105+
106+
if (useSsl || isCapella)
107+
{
108+
options.EnableTls = true;
109+
}
110+
111+
// Capella-specific configuration
112+
if (isCapella)
113+
{
114+
options.EnableDnsSrvResolution = true;
115+
options.KvIgnoreRemoteCertificateNameMismatch = true;
116+
options.HttpIgnoreRemoteCertificateMismatch = true;
117+
options.ForceIPv4 = true;
118+
}
119+
120+
// Build connection string with protocol
121+
var fullConnectionString = connectionString;
122+
if (!connectionString.StartsWith("couchbase://") && !connectionString.StartsWith("couchbases://"))
123+
{
124+
fullConnectionString = useSsl ? $"couchbases://{connectionString}" : $"couchbase://{connectionString}";
125+
}
126+
127+
// Connect on background thread to avoid UI blocking
128+
var cluster = await Task.Run(async () =>
129+
{
130+
return await Cluster.ConnectAsync(fullConnectionString, options);
131+
}).ConfigureAwait(true);
132+
133+
var connection = new ClusterConnection(cluster);
134+
135+
await Task.Run(async () =>
136+
{
137+
await connection.DetectServicesAsync();
138+
}).ConfigureAwait(true);
139+
140+
_connections[connectionId] = connection;
141+
return connection;
142+
}
143+
144+
public static async Task DisconnectAsync(string connectionId)
145+
{
146+
if (_connections.TryGetValue(connectionId, out var connection))
147+
{
148+
_connections.Remove(connectionId);
149+
connection.Dispose();
150+
}
151+
}
152+
153+
public static ClusterConnection GetConnection(string connectionId)
154+
{
155+
_connections.TryGetValue(connectionId, out var connection);
156+
return connection;
157+
}
158+
159+
public static async Task<List<BucketInfo>> GetBucketsAsync(string connectionId)
160+
{
161+
var connection = GetConnection(connectionId);
162+
if (connection == null)
163+
{
164+
throw new InvalidOperationException("Not connected to cluster");
165+
}
166+
167+
var buckets = await connection.Cluster.Buckets.GetAllBucketsAsync();
168+
169+
return buckets.Values.Select(b => new BucketInfo
170+
{
171+
Name = b.Name,
172+
BucketType = b.BucketType,
173+
RamQuotaMB = (long)(b.RamQuotaMB),
174+
NumReplicas = b.NumReplicas
175+
}).OrderBy(b => b.Name).ToList();
176+
}
177+
178+
public static async Task<List<ScopeInfo>> GetScopesAsync(string connectionId, string bucketName)
179+
{
180+
var connection = GetConnection(connectionId);
181+
if (connection == null)
182+
{
183+
throw new InvalidOperationException("Not connected to cluster");
184+
}
185+
186+
var bucket = await connection.Cluster.BucketAsync(bucketName);
187+
var scopes = await bucket.Collections.GetAllScopesAsync();
188+
189+
return scopes.Select(s => new ScopeInfo
190+
{
191+
Name = s.Name,
192+
Collections = s.Collections.Select(c => new CollectionInfo
193+
{
194+
Name = c.Name,
195+
ScopeName = s.Name
196+
}).OrderBy(c => c.Name).ToList()
197+
}).OrderBy(s => s.Name).ToList();
198+
}
199+
200+
public static async Task<List<CollectionInfo>> GetCollectionsAsync(string connectionId, string bucketName, string scopeName)
201+
{
202+
var scopes = await GetScopesAsync(connectionId, bucketName);
203+
var scope = scopes.FirstOrDefault(s => s.Name == scopeName);
204+
return scope?.Collections ?? new List<CollectionInfo>();
205+
}
206+
}
207+
}
Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,77 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using CodingWithCalvin.CouchbaseExplorer.Services;
4+
15
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
26
{
37
public class BucketNode : TreeNodeBase
48
{
9+
private bool _hasLoadedScopes;
10+
511
public override string NodeType => "Bucket";
612

13+
public string ConnectionId { get; set; }
14+
15+
public string BucketType { get; set; }
16+
17+
public long RamQuotaMB { get; set; }
18+
19+
public int NumReplicas { get; set; }
20+
721
public BucketNode()
822
{
923
// Add placeholder for lazy loading
1024
Children.Add(new PlaceholderNode());
1125
}
1226

13-
protected override void OnExpanded()
27+
protected override async void OnExpanded()
1428
{
15-
// TODO: Load scopes when expanded
29+
if (_hasLoadedScopes)
30+
{
31+
return;
32+
}
33+
34+
await LoadScopesAsync();
35+
}
36+
37+
public async Task LoadScopesAsync()
38+
{
39+
IsLoading = true;
40+
41+
try
42+
{
43+
var scopes = await CouchbaseService.GetScopesAsync(ConnectionId, Name);
44+
45+
Children.Clear();
46+
47+
foreach (var scope in scopes)
48+
{
49+
var scopeNode = new ScopeNode
50+
{
51+
Name = scope.Name,
52+
ConnectionId = ConnectionId,
53+
BucketName = Name,
54+
Parent = this
55+
};
56+
Children.Add(scopeNode);
57+
}
58+
59+
if (Children.Count == 0)
60+
{
61+
Children.Add(new PlaceholderNode { Name = "(No scopes)" });
62+
}
63+
64+
_hasLoadedScopes = true;
65+
}
66+
catch (Exception ex)
67+
{
68+
Children.Clear();
69+
Children.Add(new PlaceholderNode { Name = $"(Error: {ex.Message})" });
70+
}
71+
finally
72+
{
73+
IsLoading = false;
74+
}
1675
}
1776
}
1877
}

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public class CollectionNode : TreeNodeBase
44
{
55
public override string NodeType => "Collection";
66

7+
public string ConnectionId { get; set; }
8+
79
public string BucketName { get; set; }
810

911
public string ScopeName { get; set; }

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/ConnectionDialogViewModel.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -365,20 +365,48 @@ private async Task TestConnectionAsync()
365365

366366
try
367367
{
368-
// TODO: Implement actual Couchbase connection test
369-
// For now, simulate a connection test
370-
await Task.Delay(1000);
368+
// Use a temporary ID for testing
369+
var testConnectionId = $"test_{Guid.NewGuid()}";
371370

372-
// Placeholder: In real implementation, use Couchbase SDK to test connection
373-
// var cluster = await Cluster.ConnectAsync(connectionString, options);
374-
// await cluster.WaitUntilReadyAsync(TimeSpan.FromSeconds(10));
371+
// Use the same connection logic as the actual service
372+
var connection = await Services.CouchbaseService.ConnectAsync(
373+
testConnectionId,
374+
Host,
375+
Username,
376+
Password,
377+
UseSsl);
378+
379+
// Disconnect the test connection
380+
await Services.CouchbaseService.DisconnectAsync(testConnectionId);
375381

376382
TestStatus = TestConnectionStatus.Success;
377383
}
384+
catch (AggregateException ae)
385+
{
386+
var innermost = ae.GetBaseException();
387+
_testErrorMessage = "See details";
388+
TestStatus = TestConnectionStatus.Failed;
389+
System.Windows.MessageBox.Show(
390+
$"{innermost.GetType().Name}: {innermost.Message}",
391+
"Connection Failed",
392+
System.Windows.MessageBoxButton.OK,
393+
System.Windows.MessageBoxImage.Error);
394+
}
378395
catch (Exception ex)
379396
{
380-
_testErrorMessage = ex.Message;
397+
// Get the innermost exception for the real error
398+
var innermost = ex;
399+
while (innermost.InnerException != null)
400+
{
401+
innermost = innermost.InnerException;
402+
}
403+
_testErrorMessage = "See details";
381404
TestStatus = TestConnectionStatus.Failed;
405+
System.Windows.MessageBox.Show(
406+
$"{ex.GetType().Name}: {innermost.Message}",
407+
"Connection Failed",
408+
System.Windows.MessageBoxButton.OK,
409+
System.Windows.MessageBoxImage.Error);
382410
}
383411
}
384412

0 commit comments

Comments
 (0)