|
4 | 4 | * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 |
5 | 5 | */ |
6 | 6 | 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'; |
8 | 8 | import {findDwJson, resolveConfig} from '@salesforce/b2c-tooling-sdk/config'; |
9 | 9 | import {configureLogger} from '@salesforce/b2c-tooling-sdk/logging'; |
10 | 10 | import {findAndDeployCartridges, getActiveCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code'; |
@@ -48,6 +48,11 @@ function getScapiExplorerWebviewContent( |
48 | 48 | return html; |
49 | 49 | } |
50 | 50 |
|
| 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 | + |
51 | 56 | const WEBDAV_ROOT_LABELS: Record<string, string> = { |
52 | 57 | impex: 'Impex directory (default)', |
53 | 58 | temp: 'Temporary files', |
@@ -172,6 +177,7 @@ export function activate(context: vscode.ExtensionContext) { |
172 | 177 | vscode.commands.registerCommand('b2c-dx.promptAgent', showActivationError), |
173 | 178 | vscode.commands.registerCommand('b2c-dx.listWebDav', showActivationError), |
174 | 179 | vscode.commands.registerCommand('b2c-dx.scapiExplorer', showActivationError), |
| 180 | + vscode.commands.registerCommand('b2c-dx.odsManagement', showActivationError), |
175 | 181 | ); |
176 | 182 | } |
177 | 183 | } |
@@ -653,9 +659,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann |
653 | 659 | const tenantId = (msg.tenantId ?? '').trim(); |
654 | 660 | const apiFamily = (msg.apiFamily ?? '').trim(); |
655 | 661 | 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}`); |
659 | 663 | if (!tenantId || !apiFamily || !apiName) { |
660 | 664 | log.appendLine('[SCAPI] Fetch paths failed: Tenant Id, API Family, and API Name are required.'); |
661 | 665 | panel.webview.postMessage({ |
@@ -727,14 +731,14 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann |
727 | 731 | log.appendLine( |
728 | 732 | `[SCAPI] Normalized paths (${paths.length}): ${JSON.stringify(paths.slice(0, 10))}${paths.length > 10 ? '...' : ''}`, |
729 | 733 | ); |
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']; |
733 | 739 | const apiType = typeof apiTypeRaw === 'string' ? apiTypeRaw : undefined; |
734 | 740 | 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(', ')}`); |
738 | 742 | } else if (apiType) { |
739 | 743 | log.appendLine(`[SCAPI] API type: ${apiType}`); |
740 | 744 | } |
@@ -1070,6 +1074,148 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann |
1070 | 1074 | ); |
1071 | 1075 | }); |
1072 | 1076 |
|
| 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 | + |
1073 | 1219 | const storefrontNextCartridgeDisposable = vscode.commands.registerCommand( |
1074 | 1220 | 'b2c-dx.handleStorefrontNextCartridge', |
1075 | 1221 | () => { |
@@ -1174,6 +1320,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann |
1174 | 1320 | promptAgentDisposable, |
1175 | 1321 | listWebDavDisposable, |
1176 | 1322 | scapiExplorerDisposable, |
| 1323 | + odsManagementDisposable, |
1177 | 1324 | storefrontNextCartridgeDisposable, |
1178 | 1325 | ); |
1179 | 1326 | log.appendLine('B2C DX extension activated.'); |
|
0 commit comments