Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extensions/mssql/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,7 @@
"No pending changes. Make edits to generate a script.": "No pending changes. Make edits to generate a script.",
"Close Script Pane": "Close Script Pane",
"Modify Table": "Modify Table",
"View Table Diagram": "View Table Diagram",
"Search Database Objects (Preview)": "Search Database Objects (Preview)",
"Loading database objects": "Loading database objects",
"Connecting to {0}.../{0} is the server name": {
Expand Down Expand Up @@ -3055,6 +3056,10 @@
"message": "Failed to open Table Designer: {0}",
"comment": ["{0} is the error message"]
},
"Failed to open Schema Designer: {0}/{0} is the error message": {
"message": "Failed to open Schema Designer: {0}",
"comment": ["{0} is the error message"]
},
"Locate an Azure Data Studio settings.json file to import": "Locate an Azure Data Studio settings.json file to import",
"Ready for import": "Ready for import",
"Needs attention": "Needs attention",
Expand Down
1 change: 1 addition & 0 deletions extensions/mssql/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const cmdEditTable = "mssql.editTable";
export const cmdEditConnection = "mssql.editConnection";
export const cmdLaunchUserFeedback = "mssql.userFeedback";
export const cmdDesignSchema = "mssql.schemaDesigner";
export const cmdDesignSchemaForTable = "mssql.schemaDesignerForTable";
export const cmdBuildDataApi = "mssql.buildDataApi";
export const cmdDeployNewDatabase = "mssql.deployNewDatabase";
export const cmdStopContainer = "mssql.stopContainer";
Expand Down
7 changes: 7 additions & 0 deletions extensions/mssql/src/constants/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2784,6 +2784,13 @@ export class TableExplorer {
args: [errorMessage],
comment: ["{0} is the error message"],
});

public static failedToOpenSchemaDesigner = (errorMessage: string) =>
l10n.t({
message: "Failed to open Schema Designer: {0}",
args: [errorMessage],
comment: ["{0} is the error message"],
});
}

export class AzureDataStudioMigration {
Expand Down
21 changes: 21 additions & 0 deletions extensions/mssql/src/controllers/mainController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1832,6 +1832,27 @@ export default class MainController implements vscode.Disposable {
),
);

this._context.subscriptions.push(
vscode.commands.registerCommand(
Constants.cmdDesignSchemaForTable,
async (node: TreeNodeInfo, databaseName: string, filterTable: string) => {
const schemaDesigner =
await SchemaDesignerWebviewManager.getInstance().getSchemaDesigner(
this._context,
this._vscodeWrapper,
this,
this.schemaDesignerService,
databaseName,
node,
);

schemaDesigner.setInitialFilterTables([filterTable]);
schemaDesigner.showView(SchemaDesigner.SchemaDesignerActiveView.SchemaDesigner);
schemaDesigner.revealToForeground();
},
),
);

