Skip to content

Commit c0854a3

Browse files
authored
[Application QuickStart] [Backend] Adding support for opening Swagger (REST) and Nitro (GraphQL) endpoints for DAB directly in VS Code (#21495)
* Add Swagger UI action to DAB deployment complete screen * Adding Nitro support for GraphQl
1 parent e86d4f9 commit c0854a3

9 files changed

Lines changed: 194 additions & 39 deletions

File tree

extensions/mssql/l10n/bundle.l10n.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,8 @@
933933
},
934934
"Add to VS Code": "Add to VS Code",
935935
"Add MCP server to workspace configuration": "Add MCP server to workspace configuration",
936+
"View Swagger": "View Swagger",
937+
"Open Nitro": "Open Nitro",
936938
"Checking Docker installation": "Checking Docker installation",
937939
"Verifying Docker is installed on your system": "Verifying Docker is installed on your system",
938940
"Starting Docker Desktop": "Starting Docker Desktop",
@@ -2500,6 +2502,7 @@
25002502
"No workspace folder is open. Open a folder to add the MCP server configuration.": "No workspace folder is open. Open a folder to add the MCP server configuration.",
25012503
"Config copied to clipboard": "Config copied to clipboard",
25022504
"URL copied to clipboard": "URL copied to clipboard",
2505+
"Failed to open URL. The built-in Simple Browser may be disabled.": "Failed to open URL. The built-in Simple Browser may be disabled.",
25032506
"Connect to MSSQL": "Connect to MSSQL",
25042507
"Click to connect to a database": "Click to connect to a database",
25052508
"Connecting": "Connecting",

extensions/mssql/src/constants/locConstants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,9 @@ export class SchemaDesigner {
17241724
);
17251725
public static configCopiedToClipboard = l10n.t("Config copied to clipboard");
17261726
public static urlCopiedToClipboard = l10n.t("URL copied to clipboard");
1727+
public static failedToOpenUrl = l10n.t(
1728+
"Failed to open URL. The built-in Simple Browser may be disabled.",
1729+
);
17271730
}
17281731

