Skip to content

Commit ceb1136

Browse files
authored
feat(documents): implement batched document loading in tree view (#328)
1 parent 2139161 commit ceb1136

File tree

9 files changed

+315
-4
lines changed

9 files changed

+315
-4
lines changed

src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
<Compile Include="ViewModels\ConnectionDialogViewModel.cs" />
8888
<Compile Include="ViewModels\Converters.cs" />
8989
<Compile Include="ViewModels\CouchbaseExplorerViewModel.cs" />
90+
<Compile Include="ViewModels\DocumentBatchNode.cs" />
9091
<Compile Include="ViewModels\DocumentNode.cs" />
9192
<Compile Include="ViewModels\IndexesFolderNode.cs" />
9293
<Compile Include="ViewModels\IndexNode.cs" />

src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@
140140
</StackPanel>
141141
</HierarchicalDataTemplate>
142142

143+
<!-- Data Template for Document Batch Node -->
144+
<HierarchicalDataTemplate DataType="{x:Type vm:DocumentBatchNode}" ItemsSource="{Binding Children}">
145+
<StackPanel Orientation="Horizontal">
146+
<imaging:CrispImage Width="16" Height="16" Margin="0,0,5,0"
147+
Moniker="{x:Static catalog:KnownMonikers.FolderClosed}" />
148+
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
149+
</StackPanel>
150+
</HierarchicalDataTemplate>
151+
143152
<!-- Data Template for Document Node -->
144153
<DataTemplate DataType="{x:Type vm:DocumentNode}">
145154
<StackPanel Orientation="Horizontal">

src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,48 @@ public static async Task<List<CollectionInfo>> GetCollectionsAsync(string connec
166166
var scope = scopes.FirstOrDefault(s => s.Name == scopeName);
167167
return scope?.Collections ?? new List<CollectionInfo>();
168168
}
169+
170+
public static async Task<DocumentQueryResult> GetDocumentIdsAsync(string connectionId, string bucketName, string scopeName, string collectionName, int limit = 50, int offset = 0)
171+
{
172+
var connection = GetConnection(connectionId);
173+
if (connection == null)
174+
{
175+
throw new InvalidOperationException("Not connected to cluster");
176+
}
177+
178+
var query = $"SELECT META().id FROM `{bucketName}`.`{scopeName}`.`{collectionName}` ORDER BY META().id LIMIT {limit + 1} OFFSET {offset}";
179+
180+
var result = await connection.Cluster.QueryAsync<DocumentIdResult>(query);
181+
var documentIds = new List<string>();
182+
183+
await foreach (var row in result.Rows)
184+
{
185+
documentIds.Add(row.Id);
186+
}
187+
188+
// Check if there are more documents (we fetched limit+1 to check)
189+
var hasMore = documentIds.Count > limit;
190+
if (hasMore)
191+
{
192+
documentIds.RemoveAt(documentIds.Count - 1);
193+
}
194+
195+
return new DocumentQueryResult
196+
{
197+
DocumentIds = documentIds,
198+
HasMore = hasMore
199+
};
200+
}
201+
}
202+
203+
public class DocumentIdResult
204+
{
205+
public string Id { get; set; }
206+
}
207+
208+
public class DocumentQueryResult
209+
{
210+
public List<string> DocumentIds { get; set; } = new List<string>();
211+
public bool HasMore { get; set; }
169212
}
170213
}

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using CodingWithCalvin.CouchbaseExplorer.Services;
4+
15
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
26
{
37
public class CollectionNode : TreeNodeBase
48
{
9+
private bool _hasLoadedDocuments;
10+
private int _currentOffset;
11+
private const int BatchSize = 50;
12+
513
public override string NodeType => "Collection";
614

715
public string ConnectionId { get; set; }
@@ -16,9 +24,128 @@ public CollectionNode()
1624
Children.Add(new PlaceholderNode());
1725
}
1826

19-
protected override void OnExpanded()
27+
protected override async void OnExpanded()
28+
{
29+
if (_hasLoadedDocuments)
30+
{
31+
return;
32+
}
33+
34+
await LoadDocumentBatchesAsync();
35+
}
36+
37+
public async Task RefreshAsync()
38+
{
39+
_hasLoadedDocuments = false;
40+
_currentOffset = 0;
41+
Children.Clear();
42+
Children.Add(new PlaceholderNode { Name = "Refreshing..." });
43+
await LoadDocumentBatchesAsync();
44+
}
45+
46+
private async Task LoadDocumentBatchesAsync()
47+
{
48+
IsLoading = true;
49+
50+
try
51+
{
52+
Children.Clear();
53+
Children.Add(new PlaceholderNode { Name = "Loading documents..." });
54+
55+
var result = await CouchbaseService.GetDocumentIdsAsync(
56+
ConnectionId, BucketName, ScopeName, Name, BatchSize, _currentOffset);
57+
58+
Children.Clear();
59+
60+
if (result.DocumentIds.Count > 0)
61+
{
62+
var batchNode = new DocumentBatchNode(result.DocumentIds, _currentOffset)
63+
{
64+
ConnectionId = ConnectionId,
65+
BucketName = BucketName,
66+
ScopeName = ScopeName,
67+
CollectionName = Name,
68+
Parent = this
69+
};
70+
Children.Add(batchNode);
71+
72+
_currentOffset += result.DocumentIds.Count;
73+
74+
if (result.HasMore)
75+
{
76+
var loadMoreNode = new LoadMoreNode
77+
{
78+
Name = "Load More...",
79+
Parent = this
80+
};
81+
loadMoreNode.LoadMoreRequested += OnLoadMoreRequested;
82+
Children.Add(loadMoreNode);
83+
}
84+
}
85+
else
86+
{
87+
Children.Add(new PlaceholderNode { Name = "(No documents)" });
88+
}
89+
90+
_hasLoadedDocuments = true;
91+
}
92+
catch (Exception ex)
93+
{
94+
Children.Clear();
95+
Children.Add(new PlaceholderNode { Name = $"(Error: {ex.Message})" });
96+
}
97+
finally
98+
{
99+
IsLoading = false;
100+
}
101+
}
102+
103+
private async void OnLoadMoreRequested(LoadMoreNode node)
20104
{
21-
// TODO: Load documents in batches when expanded
105+
node.LoadMoreRequested -= OnLoadMoreRequested;
106+
Children.Remove(node);
107+
108+
IsLoading = true;
109+
110+
try
111+
{
112+
var result = await CouchbaseService.GetDocumentIdsAsync(
113+
ConnectionId, BucketName, ScopeName, Name, BatchSize, _currentOffset);
114+
115+
if (result.DocumentIds.Count > 0)
116+
{
117+
var batchNode = new DocumentBatchNode(result.DocumentIds, _currentOffset)
118+
{
119+
ConnectionId = ConnectionId,
120+
BucketName = BucketName,
121+
ScopeName = ScopeName,
122+
CollectionName = Name,
123+
Parent = this
124+
};
125+
Children.Add(batchNode);
126+
127+
_currentOffset += result.DocumentIds.Count;
128+
129+
if (result.HasMore)
130+
{
131+
var loadMoreNode = new LoadMoreNode
132+
{
133+
Name = "Load More...",
134+
Parent = this
135+
};
136+
loadMoreNode.LoadMoreRequested += OnLoadMoreRequested;
137+
Children.Add(loadMoreNode);
138+
}
139+
}
140+
}
141+
catch (Exception ex)
142+
{
143+
Children.Add(new PlaceholderNode { Name = $"(Error loading more: {ex.Message})" });
144+
}
145+
finally
146+
{
147+
IsLoading = false;
148+
}
22149
}
23150
}
24151
}

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CouchbaseExplorerViewModel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,16 @@ private async void OnRefresh(object parameter)
169169
case ScopeNode scope:
170170
await scope.RefreshAsync();
171171
break;
172+
case CollectionNode collection:
173+
await collection.RefreshAsync();
174+
break;
172175
}
173176
}
174177

