diff --git a/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj b/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
index 13acb50..cd89db5 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
+++ b/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
@@ -87,6 +87,7 @@
+
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml
index 524aa3b..6cc4bf3 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml
+++ b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml
@@ -140,6 +140,15 @@
+
+
+
+
+
+
+
+
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs b/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
index 065daeb..6887481 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
@@ -166,5 +166,48 @@ public static async Task> GetCollectionsAsync(string connec
var scope = scopes.FirstOrDefault(s => s.Name == scopeName);
return scope?.Collections ?? new List();
}
+
+ public static async Task GetDocumentIdsAsync(string connectionId, string bucketName, string scopeName, string collectionName, int limit = 50, int offset = 0)
+ {
+ var connection = GetConnection(connectionId);
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Not connected to cluster");
+ }
+
+ var query = $"SELECT META().id FROM `{bucketName}`.`{scopeName}`.`{collectionName}` ORDER BY META().id LIMIT {limit + 1} OFFSET {offset}";
+
+ var result = await connection.Cluster.QueryAsync(query);
+ var documentIds = new List();
+
+ await foreach (var row in result.Rows)
+ {
+ documentIds.Add(row.Id);
+ }
+
+ // Check if there are more documents (we fetched limit+1 to check)
+ var hasMore = documentIds.Count > limit;
+ if (hasMore)
+ {
+ documentIds.RemoveAt(documentIds.Count - 1);
+ }
+
+ return new DocumentQueryResult
+ {
+ DocumentIds = documentIds,
+ HasMore = hasMore
+ };
+ }
+ }
+
+ public class DocumentIdResult
+ {
+ public string Id { get; set; }
+ }
+
+ public class DocumentQueryResult
+ {
+ public List DocumentIds { get; set; } = new List();
+ public bool HasMore { get; set; }
}
}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs
index 37126f8..8ad82c6 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs
@@ -1,7 +1,15 @@
+using System;
+using System.Threading.Tasks;
+using CodingWithCalvin.CouchbaseExplorer.Services;
+
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
{
public class CollectionNode : TreeNodeBase
{
+ private bool _hasLoadedDocuments;
+ private int _currentOffset;
+ private const int BatchSize = 50;
+
public override string NodeType => "Collection";
public string ConnectionId { get; set; }
@@ -16,9 +24,128 @@ public CollectionNode()
Children.Add(new PlaceholderNode());
}
- protected override void OnExpanded()
+ protected override async void OnExpanded()
+ {
+ if (_hasLoadedDocuments)
+ {
+ return;
+ }
+
+ await LoadDocumentBatchesAsync();
+ }
+
+ public async Task RefreshAsync()
+ {
+ _hasLoadedDocuments = false;
+ _currentOffset = 0;
+ Children.Clear();
+ Children.Add(new PlaceholderNode { Name = "Refreshing..." });
+ await LoadDocumentBatchesAsync();
+ }
+
+ private async Task LoadDocumentBatchesAsync()
+ {
+ IsLoading = true;
+
+ try
+ {
+ Children.Clear();
+ Children.Add(new PlaceholderNode { Name = "Loading documents..." });
+
+ var result = await CouchbaseService.GetDocumentIdsAsync(
+ ConnectionId, BucketName, ScopeName, Name, BatchSize, _currentOffset);
+
+ Children.Clear();
+
+ if (result.DocumentIds.Count > 0)
+ {
+ var batchNode = new DocumentBatchNode(result.DocumentIds, _currentOffset)
+ {
+ ConnectionId = ConnectionId,
+ BucketName = BucketName,
+ ScopeName = ScopeName,
+ CollectionName = Name,
+ Parent = this
+ };
+ Children.Add(batchNode);
+
+ _currentOffset += result.DocumentIds.Count;
+
+ if (result.HasMore)
+ {
+ var loadMoreNode = new LoadMoreNode
+ {
+ Name = "Load More...",
+ Parent = this
+ };
+ loadMoreNode.LoadMoreRequested += OnLoadMoreRequested;
+ Children.Add(loadMoreNode);
+ }
+ }
+ else
+ {
+ Children.Add(new PlaceholderNode { Name = "(No documents)" });
+ }
+
+ _hasLoadedDocuments = true;
+ }
+ catch (Exception ex)
+ {
+ Children.Clear();
+ Children.Add(new PlaceholderNode { Name = $"(Error: {ex.Message})" });
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private async void OnLoadMoreRequested(LoadMoreNode node)
{
- // TODO: Load documents in batches when expanded
+ node.LoadMoreRequested -= OnLoadMoreRequested;
+ Children.Remove(node);
+
+ IsLoading = true;
+
+ try
+ {
+ var result = await CouchbaseService.GetDocumentIdsAsync(
+ ConnectionId, BucketName, ScopeName, Name, BatchSize, _currentOffset);
+
+ if (result.DocumentIds.Count > 0)
+ {
+ var batchNode = new DocumentBatchNode(result.DocumentIds, _currentOffset)
+ {
+ ConnectionId = ConnectionId,
+ BucketName = BucketName,
+ ScopeName = ScopeName,
+ CollectionName = Name,
+ Parent = this
+ };
+ Children.Add(batchNode);
+
+ _currentOffset += result.DocumentIds.Count;
+
+ if (result.HasMore)
+ {
+ var loadMoreNode = new LoadMoreNode
+ {
+ Name = "Load More...",
+ Parent = this
+ };
+ loadMoreNode.LoadMoreRequested += OnLoadMoreRequested;
+ Children.Add(loadMoreNode);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Children.Add(new PlaceholderNode { Name = $"(Error loading more: {ex.Message})" });
+ }
+ finally
+ {
+ IsLoading = false;
+ }
}
}
}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CouchbaseExplorerViewModel.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CouchbaseExplorerViewModel.cs
index a5401fa..8f547b3 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CouchbaseExplorerViewModel.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CouchbaseExplorerViewModel.cs
@@ -169,13 +169,16 @@ private async void OnRefresh(object parameter)
case ScopeNode scope:
await scope.RefreshAsync();
break;
+ case CollectionNode collection:
+ await collection.RefreshAsync();
+ break;
}
}
private bool CanRefresh(object parameter)
{
var node = parameter as TreeNodeBase ?? SelectedNode;
- return node is ConnectionNode conn ? conn.IsConnected : node is BucketNode || node is ScopeNode;
+ return node is ConnectionNode conn ? conn.IsConnected : node is BucketNode || node is ScopeNode || node is CollectionNode;
}
private void OnCollapseAll(object parameter)
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentBatchNode.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentBatchNode.cs
new file mode 100644
index 0000000..4db1fe5
--- /dev/null
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentBatchNode.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using CodingWithCalvin.CouchbaseExplorer.Services;
+
+namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
+{
+ public class DocumentBatchNode : TreeNodeBase
+ {
+ private bool _hasLoadedDocuments;
+ private List _documentIds;
+
+ public override string NodeType => "DocumentBatch";
+
+ public string ConnectionId { get; set; }
+
+ public string BucketName { get; set; }
+
+ public string ScopeName { get; set; }
+
+ public string CollectionName { get; set; }
+
+ public int StartIndex { get; set; }
+
+ public int EndIndex { get; set; }
+
+ public DocumentBatchNode()
+ {
+ Children.Add(new PlaceholderNode());
+ }
+
+ public DocumentBatchNode(List documentIds, int startIndex)
+ {
+ _documentIds = documentIds;
+ StartIndex = startIndex;
+ EndIndex = startIndex + documentIds.Count - 1;
+ Name = $"[{StartIndex + 1}-{EndIndex + 1}]";
+ Children.Add(new PlaceholderNode());
+ }
+
+ protected override async void OnExpanded()
+ {
+ if (_hasLoadedDocuments)
+ {
+ return;
+ }
+
+ await LoadDocumentsAsync();
+ }
+
+ public async Task RefreshAsync()
+ {
+ _hasLoadedDocuments = false;
+ Children.Clear();
+ Children.Add(new PlaceholderNode { Name = "Refreshing..." });
+ await LoadDocumentsAsync();
+ }
+
+ private async Task LoadDocumentsAsync()
+ {
+ IsLoading = true;
+
+ try
+ {
+ Children.Clear();
+
+ if (_documentIds != null && _documentIds.Count > 0)
+ {
+ foreach (var docId in _documentIds)
+ {
+ var docNode = new DocumentNode
+ {
+ Name = docId,
+ DocumentId = docId,
+ ConnectionId = ConnectionId,
+ BucketName = BucketName,
+ ScopeName = ScopeName,
+ CollectionName = CollectionName,
+ Parent = this
+ };
+ Children.Add(docNode);
+ }
+ }
+ else
+ {
+ Children.Add(new PlaceholderNode { Name = "(No documents)" });
+ }
+
+ _hasLoadedDocuments = true;
+ }
+ catch (Exception ex)
+ {
+ Children.Clear();
+ Children.Add(new PlaceholderNode { Name = $"(Error: {ex.Message})" });
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+ }
+}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentNode.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentNode.cs
index f1039d1..146e652 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentNode.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentNode.cs
@@ -4,6 +4,8 @@ public class DocumentNode : TreeNodeBase
{
public override string NodeType => "Document";
+ public string ConnectionId { get; set; }
+
public string DocumentId { get; set; }
public string BucketName { get; set; }
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs
index 4a1bb64..3e4af3f 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
{
public class LoadMoreNode : TreeNodeBase
@@ -6,9 +8,21 @@ public class LoadMoreNode : TreeNodeBase
public int NextOffset { get; set; }
+ public event Action LoadMoreRequested;
+
public LoadMoreNode()
{
Name = "Load More...";
}
+
+ public void RequestLoadMore()
+ {
+ LoadMoreRequested?.Invoke(this);
+ }
+
+ protected override void OnSelected()
+ {
+ RequestLoadMore();
+ }
}
}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/TreeNodeBase.cs b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/TreeNodeBase.cs
index 7f53e71..36ed9b4 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/TreeNodeBase.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/ViewModels/TreeNodeBase.cs
@@ -34,7 +34,13 @@ public bool IsExpanded
public bool IsSelected
{
get => _isSelected;
- set => SetProperty(ref _isSelected, value);
+ set
+ {
+ if (SetProperty(ref _isSelected, value) && value)
+ {
+ OnSelected();
+ }
+ }
}
public bool IsLoading
@@ -53,6 +59,10 @@ protected virtual void OnExpanded()
{
}
+ protected virtual void OnSelected()
+ {
+ }
+
protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(field, value))