17291732
export class StatusBar {

extensions/mssql/src/reactviews/common/locConstants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,8 @@ export class LocConstants {
13881388
addToVSCode: l10n.t("Add to VS Code"),
13891389
addMcpServerToWorkspace: l10n.t("Add MCP server to workspace configuration"),
13901390
mcpServerAdded: l10n.t("Added"),
1391+
viewSwagger: l10n.t("View Swagger"),
1392+
openNitro: l10n.t("Open Nitro"),
13911393

13921394
// DAB Deployment Steps
13931395
checkingDockerInstallation: l10n.t("Checking Docker installation"),

extensions/mssql/src/reactviews/pages/SchemaDesigner/dab/dabContext.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SchemaDesignerContext } from "../schemaDesignerStateProvider";
1212
interface DabContextProps {
1313
isInitialized: boolean;
1414
copyToClipboard: (text: string, copyTextType: Dab.CopyTextType) => void;
15+
openUrl: (url: string) => void;
1516
dabConfig: Dab.DabConfig | null;
1617
initializeDabConfig: () => void;
1718
syncDabConfigWithSchema: () => void;
@@ -203,6 +204,13 @@ export const DabProvider: React.FC<DabProviderProps> = ({ children }) => {
203204
[extensionRpc],
204205
);
205206

207+
const openUrl = useCallback(
208+
(url: string) => {
209+
void extensionRpc.sendNotification(Dab.OpenUrlNotification.type, { url });
210+
},
211+
[extensionRpc],
212+
);
213+
206214
const openDabConfigInEditor = useCallback(
207215
(configContent: string) => {
208216
void extensionRpc.sendNotification(Dab.OpenConfigInEditorNotification.type, {
@@ -368,6 +376,7 @@ export const DabProvider: React.FC<DabProviderProps> = ({ children }) => {
368376
value={{
369377
isInitialized,
370378
copyToClipboard,
379+
openUrl,
371380
dabConfig,
372381
initializeDabConfig,
373382
syncDabConfigWithSchema,

extensions/mssql/src/reactviews/pages/SchemaDesigner/dab/deployment/dabDeploymentComplete.tsx

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Checkmark16Regular,
1818
Checkmark20Regular,
1919
Copy16Regular,
20+
Open16Regular,
2021
Warning20Regular,
2122
} from "@fluentui/react-icons";
2223
import { useCallback, useMemo, useState } from "react";
@@ -95,11 +96,19 @@ interface DabDeploymentCompleteProps {
9596
onFinish: () => void;
9697
}
9798

99+
type ApiEndpointAction = "copy" | "addToVSCode" | "openUrl";
100+
101+
interface ApiEndpointOpenUrlConfig {
102+
url: string;
103+
label: string;
104+
}
105+
98106
interface ApiEndpoint {
99107
type: Dab.ApiType;
100108
label: string;
101109
url: string;
102-
action: "copy" | "addToVSCode";
110+
actions: ApiEndpointAction[];
111+
openUrlConfig?: ApiEndpointOpenUrlConfig;
103112
}
104113

105114
export const DabDeploymentComplete = ({
@@ -109,7 +118,7 @@ export const DabDeploymentComplete = ({
109118
onFinish,
110119
}: DabDeploymentCompleteProps) => {
111120
const classes = useStyles();
112-
const { dabConfig, copyToClipboard, addDabMcpServer } = useDabContext();
121+
const { dabConfig, copyToClipboard, openUrl, addDabMcpServer } = useDabContext();
113122
const isSuccess = !error && apiUrl;
114123
const [mcpAdded, setMcpAdded] = useState(false);
115124
const [mcpError, setMcpError] = useState<string | null>(null);
@@ -125,23 +134,31 @@ export const DabDeploymentComplete = ({
125134
type: Dab.ApiType.Rest,
126135
label: locConstants.schemaDesigner.restApi,
127136
url: `${apiUrl}/api`,
128-
action: "copy",
137+
actions: ["openUrl", "copy"],
138+
openUrlConfig: {
139+
url: `${apiUrl}/swagger/index.html`,
140+
label: locConstants.schemaDesigner.viewSwagger,
141+
},
129142
});
130143
}
131144
if (enabledTypes.includes(Dab.ApiType.GraphQL)) {
132145
result.push({
133146
type: Dab.ApiType.GraphQL,
134147
label: locConstants.schemaDesigner.graphql,
135148
url: `${apiUrl}/graphql`,
136-
action: "copy",
149+
actions: ["openUrl", "copy"],
150+
openUrlConfig: {
151+
url: `${apiUrl}/graphql`,
152+
label: locConstants.schemaDesigner.openNitro,
153+
},
137154
});
138155
}
139156
if (enabledTypes.includes(Dab.ApiType.Mcp)) {
140157
result.push({
141158
type: Dab.ApiType.Mcp,
142159
label: locConstants.schemaDesigner.mcp,
143160
url: `${apiUrl}/mcp`,
144-
action: "addToVSCode",
161+
actions: ["addToVSCode"],
145162
});
146163
}
147164
return result;
@@ -160,41 +177,59 @@ export const DabDeploymentComplete = ({
160177
[addDabMcpServer],
161178
);
162179

163-
const renderEndpointAction = useCallback(
164-
(ep: ApiEndpoint) => {
165-
if (ep.action === "copy") {
166-
return (
167-
<Button
168-
appearance="subtle"
169-
icon={<Copy16Regular />}
170-
size="small"
171-
className={classes.actionButton}
172-
onClick={() => copyToClipboard(ep.url, Dab.CopyTextType.Url)}
173-
aria-label={locConstants.schemaDesigner.copyUrl(ep.label)}
174-
title={locConstants.schemaDesigner.copyUrl(ep.label)}
175-
/>
176-
);
177-
}
178-
if (ep.action === "addToVSCode") {
179-
return (
180-
<Button
181-
appearance="subtle"
182-
icon={mcpAdded ? <Checkmark16Regular /> : <Add16Regular />}
183-
size="small"
184-
className={classes.actionButton}
185-
disabled={mcpAdded}
186-
onClick={() => void handleAddMcpServer(ep.url)}
187-
aria-label={locConstants.schemaDesigner.addMcpServerToWorkspace}
188-
title={locConstants.schemaDesigner.addMcpServerToWorkspace}>
189-
{mcpAdded
190-
? locConstants.schemaDesigner.mcpServerAdded
191-
: locConstants.schemaDesigner.addToVSCode}
192-
</Button>
193-
);
180+
const renderAction = useCallback(
181+
(ep: ApiEndpoint, action: ApiEndpointAction) => {
182+
switch (action) {
183+
case "copy":
184+
return (
185+
<Button
186+
key={action}
187+
appearance="subtle"
188+
icon={<Copy16Regular />}
189+
size="small"
190+
className={classes.actionButton}
191+
onClick={() => copyToClipboard(ep.url, Dab.CopyTextType.Url)}
192+
aria-label={locConstants.schemaDesigner.copyUrl(ep.label)}
193+
title={locConstants.schemaDesigner.copyUrl(ep.label)}
194+
/>
195+
);
196+
case "openUrl":
197+
if (!ep.openUrlConfig) {
198+
return null;
199+
}
200+
return (
201+
<Button
202+
key={action}
203+
appearance="subtle"
204+
icon={<Open16Regular />}
205+
size="small"
206+
className={classes.actionButton}
207+
onClick={() => openUrl(ep.openUrlConfig!.url)}
208+
aria-label={ep.openUrlConfig.label}
209+
title={ep.openUrlConfig.label}>
210+
{ep.openUrlConfig.label}
211+
</Button>
212+
);
213+
case "addToVSCode":
214+
return (
215+
<Button
216+
key={action}
217+
appearance="subtle"
218+
icon={mcpAdded ? <Checkmark16Regular /> : <Add16Regular />}
219+
size="small"
220+
className={classes.actionButton}
221+
disabled={mcpAdded}
222+
onClick={() => void handleAddMcpServer(ep.url)}
223+
aria-label={locConstants.schemaDesigner.addMcpServerToWorkspace}
224+
title={locConstants.schemaDesigner.addMcpServerToWorkspace}>
225+
{mcpAdded
226+
? locConstants.schemaDesigner.mcpServerAdded
227+
: locConstants.schemaDesigner.addToVSCode}
228+
</Button>
229+
);
194230
}
195-
return null;
196231
},
197-
[classes.actionButton, copyToClipboard, mcpAdded, handleAddMcpServer],
232+
[classes.actionButton, copyToClipboard, openUrl, mcpAdded, handleAddMcpServer],
198233
);
199234

200235
return (
@@ -222,7 +257,7 @@ export const DabDeploymentComplete = ({
222257
<div key={ep.type} className={classes.apiUrlRow}>
223258
<Text className={classes.apiLabel}>{ep.label}</Text>
224259
<Text className={classes.apiUrl}>{ep.url}</Text>
225-
{renderEndpointAction(ep)}
260+
{ep.actions.map((action) => renderAction(ep, action))}
226261
</div>
227262
))}
228263
</div>

extensions/mssql/src/schemaDesigner/schemaDesignerWebviewController.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,18 @@ export class SchemaDesignerWebviewController extends ReactWebviewPanelController
413413
await vscode.window.showTextDocument(doc);
414414
});
415415

416+
this.onNotification(Dab.OpenUrlNotification.type, async (payload) => {
417+
const uri = vscode.Uri.parse(payload.url, true);
418+
if (uri.scheme !== "http" && uri.scheme !== "https") {
419+
return;
420+
}
421+
try {
422+
await vscode.commands.executeCommand("simpleBrowser.show", uri.toString());
423+
} catch {
424+
void vscode.window.showErrorMessage(LocConstants.SchemaDesigner.failedToOpenUrl);
425+
}
426+
});
427+
416428
this.onNotification(Dab.CopyTextNotification.type, async (payload) => {
417429
await vscode.env.clipboard.writeText(payload.text);
418430
const message =

extensions/mssql/src/sharedInterfaces/dab.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,17 @@ export namespace Dab {
338338
export const type = new NotificationType<CopyTextParams>("dab/copyText");
339339
}
340340

341+
/**
342+
* Notification to open a URL in the VS Code built-in browser
343+
*/
344+
export interface OpenUrlParams {
345+
url: string;
346+
}
347+
348+
export namespace OpenUrlNotification {
349+
export const type = new NotificationType<OpenUrlParams>("dab/openUrl");
350+
}
351+
341352
// ============================================
342353
// Reducer types for webview state management
343354
// ============================================

extensions/mssql/test/unit/schemaDesignerWebviewController.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,77 @@ suite("SchemaDesignerWebviewController tests", () => {
903903
});
904904
});
905905

906+
suite("OpenUrlNotification handler", () => {
907+
test("should register OpenUrlNotification handler", () => {
908+
createController();
909+
910+
expect(notificationHandlers.has(Dab.OpenUrlNotification.type.method)).to.be.true;
911+
});
912+
913+
test("should open http URL in VS Code built-in browser", async () => {
914+
const executeCommandStub = sandbox
915+
.stub(vscode.commands, "executeCommand")
916+
.resolves();
917+
918+
createController();
919+
920+
const handler = notificationHandlers.get(Dab.OpenUrlNotification.type.method);
921+
expect(handler).to.be.a("function");
922+
923+
const url = "http://localhost:5000/swagger/index.html";
924+
await handler({ url });
925+
926+
expect(executeCommandStub).to.have.been.calledOnceWith("simpleBrowser.show", url);
927+
});
928+
929+
test("should open https URL in VS Code built-in browser", async () => {
930+
const executeCommandStub = sandbox
931+
.stub(vscode.commands, "executeCommand")
932+
.resolves();
933+
934+
createController();
935+
936+
const handler = notificationHandlers.get(Dab.OpenUrlNotification.type.method);
937+
938+
const url = "https://example.com/graphql";
939+
await handler({ url });
940+
941+
expect(executeCommandStub).to.have.been.calledOnceWith("simpleBrowser.show", url);
942+
});
943+
944+
test("should reject non-http/https schemes", async () => {
945+
const executeCommandStub = sandbox
946+
.stub(vscode.commands, "executeCommand")
947+
.resolves();
948+
949+
createController();
950+
951+
const handler = notificationHandlers.get(Dab.OpenUrlNotification.type.method);
952+
953+
await handler({ url: "file:///etc/passwd" });
954+
expect(executeCommandStub).to.not.have.been.called;
955+
956+
await handler({ url: "command:workbench.action.terminal.new" });
957+
expect(executeCommandStub).to.not.have.been.called;
958+
});
959+
960+
test("should show error message when simpleBrowser.show fails", async () => {
961+
sandbox
962+
.stub(vscode.commands, "executeCommand")
963+
.rejects(new Error("Command not found"));
964+
const showErrorStub = sandbox.stub(vscode.window, "showErrorMessage").resolves();
965+
966+
createController();
967+
968+
const handler = notificationHandlers.get(Dab.OpenUrlNotification.type.method);
969+
970+
const url = "http://localhost:5000/swagger/index.html";
971+
await handler({ url });
972+
973+
expect(showErrorStub).to.have.been.calledOnce;
974+
});
975+
});
976+
906977
suite("RunDeploymentStepRequest handler", () => {
907978
test("should register RunDeploymentStepRequest handler", () => {
908979
createController();

localization/xliff/vscode-mssql.xlf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2605,6 +2605,9 @@
26052605
<source xml:lang="en">Failed to open Modify Table: {0}</source>
26062606
<note>{0} is the error message</note>
26072607
</trans-unit>
2608+
<trans-unit id="++CODE++a32ffd7a33114d3b61146c6a94e4d0af2793426148cebd559e12bc469035c801">
2609+
<source xml:lang="en">Failed to open URL. The built-in Simple Browser may be disabled.</source>
2610+
</trans-unit>
26082611
<trans-unit id="++CODE++e249d9808118e3588a2777e95bb9eb4945723d9bf2c20de048ab52e7228f43e6">
26092612
<source xml:lang="en">Failed to open XEL file: {0}</source>
26102613
<note>{0} is the error message</note>
@@ -4263,6 +4266,9 @@
42634266
<trans-unit id="++CODE++8551c1d2eb56e879661b2939595c92f04d477465ccfa7e1d4dbf08e7ec82b466">
42644267
<source xml:lang="en">Open GitHub Copilot Chat to help fix these errors</source>
42654268
</trans-unit>
4269+
<trans-unit id="++CODE++e50b72dd9f8ec3560afc51c641fb87964533455bfa1568dd626dd3279f97eb26">
4270+
<source xml:lang="en">Open Nitro</source>
4271+
</trans-unit>
42664272
<trans-unit id="++CODE++82b7be031f380379c3136f863b234cc6ddbddb19fbbae4654585d10506a2e1e6">
42674273
<source xml:lang="en">Open Publish Script</source>
42684274
</trans-unit>
@@ -6366,6 +6372,9 @@
63666372
<trans-unit id="++CODE++15435b311c9371437109bd5323292504915507b034803118517fee44e9547a3c">
63676373
<source xml:lang="en">View More</source>
63686374
</trans-unit>
6375+
<trans-unit id="++CODE++c0eda6287745f09b4f3007da0bcca0fccdf5e68bfd517d4dcead4463ee8453b1">
6376+
<source xml:lang="en">View Swagger</source>
6377+
</trans-unit>
63696378
<trans-unit id="++CODE++ff01d2362e483ddaca44f0e7f02d280bef382c1a3209776e5a11ca21acf2cea4">
63706379
<source xml:lang="en">View mssql for Visual Studio Code release notes?</source>
63716380
</trans-unit>

0 commit comments

Comments
 (0)