mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-12 03:14:45 +01:00
Merge branch 'master' into users/dshilov/update-grid-after-upload
# Conflicts: # src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx
This commit is contained in:
commit
ddc94a7cd0
@ -211,3 +211,12 @@ a:focus {
|
|||||||
.fileImportImg img {
|
.fileImportImg img {
|
||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabPanesContainer {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
min-height: 500px;
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
@ -608,7 +608,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className={`paneMainContent ${styles.container}`}>
|
<div className={`paneMainContent ${styles.container}`}>
|
||||||
{!isFabricNative() && (
|
{!isFabricNative() && (
|
||||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
<Accordion className={`customAccordion ${styles.firstItem}`} collapsible>
|
||||||
{shouldShowQueryPageOptions && (
|
{shouldShowQueryPageOptions && (
|
||||||
<AccordionItem value="1">
|
<AccordionItem value="1">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
|
@ -12,6 +12,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||||
|
collapsible={true}
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="1"
|
value="1"
|
||||||
@ -573,6 +574,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||||
|
collapsible={true}
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="7"
|
value="7"
|
||||||
|
@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField is-required root-110"
|
className="ms-TextField is-required root-116"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="ms-Label root-121"
|
className="ms-Label root-127"
|
||||||
htmlFor="TextField0"
|
htmlFor="TextField0"
|
||||||
id="TextFieldLabel2"
|
id="TextFieldLabel2"
|
||||||
>
|
>
|
||||||
@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
</LabelBase>
|
</LabelBase>
|
||||||
</StyledLabelBase>
|
</StyledLabelBase>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-fieldGroup fieldGroup-111"
|
className="ms-TextField-fieldGroup fieldGroup-117"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-labelledby="TextFieldLabel2"
|
aria-labelledby="TextFieldLabel2"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-112"
|
className="ms-TextField-field field-118"
|
||||||
id="TextField0"
|
id="TextField0"
|
||||||
name="collectionIdConfirmation"
|
name="collectionIdConfirmation"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Create"
|
aria-label="Create"
|
||||||
className="ms-Button ms-Button--primary root-122"
|
className="ms-Button ms-Button--primary root-128"
|
||||||
data-is-focusable={true}
|
data-is-focusable={true}
|
||||||
data-test="Panel/OkButton"
|
data-test="Panel/OkButton"
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="ms-Button-flexContainer flexContainer-123"
|
className="ms-Button-flexContainer flexContainer-129"
|
||||||
data-automationid="splitbuttonprimary"
|
data-automationid="splitbuttonprimary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="ms-Button-textContainer textContainer-124"
|
className="ms-Button-textContainer textContainer-130"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="ms-Button-label label-126"
|
className="ms-Button-label label-132"
|
||||||
id="id__5"
|
id="id__5"
|
||||||
key="id__5"
|
key="id__5"
|
||||||
>
|
>
|
||||||
|
@ -2,9 +2,13 @@ import {
|
|||||||
DetailsList,
|
DetailsList,
|
||||||
DetailsListLayoutMode,
|
DetailsListLayoutMode,
|
||||||
DirectionalHint,
|
DirectionalHint,
|
||||||
|
FontIcon,
|
||||||
IColumn,
|
IColumn,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
TooltipHost,
|
TooltipHost,
|
||||||
|
getTheme,
|
||||||
|
mergeStyles,
|
||||||
|
mergeStyleSets,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { Upload } from "Common/Upload/Upload";
|
import { Upload } from "Common/Upload/Upload";
|
||||||
import { UploadDetailsRecord } from "Contracts/ViewModels";
|
import { UploadDetailsRecord } from "Contracts/ViewModels";
|
||||||
@ -14,6 +18,36 @@ import { getErrorMessage } from "../../Tables/Utilities";
|
|||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
const iconClass = mergeStyles({
|
||||||
|
verticalAlign: "middle",
|
||||||
|
maxHeight: "16px",
|
||||||
|
maxWidth: "16px",
|
||||||
|
});
|
||||||
|
|
||||||
|
const classNames = mergeStyleSets({
|
||||||
|
fileIconHeaderIcon: {
|
||||||
|
padding: 0,
|
||||||
|
fontSize: "16px",
|
||||||
|
},
|
||||||
|
fileIconCell: {
|
||||||
|
textAlign: "center",
|
||||||
|
selectors: {
|
||||||
|
"&:before": {
|
||||||
|
content: ".",
|
||||||
|
display: "inline-block",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
height: "100%",
|
||||||
|
width: "0px",
|
||||||
|
visibility: "hidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
||||||
|
accept: [{ color: theme.semanticColors.successIcon }, iconClass],
|
||||||
|
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
|
||||||
|
});
|
||||||
|
|
||||||
export type UploadItemsPaneProps = {
|
export type UploadItemsPaneProps = {
|
||||||
onUpload?: (data: UploadDetailsRecord[]) => void;
|
onUpload?: (data: UploadDetailsRecord[]) => void;
|
||||||
};
|
};
|
||||||
@ -66,44 +100,94 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columns: IColumn[] = [
|
const columns: IColumn[] = [
|
||||||
|
{
|
||||||
|
key: "icons",
|
||||||
|
name: "",
|
||||||
|
fieldName: "",
|
||||||
|
className: classNames.fileIconCell,
|
||||||
|
iconClassName: classNames.fileIconHeaderIcon,
|
||||||
|
isIconOnly: true,
|
||||||
|
minWidth: 16,
|
||||||
|
maxWidth: 16,
|
||||||
|
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||||
|
if (item.numFailed) {
|
||||||
|
const errorList = (
|
||||||
|
<ul
|
||||||
|
aria-label={"error list"}
|
||||||
|
style={{
|
||||||
|
margin: "5px 0",
|
||||||
|
paddingLeft: "20px",
|
||||||
|
listStyleType: "disc", // Explicitly set to use bullets (dots)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.errors.map((error, i) => (
|
||||||
|
<li key={i} style={{ display: "list-item" }}>
|
||||||
|
{error}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipHost
|
||||||
|
content={errorList}
|
||||||
|
id={`tooltip-${index}-${column.key}`}
|
||||||
|
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||||
|
>
|
||||||
|
<FontIcon iconName="Error" className={classNames.error} aria-label="error" />
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
} else if (item.numThrottled) {
|
||||||
|
return <FontIcon iconName="Warning" className={classNames.warning} aria-label="warning" />;
|
||||||
|
} else {
|
||||||
|
return <FontIcon iconName="Accept" className={classNames.accept} aria-label="accept" />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "fileName",
|
key: "fileName",
|
||||||
name: "FILE NAME",
|
name: "FILE NAME",
|
||||||
fieldName: "fileName",
|
fieldName: "fileName",
|
||||||
minWidth: 140,
|
minWidth: 120,
|
||||||
maxWidth: 140,
|
maxWidth: 140,
|
||||||
|
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||||
|
const fieldContent = item.fileName;
|
||||||
|
return (
|
||||||
|
<TooltipHost
|
||||||
|
content={fieldContent}
|
||||||
|
id={`tooltip-${index}-${column.key}`}
|
||||||
|
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||||
|
>
|
||||||
|
{fieldContent}
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
name: "STATUS",
|
name: "STATUS",
|
||||||
fieldName: "numSucceeded",
|
fieldName: "numSucceeded",
|
||||||
minWidth: 140,
|
minWidth: 120,
|
||||||
maxWidth: 140,
|
maxWidth: 140,
|
||||||
isRowHeader: true,
|
isRowHeader: true,
|
||||||
isResizable: true,
|
isResizable: true,
|
||||||
data: "string",
|
data: "string",
|
||||||
isPadded: true,
|
isPadded: true,
|
||||||
|
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||||
|
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||||
|
return (
|
||||||
|
<TooltipHost
|
||||||
|
content={fieldContent}
|
||||||
|
id={`tooltip-${index}-${column.key}`}
|
||||||
|
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||||
|
>
|
||||||
|
{fieldContent}
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
|
||||||
let fieldContent: string;
|
|
||||||
const tooltipId = `tooltip-${index}-${column.key}`;
|
|
||||||
|
|
||||||
switch (column.key) {
|
|
||||||
case "status":
|
|
||||||
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
fieldContent = item.fileName;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
|
|
||||||
{fieldContent}
|
|
||||||
</TooltipHost>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
<div className="paneMainContent">
|
<div className="paneMainContent">
|
||||||
@ -121,7 +205,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
|||||||
<DetailsList
|
<DetailsList
|
||||||
items={uploadFileData}
|
items={uploadFileData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onRenderItemColumn={_renderItemColumn}
|
|
||||||
selectionMode={SelectionMode.none}
|
selectionMode={SelectionMode.none}
|
||||||
layoutMode={DetailsListLayoutMode.justified}
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
isHeaderVisible={true}
|
isHeaderVisible={true}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Accordion top class
|
* Accordion top class
|
||||||
*/
|
*/
|
||||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||||
@ -9,7 +9,6 @@ import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUt
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
export interface SplashScreenProps {
|
export interface SplashScreenProps {
|
||||||
@ -186,12 +185,12 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
<div className={styles.footer}>
|
{/* <div className={styles.footer}>
|
||||||
Need help?{" "}
|
Need help?{" "}
|
||||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div> */}
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -43,32 +43,52 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
|||||||
await ensureCloudShellProviderRegistered();
|
await ensureCloudShellProviderRegistered();
|
||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
resolvedRegion = determineCloudShellRegion();
|
||||||
// Ask for user consent for region
|
|
||||||
const consentGranted = await askConfirmation(
|
resolvedRegion = determineCloudShellRegion();
|
||||||
terminal,
|
|
||||||
formatWarningMessage(
|
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||||
"The shell environment may be operating in a region different from that of the database, which could impact performance or data compliance. Do you wish to proceed?",
|
terminal.writeln(
|
||||||
|
formatInfoMessage(
|
||||||
|
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
terminal.writeln(formatInfoMessage("This has two potential implications:"));
|
||||||
|
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
||||||
|
terminal.writeln(
|
||||||
|
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
||||||
|
);
|
||||||
|
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
|
||||||
|
terminal.writeln(
|
||||||
|
formatInfoMessage(
|
||||||
|
" Data processed through this shell could temporarily reside in a different geographic region,",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
terminal.writeln(
|
||||||
|
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
|
||||||
|
);
|
||||||
|
terminal.writeln(formatInfoMessage(" to your organization."));
|
||||||
|
terminal.writeln("");
|
||||||
|
|
||||||
|
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
|
||||||
|
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||||
|
|
||||||
|
// Ask for user consent for region
|
||||||
|
const consentGranted = await askConfirmation(terminal, formatWarningMessage("Do you wish to proceed?"));
|
||||||
|
|
||||||
// Track user decision
|
// Track user decision
|
||||||
TelemetryProcessor.trace(
|
TelemetryProcessor.trace(
|
||||||
Action.CloudShellUserConsent,
|
Action.CloudShellUserConsent,
|
||||||
consentGranted ? ActionModifiers.Success : ActionModifiers.Cancel,
|
consentGranted ? ActionModifiers.Success : ActionModifiers.Cancel,
|
||||||
{ dataExplorerArea: Areas.CloudShell },
|
{
|
||||||
|
dataExplorerArea: Areas.CloudShell,
|
||||||
|
shellType: TerminalKind[shellType],
|
||||||
|
isConsent: consentGranted,
|
||||||
|
region: resolvedRegion,
|
||||||
|
},
|
||||||
|
startKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!consentGranted) {
|
if (!consentGranted) {
|
||||||
TelemetryProcessor.traceCancel(
|
|
||||||
Action.CloudShellTerminalSession,
|
|
||||||
{
|
|
||||||
shellType: TerminalKind[shellType],
|
|
||||||
dataExplorerArea: Areas.CloudShell,
|
|
||||||
region: resolvedRegion,
|
|
||||||
isConsent: false,
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
|
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
|
||||||
);
|
);
|
||||||
@ -262,28 +282,27 @@ export const configureSocketConnection = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sendTerminalStartupCommands = (socket: WebSocket, initCommands: string): void => {
|
export const sendTerminalStartupCommands = (socket: WebSocket, initCommands: string): void => {
|
||||||
|
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 120 minutes.
|
||||||
|
const keepSocketAlive = (socket: WebSocket) => {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
if (pingCount >= MAX_PING_COUNT) {
|
||||||
|
socket.close();
|
||||||
|
} else {
|
||||||
|
pingCount++;
|
||||||
|
// The code uses a recursive setTimeout pattern rather than setInterval,
|
||||||
|
// which ensures each new ping only happens after the previous one completes
|
||||||
|
// and naturally stops if the socket closes.
|
||||||
|
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
socket.send(initCommands);
|
socket.send(initCommands);
|
||||||
|
keepSocketAlive(socket);
|
||||||
} else {
|
} else {
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
socket.send(initCommands);
|
socket.send(initCommands);
|
||||||
|
|
||||||
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 20 minutes.
|
|
||||||
const keepSocketAlive = (socket: WebSocket) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
if (pingCount >= MAX_PING_COUNT) {
|
|
||||||
socket.close();
|
|
||||||
} else {
|
|
||||||
socket.send("");
|
|
||||||
pingCount++;
|
|
||||||
// The code uses a recursive setTimeout pattern rather than setInterval,
|
|
||||||
// which ensures each new ping only happens after the previous one completes
|
|
||||||
// and naturally stops if the socket closes.
|
|
||||||
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
keepSocketAlive(socket);
|
keepSocketAlive(socket);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,12 @@ export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close thi
|
|||||||
* the required methods.
|
* the required methods.
|
||||||
*/
|
*/
|
||||||
export abstract class AbstractShellHandler {
|
export abstract class AbstractShellHandler {
|
||||||
|
/**
|
||||||
|
* The name of the application using this shell handler.
|
||||||
|
* This is used for telemetry and logging purposes.
|
||||||
|
*/
|
||||||
|
protected APP_NAME = "CosmosExplorerTerminal";
|
||||||
|
|
||||||
abstract getShellName(): string;
|
abstract getShellName(): string;
|
||||||
abstract getSetUpCommands(): string[];
|
abstract getSetUpCommands(): string[];
|
||||||
abstract getConnectionCommand(): string;
|
abstract getConnectionCommand(): string;
|
||||||
@ -56,4 +62,30 @@ export abstract class AbstractShellHandler {
|
|||||||
|
|
||||||
return allCommands.join("\n").concat("\n");
|
return allCommands.join("\n").concat("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup commands for MongoDB shell:
|
||||||
|
*
|
||||||
|
* 1. Check if mongosh is already installed
|
||||||
|
* 2. Download mongosh package if not installed
|
||||||
|
* 3. Extract the package to access mongosh binaries
|
||||||
|
* 4. Move extracted files to ~/mongosh directory
|
||||||
|
* 5. Add mongosh binary path to system PATH
|
||||||
|
* 6. Apply PATH changes by sourcing .bashrc
|
||||||
|
*
|
||||||
|
* Each command runs conditionally only if mongosh
|
||||||
|
* is not already present in the environment.
|
||||||
|
*/
|
||||||
|
protected mongoShellSetupCommands(): string[] {
|
||||||
|
const PACKAGE_VERSION: string = "2.5.0";
|
||||||
|
return [
|
||||||
|
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||||
|
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
|
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
|
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
||||||
|
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
|
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||||
|
"source ~/.bashrc",
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ describe("CassandraShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should return correct connection command", () => {
|
test("should return correct connection command", () => {
|
||||||
const expectedCommand = "cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl";
|
const expectedCommand = `cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl`;
|
||||||
|
|
||||||
expect(handler.getConnectionCommand()).toBe(expectedCommand);
|
expect(handler.getConnectionCommand()).toBe(expectedCommand);
|
||||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-endpoint.cassandra.cosmos.azure.com:443/");
|
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-endpoint.cassandra.cosmos.azure.com:443/");
|
||||||
|
@ -68,7 +68,7 @@ describe("MongoShellHandler", () => {
|
|||||||
const commands = mongoShellHandler.getSetUpCommands();
|
const commands = mongoShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(6);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -91,7 +91,7 @@ describe("MongoShellHandler", () => {
|
|||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe(
|
expect(command).toBe(
|
||||||
"mongosh --host test-mongo.documents.azure.com --port 10255 --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||||
);
|
);
|
||||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ import { userContext } from "../../../../UserContext";
|
|||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
import { getHostFromUrl } from "../Utils/CommonUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
|
|
||||||
const PACKAGE_VERSION: string = "2.5.0";
|
|
||||||
|
|
||||||
export class MongoShellHandler extends AbstractShellHandler {
|
export class MongoShellHandler extends AbstractShellHandler {
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
@ -18,14 +16,7 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSetUpCommands(): string[] {
|
public getSetUpCommands(): string[] {
|
||||||
return [
|
return this.mongoShellSetupCommands();
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
|
||||||
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
|
||||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
"source ~/.bashrc",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConnectionCommand(): string {
|
public getConnectionCommand(): string {
|
||||||
@ -37,9 +28,17 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "echo 'Database name not found.'";
|
return "echo 'Database name not found.'";
|
||||||
}
|
}
|
||||||
return `mongosh --host ${getHostFromUrl(this._endpoint)} --port 10255 --username ${dbName} --password ${
|
return (
|
||||||
this._key
|
"mongosh mongodb://" +
|
||||||
} --tls --tlsAllowInvalidCertificates`;
|
getHostFromUrl(this._endpoint) +
|
||||||
|
":10255?appName=" +
|
||||||
|
this.APP_NAME +
|
||||||
|
" --username " +
|
||||||
|
dbName +
|
||||||
|
" --password " +
|
||||||
|
this._key +
|
||||||
|
" --tls --tlsAllowInvalidCertificates"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string {
|
||||||
|
@ -54,7 +54,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
|
|||||||
// All Azure Cosmos DB PostgreSQL deployments follow this convention.
|
// All Azure Cosmos DB PostgreSQL deployments follow this convention.
|
||||||
// Ref. https://learn.microsoft.com/en-us/azure/cosmos-db/postgresql/reference-limits#database-creation
|
// Ref. https://learn.microsoft.com/en-us/azure/cosmos-db/postgresql/reference-limits#database-creation
|
||||||
const loginName = userContext.postgresConnectionStrParams.adminLogin;
|
const loginName = userContext.postgresConnectionStrParams.adminLogin;
|
||||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require`;
|
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string {
|
||||||
|
@ -44,7 +44,7 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
const commands = vcoreMongoShellHandler.getSetUpCommands();
|
const commands = vcoreMongoShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(6);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||||
expect(commands[0]).toContain("mongosh not found");
|
expect(commands[0]).toContain("mongosh not found");
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
|
|
||||||
const PACKAGE_VERSION: string = "2.5.0";
|
|
||||||
|
|
||||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
|
||||||
@ -15,28 +13,8 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
|||||||
return "MongoDB VCore";
|
return "MongoDB VCore";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup commands for MongoDB VCore shell:
|
|
||||||
*
|
|
||||||
* 1. Check if mongosh is already installed
|
|
||||||
* 2. Download mongosh package if not installed
|
|
||||||
* 3. Extract the package to access mongosh binaries
|
|
||||||
* 4. Move extracted files to ~/mongosh directory
|
|
||||||
* 5. Add mongosh binary path to system PATH
|
|
||||||
* 6. Apply PATH changes by sourcing .bashrc
|
|
||||||
*
|
|
||||||
* Each command runs conditionally only if mongosh
|
|
||||||
* is not already present in the environment.
|
|
||||||
*/
|
|
||||||
public getSetUpCommands(): string[] {
|
public getSetUpCommands(): string[] {
|
||||||
return [
|
return this.mongoShellSetupCommands();
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
|
||||||
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
|
||||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
"source ~/.bashrc",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConnectionCommand(): string {
|
public getConnectionCommand(): string {
|
||||||
@ -45,7 +23,7 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
||||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"`;
|
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AbstractShellHandler } from "Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler";
|
|
||||||
import { IDisposable, ITerminalAddon, Terminal } from "@xterm/xterm";
|
import { IDisposable, ITerminalAddon, Terminal } from "@xterm/xterm";
|
||||||
|
import { AbstractShellHandler } from "../ShellTypes/AbstractShellHandler";
|
||||||
|
import { formatErrorMessage } from "./TerminalLogFormats";
|
||||||
|
|
||||||
interface IAttachOptions {
|
interface IAttachOptions {
|
||||||
bidirectional?: boolean;
|
bidirectional?: boolean;
|
||||||
@ -56,8 +57,27 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
this._disposables.push(terminal.onBinary((data) => this._sendBinary(data)));
|
this._disposables.push(terminal.onBinary((data) => this._sendBinary(data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._disposables.push(addSocketListener(this._socket, "close", () => this.dispose()));
|
this._disposables.push(addSocketListener(this._socket, "close", () => this._handleSocketClose(terminal)));
|
||||||
this._disposables.push(addSocketListener(this._socket, "error", () => this.dispose()));
|
this._disposables.push(addSocketListener(this._socket, "error", () => this._handleSocketClose(terminal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles socket close events by terminating processes and showing a message
|
||||||
|
*/
|
||||||
|
private _handleSocketClose(terminal: Terminal): void {
|
||||||
|
if (terminal) {
|
||||||
|
terminal.writeln(
|
||||||
|
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send exit command to terminal
|
||||||
|
if (this._bidirectional) {
|
||||||
|
terminal.write(formatErrorMessage("exit\r\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
this.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -721,7 +721,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
} else if (result.statusCode >= 400) {
|
} else if (result.statusCode >= 400) {
|
||||||
newFailed.push(result.documentId);
|
newFailed.push(result.documentId);
|
||||||
logConsoleError(
|
logConsoleError(
|
||||||
`Failed to delete document ${result.documentId.id} with status code ${result.statusCode}`,
|
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1119,6 +1119,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
stats.resources.push(response.resourceBody);
|
stats.resources.push(response.resourceBody);
|
||||||
} else if (response.statusCode === 429) {
|
} else if (response.statusCode === 429) {
|
||||||
documentsToAttempt.push(attemptedDocuments[index]);
|
documentsToAttempt.push(attemptedDocuments[index]);
|
||||||
|
} else if (response.statusCode === 409) {
|
||||||
|
stats.numFailed++;
|
||||||
|
stats.errors.push(`Document with id ${attemptedDocuments[index].id} already exists.`);
|
||||||
} else {
|
} else {
|
||||||
stats.numFailed++;
|
stats.numFailed++;
|
||||||
stats.errors.push(JSON.stringify(response.resourceBody));
|
stats.errors.push(JSON.stringify(response.resourceBody));
|
||||||
|
@ -124,7 +124,7 @@ export const extractPartitionKeyValues = (
|
|||||||
documentContent: any,
|
documentContent: any,
|
||||||
partitionKeyDefinition: PartitionKeyDefinition,
|
partitionKeyDefinition: PartitionKeyDefinition,
|
||||||
): PartitionKey[] => {
|
): PartitionKey[] => {
|
||||||
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0 || partitionKeyDefinition.systemKey) {
|
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export const extractPartitionKeyValues = (
|
|||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
partitionKeyValues.push(value);
|
partitionKeyValues.push(value);
|
||||||
} else {
|
} else if (!partitionKeyDefinition.systemKey) {
|
||||||
partitionKeyValues.push({});
|
partitionKeyValues.push({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user