mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 12:51:41 +00:00
Compare commits
7 Commits
users/aisa
...
fix_a11y_D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8e666a259 | ||
|
|
fb4ef8258f | ||
|
|
de5df90f75 | ||
|
|
66421ad276 | ||
|
|
e70fa01a8b | ||
|
|
79b6f3cf2f | ||
|
|
b765cae088 |
@@ -1,5 +1,6 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
PartitionMerge = 1 << 0,
|
||||
}
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||
key: userContext.masterKey,
|
||||
@@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum MessageTypes {
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
|
||||
@@ -1096,7 +1096,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
|
||||
@@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
it("Button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Account is serverless - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableServerless" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -274,10 +273,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -667,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 100 bytes"
|
||||
label="My partition key is larger than 101 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
@@ -887,10 +887,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
|
||||
@@ -63,7 +63,6 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
/>
|
||||
<TextField
|
||||
label={inputLabel && inputLabel}
|
||||
id="confirmCollectionId"
|
||||
defaultValue={paramValue}
|
||||
onChange={onParamValueChange}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1562,7 +1562,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</DropdownBase>
|
||||
</Dropdown>
|
||||
<StyledTextFieldBase
|
||||
id="confirmCollectionId"
|
||||
key=".0:$.1"
|
||||
label="Value"
|
||||
onChange={[Function]}
|
||||
@@ -1570,7 +1569,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<TextFieldBase
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
label="Value"
|
||||
onChange={[Function]}
|
||||
resizable={true}
|
||||
@@ -1858,12 +1856,12 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<StyledLabelBase
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField1"
|
||||
id="TextFieldLabel3"
|
||||
styles={[Function]}
|
||||
>
|
||||
<LabelBase
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField1"
|
||||
id="TextFieldLabel3"
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -2142,7 +2140,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-53"
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField1"
|
||||
id="TextFieldLabel3"
|
||||
>
|
||||
Value
|
||||
@@ -2156,7 +2154,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel3"
|
||||
className="ms-TextField-field field-77"
|
||||
id="confirmCollectionId"
|
||||
id="TextField1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
@@ -3721,7 +3719,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</Dropdown>
|
||||
<StyledTextFieldBase
|
||||
defaultValue=""
|
||||
id="confirmCollectionId"
|
||||
key=".0:$.1"
|
||||
label="Param"
|
||||
onChange={[Function]}
|
||||
@@ -3730,7 +3727,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<TextFieldBase
|
||||
defaultValue=""
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
label="Param"
|
||||
onChange={[Function]}
|
||||
resizable={true}
|
||||
@@ -4018,12 +4014,12 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<StyledLabelBase
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField5"
|
||||
id="TextFieldLabel7"
|
||||
styles={[Function]}
|
||||
>
|
||||
<LabelBase
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField5"
|
||||
id="TextFieldLabel7"
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -4302,7 +4298,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-53"
|
||||
htmlFor="confirmCollectionId"
|
||||
htmlFor="TextField5"
|
||||
id="TextFieldLabel7"
|
||||
>
|
||||
Param
|
||||
@@ -4316,7 +4312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel7"
|
||||
className="ms-TextField-field field-77"
|
||||
id="confirmCollectionId"
|
||||
id="TextField5"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Computed<boolean>;
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
@@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
databaseAccount={this.getDatabaseAccount()}
|
||||
tabId={this.getTabId()}
|
||||
/>
|
||||
) : (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
@@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
|
||||
this.container = options.container;
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
|
||||
@@ -2,15 +2,61 @@
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export class JupyterLabAppFactory {
|
||||
public static async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: () => void;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
return content?.includes("cosmosuser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
||||
}
|
||||
|
||||
private isCassandraShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
this.checkShellStarted = this.isMongoShellStarted;
|
||||
break;
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface TerminalProps {
|
||||
authType: AuthType;
|
||||
apiType: ApiType;
|
||||
subscriptionId: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (tabId: string): void => {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
|
||||
window.document.referrer
|
||||
);
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
|
||||
async function configureHosted(): Promise<Explorer> {
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
return configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
explorer = configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
return configureHostedWithResourceToken(win.hostedConfig);
|
||||
explorer = configureHostedWithResourceToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
return configureHostedWithConnectionString(win.hostedConfig);
|
||||
explorer = configureHostedWithConnectionString(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||
return configureHostedWithAAD(win.hostedConfig);
|
||||
explorer = await configureHostedWithAAD(win.hostedConfig);
|
||||
} else {
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
}
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldProcessMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
@@ -261,6 +284,8 @@ async function configurePortal(): Promise<Explorer> {
|
||||
}
|
||||
} else if (shouldForwardMessage(message, event.origin)) {
|
||||
sendMessage(message);
|
||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user