Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3f25bcc
feat: enhance conflict detection in move items wizard to include task…
tnaum-ms Jan 27, 2026
a0ba751
feat: implement verification step for folder deletion to check for ta…
tnaum-ms Jan 27, 2026
d662efa
wip: separate treeid and clusterid
tnaum-ms Jan 28, 2026
4527ae8
wip: separate treeid and clusterid: update ClusterItemBase to use tre…
tnaum-ms Jan 28, 2026
397260b
wip: separate treeid and clusterid: update credential caching to use …
tnaum-ms Jan 28, 2026
a0156bd
wip: separate treeid and clusterid: refactor: update connection handl…
tnaum-ms Jan 28, 2026
f22689c
docs: add Cluster ID architecture section with usage guidelines for t…
tnaum-ms Jan 28, 2026
62056d4
wip: separate treeid and clusterid: refactor: update identifiers to u…
tnaum-ms Jan 28, 2026
0414674
wip: separate treeid and clusterid: refactor: enhance conflict verifi…
tnaum-ms Jan 28, 2026
4137e73
wip: separate treeid and clusterid: refactor: update credential delet…
tnaum-ms Jan 28, 2026
e1ca51a
feat: implement dual-ID architecture for collection lookups by cluste…
tnaum-ms Jan 28, 2026
52fab32
refactor: update treeId to replace '/' with '-' for Azure resource ID…
tnaum-ms Jan 28, 2026
37616d5
refactor: update credential handling to use stable clusterId for cach…
tnaum-ms Jan 29, 2026
1077cb6
refactor: introduce Azure and Connection cluster models with sanitiza…
tnaum-ms Jan 29, 2026
6269a85
refactor: restructure BaseClusterModel and ClusterTreeContext for cla…
tnaum-ms Jan 29, 2026
c65e5f0
refactor: update cluster model types and improve type safety across c…
tnaum-ms Jan 29, 2026
692be84
refactor: update Azure resource models to enhance type safety and ens…
tnaum-ms Jan 29, 2026
02679b1
refactor: enhance cluster model architecture for improved type safety…
tnaum-ms Jan 29, 2026
37a8ae5
refactor: clarify cluster ID architecture and enhance model type defi…
tnaum-ms Jan 29, 2026
d762f44
refactor: enhance cluster model tracing and add unit tests for cluste…
tnaum-ms Jan 29, 2026
a8854c0
refactor: sanitize Azure Resource IDs for clusterId and treeId across…
tnaum-ms Jan 29, 2026
9ae894d
refactor: implement ownsClusterId method for discovery providers to o…
tnaum-ms Jan 29, 2026
72f8764
refactor: remove ownsClusterId method from discovery providers and op…
tnaum-ms Jan 30, 2026
222aa08
refactor: add cluster ID augmentation utilities and type guard for cl…
tnaum-ms Jan 30, 2026
2720f29
refactor: add unit tests for cluster ID augmentation and type guard f…
tnaum-ms Jan 30, 2026
dfca159
refactor: enhance DiscoveryService and DiscoveryBranchDataProvider wi…
tnaum-ms Jan 30, 2026
046561c
refactor: add unit tests for cluster ID augmentation in DiscoveryBran…
tnaum-ms Jan 30, 2026
b0b6501
refactor: enhance cluster item type guard to validate contextValue an…
tnaum-ms Jan 30, 2026
bf76f8d
refactor: simplify findCollectionByClusterId logic by removing unnece…
tnaum-ms Jan 30, 2026
c896cea
refactor: update cluster ID handling to enforce provider prefix valid…
tnaum-ms Jan 30, 2026
bba81b2
refactor: remove augmentClusterId function and simplify cluster ID va…
tnaum-ms Jan 30, 2026
72b970a
refactor: remove cluster ID augmentation functions and associated tes…
tnaum-ms Jan 30, 2026
c22bf0b
refactor: add findChildById method for efficient child node search fr…
tnaum-ms Jan 30, 2026
558c350
resolved PR feedback. refactor: update Azure Resource ID sanitization…
tnaum-ms Jan 30, 2026
56af5e2
Initial plan
Copilot Jan 30, 2026
c9bec95
Fix documentation and tests to use underscore for Azure Resource ID s…
Copilot Jan 30, 2026
fecd231
Fix Azure Resource ID sanitization documentation and tests (#476)
tnaum-ms Jan 30, 2026
c957280
Initial plan
Copilot Jan 30, 2026
8106dd4
Fix sanitization documentation and test comments to use underscore in…
Copilot Jan 30, 2026
0da279e
Fix Azure Resource ID sanitization documentation inconsistencies (#477)
tnaum-ms Jan 30, 2026
4ef49d5
Refactor base cluster model architecture for clarity (#473)
tnaum-ms Jan 30, 2026
b1c7dcd
Merge branch 'next' into dev/tnaum/move-connection-protection
tnaum-ms Jan 30, 2026
f829f95
Update error message for cluster ID validation in DiscoveryBranchData…
tnaum-ms Feb 2, 2026
95fdc64
refactor: Rename 'id' to 'azureResourceId' for clarity in Azure-relat…
tnaum-ms Feb 2, 2026
cdd68bc
fix: add missing viewId to DocumentsItem command payload
tnaum-ms Feb 2, 2026
be28c53
fix: localize error message in DiscoveryBranchDataProvider
tnaum-ms Feb 2, 2026
85bad99
docs: clarify clusterId documentation for Azure Resources View
tnaum-ms Feb 2, 2026
796620a
docs: update misleading comment in AzureVMResourceItem
tnaum-ms Feb 2, 2026
509bd1a
refactor: extract inferViewIdFromTreeId to shared helper
tnaum-ms Feb 2, 2026
9f75594
refactor: reorder import statements in openCollectionView and Discove…
tnaum-ms Feb 2, 2026
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
37 changes: 37 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,43 @@ src/commands/yourCommand/
- Use VS Code's secure storage for credentials
- Validate all user inputs

## Cluster ID Architecture (Dual ID Pattern)

> ⚠️ **CRITICAL**: Using the wrong ID causes silent bugs that only appear when users move connections between folders.

Cluster models have **two distinct ID properties** with different purposes:

| Property | Purpose | Stable? | Use For |
| ----------- | -------------------------------- | ------------------------- | ----------------------------------- |
| `treeId` | VS Code TreeView element path | ❌ Changes on folder move | `this.id`, child item paths |
| `clusterId` | Cache key (credentials, clients) | ✅ Always stable | `CredentialCache`, `ClustersClient` |

### Quick Reference

```typescript
// ✅ Tree element identification
this.id = cluster.treeId;

// ✅ Cache operations - ALWAYS use clusterId
CredentialCache.hasCredentials(cluster.clusterId);
ClustersClient.getClient(cluster.clusterId);

// ❌ WRONG - breaks when connection moves to a folder
CredentialCache.hasCredentials(this.id); // BUG!
```

### Model Types

- **`ConnectionClusterModel`** - Connections View (has `storageId`)
- **`AzureClusterModel`** - Azure/Discovery Views (has `azureResourceId`)
- **`BaseClusterModel`** - Shared interface (use for generic code)

For Discovery View, both `treeId` and `clusterId` are sanitized (all `/` replaced with `_`). The original Azure Resource ID is stored in `AzureClusterModel.azureResourceId` for Azure API calls.

> 💡 **Extensibility**: If adding a non-Azure discovery source (e.g., AWS, GCP), consider creating a new model type (e.g., `AwsClusterModel`) extending `BaseClusterModel` with source-specific metadata.

See `src/tree/models/BaseClusterModel.ts` and `docs/analysis/08-cluster-model-simplification-plan.md` for details.

## Additional Patterns

For detailed patterns, see:
Expand Down
9 changes: 7 additions & 2 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"{0} replaced": "{0} replaced",
"{0} skipped": "{0} skipped",
"{0} subfolders": "{0} subfolders",
"{0} task(s) are using connections being moved. Check the Output panel for details.": "{0} task(s) are using connections being moved. Check the Output panel for details.",
"{0} task(s) are using connections in this folder. Check the Output panel for details.": "{0} task(s) are using connections in this folder. Check the Output panel for details.",
"{0} tenants available ({1} signed in)": "{0} tenants available ({1} signed in)",
"{0} was stopped": "{0} was stopped",
Expand Down Expand Up @@ -212,10 +213,11 @@
"Cannot {0}": "Cannot {0}",
"Cannot copy collection to itself": "Cannot copy collection to itself",
"Cannot delete folder \"{0}\". The following {1} task(s) are using connections within this folder:": "Cannot delete folder \"{0}\". The following {1} task(s) are using connections within this folder:",
"Cannot move {0} {1}. The following {2} task(s) are using connections being moved:": "Cannot move {0} {1}. The following {2} task(s) are using connections being moved:",
"Cannot start task in state: {0}": "Cannot start task in state: {0}",
"Change page size": "Change page size",
"Changelog": "Changelog",
"Checking for naming conflicts…": "Checking for naming conflicts…",
"Checking for conflicts…": "Checking for conflicts…",
"Choose a cluster…": "Choose a cluster…",
"Choose a different folder": "Choose a different folder",
"Choose a RU cluster…": "Choose a RU cluster…",
Expand Down Expand Up @@ -338,6 +340,7 @@
"detailed execution analysis": "detailed execution analysis",
"Disable TLS/SSL (Not recommended)": "Disable TLS/SSL (Not recommended)",
"Disable TLS/SSL checks when connecting.": "Disable TLS/SSL checks when connecting.",
"Discovery plugin error: clusterId \"{0}\" must start with provider ID \"{1}\". Plugin \"{2}\" must prefix clusterId with its provider ID.": "Discovery plugin error: clusterId \"{0}\" must start with provider ID \"{1}\". Plugin \"{2}\" must prefix clusterId with its provider ID.",
"Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.": "Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.",
"Do not save credentials.": "Do not save credentials.",
"Do you want to include the password in the connection string?": "Do you want to include the password in the connection string?",
Expand Down Expand Up @@ -600,6 +603,8 @@
"Invalid sort syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }": "Invalid sort syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }",
"It could be better": "It could be better",
"It looks like there aren't any other folders to move these items into.\nYou might want to create a new folder first.\n\nNote: You can't move items between 'DocumentDB Local' and regular connections.": "It looks like there aren't any other folders to move these items into.\nYou might want to create a new folder first.\n\nNote: You can't move items between 'DocumentDB Local' and regular connections.",
"item": "item",
"items": "items",
"JSON View": "JSON View",
"Keep-alive timeout exceeded": "Keep-alive timeout exceeded",
"Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)": "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)",
Expand Down Expand Up @@ -678,7 +683,7 @@
"No collection selected.": "No collection selected.",
"No commands found in this document.": "No commands found in this document.",
"No Connectivity": "No Connectivity",
"No credentials found for id {credentialId}": "No credentials found for id {credentialId}",
"No credentials found for id {clusterId}": "No credentials found for id {clusterId}",
"No credentials found for the selected cluster.": "No credentials found for the selected cluster.",
"No folder selected.": "No folder selected.",
"No index changes needed at this time.": "No index changes needed at this time.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@ export async function chooseDataMigrationExtension(context: IActionContext, node
// We should allow whitelisting extensions trusted by the user to avoid repeated prompts.
// This could be done on our own but available for the user to edit in settings.
const parsedCS_WithCredentials = new DocumentDBConnectionString(credentials.connectionString);
parsedCS_WithCredentials.username = CredentialCache.getConnectionUser(node.cluster.id) ?? '';
parsedCS_WithCredentials.password = CredentialCache.getConnectionPassword(node.cluster.id) ?? '';
parsedCS_WithCredentials.username = CredentialCache.getConnectionUser(node.cluster.clusterId) ?? '';
parsedCS_WithCredentials.password = CredentialCache.getConnectionPassword(node.cluster.clusterId) ?? '';

const options = {
connectionString: parsedCS_WithCredentials.toString(),
extendedProperties: {
clusterId: node.cluster.id,
clusterId: node.cluster.clusterId,
},
};

Expand Down Expand Up @@ -207,7 +207,7 @@ export async function chooseDataMigrationExtension(context: IActionContext, node
* @returns Promise<boolean> - true if authentication succeeded, false otherwise
*/
async function ensureAuthentication(_context: IActionContext, _node: ClusterItemBase): Promise<boolean> {
if (CredentialCache.hasCredentials(_node.cluster.id)) {
if (CredentialCache.hasCredentials(_node.cluster.clusterId)) {
return Promise.resolve(true); // Credentials already exist, no need to authenticate again
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@

import { AzureWizardPromptStep, UserCancelledError, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
import * as l10n from '@vscode/l10n';
import { ext } from '../../../extensionVariables';
import { ConnectionStorageService, ItemType } from '../../../services/connectionStorageService';
import { TaskService } from '../../../services/taskService/taskService';
import {
enumerateConnectionsInFolder,
findConflictingTasks,
logTaskConflicts,
VerificationCompleteError,
} from '../verificationUtils';
import { type DeleteFolderWizardContext } from './DeleteFolderWizardContext';

/**
* Custom error to signal that conflict verification completed with no conflicts
*/
class VerificationCompleteError extends Error {
constructor() {
super('Conflict verification completed successfully');
this.name = 'VerificationCompleteError';
}
}

type ConflictAction = 'exit';

/**
Expand All @@ -31,7 +25,7 @@ type ConflictAction = 'exit';
* If conflicts are found, the user is informed and can only exit.
* Uses a loading UI while checking.
*/
export class VerifyStep extends AzureWizardPromptStep<DeleteFolderWizardContext> {
export class VerifyNoConflictsStep extends AzureWizardPromptStep<DeleteFolderWizardContext> {
public async prompt(context: DeleteFolderWizardContext): Promise<void> {
try {
// Use QuickPick with loading state while verifying
Expand Down Expand Up @@ -61,40 +55,15 @@ export class VerifyStep extends AzureWizardPromptStep<DeleteFolderWizardContext>
* If conflicts: returns options for user to exit.
*/
private async verifyAndCount(context: DeleteFolderWizardContext): Promise<IAzureQuickPickItem<ConflictAction>[]> {
context.conflictingTasks = [];

// Count all folders and connections that will be deleted
const counts = await this.countDescendants(context, context.folderItem.storageId);
context.foldersToDelete = counts.folders + 1; // +1 for the folder itself
context.connectionsToDelete = counts.connections;

// Get all resources used by active tasks
const allUsedResources = TaskService.getAllUsedResources();

// The folder's tree ID serves as a prefix for all connections within it
// Connection tree IDs follow the pattern: parentTreeId/storageId
// So any connection in this folder will have an ID starting with folderItem.id + "/"
const folderTreeIdPrefix = context.folderItem.id + '/';

// Check each task's resources to see if any connectionId starts with our folder prefix
for (const { task, resources } of allUsedResources) {
for (const resource of resources) {
if (resource.connectionId && resource.connectionId.startsWith(folderTreeIdPrefix)) {
context.conflictingTasks.push(task);
break; // Only need to add task once, even if it uses multiple connections in the folder
}
}
}

// De-duplicate tasks (in case the same task was added multiple times)
const uniqueTaskIds = new Set<string>();
context.conflictingTasks = context.conflictingTasks.filter((task) => {
if (uniqueTaskIds.has(task.taskId)) {
return false;
}
uniqueTaskIds.add(task.taskId);
return true;
});
// Enumerate all connection IDs within the folder for conflict checking
// This uses storageIds (clusterIds) which are stable identifiers used by tasks
const connectionIds = await enumerateConnectionsInFolder(context.folderItem.storageId, context.connectionType);
context.conflictingTasks = findConflictingTasks(connectionIds);

// If no conflicts, signal completion and proceed
if (context.conflictingTasks.length === 0) {
Expand All @@ -103,19 +72,14 @@ export class VerifyStep extends AzureWizardPromptStep<DeleteFolderWizardContext>

// Conflicts found - log details to output channel
const conflictCount = context.conflictingTasks.length;

ext.outputChannel.appendLog(
logTaskConflicts(
l10n.t(
'Cannot delete folder "{0}". The following {1} task(s) are using connections within this folder:',
context.folderItem.name,
conflictCount.toString(),
),
context.conflictingTasks,
);
for (const task of context.conflictingTasks) {
ext.outputChannel.appendLog(` • ${task.taskName} (${task.taskType})`);
}
ext.outputChannel.appendLog(l10n.t('Please stop these tasks first before proceeding.'));
ext.outputChannel.show();

// Return option for user - can only cancel
return [
Expand Down
Loading