175178
private bool CanRefresh(object parameter)
176179
{
177180
var node = parameter as TreeNodeBase ?? SelectedNode;
178-
return node is ConnectionNode conn ? conn.IsConnected : node is BucketNode || node is ScopeNode;
181+
return node is ConnectionNode conn ? conn.IsConnected : node is BucketNode || node is ScopeNode || node is CollectionNode;
179182
}
180183

181184
private void OnCollapseAll(object parameter)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using CodingWithCalvin.CouchbaseExplorer.Services;
5+
6+
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
7+
{
8+
public class DocumentBatchNode : TreeNodeBase
9+
{
10+
private bool _hasLoadedDocuments;
11+
private List<string> _documentIds;
12+
13+
public override string NodeType => "DocumentBatch";
14+
15+
public string ConnectionId { get; set; }
16+
17+
public string BucketName { get; set; }
18+
19+
public string ScopeName { get; set; }
20+
21+
public string CollectionName { get; set; }
22+
23+
public int StartIndex { get; set; }
24+
25+
public int EndIndex { get; set; }
26+
27+
public DocumentBatchNode()
28+
{
29+
Children.Add(new PlaceholderNode());
30+
}
31+
32+
public DocumentBatchNode(List<string> documentIds, int startIndex)
33+
{
34+
_documentIds = documentIds;
35+
StartIndex = startIndex;
36+
EndIndex = startIndex + documentIds.Count - 1;
37+
Name = $"[{StartIndex + 1}-{EndIndex + 1}]";
38+
Children.Add(new PlaceholderNode());
39+
}
40+
41+
protected override async void OnExpanded()
42+
{
43+
if (_hasLoadedDocuments)
44+
{
45+
return;
46+
}
47+
48+
await LoadDocumentsAsync();
49+
}
50+
51+
public async Task RefreshAsync()
52+
{
53+
_hasLoadedDocuments = false;
54+
Children.Clear();
55+
Children.Add(new PlaceholderNode { Name = "Refreshing..." });
56+
await LoadDocumentsAsync();
57+
}
58+
59+
private async Task LoadDocumentsAsync()
60+
{
61+
IsLoading = true;
62+
63+
try
64+
{
65+
Children.Clear();
66+
67+
if (_documentIds != null && _documentIds.Count > 0)
68+
{
69+
foreach (var docId in _documentIds)
70+
{
71+
var docNode = new DocumentNode
72+
{
73+
Name = docId,
74+
DocumentId = docId,
75+
ConnectionId = ConnectionId,
76+
BucketName = BucketName,
77+
ScopeName = ScopeName,
78+
CollectionName = CollectionName,
79+
Parent = this
80+
};
81+
Children.Add(docNode);
82+
}
83+
}
84+
else
85+
{
86+
Children.Add(new PlaceholderNode { Name = "(No documents)" });
87+
}
88+
89+
_hasLoadedDocuments = true;
90+
}
91+
catch (Exception ex)
92+
{
93+
Children.Clear();
94+
Children.Add(new PlaceholderNode { Name = $"(Error: {ex.Message})" });
95+
}
96+
finally
97+
{
98+
IsLoading = false;
99+
}
100+
}
101+
}
102+
}

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentNode.cs

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

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

911
public string BucketName { get; set; }

src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
13
namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
24
{
35
public class LoadMoreNode : TreeNodeBase
@@ -6,9 +8,21 @@ public class LoadMoreNode : TreeNodeBase
68

79
public int NextOffset { get; set; }
810

11+
public event Action<LoadMoreNode> LoadMoreRequested;
12+
913
public LoadMoreNode()
1014
{
1115
Name = "Load More...";
1216
}
17+
18+
public void RequestLoadMore()
19+
{
20+
LoadMoreRequested?.Invoke(this);
21+
}
22+
23+
protected override void OnSelected()
24+
{
25+
RequestLoadMore();
26+
}
1327
}
1428
}

0 commit comments

Comments
 (0)