mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 09:02:41 +01:00
Enable Cosmos DB Shell by default and clarify setup progress (#2593)
* Enable Cosmos DB Shell by default and clarify setup progress Default enableCosmosDBShell to true while retaining explicit opt-out support. Add localized progress for SDK installation, shell installation, update checks, and connection with safe Bash quoting. Cover default visibility, overrides, and progress messages with regression tests. Validation: npm run build:ci and 83 focused unit tests passed. * Fix Index Advisor Playwright tooltip interference
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase, TerminalKind } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { ApiType, updateUserContext, userContext } from "../../../UserContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -165,6 +166,60 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Open Cosmos DB Shell button", () => {
|
||||||
|
let originalUserContext: typeof userContext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalUserContext = { ...userContext };
|
||||||
|
mockExplorer = { openNotebookTerminal: jest.fn() } as unknown as Explorer;
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
databaseAccount: { kind: "DocumentDB", properties: { capabilities: [] } } as DatabaseAccount,
|
||||||
|
features: extractFeatures(new URLSearchParams()),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => updateUserContext(originalUserContext));
|
||||||
|
|
||||||
|
const getShellButton = () =>
|
||||||
|
CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, useSelectedNode.getState()).find(
|
||||||
|
(button) => button.commandButtonLabel === "Open Cosmos DB Shell",
|
||||||
|
);
|
||||||
|
|
||||||
|
it("shows and opens the shell with default features", () => {
|
||||||
|
const button = getShellButton();
|
||||||
|
expect(button).toBeDefined();
|
||||||
|
expect(button.disabled).toBe(false);
|
||||||
|
button.onCommandClick(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||||
|
expect(mockExplorer.openNotebookTerminal).toHaveBeenCalledWith(TerminalKind.CosmosDB);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the shell when explicitly disabled by the feature flag", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(new URLSearchParams({ "feature.enableCosmosDBShell": "false" })),
|
||||||
|
});
|
||||||
|
expect(getShellButton()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the shell when Cloud Shell is unavailable", () => {
|
||||||
|
updateUserContext({ features: { ...userContext.features, enableCloudShell: false } });
|
||||||
|
expect(getShellButton()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<ApiType>(["Mongo", "Cassandra", "Gremlin", "Tables", "Postgres", "VCoreMongo"])(
|
||||||
|
"hides the shell for the %s API",
|
||||||
|
(apiType) => {
|
||||||
|
updateUserContext({ apiType });
|
||||||
|
expect(getShellButton()).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("hides the shell for resource token authentication", () => {
|
||||||
|
updateUserContext({ authType: AuthType.ResourceToken });
|
||||||
|
expect(getShellButton()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Resource token", () => {
|
describe("Resource token", () => {
|
||||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as Localization from "Localization/t";
|
||||||
import { CosmosDBShellHandler } from "./CosmosDBShellHandler";
|
import { CosmosDBShellHandler } from "./CosmosDBShellHandler";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
@@ -14,8 +15,9 @@ jest.mock("../../../../UserContext", () => ({
|
|||||||
describe("CosmosDBShellHandler", () => {
|
describe("CosmosDBShellHandler", () => {
|
||||||
const mockKey = "testKey";
|
const mockKey = "testKey";
|
||||||
const endpoint = "https://test-account.documents.azure.com:443/";
|
const endpoint = "https://test-account.documents.azure.com:443/";
|
||||||
const tokenConnectionCommand = `export COSMOSDB_SHELL_TOKEN='aadToken123'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
const connectionProgress = "printf '%s\\n' 'Connecting to your Cosmos DB account...'";
|
||||||
const keyConnectionCommand = `export COSMOSDB_SHELL_ACCOUNT_KEY='${mockKey}'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
const tokenConnectionCommand = `${connectionProgress}; export COSMOSDB_SHELL_TOKEN='aadToken123'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
||||||
|
const keyConnectionCommand = `${connectionProgress}; export COSMOSDB_SHELL_ACCOUNT_KEY='${mockKey}'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
||||||
let cosmosDBShellHandler: CosmosDBShellHandler;
|
let cosmosDBShellHandler: CosmosDBShellHandler;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -54,6 +56,32 @@ describe("CosmosDBShellHandler", () => {
|
|||||||
expect(commands.some((c) => c === "export DOTNET_ROOT=$HOME/.dotnet")).toBe(true);
|
expect(commands.some((c) => c === "export DOTNET_ROOT=$HOME/.dotnet")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should describe each setup step only in the corresponding command branch", () => {
|
||||||
|
const commands = cosmosDBShellHandler.getSetUpCommands();
|
||||||
|
const sdkCommand = commands.find((command) => command.includes("dotnet-install.sh"));
|
||||||
|
const shellCommand = commands.find((command) => command.includes("dotnet tool install"));
|
||||||
|
|
||||||
|
expect(sdkCommand).toContain(
|
||||||
|
"then printf '%s\\n' 'Downloading and installing .NET SDK 10. First-time setup may take a few minutes.'; curl",
|
||||||
|
);
|
||||||
|
expect(shellCommand).toContain("then printf '%s\\n' 'Installing Cosmos DB Shell...'; dotnet tool install");
|
||||||
|
expect(shellCommand).toContain(
|
||||||
|
"else printf '%s\\n' 'Checking for Cosmos DB Shell updates...'; dotnet tool update",
|
||||||
|
);
|
||||||
|
expect(commands).toHaveLength(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should quote localized progress messages safely for Bash", () => {
|
||||||
|
const translation = jest.spyOn(Localization, "t").mockReturnValue("Account's $HOME `command` %s");
|
||||||
|
try {
|
||||||
|
expect(cosmosDBShellHandler.getConnectionCommand()).toContain(
|
||||||
|
"printf '%s\\n' 'Account'\\''s $HOME `command` %s'; export",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
translation.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("should not export any credential env var in setup commands for a key credential", () => {
|
it("should not export any credential env var in setup commands for a key credential", () => {
|
||||||
const commands = cosmosDBShellHandler.getSetUpCommands();
|
const commands = cosmosDBShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
@@ -107,6 +135,7 @@ describe("CosmosDBShellHandler", () => {
|
|||||||
const connectionCommand = handler.getConnectionCommand();
|
const connectionCommand = handler.getConnectionCommand();
|
||||||
|
|
||||||
expect(connectionCommand).not.toContain("cosmosdbshell --connect");
|
expect(connectionCommand).not.toContain("cosmosdbshell --connect");
|
||||||
|
expect(connectionCommand).not.toContain("Connecting to your Cosmos DB account");
|
||||||
expect(connectionCommand).toContain("Unable to acquire a Cosmos DB credential");
|
expect(connectionCommand).toContain("Unable to acquire a Cosmos DB credential");
|
||||||
expect(connectionCommand).toContain("Login for Entra ID");
|
expect(connectionCommand).toContain("Login for Entra ID");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResourceKey, t } from "Localization/t";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
|
|
||||||
@@ -65,14 +66,23 @@ export class CosmosDBShellHandler extends AbstractShellHandler {
|
|||||||
return [
|
return [
|
||||||
"export DOTNET_ROOT=$HOME/.dotnet",
|
"export DOTNET_ROOT=$HOME/.dotnet",
|
||||||
"export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH",
|
"export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH",
|
||||||
"if ! command -v cosmosdbshell &> /dev/null; then echo '⚠️ cosmosdbshell not found. Installing .NET SDK 10 and CosmosDBShell...'; fi",
|
`if ! command -v cosmosdbshell &> /dev/null && ! dotnet --list-sdks 2>/dev/null | grep -q '^10\\.'; then ${this._getProgressCommand(
|
||||||
"if ! command -v cosmosdbshell &> /dev/null && ! dotnet --list-sdks 2>/dev/null | grep -q '^10\\.'; then curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir $HOME/.dotnet; fi",
|
"cosmosDBShell.installingSdk",
|
||||||
"if ! command -v cosmosdbshell &> /dev/null; then dotnet tool install --global CosmosDBShell --prerelease; else dotnet tool update --global CosmosDBShell --prerelease; fi",
|
)}; curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir $HOME/.dotnet; fi`,
|
||||||
|
`if ! command -v cosmosdbshell &> /dev/null; then ${this._getProgressCommand(
|
||||||
|
"cosmosDBShell.installingShell",
|
||||||
|
)}; dotnet tool install --global CosmosDBShell --prerelease; else ${this._getProgressCommand(
|
||||||
|
"cosmosDBShell.updatingShell",
|
||||||
|
)}; dotnet tool update --global CosmosDBShell --prerelease; fi`,
|
||||||
"grep -qxF 'export DOTNET_ROOT=$HOME/.dotnet' ~/.bashrc || echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc",
|
"grep -qxF 'export DOTNET_ROOT=$HOME/.dotnet' ~/.bashrc || echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc",
|
||||||
"grep -qxF 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' ~/.bashrc || echo 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' >> ~/.bashrc",
|
"grep -qxF 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' ~/.bashrc || echo 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' >> ~/.bashrc",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getProgressCommand(key: ResourceKey): string {
|
||||||
|
return `printf '%s\\n' '${t(key).replace(/'/g, "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
private _getKeyConnectionCommand(key: string): string {
|
private _getKeyConnectionCommand(key: string): string {
|
||||||
// Export the key immediately before invoking the tool, on the same command line, so it
|
// Export the key immediately before invoking the tool, on the same command line, so it
|
||||||
// never appears as a --connect flag value or lands in a separate setup step. The tool
|
// never appears as a --connect flag value or lands in a separate setup step. The tool
|
||||||
@@ -118,9 +128,11 @@ export class CosmosDBShellHandler extends AbstractShellHandler {
|
|||||||
endpoint: this._endpoint,
|
endpoint: this._endpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.credential.kind === "key"
|
const connectionCommand =
|
||||||
|
this.credential.kind === "key"
|
||||||
? this._getKeyConnectionCommand(this.credential.value)
|
? this._getKeyConnectionCommand(this.credential.value)
|
||||||
: this._getTokenConnectionCommand(this.credential.value);
|
: this._getTokenConnectionCommand(this.credential.value);
|
||||||
|
return `${this._getProgressCommand("cosmosDBShell.connecting")}; ${connectionCommand}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string[] {
|
public getTerminalSuppressedData(): string[] {
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
"off": "Off",
|
"off": "Off",
|
||||||
"preview": "Preview"
|
"preview": "Preview"
|
||||||
},
|
},
|
||||||
|
"cosmosDBShell": {
|
||||||
|
"installingSdk": "Downloading and installing .NET SDK 10. First-time setup may take a few minutes.",
|
||||||
|
"installingShell": "Installing Cosmos DB Shell...",
|
||||||
|
"updatingShell": "Checking for Cosmos DB Shell updates...",
|
||||||
|
"connecting": "Connecting to your Cosmos DB account..."
|
||||||
|
},
|
||||||
"splashScreen": {
|
"splashScreen": {
|
||||||
"title": {
|
"title": {
|
||||||
"default": "Welcome to Azure Cosmos DB",
|
"default": "Welcome to Azure Cosmos DB",
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { extractFeatures, hasFlag } from "./extractFeatures";
|
import { extractFeatures, hasFlag } from "./extractFeatures";
|
||||||
|
|
||||||
describe("extractFeatures", () => {
|
describe("extractFeatures", () => {
|
||||||
|
it("enables Cosmos DB Shell by default", () => {
|
||||||
|
expect(extractFeatures(new URLSearchParams()).enableCosmosDBShell).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["feature.enableCosmosDBShell", "enableCosmosDBShell"])(
|
||||||
|
"respects explicit Cosmos DB Shell overrides via %s",
|
||||||
|
(key) => {
|
||||||
|
expect(extractFeatures(new URLSearchParams({ [key]: "false" })).enableCosmosDBShell).toBe(false);
|
||||||
|
expect(extractFeatures(new URLSearchParams({ [key]: "true" })).enableCosmosDBShell).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("correctly detects feature flags in a case insensitive manner", () => {
|
it("correctly detects feature flags in a case insensitive manner", () => {
|
||||||
const url = "https://localhost:10001/12345/notebook";
|
const url = "https://localhost:10001/12345/notebook";
|
||||||
const token = "super secret";
|
const token = "super secret";
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableContainerCopy: "true" === get("enablecontainercopy"),
|
enableContainerCopy: "true" === get("enablecontainercopy"),
|
||||||
enableRestoreContainer: "true" === get("enablerestorecontainer"),
|
enableRestoreContainer: "true" === get("enablerestorecontainer"),
|
||||||
enableCloudShell: true,
|
enableCloudShell: true,
|
||||||
enableCosmosDBShell: "true" === get("enablecosmosdbshell"),
|
enableCosmosDBShell: "true" === get("enablecosmosdbshell", "true"),
|
||||||
mongoDisableNativeAuth: "true" === get("mongodisablenativeauth"),
|
mongoDisableNativeAuth: "true" === get("mongodisablenativeauth"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ async function setupIndexAdvisorTab(page: Page, customQuery?: string) {
|
|||||||
const queryEditor = queryTab.editor();
|
const queryEditor = queryTab.editor();
|
||||||
await queryEditor.locator.waitFor({ timeout: 30 * 1000 });
|
await queryEditor.locator.waitFor({ timeout: 30 * 1000 });
|
||||||
await queryTab.executeCTA.waitFor();
|
await queryTab.executeCTA.waitFor();
|
||||||
|
await queryEditor.locator.click();
|
||||||
|
|
||||||
if (customQuery) {
|
if (customQuery) {
|
||||||
await queryEditor.locator.click();
|
|
||||||
await queryEditor.setText(customQuery);
|
await queryEditor.setText(customQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user