diff --git a/.eslintignore b/.eslintignore index c316cc298..c421dd2c0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -35,7 +35,6 @@ src/Definitions/svg.d.ts src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts - src/Explorer/Controls/Editor/EditorComponent.ts src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -79,17 +78,9 @@ src/Explorer/Tables/DataTable/DataTableBindingManager.ts src/Explorer/Tables/DataTable/DataTableBuilder.ts src/Explorer/Tables/DataTable/DataTableContextMenu.ts src/Explorer/Tables/DataTable/DataTableOperationManager.ts -src/Explorer/Tables/DataTable/DataTableOperations.ts src/Explorer/Tables/DataTable/DataTableViewModel.ts -src/Explorer/Tables/DataTable/TableCommands.ts -src/Explorer/Tables/DataTable/TableEntityCache.ts src/Explorer/Tables/DataTable/TableEntityListViewModel.ts -src/Explorer/Tables/Entities.ts -src/Explorer/Tables/QueryBuilder/ClauseGroup.ts -src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts -src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts -src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts src/Explorer/Tables/TableDataClient.ts src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts @@ -113,15 +104,10 @@ src/Explorer/Tree/ObjectId.ts src/Explorer/Tree/ResourceTokenCollection.ts src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/TreeComponents.ts -src/Explorer/Tree/Trigger.ts src/Explorer/WaitsForTemplateViewModel.ts src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.ts -src/GitHub/GitHubConnector.ts -src/GitHub/GitHubOAuthService.ts src/Index.ts -src/Juno/JunoClient.test.ts -src/Juno/JunoClient.ts src/Platform/Hosted/Authorization.ts src/ReactDevTools.ts src/Shared/Constants.ts @@ -137,15 +123,12 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx -src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx -src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx src/Explorer/Notebook/NotebookComponent/contents/index.tsx -src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx diff --git a/.eslintrc.js b/.eslintrc.js index f32961a91..22f294717 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,7 +39,6 @@ module.exports = { "@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-extraneous-class": "error", - "no-null/no-null": "error", "@typescript-eslint/no-explicit-any": "error", "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], eqeqeq: "error", diff --git a/.vscode/settings.json b/.vscode/settings.json index ba70984cf..0716cae8f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.organizeImports": true - } + }, + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/images/close-black.svg b/images/close-black.svg index 9b87397e1..a17e75636 100644 --- a/images/close-black.svg +++ b/images/close-black.svg @@ -1,16 +1,8 @@ - - + \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 889b544e0..e08536252 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,8 +37,8 @@ module.exports = { global: { branches: 25, functions: 25, - lines: 30, - statements: 30, + lines: 29, + statements: 29, }, }, @@ -129,6 +129,8 @@ module.exports = { // The test environment that will be used for testing // testEnvironment: "jest-environment-jsdom", + modulePaths: ["node_modules", "/src"], + // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, diff --git a/less/documentDB.less b/less/documentDB.less index 271f53992..79e9435c3 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2357,6 +2357,8 @@ a:link { height: 100%; flex-grow: 1; overflow: hidden; + min-height: 300px; + overflow-y: scroll; } .tabs { @@ -2832,6 +2834,8 @@ a:link { #explorerNotificationConsole { z-index: 1000; + overflow-y: auto; + overflow-x: clip; } .uniqueIndexesContainer { diff --git a/src/Common/CollapsedResourceTree.tsx b/src/Common/CollapsedResourceTree.tsx index 8a91fd5d3..9a9e1661d 100644 --- a/src/Common/CollapsedResourceTree.tsx +++ b/src/Common/CollapsedResourceTree.tsx @@ -1,6 +1,7 @@ -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import { userContext } from "../UserContext"; +import { NormalizedEventKey } from "./Constants"; export interface CollapsedResourceTreeProps { toggleLeftPaneExpanded: () => void; @@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent { + const focusButton = useRef() as MutableRefObject; + + useEffect(() => { + if (focusButton.current) { + focusButton.current.focus(); + } + }); + + const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + toggleLeftPaneExpanded(); + event.stopPropagation(); + } + }; + return (
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent - + Expand - + {userContext.apiType} API diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 293e1b3e8..f33c2650a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -96,6 +96,7 @@ export class Flights { public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; + public static readonly Phoenix = "phoenix"; } export class AfecFeatures { @@ -337,6 +338,14 @@ export enum ConflictOperationType { Delete = "delete", } +export enum ConnectionStatusType { + Connect = "Connect", + Connecting = "Connecting", + Connected = "Connected", + Failed = "Connection Failed", + ReConnect = "Reconnect", +} + export const EmulatorMasterKey = //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; @@ -346,15 +355,32 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta export class Notebook { public static readonly defaultBasePath = "./notebooks"; - public static readonly heartbeatDelayMs = 5000; + public static readonly heartbeatDelayMs = 60000; public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartMaxDelayMs = 20000; public static readonly autoSaveIntervalMs = 120000; + public static readonly memoryGuageToGB = 1048576; public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it."; public static readonly mongoShellTemporarilyDownMsg = "We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation."; public static readonly cassandraShellTemporarilyDownMsg = "We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation."; + public static saveNotebookModalTitle = "Save Notebook in temporary workspace"; + public static saveNotebookModalContent = + "This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends."; + public static newNotebookModalTitle = "Create Notebook in temporary workspace"; + public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace"; + public static newNotebookModalContent1 = + "A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed."; + public static newNotebookModalContent2 = + "To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. "; + public static galleryNotebookDownloadContent1 = + "To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed."; + public static galleryNotebookDownloadContent2 = + "To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. "; + public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits"; + public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github"; + public static learnMore = "Learn more."; } export class SparkLibrary { diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 3a5a02365..1c49141a0 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { updateUserContext } from "../UserContext"; -import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; +import { + deleteDocument, + getEndpoint, + getFeatureEndpointOrDefault, + queryDocuments, + readDocument, + updateDocument, +} from "./MongoProxyClient"; const databaseId = "testDB"; @@ -246,4 +254,31 @@ describe("MongoProxyClient", () => { expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); }); }); + describe("getFeatureEndpointOrDefault", () => { + beforeEach(() => { + resetConfigContext(); + updateConfigContext({ + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + }); + const params = new URLSearchParams({ + "feature.mongoProxyEndpoint": "https://localhost:12901", + "feature.mongoProxyAPIs": "readDocument|createDocument", + }); + const features = extractFeatures(params); + updateUserContext({ + authType: AuthType.AAD, + features: features, + }); + }); + + it("returns a local endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("readDocument"); + expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); + }); + + it("returns a production endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); + }); + }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2945f1288..668a0ab16 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; @@ -78,7 +79,7 @@ export function queryDocuments( : "", }; - const endpoint = getEndpoint() || ""; + const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; const headers = { ...defaultHeaders, @@ -141,7 +142,8 @@ export function readDocument( : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("readDocument"); + return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { method: "GET", @@ -181,7 +183,7 @@ export function createDocument( pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createDocument"); return window .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { @@ -225,7 +227,7 @@ export function updateDocument( ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("updateDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy( autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); return window .fetch( @@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy( }); } -export function getEndpoint(): string { - let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer"; +export function getFeatureEndpointOrDefault(feature: string): string { + return hasFlag(userContext.features.mongoProxyAPIs, feature) + ? getEndpoint(userContext.features.mongoProxyEndpoint) + : getEndpoint(); +} + +export function getEndpoint(customEndpoint?: string): string { + let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + url += "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { url = url.replace("api/mongo", "api/guest/mongo"); diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index 18a769b12..00693a2c0 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import refreshImg from "../../images/refresh-cosmos.svg"; import { AuthType } from "../AuthType"; @@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer"; import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; +import { NormalizedEventKey } from "./Constants"; export interface ResourceTreeContainerProps { toggleLeftPaneExpanded: () => void; @@ -18,6 +19,22 @@ export const ResourceTreeContainer: FunctionComponent { + const focusButton = useRef() as MutableRefObject; + + useEffect(() => { + if (isLeftPaneExpanded) { + if (focusButton.current) { + focusButton.current.focus(); + } + } + }); + + const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + toggleLeftPaneExpanded(); + event.stopPropagation(); + } + }; return (
{/* Collections Window - - Start */} @@ -43,9 +60,11 @@ export const ResourceTreeContainer: FunctionComponent Hide diff --git a/src/Common/Tooltip/InfoTooltip.tsx b/src/Common/Tooltip/InfoTooltip.tsx index 480aa9020..3ce33ca93 100644 --- a/src/Common/Tooltip/InfoTooltip.tsx +++ b/src/Common/Tooltip/InfoTooltip.tsx @@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent = ({ children }: return ( - + ); diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index efd5ffb78..a8957c662 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,3 +1,5 @@ +import { ConnectionStatusType } from "../Common/Constants"; + export interface DatabaseAccount { id: string; name: string; @@ -496,3 +498,8 @@ export interface MemoryUsageInfo { freeKB: number; totalKB: number; } + +export interface ContainerConnectionInfo { + status: ConnectionStatusType; + //need to add ram and rom info +} diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index d662db58b..76d710cb7 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -1,5 +1,6 @@ import { Icon, Label, Stack } from "@fluentui/react"; import * as React from "react"; +import { NormalizedEventKey } from "../../../Common/Constants"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; export interface CollapsibleSectionProps { @@ -30,6 +31,13 @@ export class CollapsibleSectionComponent extends React.Component { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + this.toggleCollapsed(); + event.stopPropagation(); + } + }; + public render(): JSX.Element { return ( <> @@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component diff --git a/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap b/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap index 95d3c46bf..675a79239 100644 --- a/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap +++ b/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap @@ -3,9 +3,14 @@ exports[`CollapsibleSectionComponent renders 1`] = ` void, cancelLabel: string, onCancel: () => void, + contentHtml?: JSX.Element, choiceGroupProps?: IChoiceGroupProps, textFieldProps?: TextFieldProps, primaryButtonDisabled?: boolean @@ -58,6 +60,7 @@ export const useDialog: UseStore = create((set, get) => ({ onOk: () => void, cancelLabel: string, onCancel: () => void, + contentHtml?: JSX.Element, choiceGroupProps?: IChoiceGroupProps, textFieldProps?: TextFieldProps, primaryButtonDisabled?: boolean @@ -76,6 +79,7 @@ export const useDialog: UseStore = create((set, get) => ({ get().closeDialog(); onCancel && onCancel(); }, + contentHtml, choiceGroupProps, textFieldProps, primaryButtonDisabled, @@ -124,6 +128,7 @@ export interface DialogProps { type?: DialogType; showCloseButton?: boolean; onDismiss?: () => void; + contentHtml?: JSX.Element; } const DIALOG_MIN_WIDTH = "400px"; @@ -150,6 +155,7 @@ export const Dialog: FC = () => { type, showCloseButton, onDismiss, + contentHtml, } = props || {}; const dialogProps: IDialogProps = { @@ -181,8 +187,7 @@ export const Dialog: FC = () => { text: secondaryButtonText, onClick: onSecondaryButtonClick, } - : {}; - + : undefined; return visible ? ( {choiceGroupProps && } @@ -192,6 +197,7 @@ export const Dialog: FC = () => { {linkProps.linkText} )} + {contentHtml && {contentHtml}} {progressIndicatorProps && } diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less index 1d68e3b7e..082fd937f 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less @@ -8,6 +8,9 @@ .input-type-head-text-field { width: 100%; } + .input-query-form { + width: 100%; + } textarea { width: 100%; line-height: 1; diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 2e9a2d104..13b91012a 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -160,18 +160,21 @@ export class InputTypeaheadComponent extends React.Component< return (
- this.setState({ isSuggestionVisible: true })} - onChange={(_event, newValue?: string) => this.handleChange(newValue)} - /> +
+ this.setState({ isSuggestionVisible: true })} + onChange={(_event, newValue?: string) => this.handleChange(newValue)} + /> + {this.props.showCancelButton && ( 1`] = ` - +
+ +
`; @@ -28,16 +34,22 @@ exports[`inputTypeahead renders