Skip to content

Commit dd792bc

Browse files
authored
@W-21111039 - ODS Management Support (#159)
* support ODS management
1 parent ae17d7e commit dd792bc

File tree

5 files changed

+434
-29
lines changed

5 files changed

+434
-29
lines changed

packages/b2c-vs-extension/.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ src/**
55
!src/webdav.html
66
!src/storefront-next-cartridge.html
77
!src/scapi-explorer.html
8+
!src/ods-management.html
89
out/**
910
**/node_modules/**
1011

packages/b2c-vs-extension/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"onCommand:b2c-dx.handleStorefrontNextCartridge",
2525
"onCommand:b2c-dx.promptAgent",
2626
"onCommand:b2c-dx.listWebDav",
27-
"onCommand:b2c-dx.scapiExplorer"
27+
"onCommand:b2c-dx.scapiExplorer",
28+
"onCommand:b2c-dx.odsManagement"
2829
],
2930
"main": "./dist/extension.js",
3031
"contributes": {
@@ -53,6 +54,11 @@
5354
"command": "b2c-dx.scapiExplorer",
5455
"title": "SCAPI API Explorer",
5556
"category": "B2C DX"
57+
},
58+
{
59+
"command": "b2c-dx.odsManagement",
60+
"title": "On Demand Sandbox (ods) Management",
61+
"category": "B2C DX"
5662
}
5763
]
5864
},

packages/b2c-vs-extension/src/extension.ts

Lines changed: 157 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
66
import {createSlasClient, getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
7-
import {createScapiSchemasClient, toOrganizationId} from '@salesforce/b2c-tooling-sdk/clients';
7+
import {createOdsClient, createScapiSchemasClient, toOrganizationId} from '@salesforce/b2c-tooling-sdk/clients';
88
import {findDwJson, resolveConfig} from '@salesforce/b2c-tooling-sdk/config';
99
import {configureLogger} from '@salesforce/b2c-tooling-sdk/logging';
1010
import {findAndDeployCartridges, getActiveCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code';
@@ -48,6 +48,11 @@ function getScapiExplorerWebviewContent(
4848
return html;
4949
}
5050

51+
function getOdsManagementWebviewContent(context: vscode.ExtensionContext): string {
52+
const htmlPath = path.join(context.extensionPath, 'src', 'ods-management.html');
53+
return fs.readFileSync(htmlPath, 'utf-8');
54+
}
55+
5156
const WEBDAV_ROOT_LABELS: Record<string, string> = {
5257
impex: 'Impex directory (default)',
5358
temp: 'Temporary files',
@@ -172,6 +177,7 @@ export function activate(context: vscode.ExtensionContext) {
172177
vscode.commands.registerCommand('b2c-dx.promptAgent', showActivationError),
173178
vscode.commands.registerCommand('b2c-dx.listWebDav', showActivationError),
174179
vscode.commands.registerCommand('b2c-dx.scapiExplorer', showActivationError),
180+
vscode.commands.registerCommand('b2c-dx.odsManagement', showActivationError),
175181
);
176182
}
177183
}
@@ -653,9 +659,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann
653659
const tenantId = (msg.tenantId ?? '').trim();
654660
const apiFamily = (msg.apiFamily ?? '').trim();
655661
const apiName = (msg.apiName ?? '').trim();
656-
log.appendLine(
657-
`[SCAPI] Fetch schema paths: tenantId=${tenantId} apiFamily=${apiFamily} apiName=${apiName}`,
658-
);
662+
log.appendLine(`[SCAPI] Fetch schema paths: tenantId=${tenantId} apiFamily=${apiFamily} apiName=${apiName}`);
659663
if (!tenantId || !apiFamily || !apiName) {
660664
log.appendLine('[SCAPI] Fetch paths failed: Tenant Id, API Family, and API Name are required.');
661665
panel.webview.postMessage({
@@ -727,14 +731,14 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann
727731
log.appendLine(
728732
`[SCAPI] Normalized paths (${paths.length}): ${JSON.stringify(paths.slice(0, 10))}${paths.length > 10 ? '...' : ''}`,
729733
);
730-
const schemaInfo = data && typeof data === 'object' && 'info' in data ? (data as {info?: Record<string, unknown>}).info : undefined;
731-
const apiTypeRaw =
732-
schemaInfo?.['x-api-type'] ?? schemaInfo?.['x-apiType'] ?? schemaInfo?.['x_api_type'];
734+
const schemaInfo =
735+
data && typeof data === 'object' && 'info' in data
736+
? (data as {info?: Record<string, unknown>}).info
737+
: undefined;
738+
const apiTypeRaw = schemaInfo?.['x-api-type'] ?? schemaInfo?.['x-apiType'] ?? schemaInfo?.['x_api_type'];
733739
const apiType = typeof apiTypeRaw === 'string' ? apiTypeRaw : undefined;
734740
if (schemaInfo && !apiType) {
735-
log.appendLine(
736-
`[SCAPI] Schema info keys (no x-api-type): ${Object.keys(schemaInfo).join(', ')}`,
737-
);
741+
log.appendLine(`[SCAPI] Schema info keys (no x-api-type): ${Object.keys(schemaInfo).join(', ')}`);
738742
} else if (apiType) {
739743
log.appendLine(`[SCAPI] API type: ${apiType}`);
740744
}
@@ -1070,6 +1074,148 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann
10701074
);
10711075
});
10721076

1077+
const DEFAULT_ODS_HOST = 'admin.dx.commercecloud.salesforce.com';
1078+
1079+
const odsManagementDisposable = vscode.commands.registerCommand('b2c-dx.odsManagement', () => {
1080+
const panel = vscode.window.createWebviewPanel(
1081+
'b2c-dx-ods-management',
1082+
'On Demand Sandbox (ODS) Management',
1083+
vscode.ViewColumn.One,
1084+
{enableScripts: true},
1085+
);
1086+
panel.webview.html = getOdsManagementWebviewContent(context);
1087+
1088+
async function getOdsConfig() {
1089+
const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath;
1090+
const dwPath = findDwJson(startDir);
1091+
return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir});
1092+
}
1093+
1094+
function realmFromHostname(hostname: string | undefined): string {
1095+
if (!hostname || typeof hostname !== 'string') return '';
1096+
const firstSegment = hostname.split('.')[0] ?? '';
1097+
return firstSegment.split('-')[0] ?? '';
1098+
}
1099+
1100+
async function fetchSandboxList(): Promise<{sandboxes: unknown[]; error?: string}> {
1101+
try {
1102+
const config = await getOdsConfig();
1103+
if (!config.hasOAuthConfig()) {
1104+
return {sandboxes: [], error: 'OAuth credentials required. Set clientId and clientSecret in dw.json.'};
1105+
}
1106+
const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST;
1107+
const authStrategy = config.createOAuth();
1108+
const odsClient = createOdsClient({host}, authStrategy);
1109+
const result = await odsClient.GET('/sandboxes', {
1110+
params: {query: {include_deleted: false}},
1111+
});
1112+
if (result.error) {
1113+
return {
1114+
sandboxes: [],
1115+
error: getApiErrorMessage(result.error, result.response),
1116+
};
1117+
}
1118+
const sandboxes = result.data?.data ?? [];
1119+
return {sandboxes: sandboxes as unknown[]};
1120+
} catch (err) {
1121+
const message = err instanceof Error ? err.message : String(err);
1122+
return {sandboxes: [], error: message};
1123+
}
1124+
}
1125+
1126+
panel.webview.onDidReceiveMessage(async (msg: {type: string; sandboxId?: string}) => {
1127+
if (msg.type === 'odsListRequest') {
1128+
const {sandboxes, error} = await fetchSandboxList();
1129+
panel.webview.postMessage({type: 'odsListResult', sandboxes, error});
1130+
return;
1131+
}
1132+
if (msg.type === 'odsDeleteClick' && msg.sandboxId) {
1133+
try {
1134+
const config = await getOdsConfig();
1135+
if (!config.hasOAuthConfig()) {
1136+
vscode.window.showErrorMessage('B2C DX: OAuth credentials required for ODS. Configure dw.json.');
1137+
return;
1138+
}
1139+
const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST;
1140+
const authStrategy = config.createOAuth();
1141+
const odsClient = createOdsClient({host}, authStrategy);
1142+
const deleteResult = await odsClient.DELETE('/sandboxes/{sandboxId}', {
1143+
params: {path: {sandboxId: msg.sandboxId}},
1144+
});
1145+
if (deleteResult.error) {
1146+
vscode.window.showErrorMessage(
1147+
`B2C DX: Delete sandbox failed. ${getApiErrorMessage(deleteResult.error, deleteResult.response)}`,
1148+
);
1149+
return;
1150+
}
1151+
vscode.window.showInformationMessage('B2C DX: Sandbox deleted.');
1152+
const {sandboxes, error} = await fetchSandboxList();
1153+
panel.webview.postMessage({type: 'odsListResult', sandboxes, error});
1154+
} catch (err) {
1155+
const message = err instanceof Error ? err.message : String(err);
1156+
vscode.window.showErrorMessage(`B2C DX: ${message}`);
1157+
}
1158+
return;
1159+
}
1160+
if (msg.type === 'odsCreateClick') {
1161+
try {
1162+
const config = await getOdsConfig();
1163+
if (!config.hasOAuthConfig()) {
1164+
vscode.window.showErrorMessage('B2C DX: OAuth credentials required for ODS. Configure dw.json.');
1165+
return;
1166+
}
1167+
const hostname = config.values.hostname;
1168+
const defaultRealm = realmFromHostname(hostname as string | undefined);
1169+
1170+
const realm = await vscode.window.showInputBox({
1171+
title: 'Create ODS Sandbox',
1172+
prompt: 'Realm (four-letter ID)',
1173+
value: defaultRealm,
1174+
placeHolder: 'e.g. zyoc',
1175+
});
1176+
if (realm === undefined) return;
1177+
1178+
const ttlStr = await vscode.window.showInputBox({
1179+
title: 'Create ODS Sandbox',
1180+
prompt: 'TTL (hours). Sandbox lifetime in hours.',
1181+
value: '480',
1182+
placeHolder: '480',
1183+
});
1184+
if (ttlStr === undefined) return;
1185+
1186+
const ttl = parseInt(ttlStr.trim(), 10);
1187+
if (Number.isNaN(ttl) || ttl < 0) {
1188+
vscode.window.showErrorMessage('B2C DX: TTL must be a non-negative number.');
1189+
return;
1190+
}
1191+
1192+
const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST;
1193+
const authStrategy = config.createOAuth();
1194+
const odsClient = createOdsClient({host}, authStrategy);
1195+
const createResult = await odsClient.POST('/sandboxes', {
1196+
body: {
1197+
realm: realm.trim(),
1198+
ttl: ttl === 0 ? undefined : ttl,
1199+
analyticsEnabled: false,
1200+
},
1201+
});
1202+
if (createResult.error) {
1203+
vscode.window.showErrorMessage(
1204+
`B2C DX: Create sandbox failed. ${getApiErrorMessage(createResult.error, createResult.response)}`,
1205+
);
1206+
return;
1207+
}
1208+
vscode.window.showInformationMessage('B2C DX: Sandbox creation started.');
1209+
const {sandboxes, error} = await fetchSandboxList();
1210+
panel.webview.postMessage({type: 'odsListResult', sandboxes, error});
1211+
} catch (err) {
1212+
const message = err instanceof Error ? err.message : String(err);
1213+
vscode.window.showErrorMessage(`B2C DX: ${message}`);
1214+
}
1215+
}
1216+
});
1217+
});
1218+
10731219
const storefrontNextCartridgeDisposable = vscode.commands.registerCommand(
10741220
'b2c-dx.handleStorefrontNextCartridge',
10751221
() => {
@@ -1174,6 +1320,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann
11741320
promptAgentDisposable,
11751321
listWebDavDisposable,
11761322
scapiExplorerDisposable,
1323+
odsManagementDisposable,
11771324
storefrontNextCartridgeDisposable,
11781325
);
11791326
log.appendLine('B2C DX extension activated.');

0 commit comments

Comments
 (0)