this._context.subscriptions.push(
vscode.commands.registerCommand(
Constants.cmdBuildDataApi,
Expand Down
1 change: 1 addition & 0 deletions extensions/mssql/src/reactviews/common/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,7 @@ export class LocConstants {
noPendingChanges: l10n.t("No pending changes. Make edits to generate a script."),
closeScriptPane: l10n.t("Close Script Pane"),
modifyTable: l10n.t("Modify Table"),
viewTableDiagram: l10n.t("View Table Diagram"),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
Tooltip,
makeStyles,
} from "@fluentui/react-components";
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { SchemaDesignerContext } from "../schemaDesignerStateProvider";
import { useSchemaDesignerSelector } from "../schemaDesignerSelector";
import { locConstants } from "../../../common/locConstants";
import { Edge, Node, useReactFlow } from "@xyflow/react";
import { SchemaDesigner } from "../../../../sharedInterfaces/schemaDesigner";
Expand Down Expand Up @@ -51,6 +52,7 @@ export function FilterTablesButton() {
const classes = useStyles();
const reactFlow = useReactFlow();
const isCompact = useIsToolbarCompact();
const initialFilterTables = useSchemaDesignerSelector((s) => s?.initialFilterTables);
if (!context) {
return undefined;
}
Expand All @@ -61,8 +63,31 @@ export function FilterTablesButton() {
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
const [showTableRelationships, setShowTableRelationships] = useState(false);
const filterLabel = locConstants.schemaDesigner.filter(selectedTables.length);
const initialFilterConsumedRef = useRef(false);
const initialFilterJustAppliedRef = useRef(false);

function loadTables() {
// When an initial filter from the extension host is pending (e.g., navigating
// from Table Explorer), skip normal table loading so we don't clear the filter
// that will be applied by the initialFilterTables effect.
if (
initialFilterTables &&
initialFilterTables.length > 0 &&
!initialFilterConsumedRef.current
) {
setFilterText("");
return;
}

// When the initial filter was just applied in the same animation frame,
// skip normal loading to prevent clearing the filter before React
// re-renders and applies the node visibility changes.
if (initialFilterJustAppliedRef.current) {
initialFilterJustAppliedRef.current = false;
setFilterText("");
return;
}

// When loading tables (e.g., when filter button is clicked), we should maintain
// the current explicitly selected tables, not include related tables as selected
const nodes = reactFlow.getNodes();
Expand Down Expand Up @@ -195,6 +220,48 @@ export function FilterTablesButton() {
};
}, [context.schemaRevision]);

// Apply initial filter tables from extension host (e.g., when navigating from Table Explorer).
// Uses requestAnimationFrame polling because when isInitialized becomes true, the nodes
// may not yet be set on the ReactFlow instance (they're set by the caller after init).
// The initialFilterConsumedRef prevents loadTables() from clearing the filter before
// this effect has a chance to apply it.
useEffect(() => {
if (!initialFilterTables || initialFilterTables.length === 0 || !context.isInitialized) {
return;
}

// Reset consumed flag so loadTables() defers to this effect
initialFilterConsumedRef.current = false;

let cancelled = false;
let retries = 0;
const MAX_RETRIES = 300;
const applyFilter = () => {
if (cancelled) {
return;
}
const nodes = reactFlow.getNodes();
if (nodes.length > 0) {
initialFilterConsumedRef.current = true;
initialFilterJustAppliedRef.current = true;
setSelectedTables([...initialFilterTables]);
setShowTableRelationships(true);
context.resetView();
} else if (retries < MAX_RETRIES) {
retries++;
requestAnimationFrame(applyFilter);
} else {
// Max retries reached; mark filter as consumed so loadTables() can proceed
initialFilterConsumedRef.current = true;
}
};
requestAnimationFrame(applyFilter);

return () => {
cancelled = true;
};
}, [initialFilterTables, context.isInitialized]);

// Function to highlight text based on search
const highlightText = (text: string, searchText: string) => {
if (!searchText || searchText.trim() === "") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export const TableExplorerStateProvider: React.FC<{
modifyTable: function (): void {
extensionRpc.action("modifyTable", {});
},

viewTableDiagram: function (): void {
extensionRpc.action("viewTableDiagram", {});
},
}),
[extensionRpc],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

import React from "react";
import { Toolbar, ToolbarButton, Combobox, Option, Button } from "@fluentui/react-components";
import { SaveRegular, AddRegular, CodeRegular, ArrowSyncRegular } from "@fluentui/react-icons";
import {
SaveRegular,
AddRegular,
CodeRegular,
ArrowSyncRegular,
OrganizationRegular,
} from "@fluentui/react-icons";
import { locConstants as loc } from "../../common/locConstants";
import { useTableExplorerContext } from "./TableExplorerStateProvider";
import { useTableExplorerSelector } from "./tableExplorerSelector";
Expand Down Expand Up @@ -123,6 +129,14 @@ export const TableExplorerToolbar: React.FC<TableExplorerToolbarProps> = ({
disabled={isLoading}>
{showScriptPane ? loc.tableExplorer.hideScript : loc.tableExplorer.showScript}
</ToolbarButton>
<ToolbarButton
aria-label={loc.tableExplorer.viewTableDiagram}
title={loc.tableExplorer.viewTableDiagram}
icon={<OrganizationRegular />}
onClick={() => context.viewTableDiagram()}
disabled={isLoading}>
{loc.tableExplorer.viewTableDiagram}
</ToolbarButton>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginLeft: "auto" }}>
<span style={{ fontSize: "12px" }}>{loc.tableExplorer.totalRowsToFetch}</span>
<Combobox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ export class SchemaDesignerWebviewController extends ReactWebviewPanelController
this.setupConfigurationListener();
}

/**
* Sets the initial filter tables for the Schema Designer.
* When set, the FilterTablesButton will apply this filter after initialization.
* @param tables Array of fully qualified table names (e.g., ["dbo.Students"])
*/
public setInitialFilterTables(tables: string[]): void {
this.updateState({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got comments about mutating the state from @Benjin the consensus is that in controllers it is safe to update the value no need to create a new instance every time we have to chnage the state.

...this.state,
initialFilterTables: tables,
});
}

private setupRequestHandlers() {
this.onRequest(SchemaDesigner.InitializeSchemaDesignerRequest.type, async () => {
const schemaDesignerInitActivity = startActivity(
Expand Down
1 change: 1 addition & 0 deletions extensions/mssql/src/sharedInterfaces/schemaDesigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export namespace SchemaDesigner {
copilotChatDiscoveryDismissed?: CopilotChat.DiscoveryDismissedState;
activeView?: SchemaDesignerActiveView;
isDabDeploymentSupported?: boolean;
initialFilterTables?: string[];
}

export interface ExportFileOptions {
Expand Down
2 changes: 2 additions & 0 deletions extensions/mssql/src/sharedInterfaces/tableExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export interface TableExplorerContextProps {
setCurrentPage: (pageNumber: number) => void;
saveResults: (format: SupportedSaveFormats, data: ExportData) => void;
modifyTable: () => void;
viewTableDiagram: () => void;
}

export interface TableExplorerReducers {
Expand All @@ -227,6 +228,7 @@ export interface TableExplorerReducers {
setCurrentPage: { pageNumber: number };
saveResults: { format: SupportedSaveFormats; data: ExportData };
modifyTable: {};
viewTableDiagram: {};
}

export interface ExportData {
Expand Down
1 change: 1 addition & 0 deletions extensions/mssql/src/sharedInterfaces/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export enum TelemetryActions {
SetDatabase = "SetDatabase",
EditData = "EditData",
ModifyTable = "ModifyTable",
ViewTableDiagram = "ViewTableDiagram",
CopyObjectName = "CopyObjectName",
RefreshResults = "RefreshResults",
Search = "Search",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,65 @@ export class TableExplorerWebViewController extends ReactWebviewPanelController<

return state;
});

this.registerReducer("viewTableDiagram", async (state, _payload) => {
this.logger.info(`Opening Schema Designer - OperationId: ${this.operationId}`);

const startTime = Date.now();
const endActivity = startActivity(
TelemetryViews.TableExplorer,
TelemetryActions.ViewTableDiagram,
uuid(),
{
startTime: startTime.toString(),
operationId: this.operationId,
},
);

try {
const databaseName = ObjectExplorerUtils.getDatabaseName(this._targetNode);
const schemaName = state.schemaName || this._targetNode.metadata.schema || "";
const tableName = state.tableName;
const filterTable = schemaName ? `${schemaName}.${tableName}` : tableName;

await vscode.commands.executeCommand(
Constants.cmdDesignSchemaForTable,
this._targetNode,
databaseName,
filterTable,
);

this.logger.info(
`Schema Designer opened successfully - OperationId: ${this.operationId}`,
);

endActivity.end(ActivityStatus.Succeeded, {
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.operationId,
});
} catch (error) {
this.logger.error(
`Error opening Schema Designer: ${getErrorMessage(error)} - OperationId: ${this.operationId}`,
);

endActivity.endFailed(
new Error("Failed to open Schema Designer"),
true /* includeErrorMessage */,
undefined /* errorCode */,
undefined /* errorType */,
{
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.operationId,
},
);

vscode.window.showErrorMessage(
LocConstants.TableExplorer.failedToOpenSchemaDesigner(getErrorMessage(error)),
);
}

return state;
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,48 @@ suite("SchemaDesignerWebviewController tests", () => {
});
});

suite("setInitialFilterTables", () => {
test("should update state with initial filter tables", () => {
const ctrl = createController();

ctrl.setInitialFilterTables(["dbo.Users", "dbo.Orders"]);

expect(ctrl.state.initialFilterTables).to.deep.equal(["dbo.Users", "dbo.Orders"]);
});

test("should update state with a single table", () => {
const ctrl = createController();

ctrl.setInitialFilterTables(["dbo.Students"]);

expect(ctrl.state.initialFilterTables).to.deep.equal(["dbo.Students"]);
});

test("should update state with empty array", () => {
const ctrl = createController();

ctrl.setInitialFilterTables([]);

expect(ctrl.state.initialFilterTables).to.deep.equal([]);
});

test("should overwrite previous filter tables when called again", () => {
const ctrl = createController();

ctrl.setInitialFilterTables(["dbo.Users"]);
expect(ctrl.state.initialFilterTables).to.deep.equal(["dbo.Users"]);

ctrl.setInitialFilterTables(["dbo.Orders", "dbo.Products"]);
expect(ctrl.state.initialFilterTables).to.deep.equal(["dbo.Orders", "dbo.Products"]);
});

test("should not have initialFilterTables in state before setInitialFilterTables is called", () => {
const ctrl = createController();

expect(ctrl.state.initialFilterTables).to.be.undefined;
});
});

suite("resolveSqlServerContainerName", () => {
test("should return containerName from treeNode connection profile", () => {
sandbox.stub(treeNode, "connectionProfile").get(
Expand Down
Loading
Loading