diff --git a/packages/b2c-vs-extension/.vscodeignore b/packages/b2c-vs-extension/.vscodeignore index 14a55be7..70e6d349 100644 --- a/packages/b2c-vs-extension/.vscodeignore +++ b/packages/b2c-vs-extension/.vscodeignore @@ -5,6 +5,7 @@ src/** !src/webdav.html !src/storefront-next-cartridge.html !src/scapi-explorer.html +!src/ods-management.html out/** **/node_modules/** diff --git a/packages/b2c-vs-extension/package.json b/packages/b2c-vs-extension/package.json index 1c865a96..dabf26e8 100644 --- a/packages/b2c-vs-extension/package.json +++ b/packages/b2c-vs-extension/package.json @@ -24,7 +24,8 @@ "onCommand:b2c-dx.handleStorefrontNextCartridge", "onCommand:b2c-dx.promptAgent", "onCommand:b2c-dx.listWebDav", - "onCommand:b2c-dx.scapiExplorer" + "onCommand:b2c-dx.scapiExplorer", + "onCommand:b2c-dx.odsManagement" ], "main": "./dist/extension.js", "contributes": { @@ -53,6 +54,11 @@ "command": "b2c-dx.scapiExplorer", "title": "SCAPI API Explorer", "category": "B2C DX" + }, + { + "command": "b2c-dx.odsManagement", + "title": "On Demand Sandbox (ods) Management", + "category": "B2C DX" } ] }, diff --git a/packages/b2c-vs-extension/src/extension.ts b/packages/b2c-vs-extension/src/extension.ts index bd7204f5..05d210d3 100644 --- a/packages/b2c-vs-extension/src/extension.ts +++ b/packages/b2c-vs-extension/src/extension.ts @@ -4,7 +4,7 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ import {createSlasClient, getApiErrorMessage} from '@salesforce/b2c-tooling-sdk'; -import {createScapiSchemasClient, toOrganizationId} from '@salesforce/b2c-tooling-sdk/clients'; +import {createOdsClient, createScapiSchemasClient, toOrganizationId} from '@salesforce/b2c-tooling-sdk/clients'; import {findDwJson, resolveConfig} from '@salesforce/b2c-tooling-sdk/config'; import {configureLogger} from '@salesforce/b2c-tooling-sdk/logging'; import {findAndDeployCartridges, getActiveCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code'; @@ -48,6 +48,11 @@ function getScapiExplorerWebviewContent( return html; } +function getOdsManagementWebviewContent(context: vscode.ExtensionContext): string { + const htmlPath = path.join(context.extensionPath, 'src', 'ods-management.html'); + return fs.readFileSync(htmlPath, 'utf-8'); +} + const WEBDAV_ROOT_LABELS: Record = { impex: 'Impex directory (default)', temp: 'Temporary files', @@ -172,6 +177,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('b2c-dx.promptAgent', showActivationError), vscode.commands.registerCommand('b2c-dx.listWebDav', showActivationError), vscode.commands.registerCommand('b2c-dx.scapiExplorer', showActivationError), + vscode.commands.registerCommand('b2c-dx.odsManagement', showActivationError), ); } } @@ -653,9 +659,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann const tenantId = (msg.tenantId ?? '').trim(); const apiFamily = (msg.apiFamily ?? '').trim(); const apiName = (msg.apiName ?? '').trim(); - log.appendLine( - `[SCAPI] Fetch schema paths: tenantId=${tenantId} apiFamily=${apiFamily} apiName=${apiName}`, - ); + log.appendLine(`[SCAPI] Fetch schema paths: tenantId=${tenantId} apiFamily=${apiFamily} apiName=${apiName}`); if (!tenantId || !apiFamily || !apiName) { log.appendLine('[SCAPI] Fetch paths failed: Tenant Id, API Family, and API Name are required.'); panel.webview.postMessage({ @@ -727,14 +731,14 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann log.appendLine( `[SCAPI] Normalized paths (${paths.length}): ${JSON.stringify(paths.slice(0, 10))}${paths.length > 10 ? '...' : ''}`, ); - const schemaInfo = data && typeof data === 'object' && 'info' in data ? (data as {info?: Record}).info : undefined; - const apiTypeRaw = - schemaInfo?.['x-api-type'] ?? schemaInfo?.['x-apiType'] ?? schemaInfo?.['x_api_type']; + const schemaInfo = + data && typeof data === 'object' && 'info' in data + ? (data as {info?: Record}).info + : undefined; + const apiTypeRaw = schemaInfo?.['x-api-type'] ?? schemaInfo?.['x-apiType'] ?? schemaInfo?.['x_api_type']; const apiType = typeof apiTypeRaw === 'string' ? apiTypeRaw : undefined; if (schemaInfo && !apiType) { - log.appendLine( - `[SCAPI] Schema info keys (no x-api-type): ${Object.keys(schemaInfo).join(', ')}`, - ); + log.appendLine(`[SCAPI] Schema info keys (no x-api-type): ${Object.keys(schemaInfo).join(', ')}`); } else if (apiType) { log.appendLine(`[SCAPI] API type: ${apiType}`); } @@ -1070,6 +1074,148 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann ); }); + const DEFAULT_ODS_HOST = 'admin.dx.commercecloud.salesforce.com'; + + const odsManagementDisposable = vscode.commands.registerCommand('b2c-dx.odsManagement', () => { + const panel = vscode.window.createWebviewPanel( + 'b2c-dx-ods-management', + 'On Demand Sandbox (ODS) Management', + vscode.ViewColumn.One, + {enableScripts: true}, + ); + panel.webview.html = getOdsManagementWebviewContent(context); + + async function getOdsConfig() { + const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; + const dwPath = findDwJson(startDir); + return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + } + + function realmFromHostname(hostname: string | undefined): string { + if (!hostname || typeof hostname !== 'string') return ''; + const firstSegment = hostname.split('.')[0] ?? ''; + return firstSegment.split('-')[0] ?? ''; + } + + async function fetchSandboxList(): Promise<{sandboxes: unknown[]; error?: string}> { + try { + const config = await getOdsConfig(); + if (!config.hasOAuthConfig()) { + return {sandboxes: [], error: 'OAuth credentials required. Set clientId and clientSecret in dw.json.'}; + } + const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST; + const authStrategy = config.createOAuth(); + const odsClient = createOdsClient({host}, authStrategy); + const result = await odsClient.GET('/sandboxes', { + params: {query: {include_deleted: false}}, + }); + if (result.error) { + return { + sandboxes: [], + error: getApiErrorMessage(result.error, result.response), + }; + } + const sandboxes = result.data?.data ?? []; + return {sandboxes: sandboxes as unknown[]}; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return {sandboxes: [], error: message}; + } + } + + panel.webview.onDidReceiveMessage(async (msg: {type: string; sandboxId?: string}) => { + if (msg.type === 'odsListRequest') { + const {sandboxes, error} = await fetchSandboxList(); + panel.webview.postMessage({type: 'odsListResult', sandboxes, error}); + return; + } + if (msg.type === 'odsDeleteClick' && msg.sandboxId) { + try { + const config = await getOdsConfig(); + if (!config.hasOAuthConfig()) { + vscode.window.showErrorMessage('B2C DX: OAuth credentials required for ODS. Configure dw.json.'); + return; + } + const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST; + const authStrategy = config.createOAuth(); + const odsClient = createOdsClient({host}, authStrategy); + const deleteResult = await odsClient.DELETE('/sandboxes/{sandboxId}', { + params: {path: {sandboxId: msg.sandboxId}}, + }); + if (deleteResult.error) { + vscode.window.showErrorMessage( + `B2C DX: Delete sandbox failed. ${getApiErrorMessage(deleteResult.error, deleteResult.response)}`, + ); + return; + } + vscode.window.showInformationMessage('B2C DX: Sandbox deleted.'); + const {sandboxes, error} = await fetchSandboxList(); + panel.webview.postMessage({type: 'odsListResult', sandboxes, error}); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`B2C DX: ${message}`); + } + return; + } + if (msg.type === 'odsCreateClick') { + try { + const config = await getOdsConfig(); + if (!config.hasOAuthConfig()) { + vscode.window.showErrorMessage('B2C DX: OAuth credentials required for ODS. Configure dw.json.'); + return; + } + const hostname = config.values.hostname; + const defaultRealm = realmFromHostname(hostname as string | undefined); + + const realm = await vscode.window.showInputBox({ + title: 'Create ODS Sandbox', + prompt: 'Realm (four-letter ID)', + value: defaultRealm, + placeHolder: 'e.g. zyoc', + }); + if (realm === undefined) return; + + const ttlStr = await vscode.window.showInputBox({ + title: 'Create ODS Sandbox', + prompt: 'TTL (hours). Sandbox lifetime in hours.', + value: '480', + placeHolder: '480', + }); + if (ttlStr === undefined) return; + + const ttl = parseInt(ttlStr.trim(), 10); + if (Number.isNaN(ttl) || ttl < 0) { + vscode.window.showErrorMessage('B2C DX: TTL must be a non-negative number.'); + return; + } + + const host = config.values.sandboxApiHost ?? DEFAULT_ODS_HOST; + const authStrategy = config.createOAuth(); + const odsClient = createOdsClient({host}, authStrategy); + const createResult = await odsClient.POST('/sandboxes', { + body: { + realm: realm.trim(), + ttl: ttl === 0 ? undefined : ttl, + analyticsEnabled: false, + }, + }); + if (createResult.error) { + vscode.window.showErrorMessage( + `B2C DX: Create sandbox failed. ${getApiErrorMessage(createResult.error, createResult.response)}`, + ); + return; + } + vscode.window.showInformationMessage('B2C DX: Sandbox creation started.'); + const {sandboxes, error} = await fetchSandboxList(); + panel.webview.postMessage({type: 'odsListResult', sandboxes, error}); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`B2C DX: ${message}`); + } + } + }); + }); + const storefrontNextCartridgeDisposable = vscode.commands.registerCommand( 'b2c-dx.handleStorefrontNextCartridge', () => { @@ -1174,6 +1320,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann promptAgentDisposable, listWebDavDisposable, scapiExplorerDisposable, + odsManagementDisposable, storefrontNextCartridgeDisposable, ); log.appendLine('B2C DX extension activated.'); diff --git a/packages/b2c-vs-extension/src/ods-management.html b/packages/b2c-vs-extension/src/ods-management.html new file mode 100644 index 00000000..f5eee18c --- /dev/null +++ b/packages/b2c-vs-extension/src/ods-management.html @@ -0,0 +1,217 @@ + + + + + + ODS Management + + + +

On Demand Sandbox (ODS) Management

+
+ +
+
Loading sandboxes…
+ + + + + diff --git a/packages/b2c-vs-extension/src/scapi-explorer.html b/packages/b2c-vs-extension/src/scapi-explorer.html index 389012aa..afa42f22 100644 --- a/packages/b2c-vs-extension/src/scapi-explorer.html +++ b/packages/b2c-vs-extension/src/scapi-explorer.html @@ -79,6 +79,15 @@ .create-slas-form { flex: 0 0 auto; } + .create-slas-form-inline { + display: flex; + gap: 0.75rem; + align-items: flex-end; + flex-wrap: wrap; + } + .create-slas-form-inline .form-row { + margin-bottom: 0; + } .create-slas-result { flex: 1 1 auto; min-width: 0; @@ -174,7 +183,9 @@ gap: 0.5rem; margin-bottom: 0.5rem; } - .token-heading-row h3 { margin: 0; } + .token-heading-row h3 { + margin: 0; + } .copy-icon-btn, .copy-token-btn { display: inline-flex; @@ -190,7 +201,6 @@ cursor: pointer; flex-shrink: 0; } - .copy-icon-btn:hover, .copy-token-btn:hover { background: var(--vscode-toolbar-hoverBackground); } @@ -340,20 +350,27 @@

SCAPI API Explorer

-
- - -
-
- - +
+
+ + +
+
+ + +
+
-
@@ -385,7 +405,10 @@

Generate Bearer Token

@@ -394,7 +417,10 @@

Generate Bearer Token

@@ -450,7 +476,10 @@

SCAPI Shop API Call

Generated curl

@@ -729,7 +761,9 @@

Error

.then(function () { var t = btn.getAttribute('title') || fallbackTitle; btn.setAttribute('title', 'Copied!'); - setTimeout(function () { btn.setAttribute('title', t); }, 2000); + setTimeout(function () { + btn.setAttribute('title', t); + }, 2000); }) .catch(function () {}); }