Skip to content

Commit de6b1ac

Browse files
committed
adding save query and multi realm support
1 parent a019765 commit de6b1ac

9 files changed

Lines changed: 1201 additions & 70 deletions

File tree

packages/b2c-vs-extension/package.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@
272272
"icon": "$(refresh)",
273273
"category": "B2C-DX Analytics"
274274
},
275+
{
276+
"command": "b2c-dx.cipAnalytics.resetFromDwJson",
277+
"title": "Reset from dw.json…",
278+
"icon": "$(discard)",
279+
"category": "B2C-DX Analytics"
280+
},
275281
{
276282
"command": "b2c-dx.cipAnalytics.openReport",
277283
"title": "Open Report",
@@ -307,6 +313,12 @@
307313
"icon": "$(server)",
308314
"category": "B2C-DX Analytics"
309315
},
316+
{
317+
"command": "b2c-dx.cipAnalytics.switchConnection",
318+
"title": "Switch Connection…",
319+
"icon": "$(plug)",
320+
"category": "B2C-DX Analytics"
321+
},
310322
{
311323
"command": "b2c-dx.cipAnalytics.addRealm",
312324
"title": "Add Realm…",
@@ -694,7 +706,7 @@
694706
"group": "navigation"
695707
},
696708
{
697-
"command": "b2c-dx.cipAnalytics.switchRealm",
709+
"command": "b2c-dx.cipAnalytics.removeRealm",
698710
"when": "view == b2cCipAnalytics",
699711
"group": "navigation@0"
700712
},
@@ -708,6 +720,11 @@
708720
"when": "view == b2cCipAnalytics",
709721
"group": "navigation@4"
710722
},
723+
{
724+
"command": "b2c-dx.cipAnalytics.resetFromDwJson",
725+
"when": "view == b2cCipAnalytics",
726+
"group": "navigation@9"
727+
},
711728
{
712729
"command": "b2c-dx.codeSync.deploy",
713730
"when": "view == b2cCartridgeExplorer",
@@ -730,6 +747,11 @@
730747
}
731748
],
732749
"view/item/context": [
750+
{
751+
"command": "b2c-dx.cipAnalytics.switchConnection",
752+
"when": "view == b2cCipAnalytics && viewItem == cipRealm",
753+
"group": "inline@0"
754+
},
733755
{
734756
"command": "b2c-dx.cipAnalytics.removeRealm",
735757
"when": "view == b2cCipAnalytics && viewItem == cipRealm",

packages/b2c-vs-extension/src/cip-analytics/cip-connection-service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,36 @@ export class CipConnectionService implements vscode.Disposable {
208208
return realm.id;
209209
}
210210

211+
/** Remove a realm group and all connections within it. */
212+
async removeRealmGroup(groupId: string): Promise<void> {
213+
const exists = this.groups.some((g) => g.id === groupId);
214+
if (!exists) return;
215+
216+
const removedRealmIds = new Set(this.realms.filter((r) => r.groupId === groupId).map((r) => r.id));
217+
218+
this.groups = this.groups.filter((g) => g.id !== groupId);
219+
this.realms = this.realms.filter((r) => r.groupId !== groupId);
220+
for (const realmId of removedRealmIds) {
221+
this.realmStatusMap.delete(realmId);
222+
}
223+
224+
await this.workspaceState.update(GROUPS_KEY, this.groups);
225+
await this.persistRealms();
226+
227+
if (this.connection.groupId === groupId || removedRealmIds.has(this.connection.id)) {
228+
const fallback = this.realms[0];
229+
if (fallback) {
230+
this.connection = {...fallback, status: 'disconnected', message: undefined};
231+
await this.workspaceState.update(ACTIVE_REALM_KEY, fallback.id);
232+
} else {
233+
this.connection = makeBlankConnection();
234+
await this.workspaceState.update(ACTIVE_REALM_KEY, undefined);
235+
}
236+
}
237+
238+
this._onDidChange.fire(this.get());
239+
}
240+
211241
/** Remove a saved realm by id. If it was active, falls back to the first remaining realm. */
212242
async removeRealm(id: string): Promise<void> {
213243
this.realms = this.realms.filter((r) => r.id !== id);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import {randomUUID} from 'node:crypto';
6+
import * as vscode from 'vscode';
7+
8+
/** A single query the user has saved for reuse from the Query Builder. */
9+
export interface CipSavedQuery {
10+
id: string;
11+
name: string;
12+
sql: string;
13+
description?: string;
14+
/** Tenant the query was authored against. Used to scope visibility per realm. */
15+
tenantId: string;
16+
createdAt: number;
17+
updatedAt: number;
18+
}
19+
20+
/** Persistence key inside `vscode.Memento` (workspaceState). */
21+
const STORE_KEY = 'b2c-dx.cipAnalytics.savedQueries';
22+
23+
/**
24+
* Workspace-scoped saved-query store for the Query Builder. Mirrors the shape of
25+
* {@link CipConnectionService}: in-memory cache + persisted Memento + onDidChange event.
26+
*
27+
* Queries carry the tenant they were authored against so the UI can foreground
28+
* the active tenant's queries and dim cross-tenant ones — handy when the same
29+
* workspace switches between e.g. `zzat_prd` and `bjmp_prd`.
30+
*/
31+
export class CipQueryLibraryService implements vscode.Disposable {
32+
private queries: CipSavedQuery[];
33+
private readonly _onDidChange = new vscode.EventEmitter<CipSavedQuery[]>();
34+
readonly onDidChange = this._onDidChange.event;
35+
36+
constructor(private readonly workspaceState: vscode.Memento) {
37+
const stored = this.workspaceState.get<CipSavedQuery[]>(STORE_KEY);
38+
this.queries = Array.isArray(stored) ? stored.filter(this.isValid) : [];
39+
}
40+
41+
/** All saved queries across all tenants, newest-updated first. */
42+
list(): CipSavedQuery[] {
43+
return [...this.queries].sort((a, b) => b.updatedAt - a.updatedAt);
44+
}
45+
46+
/** Saved queries scoped to a tenant, newest-updated first. */
47+
listForTenant(tenantId: string): CipSavedQuery[] {
48+
return this.list().filter((q) => q.tenantId === tenantId);
49+
}
50+
51+
get(id: string): CipSavedQuery | undefined {
52+
return this.queries.find((q) => q.id === id);
53+
}
54+
55+
async save(input: {name: string; sql: string; description?: string; tenantId: string}): Promise<CipSavedQuery> {
56+
const now = Date.now();
57+
const entry: CipSavedQuery = {
58+
id: randomUUID(),
59+
name: input.name.trim(),
60+
sql: input.sql,
61+
description: input.description?.trim() || undefined,
62+
tenantId: input.tenantId,
63+
createdAt: now,
64+
updatedAt: now,
65+
};
66+
this.queries = [entry, ...this.queries];
67+
await this.persist();
68+
return entry;
69+
}
70+
71+
/** Update name / description / sql on an existing entry. Bumps `updatedAt`. */
72+
async update(
73+
id: string,
74+
patch: Partial<Pick<CipSavedQuery, 'name' | 'sql' | 'description'>>,
75+
): Promise<CipSavedQuery | undefined> {
76+
const idx = this.queries.findIndex((q) => q.id === id);
77+
if (idx < 0) return undefined;
78+
const prev = this.queries[idx];
79+
const next: CipSavedQuery = {
80+
...prev,
81+
...(patch.name !== undefined ? {name: patch.name.trim()} : {}),
82+
...(patch.sql !== undefined ? {sql: patch.sql} : {}),
83+
...(patch.description !== undefined ? {description: patch.description.trim() || undefined} : {}),
84+
updatedAt: Date.now(),
85+
};
86+
this.queries = [...this.queries.slice(0, idx), next, ...this.queries.slice(idx + 1)];
87+
await this.persist();
88+
return next;
89+
}
90+
91+
async delete(id: string): Promise<void> {
92+
const before = this.queries.length;
93+
this.queries = this.queries.filter((q) => q.id !== id);
94+
if (this.queries.length !== before) {
95+
await this.persist();
96+
}
97+
}
98+
99+
dispose(): void {
100+
this._onDidChange.dispose();
101+
}
102+
103+
private async persist(): Promise<void> {
104+
await this.workspaceState.update(STORE_KEY, this.queries);
105+
this._onDidChange.fire(this.list());
106+
}
107+
108+
/** Defensive validator — discards malformed entries from older versions of the schema. */
109+
private isValid = (q: unknown): q is CipSavedQuery => {
110+
if (!q || typeof q !== 'object') return false;
111+
const v = q as Record<string, unknown>;
112+
return (
113+
typeof v.id === 'string' &&
114+
typeof v.name === 'string' &&
115+
typeof v.sql === 'string' &&
116+
typeof v.tenantId === 'string' &&
117+
typeof v.createdAt === 'number' &&
118+
typeof v.updatedAt === 'number'
119+
);
120+
};
121+
}

0 commit comments

Comments
 (0)