Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<Compile Include="ViewModels\ConnectionDialogViewModel.cs" />
<Compile Include="ViewModels\Converters.cs" />
<Compile Include="ViewModels\CouchbaseExplorerViewModel.cs" />
<Compile Include="ViewModels\DocumentBatchNode.cs" />
<Compile Include="ViewModels\DocumentNode.cs" />
<Compile Include="ViewModels\IndexesFolderNode.cs" />
<Compile Include="ViewModels\IndexNode.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@
</StackPanel>
</HierarchicalDataTemplate>

<!-- Data Template for Document Batch Node -->
<HierarchicalDataTemplate DataType="{x:Type vm:DocumentBatchNode}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<imaging:CrispImage Width="16" Height="16" Margin="0,0,5,0"
Moniker="{x:Static catalog:KnownMonikers.FolderClosed}" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
</StackPanel>
</HierarchicalDataTemplate>

<!-- Data Template for Document Node -->
<DataTemplate DataType="{x:Type vm:DocumentNode}">
<StackPanel Orientation="Horizontal">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,48 @@ public static async Task<List<CollectionInfo>> GetCollectionsAsync(string connec
var scope = scopes.FirstOrDefault(s => s.Name == scopeName);
return scope?.Collections ?? new List<CollectionInfo>();
}

public static async Task<DocumentQueryResult> 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<DocumentIdResult>(query);
var documentIds = new List<string>();

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<string> DocumentIds { get; set; } = new List<string>();
public bool HasMore { get; set; }
}
}
131 changes: 129 additions & 2 deletions src/CodingWithCalvin.CouchbaseExplorer/ViewModels/CollectionNode.cs
Original file line number Diff line number Diff line change
@@ -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; }
Expand All @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
102 changes: 102 additions & 0 deletions src/CodingWithCalvin.CouchbaseExplorer/ViewModels/DocumentBatchNode.cs
Original file line number Diff line number Diff line change
@@ -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<string> _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<string> 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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
14 changes: 14 additions & 0 deletions src/CodingWithCalvin.CouchbaseExplorer/ViewModels/LoadMoreNode.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace CodingWithCalvin.CouchbaseExplorer.ViewModels
{
public class LoadMoreNode : TreeNodeBase
Expand All @@ -6,9 +8,21 @@ public class LoadMoreNode : TreeNodeBase

public int NextOffset { get; set; }

public event Action<LoadMoreNode> LoadMoreRequested;

public LoadMoreNode()
{
Name = "Load More...";
}

public void RequestLoadMore()
{
LoadMoreRequested?.Invoke(this);
}

protected override void OnSelected()
{
RequestLoadMore();
}
}
}
Loading