From 5a5bf34d4d7b32416808ba5503517f420507ac33 Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Fri, 19 Jan 2024 07:08:11 -0800 Subject: [PATCH 001/102] Update logic for NPS survey for existing accounts > 90 days (#1725) * Update logic for NPS survey for existing accounts > 90 days * Remove lint error * Address comments * Fix error in code --- src/Explorer/Explorer.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 3c34d77db..e58602cfd 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -277,10 +277,6 @@ export default class Explorer { const NINETY_DAYS_IN_MS = 7776000000; const ONE_DAY_IN_MS = 86400000; const THREE_DAYS_IN_MS = 259200000; - const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs( - userContext.databaseAccount?.systemData?.createdAt || "", - NINETY_DAYS_IN_MS, - ); const lastSubmitted: string = localStorage.getItem("lastSubmitted"); if (lastSubmitted !== null) { @@ -302,17 +298,11 @@ export default class Explorer { this.sendNPSMessage(); } } else { - // An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer. + // Show survey when an existing account is older than 3 days if ( - !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) && - isAccountNewerThanNinetyDays + !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) ) { this.sendNPSMessage(); - } else { - // An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer. - if (this.getRandomInt(100) < 33) { - this.sendNPSMessage(); - } } } } From 70635e426f44f6a4ec64877ef672e10f68ef242a Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:37:17 -0600 Subject: [PATCH 002/102] Fix the teaching bubble popup and enable copilot card (#1722) * Fix the teaching bubble popup and enable copilot card * add close copilot button title * fix compilation --- .../Panes/SettingsPane/SettingsPane.tsx | 2 +- .../QueryCopilot/QueryCopilotContext.tsx | 3 +++ .../QueryCopilot/QueryCopilotPromptbar.tsx | 23 +++++++++++++------ src/Explorer/QueryCopilot/QueryCopilotTab.tsx | 4 ++-- .../Shared/QueryCopilotResults.tsx | 8 +++++-- src/Explorer/SplashScreen/SplashScreen.tsx | 2 +- src/hooks/useQueryCopilot.ts | 4 +++- 7 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index ef25a7d73..ca60458f5 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -234,7 +234,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const handleSampleDatabaseChange = async (ev: React.MouseEvent, checked?: boolean): Promise => { setCopilotSampleDBEnabled(checked); useQueryCopilot.getState().setCopilotSampleDBEnabled(checked); - setRefreshExplorer(!refreshExplorer); + setRefreshExplorer(false); }; const choiceButtonStyles = { diff --git a/src/Explorer/QueryCopilot/QueryCopilotContext.tsx b/src/Explorer/QueryCopilot/QueryCopilotContext.tsx index 1ec2530d5..e1a5106ea 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotContext.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotContext.tsx @@ -30,6 +30,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme queryResults: undefined, errorMessage: "", isSamplePromptsOpen: false, + showPromptTeachingBubble: true, showDeletePopup: false, showFeedbackBar: false, showCopyPopup: false, @@ -65,6 +66,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }), setErrorMessage: (errorMessage: string) => set({ errorMessage }), setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }), + setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }), setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }), setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }), setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }), @@ -103,6 +105,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme queryResults: undefined, errorMessage: "", isSamplePromptsOpen: false, + showPromptTeachingBubble: true, showDeletePopup: false, showFeedbackBar: false, showCopyPopup: false, diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index d3ed8b24c..acb839623 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -18,7 +18,6 @@ import { Text, TextField, } from "@fluentui/react"; -import { useBoolean } from "@fluentui/react-hooks"; import { HttpStatusCodes } from "Common/Constants"; import { handleError } from "Common/ErrorHandlingUtils"; import { createUri } from "Common/UrlUtility"; @@ -71,7 +70,7 @@ export const QueryCopilotPromptbar: React.FC = ({ databaseId, containerId, }: QueryCopilotPromptProps): JSX.Element => { - const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); + const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState(false); const inputEdited = useRef(false); const { openFeedbackModal, @@ -94,6 +93,8 @@ export const QueryCopilotPromptbar: React.FC = ({ setIsSamplePromptsOpen, showSamplePrompts, setShowSamplePrompts, + showPromptTeachingBubble, + setShowPromptTeachingBubble, showDeletePopup, setShowDeletePopup, showFeedbackBar, @@ -272,16 +273,23 @@ export const QueryCopilotPromptbar: React.FC = ({ }; const showTeachingBubble = (): void => { - if (!inputEdited.current) { + if (showPromptTeachingBubble && !inputEdited.current) { setTimeout(() => { if (!inputEdited.current && !isWelcomModalVisible()) { - toggleCopilotTeachingBubbleVisible(); + setCopilotTeachingBubbleVisible(true); inputEdited.current = true; } }, 30000); + } else { + toggleCopilotTeachingBubbleVisible(false); } }; + const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => { + setCopilotTeachingBubbleVisible(visible); + setShowPromptTeachingBubble(visible); + }; + const isWelcomModalVisible = (): boolean => { return localStorage.getItem("hideWelcomeModal") !== "true"; }; @@ -340,6 +348,7 @@ export const QueryCopilotPromptbar: React.FC = ({ }, }} ariaLabel="Close" + title="Close copilot" /> @@ -364,13 +373,13 @@ export const QueryCopilotPromptbar: React.FC = ({ placeholder="Ask a question in natural language and we’ll generate the query for you." aria-labelledby="copilot-textfield-label" /> - {copilotTeachingBubbleVisible && ( + {showPromptTeachingBubble && copilotTeachingBubbleVisible && ( toggleCopilotTeachingBubbleVisible(false)} hasSmallHeadline={true} headline="Write a prompt" > @@ -378,7 +387,7 @@ export const QueryCopilotPromptbar: React.FC = ({ { setShowSamplePrompts(true); - toggleCopilotTeachingBubbleVisible(); + toggleCopilotTeachingBubbleVisible(false); }} style={{ color: "white", fontWeight: 600 }} > diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index c6e787ca0..240881310 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -10,7 +10,7 @@ import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotCl import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { userContext } from "UserContext"; -import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { useSidePanel } from "hooks/useSidePanel"; import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs"; import React, { useState } from "react"; @@ -37,7 +37,7 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query const executeQueryBtn = { iconSrc: ExecuteQueryIcon, iconAlt: executeQueryBtnLabel, - onCommandClick: () => OnExecuteQueryClick(useQueryCopilot), + onCommandClick: () => OnExecuteQueryClick(useQueryCopilot as Partial), commandButtonLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel, hasPopup: false, diff --git a/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx b/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx index 8d872e174..3b552cfa8 100644 --- a/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx +++ b/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx @@ -1,6 +1,6 @@ import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; -import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import React from "react"; export const QueryCopilotResults: React.FC = (): JSX.Element => { @@ -12,7 +12,11 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => { queryResults={useQueryCopilot.getState().queryResults} isExecuting={useQueryCopilot.getState().isExecuting} executeQueryDocumentsPage={(firstItemIndex: number) => - QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot) + QueryDocumentsPerPage( + firstItemIndex, + useQueryCopilot.getState().queryIterator, + useQueryCopilot as Partial, + ) } /> ); diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index ce5385329..aa7e7dcd9 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -148,7 +148,7 @@ export class SplashScreen extends React.Component { /> - {useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && ( + {useQueryCopilot.getState().copilotEnabled && ( void; setErrorMessage: (errorMessage: string) => void; setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void; + setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => void; setShowDeletePopup: (showDeletePopup: boolean) => void; setShowFeedbackBar: (showFeedbackBar: boolean) => void; setshowCopyPopup: (showCopyPopup: boolean) => void; @@ -93,7 +95,7 @@ export interface QueryCopilotState { resetQueryCopilotStates: () => void; } -type QueryCopilotStore = UseStore; +type QueryCopilotStore = UseStore>; export const useQueryCopilot: QueryCopilotStore = create((set) => ({ copilotEnabled: false, From 323305e4851e4cc05eaba829348ffc42ab3a0e7d Mon Sep 17 00:00:00 2001 From: MokireddySampath <120497218+MokireddySampath@users.noreply.github.com> Date: Sat, 20 Jan 2024 09:17:47 +0530 Subject: [PATCH 003/102] state of the buttons will now be updated by screen reader (#1716) --- .../QueryCopilot/QueryCopilotPromptbar.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index acb839623..b4e5c80a0 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -543,6 +543,7 @@ export const QueryCopilotPromptbar: React.FC = ({ Provide feedback on the query generated {showCallout && !hideFeedbackModalForLikedQueries && ( { @@ -578,11 +579,19 @@ export const QueryCopilotPromptbar: React.FC = ({ { setShowCallout(!likeQuery); setLikeQuery(!likeQuery); + if (likeQuery === true) { + document.getElementById("likeStatus").innerHTML = "Unpressed"; + } + if (likeQuery === false) { + document.getElementById("likeStatus").innerHTML = "Liked"; + } if (dislikeQuery) { setDislikeQuery(!dislikeQuery); } @@ -590,17 +599,25 @@ export const QueryCopilotPromptbar: React.FC = ({ /> { + let toggleStatusValue = "Unpressed"; if (!dislikeQuery) { openFeedbackModal(generatedQuery, false, userPrompt); setLikeQuery(false); + toggleStatusValue = "Disliked"; } setDislikeQuery(!dislikeQuery); setShowCallout(false); + document.getElementById("likeStatus").innerHTML = toggleStatusValue; }} aria-label="Dislike" /> + + + Date: Tue, 23 Jan 2024 00:08:27 +0530 Subject: [PATCH 004/102] =?UTF-8?q?role=20of=20heading=20has=20been=20adde?= =?UTF-8?q?d=20to=20the=20text=20that=20is=20visually=20appearing=E2=80=A6?= =?UTF-8?q?=20(#1701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * role of heading has been added to the text that is visually appearing as heading * Update WelcomeModal.test.tsx.snap --- src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx | 4 +++- .../Modal/__snapshots__/WelcomeModal.test.tsx.snap | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx index 8c22cec43..94316a0cc 100644 --- a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx +++ b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx @@ -50,7 +50,9 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => - Welcome to Microsoft Copilot for Azure in Cosmos DB + + Welcome to Microsoft Copilot for Azure in Cosmos DB (preview) + diff --git a/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap b/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap index 324542a15..a9fdcc4d6 100644 --- a/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap @@ -67,9 +67,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is } > - Welcome to Microsoft Copilot for Azure in Cosmos DB + Welcome to Microsoft Copilot for Azure in Cosmos DB (preview) Date: Tue, 23 Jan 2024 13:28:13 +0530 Subject: [PATCH 005/102] Empty coumnheader has been given the icon with name of description (#1718) --- src/Explorer/Tabs/QueryTab/QueryResultSection.tsx | 11 +++++++---- src/Explorer/Tabs/QueryTab/QueryTabComponent.less | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx index 1872a8750..fc93d98a1 100644 --- a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx @@ -3,13 +3,13 @@ import { DetailsListLayoutMode, IColumn, Icon, + IconButton, Link, Pivot, PivotItem, SelectionMode, Stack, Text, - IconButton, TooltipHost, } from "@fluentui/react"; import { HttpHeaders, NormalizedEventKey } from "Common/Constants"; @@ -18,15 +18,15 @@ import { QueryMetrics } from "Contracts/DataModels"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { userContext } from "UserContext"; +import copy from "clipboard-copy"; import { useNotificationConsole } from "hooks/useNotificationConsole"; import React from "react"; +import CopilotCopy from "../../../../images/CopilotCopy.svg"; import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg"; import QueryEditorNext from "../../../../images/Query-Editor-Next.svg"; import RunQuery from "../../../../images/RunQuery.png"; import InfoColor from "../../../../images/info_color.svg"; import { QueryResults } from "../../../Contracts/ViewModels"; -import copy from "clipboard-copy"; -import CopilotCopy from "../../../../images/CopilotCopy.svg"; interface QueryResultProps { isMongoDB: boolean; @@ -62,9 +62,12 @@ export const QueryResultSection: React.FC = ({ const columns: IColumn[] = [ { key: "column1", - name: "", + name: "Description", + iconName: "Info", + isIconOnly: true, minWidth: 10, maxWidth: 12, + iconClassName: "iconheadercell", data: String, fieldName: "", onRender: (item: IDocument) => { diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less index 13daf455c..b0ae00360 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less @@ -91,9 +91,6 @@ div[role="tabpanel"] { height: 100%; - div:nth-child(1) { - height: 100%; - } } .result-metadata { @@ -283,3 +280,6 @@ } } } +.iconheadercell { + font-size: 12px; +} From e207f3702bf1c6e52cb60fcacd90ba4266bab49b Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:46:28 -0800 Subject: [PATCH 006/102] Add more logs for NPS (#1729) --- src/Explorer/Explorer.tsx | 27 +++++++++++++++++++ .../QueryCopilot/QueryCopilotPromptbar.tsx | 4 +-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index e58602cfd..fd0d50bfb 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -278,16 +278,31 @@ export default class Explorer { const ONE_DAY_IN_MS = 86400000; const THREE_DAYS_IN_MS = 259200000; const lastSubmitted: string = localStorage.getItem("lastSubmitted"); + Logger.logInfo(`NPS Survey last shown date: ${lastSubmitted}`, "Explorer/openNPSSurveyDialog"); if (lastSubmitted !== null) { + Logger.logInfo(`NPS Survey last shown is not empty ${lastSubmitted}`, "Explorer/openNPSSurveyDialog"); + let lastSubmittedDate: number = parseInt(lastSubmitted); + Logger.logInfo(`NPS Survey last shown is parsed ${lastSubmittedDate.toString()}`, "Explorer/openNPSSurveyDialog"); + if (isNaN(lastSubmittedDate)) { + Logger.logInfo( + `NPS Survey last shown is not a number ${lastSubmittedDate.toString()}`, + "Explorer/openNPSSurveyDialog", + ); lastSubmittedDate = 0; } const nowMs: number = Date.now(); + Logger.logInfo(`NPS Survey current date ${nowMs.toString()}`, "Explorer/openNPSSurveyDialog"); + const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate; if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) { + Logger.logInfo( + `NPS Survey last shown is less than ninety days ${millisecsSinceLastSubmitted.toString()}`, + "Explorer/openNPSSurveyDialog", + ); return; } } @@ -295,6 +310,10 @@ export default class Explorer { // Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer. if (userContext.isTryCosmosDBSubscription) { if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) { + Logger.logInfo( + `Displaying NPS Survey for Try Cosmos DB ${userContext.apiType}`, + "Explorer/openNPSSurveyDialog", + ); this.sendNPSMessage(); } } else { @@ -302,6 +321,10 @@ export default class Explorer { if ( !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) ) { + Logger.logInfo( + `Displaying NPS Survey for users with existing ${userContext.apiType} account older than 3 days`, + "Explorer/openNPSSurveyDialog", + ); this.sendNPSMessage(); } } @@ -309,6 +332,10 @@ export default class Explorer { private sendNPSMessage() { sendMessage({ type: MessageTypes.DisplayNPSSurvey }); + Logger.logInfo( + `NPS Survey logging current date when survey is shown ${Date.now().toString()}`, + "Explorer/openNPSSurveyDialog", + ); localStorage.setItem("lastSubmitted", Date.now().toString()); } diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index b4e5c80a0..557aee3d6 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -579,10 +579,9 @@ export const QueryCopilotPromptbar: React.FC = ({ { setShowCallout(!likeQuery); setLikeQuery(!likeQuery); @@ -613,7 +612,6 @@ export const QueryCopilotPromptbar: React.FC = ({ setShowCallout(false); document.getElementById("likeStatus").innerHTML = toggleStatusValue; }} - aria-label="Dislike" /> From dbb0324a64f6099300e9ca0c4eaf549d164fd289 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 30 Jan 2024 16:21:29 -0500 Subject: [PATCH 007/102] RU Threshold (#1728) * ru threshold beta * use new ru threshold package * fix typo * fix merge issue * fix package-lock.json * fix test * fixed settings pane test * fixed merge issue * sync with main * fixed settings pane check * fix checks * fixed aria-label error * fixed aria-label error * fixed aria-label error * fixed aria-label error * remove learn more --------- Co-authored-by: Asier Isayas --- package-lock.json | 18 +-- package.json | 2 +- src/Common/IteratorUtilities.ts | 11 +- src/Common/dataAccess/queryDocumentsPage.ts | 4 +- .../Panes/SettingsPane/SettingsPane.tsx | 145 ++++++++++++------ .../__snapshots__/SettingsPane.test.tsx.snap | 68 ++++++++ .../Tabs/QueryTab/QueryTabComponent.tsx | 18 ++- src/Explorer/Tabs/Tabs.tsx | 17 +- src/Explorer/Tabs/TriggerTabContent.tsx | 2 +- src/Shared/StorageUtility.ts | 27 ++++ 10 files changed, 250 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd4ba95b8..d3a36e3f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.0", + "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", "@azure/ms-rest-nodeauth": "3.0.7", @@ -396,9 +396,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/cosmos": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.0.tgz", - "integrity": "sha512-/Z27p1+FTkmjmm8jk90zi/HrczPHw2t8WecFnsnTe4xGocWl0Z4clP0YlLUTJPhRLWYa5upwD9rMvKJkS1f1kg==", + "version": "4.0.1-beta.2", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.2.tgz", + "integrity": "sha512-iuqg/QwLQlxgRi4pnXU8JUYv+f24wkRvJ9ZZI4/sYk+DxSgkuQ194Cc2IpckpeO8z7ZpcBkVQFa82wcZVVZ8Zg==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -408,14 +408,14 @@ "fast-json-stable-stringify": "^2.1.0", "jsbi": "^3.1.3", "node-abort-controller": "^3.0.0", - "priorityqueuejs": "^1.0.0", + "priorityqueuejs": "^2.0.0", "semaphore": "^1.0.5", "tslib": "^2.2.0", "universal-user-agent": "^6.0.0", "uuid": "^8.3.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/cosmos-language-service": { @@ -33707,9 +33707,9 @@ } }, "node_modules/priorityqueuejs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", - "integrity": "sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==" }, "node_modules/prismjs": { "version": "1.29.0", diff --git a/package.json b/package.json index 3f4252d56..8af052a64 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.0", + "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", "@azure/ms-rest-nodeauth": "3.0.7", diff --git a/src/Common/IteratorUtilities.ts b/src/Common/IteratorUtilities.ts index f85ad7fb2..6283488b8 100644 --- a/src/Common/IteratorUtilities.ts +++ b/src/Common/IteratorUtilities.ts @@ -1,3 +1,4 @@ +import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../Contracts/ViewModels"; interface QueryResponse { @@ -10,13 +11,17 @@ interface QueryResponse { } export interface MinimalQueryIterator { - fetchNext: () => Promise; + fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise; } // Pick, "fetchNext">; -export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise { - return documentsIterator.fetchNext().then((response) => { +export function nextPage( + documentsIterator: MinimalQueryIterator, + firstItemIndex: number, + queryOperationOptions?: QueryOperationOptions, +): Promise { + return documentsIterator.fetchNext(queryOperationOptions).then((response) => { const documents = response.resources; // eslint-disable-next-line @typescript-eslint/no-explicit-any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 556ed290c..17e84ba28 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,3 +1,4 @@ +import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { getEntityName } from "../DocumentUtility"; @@ -8,12 +9,13 @@ export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, firstItemIndex: number, + queryOperationOptions?: QueryOperationOptions, ): Promise => { const entityName = getEntityName(); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); try { - const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions); const itemCount = (result.documents && result.documents.length) || 0; logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index ca60458f5..d0bc5b23d 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -11,7 +11,13 @@ import { import * as Constants from "Common/Constants"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { configContext } from "ConfigContext"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { + DefaultRUThreshold, + LocalStorageUtility, + StorageKey, + getRUThreshold, + ruThresholdEnabled as isRUThresholdEnabled, +} from "Shared/StorageUtility"; import * as StringUtility from "Shared/StringUtility"; import { userContext } from "UserContext"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; @@ -35,6 +41,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? Constants.Queries.UnlimitedPageOption : Constants.Queries.CustomPageOption, ); + const [ruThresholdEnabled, setRUThresholdEnabled] = useState(isRUThresholdEnabled()); + const [ruThreshold, setRUThreshold] = useState(getRUThreshold()); const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState( LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), ); @@ -103,6 +111,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, ); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); + LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval); @@ -120,6 +129,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ); } + if (ruThresholdEnabled) { + LocalStorageUtility.setEntryNumber(StorageKey.RUThreshold, ruThreshold); + } + if (queryTimeoutEnabled) { LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout); LocalStorageUtility.setEntryBoolean( @@ -195,6 +208,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setPageOption(option.key); }; + const handleOnRUThresholdToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { + setRUThresholdEnabled(checked); + }; + + const handleOnRUThresholdSpinButtonChange = (ev: React.MouseEvent, newValue?: string): void => { + const ruThreshold = Number(newValue); + if (!isNaN(ruThreshold)) { + setRUThreshold(ruThreshold); + } + }; + const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { setQueryTimeoutEnabled(checked); }; @@ -259,7 +283,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ], }; - const queryTimeoutToggleStyles: IToggleStyles = { + const toggleStyles: IToggleStyles = { label: { fontSize: 12, fontWeight: 400, @@ -272,7 +296,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ text: {}, }; - const queryTimeoutSpinButtonStyles: ISpinButtonStyles = { + const spinButtonStyles: ISpinButtonStyles = { label: { fontSize: 12, fontWeight: 400, @@ -338,48 +362,83 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} {userContext.apiType === "SQL" && ( -
-
-
- - Query Timeout - - - When a query reaches a specified time limit, a popup with an option to cancel the query will show - unless automatic cancellation has been enabled - -
-
- -
- {queryTimeoutEnabled && ( + <> +
+
+
+ + RU Threshold + + If a query exceeds a configured RU threshold, the query will be aborted. +
-
- )} + {ruThresholdEnabled && ( +
+ +
+ )} +
-
+
+
+
+ + Query Timeout + + + When a query reaches a specified time limit, a popup with an option to cancel the query will show + unless automatic cancellation has been enabled + +
+
+ +
+ {queryTimeoutEnabled && ( +
+ + +
+ )} +
+
+ )}
@@ -404,7 +463,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
@@ -426,7 +485,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
@@ -448,7 +507,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)} onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)} onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index f4de6deab..f6e6a66ba 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -97,6 +97,74 @@ exports[`Settings Pane should render Default properly 1`] = `
+
+
+
+ + RU Threshold + + + If a query exceeds a configured RU threshold, the query will be aborted. + +
+
+ +
+
+ +
+
+
diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 20cb683d1..243f5a39a 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ -import { FeedOptions } from "@azure/cosmos"; +import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; @@ -10,7 +10,7 @@ import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopil import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { TabsState, useTabs } from "hooks/useTabs"; @@ -303,8 +303,20 @@ export default class QueryTabComponent extends React.Component - await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); + await queryDocumentsPage( + this.props.collection && this.props.collection.id(), + this._iterator, + firstItemIndex, + queryOperationOptions, + ); this.props.tabsBaseInstance.isExecuting(true); this.setState({ isExecuting: true, diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 8dd1d09ec..91dfbc120 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -10,6 +10,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; +import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { userContext } from "UserContext"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import ko from "knockout"; @@ -29,7 +30,9 @@ interface TabsProps { export const Tabs = ({ explorer }: TabsProps): JSX.Element => { const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs(); - + const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState( + userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(), + ); return (
{networkSettingsWarning && ( @@ -54,6 +57,18 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { {networkSettingsWarning} )} + {showRUThresholdMessageBar && ( + { + setShowRUThresholdMessageBar(false); + }} + > + { + "Avoid high cost queries! We automatically abort them if they exceed the set RU limit. To adjust your limit go to Settings > RU threshold." + } + + )}
diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index f6e6a66ba..6f4ac0c06 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -137,14 +137,14 @@ exports[`Settings Pane should render Default properly 1`] = `
{ onDismiss={() => { setShowRUThresholdMessageBar(false); }} + styles={{ + innerText: { + fontWeight: "bold", + }, + }} > { - "Avoid high cost queries! We automatically abort them if they exceed the set RU limit. To adjust your limit go to Settings > RU threshold." + "To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'." } )} diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index b9b02ff11..b229ac7db 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -51,4 +51,4 @@ export const getRUThreshold = (): number => { return DefaultRUThreshold; }; -export const DefaultRUThreshold = 100; +export const DefaultRUThreshold = 5000; From 2d06eef9cc1be1896215b9f760b08d75b7dc7af8 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:46:25 -0500 Subject: [PATCH 011/102] Added CESCVA MessageType for FE (#1737) --- src/Contracts/MessageTypes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Contracts/MessageTypes.ts b/src/Contracts/MessageTypes.ts index 75d11b426..1cbd86bb8 100644 --- a/src/Contracts/MessageTypes.ts +++ b/src/Contracts/MessageTypes.ts @@ -46,6 +46,7 @@ export enum MessageTypes { GetAuthorizationToken, // Data Explorer -> Fabric GetAllResourceTokens, // Data Explorer -> Fabric Ready, // Data Explorer -> Fabric + OpenCESCVAFeedbackBlade, } export interface AuthorizationToken { From 6d98b4a500b3e1ef73bfb68b4fd34cc36cc1677b Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:54:50 -0800 Subject: [PATCH 012/102] Remove localStorage for NPS (#1733) * Remove localStorage for NPS * Run npm format * Update comment --- src/Explorer/Explorer.tsx | 57 +++++---------------------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index fd0d50bfb..2aacbdf7e 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -265,80 +265,37 @@ export default class Explorer { // TODO: return result } - private getRandomInt(max: number) { - return Math.floor(Math.random() * max); - } - public openNPSSurveyDialog(): void { if (!Platform.Portal) { return; } - const NINETY_DAYS_IN_MS = 7776000000; const ONE_DAY_IN_MS = 86400000; - const THREE_DAYS_IN_MS = 259200000; - const lastSubmitted: string = localStorage.getItem("lastSubmitted"); - Logger.logInfo(`NPS Survey last shown date: ${lastSubmitted}`, "Explorer/openNPSSurveyDialog"); - - if (lastSubmitted !== null) { - Logger.logInfo(`NPS Survey last shown is not empty ${lastSubmitted}`, "Explorer/openNPSSurveyDialog"); - - let lastSubmittedDate: number = parseInt(lastSubmitted); - Logger.logInfo(`NPS Survey last shown is parsed ${lastSubmittedDate.toString()}`, "Explorer/openNPSSurveyDialog"); - - if (isNaN(lastSubmittedDate)) { - Logger.logInfo( - `NPS Survey last shown is not a number ${lastSubmittedDate.toString()}`, - "Explorer/openNPSSurveyDialog", - ); - lastSubmittedDate = 0; - } - - const nowMs: number = Date.now(); - Logger.logInfo(`NPS Survey current date ${nowMs.toString()}`, "Explorer/openNPSSurveyDialog"); - - const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate; - if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) { - Logger.logInfo( - `NPS Survey last shown is less than ninety days ${millisecsSinceLastSubmitted.toString()}`, - "Explorer/openNPSSurveyDialog", - ); - return; - } - } + const SEVEN_DAYS_IN_MS = 604800000; // Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer. if (userContext.isTryCosmosDBSubscription) { if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) { Logger.logInfo( - `Displaying NPS Survey for Try Cosmos DB ${userContext.apiType}`, + `Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`, "Explorer/openNPSSurveyDialog", ); - this.sendNPSMessage(); + sendMessage({ type: MessageTypes.DisplayNPSSurvey }); } } else { - // Show survey when an existing account is older than 3 days + // Show survey when an existing account is older than 7 days if ( - !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) + !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS) ) { Logger.logInfo( - `Displaying NPS Survey for users with existing ${userContext.apiType} account older than 3 days`, + `Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`, "Explorer/openNPSSurveyDialog", ); - this.sendNPSMessage(); + sendMessage({ type: MessageTypes.DisplayNPSSurvey }); } } } - private sendNPSMessage() { - sendMessage({ type: MessageTypes.DisplayNPSSurvey }); - Logger.logInfo( - `NPS Survey logging current date when survey is shown ${Date.now().toString()}`, - "Explorer/openNPSSurveyDialog", - ); - localStorage.setItem("lastSubmitted", Date.now().toString()); - } - public async refreshDatabaseForResourceToken(): Promise { const databaseId = userContext.parsedResourceToken?.databaseId; const collectionId = userContext.parsedResourceToken?.collectionId; From 35ca7944ae06899ed5bcf1e181f557abf4fd41b2 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 7 Feb 2024 11:31:13 -0500 Subject: [PATCH 013/102] Limit RU threshold only to NoSQL (#1739) * limit RU threshold only to NoSQL * limit RU threshold only to NoSQL --------- Co-authored-by: Asier Isayas --- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 243f5a39a..783e24dfa 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -304,7 +304,7 @@ export default class QueryTabComponent extends React.Component Date: Thu, 8 Feb 2024 11:49:40 -0800 Subject: [PATCH 014/102] Remove references to addCollectionDefaultFlight parameter. (#1741) --- src/Contracts/ViewModels.ts | 1 - src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx | 8 ++------ .../CassandraAddCollectionPane.tsx | 1 - src/Shared/Constants.ts | 1 - src/UserContext.ts | 2 -- src/hooks/useKnockoutExplorer.ts | 2 -- test/testExplorer/TestExplorer.ts | 1 - 7 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 7521647df..d95d8ce05 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -388,7 +388,6 @@ export interface DataExplorerInputsFrame { extensionEndpoint?: string; subscriptionType?: SubscriptionType; quotaId?: string; - addCollectionDefaultFlight?: string; isTryCosmosDBSubscription?: boolean; loadDatabaseAccountTimestamp?: number; sharedThroughputMinimum?: number; diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 0ec99c0fa..e9a9dbd9b 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -1,12 +1,11 @@ import { Checkbox, Stack, Text, TextField } from "@fluentui/react"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as Constants from "../../../Common/Constants"; -import { createDatabase } from "../../../Common/dataAccess/createDatabase"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; +import { createDatabase } from "../../../Common/dataAccess/createDatabase"; import * as DataModels from "../../../Contracts/DataModels"; import { SubscriptionType } from "../../../Contracts/SubscriptionType"; -import { useSidePanel } from "../../../hooks/useSidePanel"; import * as SharedConstants from "../../../Shared/Constants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; @@ -14,6 +13,7 @@ import { userContext } from "../../../UserContext"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils"; +import { useSidePanel } from "../../../hooks/useSidePanel"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; import { useDatabases } from "../../useDatabases"; @@ -63,9 +63,6 @@ export const AddDatabasePanel: FunctionComponent = ({ }, subscriptionType: SubscriptionType[subscriptionType], subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - flight: userContext.addCollectionFlight, - }, dataExplorerArea: Constants.Areas.ContextualPane, }; @@ -75,7 +72,6 @@ export const AddDatabasePanel: FunctionComponent = ({ subscriptionQuotaId: userContext.quotaId, defaultsCheck: { throughput, - flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, }; diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 0dd145af9..dbef20b12 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -59,7 +59,6 @@ export const CassandraAddCollectionPane: FunctionComponent => { extensionEndpoint: "/proxy", subscriptionType: 3, quotaId: "Internal_2014-09-01", - addCollectionDefaultFlight: "2", isTryCosmosDBSubscription: false, masterKey: keys.primaryMasterKey, loadDatabaseAccountTimestamp: 1604663109836, From f403b086adf2131c37ed2c349e2582e17ea8c47c Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Fri, 9 Feb 2024 14:10:57 +0000 Subject: [PATCH 015/102] Hide buttons for Fabric or when no write access (#1742) --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/Tabs/DocumentsTab.ts | 5 +++++ src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 3 ++- src/Platform/Fabric/FabricUtil.ts | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 78810e9db..a24c38ce5 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -135,7 +135,7 @@ export function createStaticCommandBarButtons( buttons.push(newSqlQueryBtn); } - if (isQuerySupported && selectedNodeState.findSelectedCollection()) { + if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index d4e532ca4..e9c6bee4e 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -881,6 +881,11 @@ export default class DocumentsTab extends TabsBase { } protected getTabsButtons(): CommandButtonComponentProps[] { + if (!userContext.hasWriteAccess) { + // All the following buttons require write access + return []; + } + const buttons: CommandButtonComponentProps[] = []; const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; if (this.newDocumentButton.visible()) { diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 783e24dfa..ff44a4be3 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; +import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; @@ -402,7 +403,7 @@ export default class QueryTabComponent extends React.Component => { updateUserContext({ fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo }, databaseAccount: { ...userContext.databaseAccount }, + hasWriteAccess: false, // TODO: receive from fabricDatabaseConnectionInfo }); scheduleRefreshDatabaseResourceToken(); } catch (error) { From 8b0b3b07d6dcb99e699f79482c4cc34a26eec37b Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Fri, 9 Feb 2024 10:58:10 -0500 Subject: [PATCH 016/102] Add Mongo Proxy to Data Explorer (Standalone and Portal) (#1738) * Mongo Proxy backend API * merge main into current * allow mongo proxy endpoints to be constants * allow mongo proxy endpoints to be constants * fix test * check for allowed mongo proxy endpoint * check for allowed mongo proxy endpoint --------- Co-authored-by: Asier Isayas --- .eslintignore | 1 + src/Common/Constants.ts | 4 + src/Common/MongoProxyClient.ts | 340 +++++++++++++++++- src/ConfigContext.ts | 18 +- src/Contracts/ViewModels.ts | 1 + src/Explorer/Explorer.tsx | 2 +- src/Juno/JunoClient.ts | 4 +- src/Phoenix/PhoenixClient.ts | 2 +- ...EndpointValidation.ts => EndpointUtils.ts} | 16 + src/Utils/NetworkUtility.test.ts | 2 +- src/Utils/NetworkUtility.ts | 2 +- src/hooks/useKnockoutExplorer.ts | 1 + tsconfig.strict.json | 1 + 13 files changed, 376 insertions(+), 18 deletions(-) rename src/Utils/{EndpointValidation.ts => EndpointUtils.ts} (83%) diff --git a/.eslintignore b/.eslintignore index c188701b9..78e9b517e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -145,4 +145,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx __mocks__/monaco-editor.ts src/Explorer/Tree/ResourceTree.tsx +src/Utils/EndpointUtils.ts src/Utils/PriorityBasedExecutionUtils.ts \ No newline at end of file diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 4d86f5ff7..bfecb27c1 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -211,6 +211,10 @@ export class HttpHeaders { public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; } +export class ContentType { + public static applicationJson: string = "application/json"; +} + export class ApiType { // Mapped to hexadecimal values in the backend public static readonly MongoDB: number = 1; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index ea4103ff6..2a4d9fed7 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,6 +1,6 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; +import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils"; import queryString from "querystring"; -import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation"; import { AuthType } from "../AuthType"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; @@ -10,7 +10,7 @@ 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"; +import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { MinimalQueryIterator } from "./IteratorUtilities"; import { sendMessage } from "./MessageHandler"; @@ -62,6 +62,73 @@ export function queryDocuments( isResourceList: boolean, query: string, continuationToken?: string, +): Promise { + if (!useMongoProxyEndpoint("resourcelist")) { + return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); + } + + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, + resourceID: collection.rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + collection && collection.partitionKey && !collection.partitionKey.systemKey + ? collection.partitionKeyProperties?.[0] + : "", + query, + }; + + const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; + + const headers = { + ...defaultHeaders, + ...authHeaders(), + [CosmosSDKConstants.HttpHeaders.IsQuery]: "true", + [CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true", + [CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true", + [CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true", + [CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true", + [HttpHeaders.contentType]: "application/query+json", + }; + + if (continuationToken) { + headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken; + } + + const path = isResourceList ? "/resourcelist" : ""; + + return window + .fetch(`${endpoint}${path}`, { + method: "POST", + body: JSON.stringify(params), + headers, + }) + .then(async (response) => { + if (response.ok) { + return { + continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation), + documents: (await response.json()).Documents as DataModels.DocumentId[], + headers: response.headers, + }; + } + await errorHandling(response, "querying documents", params); + return undefined; + }); +} + +function queryDocuments_ToBeDeprecated( + databaseId: string, + collection: Collection, + isResourceList: boolean, + query: string, + continuationToken?: string, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -122,6 +189,54 @@ export function readDocument( databaseId: string, collection: Collection, documentId: DocumentId, +): Promise { + if (!useMongoProxyEndpoint("readDocument")) { + return readDocument_ToBeDeprecated(databaseId, collection, documentId); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 4).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + }; + + const endpoint = getFeatureEndpointOrDefault("readDocument"); + + return window + .fetch(endpoint, { + method: "POST", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "reading document", params); + }); +} + +export function readDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -169,6 +284,51 @@ export function createDocument( collection: Collection, partitionKeyProperty: string, documentContent: unknown, +): Promise { + if (!useMongoProxyEndpoint("createDocument")) { + return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, + resourceID: collection.rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", + documentContent: JSON.stringify(documentContent), + }; + + const endpoint = getFeatureEndpointOrDefault("createDocument"); + + return window + .fetch(`${endpoint}/createDocument`, { + method: "POST", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "creating document", params); + }); +} + +export function createDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + partitionKeyProperty: string, + documentContent: unknown, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -208,6 +368,56 @@ export function updateDocument( collection: Collection, documentId: DocumentId, documentContent: string, +): Promise { + if (!useMongoProxyEndpoint("updateDocument")) { + return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 5).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + documentContent, + }; + const endpoint = getFeatureEndpointOrDefault("updateDocument"); + + return window + .fetch(endpoint, { + method: "PUT", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "updating document", params); + }); +} + +export function updateDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, + documentContent: string, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -237,7 +447,7 @@ export function updateDocument( headers: { ...defaultHeaders, ...authHeaders(), - [HttpHeaders.contentType]: "application/json", + [HttpHeaders.contentType]: ContentType.applicationJson, [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) @@ -250,6 +460,53 @@ export function updateDocument( } export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise { + if (!useMongoProxyEndpoint("deleteDocument")) { + deleteDocument_ToBeDeprecated(databaseId, collection, documentId); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 5).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + }; + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + + return window + .fetch(endpoint, { + method: "DELETE", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return undefined; + } + return await errorHandling(response, "deleting document", params); + }); +} + +export function deleteDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, +): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const idComponents = documentId.self.split("/"); @@ -277,7 +534,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum headers: { ...defaultHeaders, ...authHeaders(), - [HttpHeaders.contentType]: "application/json", + [HttpHeaders.contentType]: ContentType.applicationJson, [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) @@ -291,6 +548,52 @@ export function deleteDocument(databaseId: string, collection: Collection, docum export function createMongoCollectionWithProxy( params: DataModels.CreateCollectionParams, +): Promise { + if (!useMongoProxyEndpoint("createCollectionWithProxy")) { + createMongoCollectionWithProxy_ToBeDeprecated(params); + } + const { databaseAccount } = userContext; + const shardKey: string = params.partitionKey?.paths[0]; + + const createCollectionParams = { + databaseID: params.databaseId, + collectionID: params.collectionId, + resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint, + resourceID: "", + resourceType: "colls", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: shardKey, + isAutoscale: !!params.autoPilotMaxThroughput, + hasSharedThroughput: params.databaseLevelThroughput, + offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput, + createDatabase: params.createNewDatabase, + isSharded: !!shardKey, + }; + + const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); + + return window + .fetch(`${endpoint}/createCollection`, { + method: "POST", + body: JSON.stringify(createCollectionParams), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "creating collection", createCollectionParams); + }); +} + +export function createMongoCollectionWithProxy_ToBeDeprecated( + params: DataModels.CreateCollectionParams, ): Promise { const { databaseAccount } = userContext; const shardKey: string = params.partitionKey?.paths[0]; @@ -334,13 +637,17 @@ export function createMongoCollectionWithProxy( return await errorHandling(response, "creating collection", mongoParams); }); } - export function getFeatureEndpointOrDefault(feature: string): string { - const endpoint = - hasFlag(userContext.features.mongoProxyAPIs, feature) && - validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints) - ? userContext.features.mongoProxyEndpoint - : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + let endpoint; + if (useMongoProxyEndpoint(feature)) { + endpoint = configContext.MONGO_PROXY_ENDPOINT; + } else { + endpoint = + hasFlag(userContext.features.mongoProxyAPIs, feature) && + validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated) + ? userContext.features.mongoProxyEndpoint + : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + } return getEndpoint(endpoint); } @@ -349,7 +656,11 @@ export function getEndpoint(endpoint: string): string { let url = endpoint + "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { - url = url.replace("api/mongo", "api/guest/mongo"); + if (endpoint === configContext.MONGO_PROXY_ENDPOINT) { + url = url.replace("api/mongo", "api/connectionstring/mongo"); + } else { + url = url.replace("api/mongo", "api/guest/mongo"); + } } return url; } @@ -370,3 +681,10 @@ async function errorHandling(response: Response, action: string, params: unknown export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; } + +function useMongoProxyEndpoint(api: string): boolean { + return ( + configContext.NEW_MONGO_APIS?.includes(api) && + [MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT) + ); +} diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 1679a3657..c04bc9671 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -7,11 +7,12 @@ import { allowedHostedExplorerEndpoints, allowedJunoOrigins, allowedMongoBackendEndpoints, + allowedMongoProxyEndpoints, allowedMsalRedirectEndpoints, defaultAllowedArmEndpoints, defaultAllowedBackendEndpoints, validateEndpoint, -} from "Utils/EndpointValidation"; +} from "Utils/EndpointUtils"; export enum Platform { Portal = "Portal", @@ -38,6 +39,8 @@ export interface ConfigContext { ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; BACKEND_ENDPOINT?: string; MONGO_BACKEND_ENDPOINT?: string; + MONGO_PROXY_ENDPOINT?: string; + NEW_MONGO_APIS?: string[]; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -82,6 +85,15 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com", + NEW_MONGO_APIS: [ + // "resourcelist", + // "createDocument", + // "readDocument", + // "updateDocument", + // "deleteDocument", + // "createCollectionWithProxy", + ], isTerminalEnabled: false, isPhoenixEnabled: false, }; @@ -127,6 +139,10 @@ export function updateConfigContext(newContext: Partial): void { delete newContext.BACKEND_ENDPOINT; } + if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) { + delete newContext.MONGO_PROXY_ENDPOINT; + } + if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) { delete newContext.MONGO_BACKEND_ENDPOINT; } diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index d95d8ce05..885ebabd0 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -386,6 +386,7 @@ export interface DataExplorerInputsFrame { dnsSuffix?: string; serverId?: string; extensionEndpoint?: string; + mongoProxyEndpoint?: string; subscriptionType?: SubscriptionType; quotaId?: string; isTryCosmosDBSubscription?: boolean; diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 2aacbdf7e..3a945d3ed 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -7,7 +7,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop import { IGalleryItem } from "Juno/JunoClient"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; -import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; +import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as ko from "knockout"; import React from "react"; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 7cef27da1..0b0618e72 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,6 +1,6 @@ -import ko from "knockout"; -import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; +import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; import { GetGithubClientId } from "Utils/GitHubUtils"; +import ko from "knockout"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index 685bbbbb2..d5e304b8e 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -2,7 +2,7 @@ import { configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { userContext } from "UserContext"; -import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; +import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import promiseRetry, { AbortError } from "p-retry"; import { diff --git a/src/Utils/EndpointValidation.ts b/src/Utils/EndpointUtils.ts similarity index 83% rename from src/Utils/EndpointValidation.ts rename to src/Utils/EndpointUtils.ts index 0f9eb44ef..656f55903 100644 --- a/src/Utils/EndpointValidation.ts +++ b/src/Utils/EndpointUtils.ts @@ -67,7 +67,23 @@ export const PortalBackendIPs: { [key: string]: string[] } = { //usnat: ["7.28.202.68"], }; +export class MongoProxyEndpoints { + public static readonly Development: string = "https://localhost:7238"; + public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; +} + export const allowedMongoProxyEndpoints: ReadonlyArray = [ + MongoProxyEndpoints.Development, + MongoProxyEndpoints.MPAC, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, +]; + +export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray = [ "https://main.documentdb.ext.azure.com", "https://main.documentdb.ext.azure.cn", "https://main.documentdb.ext.azure.us", diff --git a/src/Utils/NetworkUtility.test.ts b/src/Utils/NetworkUtility.test.ts index ed22b502e..4ee4b5cd2 100644 --- a/src/Utils/NetworkUtility.test.ts +++ b/src/Utils/NetworkUtility.test.ts @@ -1,7 +1,7 @@ import { resetConfigContext, updateConfigContext } from "ConfigContext"; import { DatabaseAccount, IpRule } from "Contracts/DataModels"; import { updateUserContext } from "UserContext"; -import { PortalBackendIPs } from "Utils/EndpointValidation"; +import { PortalBackendIPs } from "Utils/EndpointUtils"; import { getNetworkSettingsWarningMessage } from "./NetworkUtility"; describe("NetworkUtility tests", () => { diff --git a/src/Utils/NetworkUtility.ts b/src/Utils/NetworkUtility.ts index 8f4624297..96f3ae124 100644 --- a/src/Utils/NetworkUtility.ts +++ b/src/Utils/NetworkUtility.ts @@ -1,7 +1,7 @@ import { configContext } from "ConfigContext"; import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "UserContext"; -import { PortalBackendIPs } from "Utils/EndpointValidation"; +import { PortalBackendIPs } from "Utils/EndpointUtils"; export const getNetworkSettingsWarningMessage = async ( setStateFunc: (warningMessage: string) => void, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 629bd0b7a..4de6d5fc2 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -478,6 +478,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { updateConfigContext({ BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT, ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), + MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, }); updateUserContext({ diff --git a/tsconfig.strict.json b/tsconfig.strict.json index be0195655..969c39d01 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -112,6 +112,7 @@ "./src/Utils/BlobUtils.ts", "./src/Utils/CapabilityUtils.ts", "./src/Utils/CloudUtils.ts", + "./src/Utils/EndpointUtils.ts", "./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.test.ts", From e43b4eee5c138bdbe3de82e12af1aca16f4d7139 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:35:14 -0500 Subject: [PATCH 017/102] Correcting import for MessageTypes (#1743) --- src/Contracts/ExplorerContracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index 42e5d3dc0..6e49b0555 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -1,6 +1,6 @@ -import { MessageTypes } from "Contracts/MessageTypes"; import * as ActionContracts from "./ActionContracts"; import * as Diagnostics from "./Diagnostics"; +import { MessageTypes } from "./MessageTypes"; import * as Versions from "./Versions"; export { ActionContracts, Diagnostics, MessageTypes, Versions }; From a914fd020cb21f0178b61f8e068b8e531b3911cd Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:00:27 -0600 Subject: [PATCH 018/102] Partition Key Change with Container Copy (#1734) * initial commit * Add change partition key logic * Update snapshot * Update snapshot * Update snapshot * Update snapshot * Cleanup code * Disable Change on progress job * add the database information in the panel * add the database information in the panel * clear in progress message and remove large partition key row * hide from national cloud * hide from national cloud * Add check for public cloud --- src/Common/dataAccess/dataTransfers.ts | 188 +++++++++ .../Controls/Settings/SettingsComponent.tsx | 20 + .../PartitionKeyComponent.tsx | 216 ++++++++++ .../Controls/Settings/SettingsUtils.tsx | 49 +++ .../SettingsComponent.test.tsx.snap | 92 ++++ .../ChangePartitionKeyPane.tsx | 396 ++++++++++++++++++ src/UserContext.ts | 2 +- src/Utils/CloudUtils.ts | 10 +- .../dataTransferService/dataTransferJobs.ts | 78 ++++ .../dataTransferService/types.ts | 101 +++++ src/hooks/useDataTransferJobs.tsx | 60 +++ utils/armClientGenerator/generator.ts | 26 +- 12 files changed, 1219 insertions(+), 19 deletions(-) create mode 100644 src/Common/dataAccess/dataTransfers.ts create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx create mode 100644 src/Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane.tsx create mode 100644 src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts create mode 100644 src/Utils/arm/generatedClients/dataTransferService/types.ts create mode 100644 src/hooks/useDataTransferJobs.tsx diff --git a/src/Common/dataAccess/dataTransfers.ts b/src/Common/dataAccess/dataTransfers.ts new file mode 100644 index 000000000..138257c15 --- /dev/null +++ b/src/Common/dataAccess/dataTransfers.ts @@ -0,0 +1,188 @@ +import { ApiType, userContext } from "UserContext"; +import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; +import { + cancel, + create, + get, + listByDatabaseAccount, +} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; +import { + CosmosCassandraDataTransferDataSourceSink, + CosmosMongoDataTransferDataSourceSink, + CosmosSqlDataTransferDataSourceSink, + CreateJobRequest, + DataTransferJobFeedResults, + DataTransferJobGetResults, +} from "Utils/arm/generatedClients/dataTransferService/types"; +import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs"; +import promiseRetry, { AbortError, FailedAttemptError } from "p-retry"; + +export interface DataTransferParams { + jobName: string; + apiType: ApiType; + subscriptionId: string; + resourceGroupName: string; + accountName: string; + sourceDatabaseName: string; + sourceCollectionName: string; + targetDatabaseName: string; + targetCollectionName: string; +} + +export const getDataTransferJobs = async ( + subscriptionId: string, + resourceGroup: string, + accountName: string, +): Promise => { + let dataTransferJobs: DataTransferJobGetResults[] = []; + let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount( + subscriptionId, + resourceGroup, + accountName, + ); + dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; + while (dataTransferFeeds?.nextLink) { + const nextResponse = await window.fetch(dataTransferFeeds.nextLink, { + headers: { + Authorization: userContext.authorizationToken, + }, + }); + if (nextResponse.ok) { + dataTransferFeeds = await nextResponse.json(); + dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; + } else { + break; + } + } + return dataTransferJobs; +}; + +export const initiateDataTransfer = async (params: DataTransferParams): Promise => { + const { + jobName, + apiType, + subscriptionId, + resourceGroupName, + accountName, + sourceDatabaseName, + sourceCollectionName, + targetDatabaseName, + targetCollectionName, + } = params; + const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName); + const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName); + const body: CreateJobRequest = { + properties: { + source: sourcePayload, + destination: targetPayload, + }, + }; + return create(subscriptionId, resourceGroupName, accountName, jobName, body); +}; + +export const pollDataTransferJob = async ( + jobName: string, + subscriptionId: string, + resourceGroupName: string, + accountName: string, +): Promise => { + const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + if (currentPollingJobs.has(jobName)) { + return; + } + let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`); + return await promiseRetry( + () => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage), + { + retries: 500, + maxTimeout: 5000, + onFailedAttempt: (error: FailedAttemptError) => { + clearMessage(); + clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message); + }, + }, + ); +}; + +const pollDataTransferJobOperation = async ( + jobName: string, + subscriptionId: string, + resourceGroupName: string, + accountName: string, + clearMessage?: () => void, +): Promise => { + if (!userContext.authorizationToken) { + throw new Error("No authority token provided"); + } + + addToPolling(jobName); + + const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName); + const status = body?.properties?.status; + + updateDataTransferJob(body); + + if (status === "Cancelled" || status === "Failed" || status === "Faulted") { + removeFromPolling(jobName); + const errorMessage = body?.properties?.error + ? JSON.stringify(body?.properties?.error) + : "Operation could not be completed"; + const error = new Error(errorMessage); + clearMessage && clearMessage(); + NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`); + throw new AbortError(error); + } + if (status === "Completed") { + removeFromPolling(jobName); + clearMessage && clearMessage(); + NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`); + return body; + } + const processedCount = body.properties.processedCount; + const totalCount = body.properties.totalCount; + const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`; + throw new Error(retryMessage); +}; + +export const cancelDataTransferJob = async ( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise => { + const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName); + updateDataTransferJob(cancelResult); + removeFromPolling(cancelResult?.properties?.jobName); +}; + +const createPayload = ( + apiType: ApiType, + databaseName: string, + containerName: string, +): + | CosmosSqlDataTransferDataSourceSink + | CosmosMongoDataTransferDataSourceSink + | CosmosCassandraDataTransferDataSourceSink => { + switch (apiType) { + case "SQL": + return { + component: "CosmosDBSql", + databaseName: databaseName, + containerName: containerName, + } as CosmosSqlDataTransferDataSourceSink; + case "Mongo": + return { + component: "CosmosDBMongo", + databaseName: databaseName, + collectionName: containerName, + } as CosmosMongoDataTransferDataSourceSink; + case "Cassandra": + return { + component: "CosmosDBCassandra", + keyspaceName: databaseName, + tableName: containerName, + }; + default: + throw new Error(`Unsupported API type for data transfer: ${apiType}`); + } +}; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 838bf4182..18a0e1e9e 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1,5 +1,6 @@ import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react"; import { useDatabases } from "Explorer/useDatabases"; +import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; @@ -18,6 +19,10 @@ import { userContext } from "../../../UserContext"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import { + PartitionKeyComponent, + PartitionKeyComponentProps, +} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import "./SettingsComponent.less"; @@ -128,6 +133,7 @@ export class SettingsComponent extends React.Component, + }); + } + const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, selectedKey: SettingsV2TabTypes[this.state.selectedTab], diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx new file mode 100644 index 000000000..2efe7fb92 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -0,0 +1,216 @@ +import { + DefaultButton, + FontWeights, + Link, + MessageBar, + MessageBarType, + PrimaryButton, + ProgressIndicator, + Stack, + Text, +} from "@fluentui/react"; +import * as React from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; + +import { handleError } from "Common/ErrorHandlingUtils"; +import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers"; +import Explorer from "Explorer/Explorer"; +import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane"; +import { + CosmosSqlDataTransferDataSourceSink, + DataTransferJobGetResults, +} from "Utils/arm/generatedClients/dataTransferService/types"; +import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs"; +import { useSidePanel } from "hooks/useSidePanel"; +import { userContext } from "../../../../UserContext"; + +export interface PartitionKeyComponentProps { + database: ViewModels.Database; + collection: ViewModels.Collection; + explorer: Explorer; +} + +export const PartitionKeyComponent: React.FC = ({ database, collection, explorer }) => { + const { dataTransferJobs } = useDataTransferJobs(); + const [portalDataTransferJob, setPortalDataTransferJob] = React.useState(null); + + React.useEffect(() => { + const loadDataTransferJobs = refreshDataTransferOperations; + loadDataTransferJobs(); + }, []); + + React.useEffect(() => { + const currentJob = findPortalDataTransferJob(); + setPortalDataTransferJob(currentJob); + startPollingforUpdate(currentJob); + }, [dataTransferJobs]); + + const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash"; + + const getPartitionKeyValue = (): string => { + return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", "); + }; + + const partitionKeyName = "Partition key"; + const partitionKeyValue = getPartitionKeyValue(); + + const textHeadingStyle = { + root: { fontWeight: FontWeights.semibold, fontSize: 16 }, + }; + + const textSubHeadingStyle = { + root: { fontWeight: FontWeights.semibold }, + }; + + const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => { + if (isCurrentJobInProgress(currentJob)) { + const jobName = currentJob?.properties?.jobName; + try { + pollDataTransferJob( + jobName, + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + } catch (error) { + handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`); + } + } + }; + + const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => { + await cancelDataTransferJob( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + currentJob?.properties?.jobName, + ); + }; + + const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => { + const jobStatus = currentJob?.properties?.status; + return ( + jobStatus && + jobStatus !== "Completed" && + jobStatus !== "Cancelled" && + jobStatus !== "Failed" && + jobStatus !== "Faulted" + ); + }; + + const refreshDataTransferOperations = async () => { + await refreshDataTransferJobs( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + }; + + const findPortalDataTransferJob = (): DataTransferJobGetResults => { + return dataTransferJobs.find((feed: DataTransferJobGetResults) => { + const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties + ?.source as CosmosSqlDataTransferDataSourceSink; + return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id(); + }); + }; + + const getProgressDescription = (): string => { + const processedCount = portalDataTransferJob?.properties?.processedCount; + const totalCount = portalDataTransferJob?.properties?.totalCount; + const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : ""; + return `${portalDataTransferJob?.properties?.status} ${processedCountString}`; + }; + + const startPartitionkeyChangeWorkflow = () => { + useSidePanel + .getState() + .openSidePanel( + "Change partition key", + , + ); + }; + + const getPercentageComplete = () => { + const processedCount = portalDataTransferJob?.properties?.processedCount; + const totalCount = portalDataTransferJob?.properties?.totalCount; + const jobStatus = portalDataTransferJob?.properties?.status; + const isCancelled = jobStatus === "Cancelled"; + const isCompleted = jobStatus === "Completed"; + if (totalCount <= 0 && !isCompleted) { + return isCancelled ? 0 : null; + } + return isCompleted ? 1 : processedCount / totalCount; + }; + + return ( + + + Change {partitionKeyName.toLowerCase()} + + + Current {partitionKeyName.toLowerCase()} + Partitioning + + + {partitionKeyValue} + {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} + + + + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the + source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination container + selected. Data will then be copied to the destination container. + + + {portalDataTransferJob && ( + + {partitionKeyName} change job + + + {isCurrentJobInProgress(portalDataTransferJob) && ( + cancelRunningDataTransferJob(portalDataTransferJob)} /> + )} + + + )} + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index a533b6446..930249d9d 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -45,6 +45,7 @@ export enum SettingsV2TabTypes { ConflictResolutionTab, SubSettingsTab, IndexingPolicyTab, + PartitionKeyTab, } export interface IsComponentDirtyResult { @@ -146,6 +147,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Settings"; case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; + case SettingsV2TabTypes.PartitionKeyTab: + return "Partition Keys"; default: throw new Error(`Unknown tab ${tab}`); } @@ -199,3 +202,49 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => { export const isIndexTransforming = (indexTransformationProgress: number): boolean => // index transformation progress can be 0 indexTransformationProgress !== undefined && indexTransformationProgress !== 100; + +export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => { + const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key"; + return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName; +}; + +export const getPartitionKeyTooltipText = (apiType: string): string => { + if (apiType === "Mongo") { + return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data."; + } + let tooltipText = `The ${getPartitionKeyName( + apiType, + true, + )} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`; + if (apiType === "SQL") { + tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."; + } + return tooltipText; +}; + +export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => { + if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) { + const subtext = "For small workloads, the item ID is a suitable choice for the partition key."; + return subtext; + } + return ""; +}; + +export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => { + switch (apiType) { + case "Mongo": + return "e.g., categoryId"; + case "Gremlin": + return "e.g., /address"; + case "SQL": + return `${ + index === undefined + ? "Required - first partition key e.g., /TenantId" + : index === 0 + ? "second partition key e.g., /UserId" + : "third partition key e.g., /SessionId" + }`; + default: + return "e.g., /address/zipCode"; + } +}; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 5e905e786..1ca3cddf1 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -204,6 +204,98 @@ exports[`SettingsComponent renders 1`] = ` shouldDiscardIndexingPolicy={false} /> + + +
diff --git a/src/Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane.tsx b/src/Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane.tsx new file mode 100644 index 000000000..bf3f2d015 --- /dev/null +++ b/src/Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane.tsx @@ -0,0 +1,396 @@ +import { + DefaultButton, + DirectionalHint, + Dropdown, + IDropdownOption, + Icon, + IconButton, + Link, + Stack, + Text, + TooltipHost, +} from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import { handleError } from "Common/ErrorHandlingUtils"; +import { createCollection } from "Common/dataAccess/createCollection"; +import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers"; +import * as DataModels from "Contracts/DataModels"; +import * as ViewModels from "Contracts/ViewModels"; +import { + getPartitionKeyName, + getPartitionKeyPlaceHolder, + getPartitionKeySubtext, + getPartitionKeyTooltipText, +} from "Explorer/Controls/Settings/SettingsUtils"; +import Explorer from "Explorer/Explorer"; +import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm"; +import { useDatabases } from "Explorer/useDatabases"; +import { userContext } from "UserContext"; +import { getCollectionName } from "Utils/APITypeUtils"; +import { useSidePanel } from "hooks/useSidePanel"; +import * as React from "react"; + +export interface ChangePartitionKeyPaneProps { + sourceDatabase: ViewModels.Database; + sourceCollection: ViewModels.Collection; + explorer: Explorer; + onClose: () => Promise; +} + +export const ChangePartitionKeyPane: React.FC = ({ + sourceDatabase, + sourceCollection, + explorer, + onClose, +}) => { + const [targetCollectionId, setTargetCollectionId] = React.useState(); + const [createNewContainer, setCreateNewContainer] = React.useState(true); + const [formError, setFormError] = React.useState(); + const [isExecuting, setIsExecuting] = React.useState(false); + const [subPartitionKeys, setSubPartitionKeys] = React.useState([]); + const [partitionKey, setPartitionKey] = React.useState(); + + const getCollectionOptions = (): IDropdownOption[] => { + return sourceDatabase + .collections() + .filter((collection) => collection.id !== sourceCollection.id) + .map((collection) => ({ + key: collection.id(), + text: collection.id(), + })); + }; + + const submit = async () => { + if (!validateInputs()) { + return; + } + setIsExecuting(true); + try { + createNewContainer && (await createContainer()); + await createDataTransferJob(); + await onClose(); + } catch (error) { + handleError(error, "ChangePartitionKey", "Failed to start data transfer job"); + } + setIsExecuting(false); + useSidePanel.getState().closeSidePanel(); + }; + + const validateInputs = (): boolean => { + if (!createNewContainer && !targetCollectionId) { + setFormError("Choose an existing container"); + return false; + } + return true; + }; + + const createDataTransferJob = async () => { + const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`; + const dataTransferParams: DataTransferParams = { + jobName, + apiType: userContext.apiType, + subscriptionId: userContext.subscriptionId, + resourceGroupName: userContext.resourceGroup, + accountName: userContext.databaseAccount.name, + sourceDatabaseName: sourceDatabase.id(), + sourceCollectionName: sourceCollection.id(), + targetDatabaseName: sourceDatabase.id(), + targetCollectionName: targetCollectionId, + }; + await initiateDataTransfer(dataTransferParams); + }; + + const createContainer = async () => { + const partitionKeyString = partitionKey.trim(); + const partitionKeyData: DataModels.PartitionKey = partitionKeyString + ? { + paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])], + kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash", + version: 2, + } + : undefined; + + const createCollectionParams: DataModels.CreateCollectionParams = { + createNewDatabase: false, + collectionId: targetCollectionId, + databaseId: sourceDatabase.id(), + databaseLevelThroughput: isSelectedDatabaseSharedThroughput(), + offerThroughput: sourceCollection.offer()?.manualThroughput, + autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput, + partitionKey: partitionKeyData, + }; + await createCollection(createCollectionParams); + await explorer.refreshAllDatabases(); + }; + + const isSelectedDatabaseSharedThroughput = (): boolean => { + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === sourceDatabase.id()); + return !!selectedDatabase?.offer(); + }; + + return ( + + + + When changing a container’s partition key, you will need to create a destination container with the correct + partition key. You may also select an existing destination container.  + + Learn more + + + + + + + Database id + + + + + + + + +
+ setCreateNewContainer(true)} + /> + New container + + setCreateNewContainer(false)} + /> + Existing container +
+
+ {createNewContainer ? ( + + + + + + {`${getCollectionName()} id`} + + + + + + ) => setTargetCollectionId(event.target.value)} + /> + + + + + + {getPartitionKeyName(userContext.apiType)} + + + + + + + + {getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)} + + + ) => { + if (!partitionKey && !event.target.value.startsWith("/")) { + setPartitionKey("/" + event.target.value); + } else { + setPartitionKey(event.target.value); + } + }} + /> + {subPartitionKeys.map((subPartitionKey: string, index: number) => { + return ( + +
+ 0 ? 1 : 0} + className="panelTextField" + autoComplete="off" + placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)} + aria-label={getPartitionKeyName(userContext.apiType)} + pattern={".*"} + title={""} + value={subPartitionKey} + onChange={(event: React.ChangeEvent) => { + const keys = [...subPartitionKeys]; + if (!keys[index] && !event.target.value.startsWith("/")) { + keys[index] = "/" + event.target.value.trim(); + setSubPartitionKeys(keys); + } else { + keys[index] = event.target.value.trim(); + setSubPartitionKeys(keys); + } + }} + /> + { + const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j); + setSubPartitionKeys(keys); + }} + /> +
+ ); + })} + + = Constants.BackendDefaults.maxNumMultiHashPartition} + onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} + > + Add hierarchical partition key + + {subPartitionKeys.length > 0 && ( + + This feature allows you to + partition your data with up to three levels of keys for better data distribution. Requires .NET V3, + Java V4 SDK, or preview JavaScript V3 SDK.{" "} + + Learn more + + + )} + +
+
+ ) : ( + + + + + {`${getCollectionName()}`} + + + + + + + , collection: IDropdownOption) => { + setTargetCollectionId(collection.key as string); + setFormError(""); + }} + defaultSelectedKey={targetCollectionId} + responsiveMode={999} + /> + + )} +
+
+ ); +}; diff --git a/src/UserContext.ts b/src/UserContext.ts index ec2cc8420..75c5f125e 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -86,7 +86,7 @@ interface UserContext { } export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; -export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev"; +export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod1" | "rx" | "ex" | "prod" | "dev"; const ONE_WEEK_IN_MS = 604800000; diff --git a/src/Utils/CloudUtils.ts b/src/Utils/CloudUtils.ts index 089bbf0b6..2593aa4dc 100644 --- a/src/Utils/CloudUtils.ts +++ b/src/Utils/CloudUtils.ts @@ -1,9 +1,9 @@ import { userContext } from "../UserContext"; export function isRunningOnNationalCloud(): boolean { - return ( - userContext.portalEnv === "blackforest" || - userContext.portalEnv === "fairfax" || - userContext.portalEnv === "mooncake" - ); + return !isRunningOnPublicCloud(); +} + +export function isRunningOnPublicCloud(): boolean { + return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod"; } diff --git a/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts new file mode 100644 index 000000000..0c2b7c916 --- /dev/null +++ b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts @@ -0,0 +1,78 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2023-11-15-preview"; + +/* Creates a Data Transfer Job. */ +export async function create( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, + body: Types.CreateJobRequest, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Get a Data Transfer Job. */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Pause a Data Transfer Job. */ +export async function pause( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/pause`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Resumes a Data Transfer Job. */ +export async function resume( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/resume`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Cancels a Data Transfer Job. */ +export async function cancel( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/cancel`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Get a list of Data Transfer jobs. */ +export async function listByDatabaseAccount( + subscriptionId: string, + resourceGroupName: string, + accountName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/dataTransferService/types.ts b/src/Utils/arm/generatedClients/dataTransferService/types.ts new file mode 100644 index 000000000..27c3db709 --- /dev/null +++ b/src/Utils/arm/generatedClients/dataTransferService/types.ts @@ -0,0 +1,101 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json +*/ + +/* Base class for all DataTransfer source/sink */ +export interface DataTransferDataSourceSink { + /* undocumented */ + component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBSql" | "AzureBlobStorage"; +} + +/* A base CosmosDB data source/sink */ +export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & { + /* undocumented */ + remoteAccountName?: string; +}; + +/* A CosmosDB Cassandra API data source/sink */ +export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + keyspaceName: string; + /* undocumented */ + tableName: string; +}; + +/* A CosmosDB Mongo API data source/sink */ +export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + databaseName: string; + /* undocumented */ + collectionName: string; +}; + +/* A CosmosDB No Sql API data source/sink */ +export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + databaseName: string; + /* undocumented */ + containerName: string; +}; + +/* An Azure Blob Storage data source/sink */ +export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & { + /* undocumented */ + containerName: string; + /* undocumented */ + endpointUrl?: string; +}; + +/* The properties of a DataTransfer Job */ +export interface DataTransferJobProperties { + /* Job Name */ + readonly jobName?: string; + /* Source DataStore details */ + source: DataTransferDataSourceSink; + + /* Destination DataStore details */ + destination: DataTransferDataSourceSink; + + /* Job Status */ + readonly status?: string; + /* Processed Count. */ + readonly processedCount?: number; + /* Total Count. */ + readonly totalCount?: number; + /* Last Updated Time (ISO-8601 format). */ + readonly lastUpdatedUtcTime?: string; + /* Worker count */ + workerCount?: number; + /* Error response for Faulted job */ + readonly error?: unknown; + + /* Total Duration of Job */ + readonly duration?: string; + /* Mode of job execution */ + mode?: "Offline" | "Online"; +} + +/* Parameters to create Data Transfer Job */ +export type CreateJobRequest = unknown & { + /* Data Transfer Create Job Properties */ + properties: DataTransferJobProperties; +}; + +/* A Cosmos DB Data Transfer Job */ +export type DataTransferJobGetResults = unknown & { + /* undocumented */ + properties?: DataTransferJobProperties; +}; + +/* The List operation response, that contains the Data Transfer jobs and their properties. */ +export interface DataTransferJobFeedResults { + /* List of Data Transfer jobs and their properties. */ + readonly value?: DataTransferJobGetResults[]; + + /* URL to get the next set of Data Transfer job list results if there are any. */ + readonly nextLink?: string; +} diff --git a/src/hooks/useDataTransferJobs.tsx b/src/hooks/useDataTransferJobs.tsx new file mode 100644 index 000000000..5414dae28 --- /dev/null +++ b/src/hooks/useDataTransferJobs.tsx @@ -0,0 +1,60 @@ +import { getDataTransferJobs } from "Common/dataAccess/dataTransfers"; +import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types"; +import create, { UseStore } from "zustand"; + +export interface DataTransferJobsState { + dataTransferJobs: DataTransferJobGetResults[]; + pollingDataTransferJobs: Set; + setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => void; + setPollingDataTransferJobs: (pollingDataTransferJobs: Set) => void; +} + +type DataTransferJobStore = UseStore; + +export const useDataTransferJobs: DataTransferJobStore = create((set) => ({ + dataTransferJobs: [], + pollingDataTransferJobs: new Set(), + setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }), + setPollingDataTransferJobs: (pollingDataTransferJobs: Set) => set({ pollingDataTransferJobs }), +})); + +export const refreshDataTransferJobs = async ( + subscriptionId: string, + resourceGroup: string, + accountName: string, +): Promise => { + const dataTransferJobs: DataTransferJobGetResults[] = await getDataTransferJobs( + subscriptionId, + resourceGroup, + accountName, + ); + const jobRegex = /^Portal_(.+)_(\d{10,})$/; + const sortedJobs: DataTransferJobGetResults[] = dataTransferJobs?.sort( + (a, b) => + new Date(b?.properties?.lastUpdatedUtcTime).getTime() - new Date(a?.properties?.lastUpdatedUtcTime).getTime(), + ); + const filteredJobs = sortedJobs.filter((job) => jobRegex.test(job?.properties?.jobName)); + useDataTransferJobs.getState().setDataTransferJobs(filteredJobs); + return filteredJobs; +}; + +export const updateDataTransferJob = (updateJob: DataTransferJobGetResults) => { + const updatedDataTransferJobs = useDataTransferJobs + .getState() + .dataTransferJobs.map((job: DataTransferJobGetResults) => + job?.properties?.jobName === updateJob?.properties?.jobName ? updateJob : job, + ); + useDataTransferJobs.getState().setDataTransferJobs(updatedDataTransferJobs); +}; + +export const addToPolling = (addJob: string) => { + const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + pollingJobs.add(addJob); + useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs); +}; + +export const removeFromPolling = (removeJob: string) => { + const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + pollingJobs.delete(removeJob); + useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs); +}; diff --git a/utils/armClientGenerator/generator.ts b/utils/armClientGenerator/generator.ts index 01a7e2e72..01745830a 100644 --- a/utils/armClientGenerator/generator.ts +++ b/utils/armClientGenerator/generator.ts @@ -16,13 +16,13 @@ Results of this file should be checked into the repo. */ // CHANGE THESE VALUES TO GENERATE NEW CLIENTS -const version = "2023-09-15-preview"; +const version = "2023-11-15-preview"; /* The following are legal options for resourceName but you generally will only use cosmos-db: "cosmos-db" | "managedCassandra" | "mongorbac" | "notebook" | "privateEndpointConnection" | "privateLinkResources" | -"rbac" | "restorable" | "services" +"rbac" | "restorable" | "services" | "dataTransferService" */ const githubResourceName = "cosmos-db"; -const deResourceName = "cosmos"; +const deResourceName = "cosmos-db"; const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/${version}/${githubResourceName}.json`; const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${deResourceName}`); @@ -117,9 +117,9 @@ const propertyToType = (property: Property, prop: string, required: boolean) => if (property.allOf) { outputTypes.push(` /* ${property.description || "undocumented"} */ - ${property.readOnly ? "readonly " : ""}${prop}${ - required ? "" : "?" - }: ${property.allOf.map((allof: { $ref: string }) => refToType(allof.$ref)).join(" & ")}`); + ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.allOf + .map((allof: { $ref: string }) => refToType(allof.$ref)) + .join(" & ")}`); } else if (property.$ref) { const type = refToType(property.$ref); outputTypes.push(` @@ -142,8 +142,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) => outputTypes.push(` /* ${property.description || "undocumented"} */ ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.enum - .map((v: string) => `"${v}"`) - .join(" | ")} + .map((v: string) => `"${v}"`) + .join(" | ")} `); } else { if (property.type === undefined) { @@ -153,8 +153,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) => outputTypes.push(` /* ${property.description || "undocumented"} */ ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${ - propertyMap[property.type] ? propertyMap[property.type] : property.type - }`); + propertyMap[property.type] ? propertyMap[property.type] : property.type + }`); } } }; @@ -247,7 +247,7 @@ async function main() { const operation = schema.paths[path][method]; const [, methodName] = operation.operationId.split("_"); const bodyParameter = operation.parameters.find( - (parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true + (parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true, ); outputClient.push(` /* ${operation.description || "undocumented"} */ @@ -259,8 +259,8 @@ async function main() { ) : Promise<${responseType(operation, "Types")}> { const path = \`${path.replace(/{/g, "${")}\` return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "${method.toLocaleUpperCase()}", apiVersion, ${ - bodyParameter ? "body" : "" - } }) + bodyParameter ? "body" : "" + } }) } `); } From f87611a39d37a1e4747c14b842dc76d436a8389b Mon Sep 17 00:00:00 2001 From: vchske Date: Wed, 14 Feb 2024 09:55:45 -0800 Subject: [PATCH 019/102] Fixing manual throughput cost estimate (#1740) * Fixing manual throughput cost estimate * Fix test and prettier errors --- .../ThroughputInputAutoPilotV3Component.tsx | 2 +- ...ThroughputInputAutoPilotV3Component.test.tsx.snap | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 6346e8733..664ae01c7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< }; const costElement = (): JSX.Element => { - const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true); + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false); return ( {newThroughput && newThroughputCostElement()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 6a0f9efd1..605732141 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` > $ - 0.012 + 0.0080 /hr $ - 0.29 + 0.19 /day $ - 8.76 + 5.84 /mo @@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` > $ - 0.012 + 0.0080 /hr $ - 0.29 + 0.19 /day $ - 8.76 + 5.84 /mo From 5d80ecb4629a354060718885e41bbf0b8686c069 Mon Sep 17 00:00:00 2001 From: vchske Date: Fri, 16 Feb 2024 16:22:24 -0800 Subject: [PATCH 020/102] Fixing terminal tab to display correct API type for network warning (#1747) --- src/Explorer/Tabs/TerminalTab.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 538c596c5..986d4ef11 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -34,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { private getTabId: () => string, private getUsername: () => string, private isAllPublicIPAddressesEnabled: ko.Observable, + private kind: ViewModels.TerminalKind, ) {} public renderComponent(): JSX.Element { @@ -42,7 +43,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { ); } @@ -58,6 +59,18 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { ); } + + private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string { + switch (terminalKind) { + case ViewModels.TerminalKind.Postgres: + return "PostgreSQL"; + case ViewModels.TerminalKind.Mongo: + case ViewModels.TerminalKind.VCoreMongo: + return "MongoDB"; + default: + return ""; + } + } } export default class TerminalTab extends TabsBase { @@ -76,6 +89,7 @@ export default class TerminalTab extends TabsBase { () => this.tabId, () => this.getUsername(), this.isAllPublicIPAddressesEnabled, + options.kind, ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { if ( From 9cebe5f9ba1606fb3c4a3942d99856231fea4490 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:00:31 -0500 Subject: [PATCH 021/102] Adding CESCVA feedback button (#1736) * Adding CESCVA feedback button * Feedback message logic added --- src/Explorer/Explorer.tsx | 8 ++++++++ .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 3a945d3ed..4af478475 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -296,6 +296,14 @@ export default class Explorer { } } + public async openCESCVAFeedbackBlade(): Promise { + sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); + Logger.logInfo( + `CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`, + "Explorer/openCESCVAFeedbackBlade", + ); + } + public async refreshDatabaseForResourceToken(): Promise { const databaseId = userContext.parsedResourceToken?.databaseId; const collectionId = userContext.parsedResourceToken?.collectionId; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index a24c38ce5..ed5e4738e 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -236,7 +236,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, iconAlt: label, - onCommandClick: () => container.provideFeedbackEmail(), + onCommandClick: () => container.openCESCVAFeedbackBlade(), commandButtonLabel: undefined, ariaLabel: label, tooltipText: label, From 12366bb64503edeae75db47395212d25007e4158 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 21 Feb 2024 16:22:17 +0000 Subject: [PATCH 022/102] Revert "Hide buttons for Fabric or when no write access (#1742)" (#1751) This reverts commit f403b086adf2131c37ed2c349e2582e17ea8c47c. --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/Tabs/DocumentsTab.ts | 5 ----- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 3 +-- src/Platform/Fabric/FabricUtil.ts | 1 - 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index ed5e4738e..cdb478ad8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -135,7 +135,7 @@ export function createStaticCommandBarButtons( buttons.push(newSqlQueryBtn); } - if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) { + if (isQuerySupported && selectedNodeState.findSelectedCollection()) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index e9c6bee4e..d4e532ca4 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -881,11 +881,6 @@ export default class DocumentsTab extends TabsBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - if (!userContext.hasWriteAccess) { - // All the following buttons require write access - return []; - } - const buttons: CommandButtonComponentProps[] = []; const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; if (this.newDocumentButton.visible()) { diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index ff44a4be3..783e24dfa 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; -import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; @@ -403,7 +402,7 @@ export default class QueryTabComponent extends React.Component => { updateUserContext({ fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo }, databaseAccount: { ...userContext.databaseAccount }, - hasWriteAccess: false, // TODO: receive from fabricDatabaseConnectionInfo }); scheduleRefreshDatabaseResourceToken(); } catch (error) { From 5a64fc258205b671bffd3ed551664e84168cd2b6 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:30:04 -0500 Subject: [PATCH 023/102] Revert "Adding CESCVA feedback button (#1736)" (#1753) This reverts commit 9cebe5f9ba1606fb3c4a3942d99856231fea4490. --- src/Explorer/Explorer.tsx | 8 -------- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..3a945d3ed 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -296,14 +296,6 @@ export default class Explorer { } } - public async openCESCVAFeedbackBlade(): Promise { - sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); - Logger.logInfo( - `CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`, - "Explorer/openCESCVAFeedbackBlade", - ); - } - public async refreshDatabaseForResourceToken(): Promise { const databaseId = userContext.parsedResourceToken?.databaseId; const collectionId = userContext.parsedResourceToken?.collectionId; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index cdb478ad8..78810e9db 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -236,7 +236,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, iconAlt: label, - onCommandClick: () => container.openCESCVAFeedbackBlade(), + onCommandClick: () => container.provideFeedbackEmail(), commandButtonLabel: undefined, ariaLabel: label, tooltipText: label, From a36f3f79228079574be04f6ccef3be109c52bde4 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Thu, 22 Feb 2024 17:34:30 +0000 Subject: [PATCH 024/102] Hide Save/Open query buttons and New Document/Save/Update buttons for Fabric read-only (#1755) * Remove save query button and new document buttons for Fabric. Introduce a isReadOnly flag in Fabric context * Fix user context init --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/Tabs/DocumentsTab.ts | 5 +++++ src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 3 ++- src/Platform/Fabric/FabricUtil.ts | 6 +++++- src/UserContext.ts | 1 + src/hooks/useKnockoutExplorer.ts | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 78810e9db..a24c38ce5 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -135,7 +135,7 @@ export function createStaticCommandBarButtons( buttons.push(newSqlQueryBtn); } - if (isQuerySupported && selectedNodeState.findSelectedCollection()) { + if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index d4e532ca4..04ec0046b 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -881,6 +881,11 @@ export default class DocumentsTab extends TabsBase { } protected getTabsButtons(): CommandButtonComponentProps[] { + if (userContext.fabricContext?.isReadOnly) { + // All the following buttons require write access + return []; + } + const buttons: CommandButtonComponentProps[] = []; const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; if (this.newDocumentButton.visible()) { diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 783e24dfa..ff44a4be3 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; +import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; @@ -402,7 +403,7 @@ export default class QueryTabComponent extends React.Component => { } updateUserContext({ - fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo }, + fabricContext: { + ...userContext.fabricContext, + databaseConnectionInfo: fabricDatabaseConnectionInfo, + isReadOnly: true, + }, databaseAccount: { ...userContext.databaseAccount }, }); scheduleRefreshDatabaseResourceToken(); diff --git a/src/UserContext.ts b/src/UserContext.ts index 75c5f125e..e2fa7fd43 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -50,6 +50,7 @@ export interface VCoreMongoConnectionParams { interface FabricContext { connectionId: string; databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; + isReadOnly: boolean; } interface UserContext { diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 4de6d5fc2..e32b7c3af 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -328,6 +328,7 @@ function createExplorerFabric(params: { connectionId: string }): Explorer { fabricContext: { connectionId: params.connectionId, databaseConnectionInfo: undefined, + isReadOnly: true, }, authType: AuthType.ConnectionString, databaseAccount: { From c9abcc1728b53f5eb2adf49a8dee243ac5eedda3 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 22 Feb 2024 15:53:01 -0500 Subject: [PATCH 025/102] Prompt Mongo and Cassandra users to allow list Mongo and Cassandra proxies in Azure Portal (#1754) * Mongo Proxy backend API * merge main into current * allow mongo proxy endpoints to be constants * allow mongo proxy endpoints to be constants * fix test * show ip address warning for Mongo and Cassandra accounts * show ip address warning for Mongo and Cassandra accounts * removed string from prod * make mongo proxy endpoint mandatory * added MongoProxyEndpointsV2 * added MongoProxyEndpointsV2 * moved mongo and cassandra endpoints to Constants * moved mongo and cassandra endpoints to Constants * moved mongo and cassandra endpoints to Constants --------- Co-authored-by: Asier Isayas --- src/Common/Constants.ts | 16 ++++++ src/Common/MongoProxyClient.ts | 21 ++++++-- src/ConfigContext.ts | 16 ++++-- src/Contracts/ViewModels.ts | 1 + src/Explorer/Tabs/Tabs.tsx | 88 ++++++++++++++++++++++++++++++-- src/Utils/EndpointUtils.ts | 32 ++++++++---- src/hooks/useKnockoutExplorer.ts | 1 + 7 files changed, 153 insertions(+), 22 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index bfecb27c1..3a00cb10e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -435,6 +435,22 @@ export class JunoEndpoints { public static readonly Stage = "https://tools-staging.cosmos.azure.com"; } +export class MongoProxyEndpoints { + public static readonly Development: string = "https://localhost:7238"; + public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; +} + +export class CassandraProxyEndpoints { + public static readonly Development: string = "https://localhost:7240"; + public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn"; +} + export class PriorityLevel { public static readonly High = "high"; public static readonly Low = "low"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2a4d9fed7..2a3671f7c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,5 +1,9 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; -import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils"; +import { + allowedMongoProxyEndpoints, + allowedMongoProxyEndpoints_ToBeDeprecated, + validateEndpoint, +} from "Utils/EndpointUtils"; import queryString from "querystring"; import { AuthType } from "../AuthType"; import { configContext } from "../ConfigContext"; @@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId"; import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; -import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants"; +import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants"; import { MinimalQueryIterator } from "./IteratorUtilities"; import { sendMessage } from "./MessageHandler"; @@ -644,7 +648,10 @@ export function getFeatureEndpointOrDefault(feature: string): string { } else { endpoint = hasFlag(userContext.features.mongoProxyAPIs, feature) && - validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated) + validateEndpoint(userContext.features.mongoProxyEndpoint, [ + ...allowedMongoProxyEndpoints, + ...allowedMongoProxyEndpoints_ToBeDeprecated, + ]) ? userContext.features.mongoProxyEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; } @@ -683,8 +690,14 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { + let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if (userContext.databaseAccount.properties.ipRules?.length > 0) { + canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + return ( + canAccessMongoProxy && configContext.NEW_MONGO_APIS?.includes(api) && - [MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT) + [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT) ); } diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index c04bc9671..3422b200f 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,7 +1,8 @@ -import { JunoEndpoints } from "Common/Constants"; +import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { allowedAadEndpoints, allowedArcadiaEndpoints, + allowedCassandraProxyEndpoints, allowedEmulatorEndpoints, allowedGraphEndpoints, allowedHostedExplorerEndpoints, @@ -40,7 +41,9 @@ export interface ConfigContext { BACKEND_ENDPOINT?: string; MONGO_BACKEND_ENDPOINT?: string; MONGO_PROXY_ENDPOINT?: string; + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; NEW_MONGO_APIS?: string[]; + CASSANDRA_PROXY_ENDPOINT?: string; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -85,7 +88,7 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", - MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ // "resourcelist", // "createDocument", @@ -94,6 +97,8 @@ let configContext: Readonly = { // "deleteDocument", // "createCollectionWithProxy", ], + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, + CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, isTerminalEnabled: false, isPhoenixEnabled: false, }; @@ -147,6 +152,10 @@ export function updateConfigContext(newContext: Partial): void { delete newContext.MONGO_BACKEND_ENDPOINT; } + if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) { + delete newContext.CASSANDRA_PROXY_ENDPOINT; + } + if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) { delete newContext.JUNO_ENDPOINT; } @@ -164,10 +173,7 @@ export function updateConfigContext(newContext: Partial): void { // Injected for local development. These will be removed in the production bundle by webpack if (process.env.NODE_ENV === "development") { - const port: string = process.env.PORT || "1234"; updateConfigContext({ - BACKEND_ENDPOINT: "https://localhost:" + port, - MONGO_BACKEND_ENDPOINT: "https://localhost:" + port, PROXY_PATH: "/proxy", EMULATOR_ENDPOINT: "https://localhost:8081", }); diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 885ebabd0..3bfb62ae1 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame { serverId?: string; extensionEndpoint?: string; mongoProxyEndpoint?: string; + cassandraProxyEndpoint?: string; subscriptionType?: SubscriptionType; quotaId?: string; isTryCosmosDBSubscription?: boolean; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 6de7aa945..ff12d63f8 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -1,5 +1,8 @@ -import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; +import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; +import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { sendMessage } from "Common/MessageHandler"; +import { configContext, updateConfigContext } from "ConfigContext"; +import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; @@ -12,6 +15,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { userContext } from "UserContext"; +import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import ko from "knockout"; import React, { MutableRefObject, useEffect, useRef, useState } from "react"; @@ -33,6 +37,10 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState( userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(), ); + const [ + showMongoAndCassandraProxiesNetworkSettingsWarningState, + setShowMongoAndCassandraProxiesNetworkSettingsWarningState, + ] = useState(showMongoAndCassandraProxiesNetworkSettingsWarning()); return (
{networkSettingsWarning && ( @@ -69,9 +77,25 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { }, }} > - { - "To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'." - } + {`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove + the limit, go to the Settings cog on the right and find "RU Threshold".`} + + Learn More + + + )} + {showMongoAndCassandraProxiesNetworkSettingsWarningState && ( + { + setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false); + }} + > + {`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please + re-enable "Allow access from Azure Portal" on the Networking blade for your account.`} )}
@@ -299,3 +323,59 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); } }; + +const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { + const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; + if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { + const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; + const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); + const ipRulesIncludeLegacyPortalBackend: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule)) + ?.length === legacyPortalBackendIPs.length; + + if (!ipRulesIncludeLegacyPortalBackend) { + return false; + } + + if (userContext.apiType === "Mongo") { + const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes( + configContext.MONGO_PROXY_ENDPOINT, + ); + + const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint + ? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]] + : MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT]; + + const ipRulesIncludeMongoProxy: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule)) + ?.length === mongoProxyOutboundIPs.length; + + if (ipRulesIncludeMongoProxy) { + updateConfigContext({ + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, + }); + } + + return !ipRulesIncludeMongoProxy; + } else if (userContext.apiType === "Cassandra") { + const isProdOrMpacCassandraProxyEndpoint: boolean = [ + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ].includes(configContext.CASSANDRA_PROXY_ENDPOINT); + + const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint + ? [ + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac], + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod], + ] + : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; + + const ipRulesIncludeCassandraProxy: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule)) + ?.length === cassandraProxyOutboundIPs.length; + + return !ipRulesIncludeCassandraProxy; + } + } + return false; +}; diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 656f55903..3443d8c71 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -1,4 +1,4 @@ -import { JunoEndpoints } from "Common/Constants"; +import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; import * as Logger from "../Common/Logger"; export function validateEndpoint( @@ -67,17 +67,16 @@ export const PortalBackendIPs: { [key: string]: string[] } = { //usnat: ["7.28.202.68"], }; -export class MongoProxyEndpoints { - public static readonly Development: string = "https://localhost:7238"; - public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; - public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; - public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; - public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; -} +export const MongoProxyOutboundIPs: { [key: string]: string[] } = { + [MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"], + [MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"], + [MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"], + [MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"], +}; export const allowedMongoProxyEndpoints: ReadonlyArray = [ MongoProxyEndpoints.Development, - MongoProxyEndpoints.MPAC, + MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Mooncake, @@ -91,6 +90,21 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray = "https://localhost:12901", ]; +export const allowedCassandraProxyEndpoints: ReadonlyArray = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + CassandraProxyEndpoints.Fairfax, + CassandraProxyEndpoints.Mooncake, +]; + +export const CassandraProxyOutboundIPs: { [key: string]: string[] } = { + [CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"], + [CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"], + [CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"], + [CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"], +}; + export const allowedEmulatorEndpoints: ReadonlyArray = ["https://localhost:8081"]; export const allowedMongoBackendEndpoints: ReadonlyArray = ["https://localhost:1234"]; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index e32b7c3af..e6338e80b 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -480,6 +480,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT, ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, + CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint, }); updateUserContext({ From e9571c0f2d07045821c2bd312f6286de96c1461f Mon Sep 17 00:00:00 2001 From: Vsevolod Kukol Date: Tue, 27 Feb 2024 17:25:35 +0000 Subject: [PATCH 026/102] Update layout and colors for Fabric per req from Design (#1756) --- less/documentDBFabric.less | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index 57ad43316..be4e5c17e 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -25,29 +25,30 @@ a:focus { } .resourceTreeAndTabs { - border-radius: @FabricBoxBorderRadius; + border-radius: 0px; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; - margin-top: 4px; + margin-top: 0px; + margin-bottom: 0px; background-color: #ffffff; } .tabsManagerContainer { - background-color: #fafafa + background-color: #ffffff } .nav-tabs-margin { padding-top: 8px; - background-color: #fafafa + background-color: #ffffff } .commandBarContainer { background-color: #ffffff; - border-bottom: none; - border-radius: @FabricBoxBorderRadius; + border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; margin-top: 0px; + margin-bottom: 0px; padding-top: 2px; padding: 0px; height: 40px; @@ -162,9 +163,10 @@ a:focus { .dataExplorerErrorConsoleContainer { - border-radius: @FabricBoxBorderRadius; + border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; + margin-top: 0px; width: auto; align-self: auto; } From 16eb096fdba6e89502039c3251056aabb5838211 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 28 Feb 2024 18:34:33 +0000 Subject: [PATCH 027/102] Don't show Settings buttons for Fabric readonly (#1757) * Don't show Settings buttons for Fabric readonly * Fix format --- .../CommandBarComponentButtonFactory.tsx | 28 +++++++++++-------- src/Explorer/Tabs/DocumentsTab.ts | 3 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index a24c38ce5..2c398c418 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -196,18 +196,22 @@ export function createContextCommandBarButtons( } export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = [ - { - iconSrc: SettingsIcon, - iconAlt: "Settings", - onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), - commandButtonLabel: undefined, - ariaLabel: "Settings", - tooltipText: "Settings", - hasPopup: true, - disabled: false, - }, - ]; + const buttons: CommandButtonComponentProps[] = + configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly + ? [] + : [ + { + iconSrc: SettingsIcon, + iconAlt: "Settings", + onCommandClick: () => + useSidePanel.getState().openSidePanel("Settings", ), + commandButtonLabel: undefined, + ariaLabel: "Settings", + tooltipText: "Settings", + hasPopup: true, + disabled: false, + }, + ]; const showOpenFullScreen = configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin"; diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 04ec0046b..3591378da 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -1,4 +1,5 @@ import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { Platform, configContext } from "ConfigContext"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; @@ -881,7 +882,7 @@ export default class DocumentsTab extends TabsBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - if (userContext.fabricContext?.isReadOnly) { + if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { // All the following buttons require write access return []; } From b480c635cac5cbcb9d61fd8a2c88a6ee92b7b584 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 29 Feb 2024 12:10:15 -0500 Subject: [PATCH 028/102] fix create mongo collection and delete mongo document switching (#1758) Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2a3671f7c..357ce1f50 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -465,7 +465,7 @@ export function updateDocument_ToBeDeprecated( export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise { if (!useMongoProxyEndpoint("deleteDocument")) { - deleteDocument_ToBeDeprecated(databaseId, collection, documentId); + return deleteDocument_ToBeDeprecated(databaseId, collection, documentId); } const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -554,7 +554,7 @@ export function createMongoCollectionWithProxy( params: DataModels.CreateCollectionParams, ): Promise { if (!useMongoProxyEndpoint("createCollectionWithProxy")) { - createMongoCollectionWithProxy_ToBeDeprecated(params); + return createMongoCollectionWithProxy_ToBeDeprecated(params); } const { databaseAccount } = userContext; const shardKey: string = params.partitionKey?.paths[0]; From 932f211038fb160567881e9e80f38297463bc24b Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:19:12 -0500 Subject: [PATCH 029/102] Revert "Revert "Adding CESCVA feedback button (#1736)" (#1753)" (#1759) This reverts commit 5a64fc258205b671bffd3ed551664e84168cd2b6. --- src/Explorer/Explorer.tsx | 8 ++++++++ .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 3a945d3ed..4af478475 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -296,6 +296,14 @@ export default class Explorer { } } + public async openCESCVAFeedbackBlade(): Promise { + sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); + Logger.logInfo( + `CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`, + "Explorer/openCESCVAFeedbackBlade", + ); + } + public async refreshDatabaseForResourceToken(): Promise { const databaseId = userContext.parsedResourceToken?.databaseId; const collectionId = userContext.parsedResourceToken?.collectionId; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 2c398c418..ecfd3fdb9 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -240,7 +240,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, iconAlt: label, - onCommandClick: () => container.provideFeedbackEmail(), + onCommandClick: () => container.openCESCVAFeedbackBlade(), commandButtonLabel: undefined, ariaLabel: label, tooltipText: label, From 76ad930930937adcc3f2ed251201df75869b3004 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:08:13 -0800 Subject: [PATCH 030/102] Improve error handling when acquiring aad tokens (#1746) * Mostly working - some cosmetic changes remaining. * Cosmetic changes and other tidy ups. * More clean up. * Move msal back to dependencies. Fix typo. * msal should be prod dependency * Revert msal package update as it is causing issues with unit test execution. * Add tracing for unhandled exceptions when acquiring tokens. --- .vscode/settings.json | 4 +- package.json | 4 +- src/HostedExplorer.tsx | 9 ++- .../Hosted/AadAuthorizationFailure.less | 52 +++++++++++++ .../Components/AadAuthorizationFailure.tsx | 29 +++++++ src/Utils/AuthorizationUtils.ts | 48 +++++++++++- src/hooks/useAADAuth.ts | 77 +++++++++++++++---- src/hooks/useKnockoutExplorer.ts | 20 +++-- 8 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 src/Platform/Hosted/AadAuthorizationFailure.less create mode 100644 src/Platform/Hosted/Components/AadAuthorizationFailure.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index d66e1654c..a57d40961 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,8 +20,8 @@ "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" }, "typescript.preferences.importModuleSpecifier": "non-relative", "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/package.json b/package.json index 8af052a64..a7bfec4b8 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", - "patch-package": "8.0.0", "p-retry": "4.6.2", + "patch-package": "8.0.0", "plotly.js-cartesian-dist-min": "1.52.3", "post-robot": "10.0.42", "q": "1.5.1", @@ -238,4 +238,4 @@ "printWidth": 120, "endOfLine": "auto" } -} \ No newline at end of file +} diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 1eb2f5711..6f9c62866 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -1,5 +1,6 @@ import { initializeIcons } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; +import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure"; import * as React from "react"; import { render } from "react-dom"; import ChevronRight from "../images/chevron-right.svg"; @@ -32,7 +33,8 @@ const App: React.FunctionComponent = () => { // For showing/hiding panel const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const config = useConfig(); - const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); + const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } = + useAADAuth(); const [databaseAccount, setDatabaseAccount] = React.useState(); const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); const [connectionString, setConnectionString] = React.useState(); @@ -136,7 +138,10 @@ const App: React.FunctionComponent = () => { {!isLoggedIn && !encryptedTokenMetadata && ( )} - {isLoggedIn && } + {isLoggedIn && authFailure && } + {isLoggedIn && !authFailure && ( + + )} ); }; diff --git a/src/Platform/Hosted/AadAuthorizationFailure.less b/src/Platform/Hosted/AadAuthorizationFailure.less new file mode 100644 index 000000000..696a8337b --- /dev/null +++ b/src/Platform/Hosted/AadAuthorizationFailure.less @@ -0,0 +1,52 @@ +.aadAuthFailureContainer { + height: 100%; + width: 100%; +} +.aadAuthFailureContainer .aadAuthFailureFormContainer { + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + width: 100%; +} +.aadAuthFailureContainer .aadAuthFailure { + text-align: center; + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + justify-content: center; + height: 100%; + margin-bottom: 60px; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureTitle { + font-size: 16px; + font-weight: 500; + color: #d12d2d; + margin: 16px 8px 8px 8px; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureMessage { + font-size: 14px; + color: #393939; + margin: 16px 16px 16px 16px; + word-wrap: break-word; + white-space: pre-wrap; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureLink { + margin: 8px; + font-size: 14px; + color: #0058ad; + cursor: pointer; +} + +.aadAuthFailureContainer .aadAuthFailure .aadAuthFailureContent { + margin: 8px; + color: #393939; +} diff --git a/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx b/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx new file mode 100644 index 000000000..00d360a2e --- /dev/null +++ b/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx @@ -0,0 +1,29 @@ +import { AadAuthFailure } from "hooks/useAADAuth"; +import * as React from "react"; +import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; +import "../AadAuthorizationFailure.less"; + +interface Props { + authFailure: AadAuthFailure; +} + +export const AadAuthorizationFailure: React.FunctionComponent = ({ authFailure }: Props) => { + return ( +
+
+
+

+ Azure Cosmos DB +

+

Authorization Failure

+

{authFailure.failureMessage}

+ {authFailure.failureLinkTitle && ( +

+ {authFailure.failureLinkTitle} +

+ )} +
+
+
+ ); +}; diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 7fe1709c0..35fd4ed39 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,9 +1,11 @@ import * as msal from "@azure/msal-browser"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; import { AuthType } from "../AuthType"; import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { configContext } from "../ConfigContext"; import * as ViewModels from "../Contracts/ViewModels"; +import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext"; export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { @@ -43,8 +45,8 @@ export function decryptJWTToken(token: string) { return JSON.parse(tokenPayload); } -export function getMsalInstance() { - const config: msal.Configuration = { +export async function getMsalInstance() { + const msalConfig: msal.Configuration = { cache: { cacheLocation: "localStorage", }, @@ -55,8 +57,46 @@ export function getMsalInstance() { }; if (process.env.NODE_ENV === "development") { - config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; + msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; } - const msalInstance = new msal.PublicClientApplication(config); + + const msalInstance = new msal.PublicClientApplication(msalConfig); return msalInstance; } + +export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) { + const tokenRequest = { + account: msalInstance.getActiveAccount() || null, + ...request, + }; + + try { + // attempt silent acquisition first + return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken; + } catch (silentError) { + if (silentError instanceof msal.InteractionRequiredAuthError) { + try { + // The error indicates that we need to acquire the token interactively. + // This will display a pop-up to re-establish authorization. If user does not + // have pop-ups enabled in their browser, this will fail. + return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken; + } catch (interactiveError) { + traceFailure(Action.SignInAad, { + request: JSON.stringify(tokenRequest), + acquireTokenType: "interactive", + errorMessage: JSON.stringify(interactiveError), + }); + + throw interactiveError; + } + } else { + traceFailure(Action.SignInAad, { + request: JSON.stringify(tokenRequest), + acquireTokenType: "silent", + errorMessage: JSON.stringify(silentError), + }); + + throw silentError; + } + } +} diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 749a39632..c20f953f7 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -2,9 +2,9 @@ import * as msal from "@azure/msal-browser"; import { useBoolean } from "@fluentui/react-hooks"; import * as React from "react"; import { configContext } from "../ConfigContext"; -import { getMsalInstance } from "../Utils/AuthorizationUtils"; +import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils"; -const msalInstance = getMsalInstance(); +const msalInstance = await getMsalInstance(); const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedTenantId = localStorage.getItem("cachedTenantId"); @@ -18,6 +18,13 @@ interface ReturnType { tenantId: string; account: msal.AccountInfo; switchTenant: (tenantId: string) => void; + authFailure: AadAuthFailure; +} + +export interface AadAuthFailure { + failureMessage: string; + failureLinkTitle?: string; + failureLinkAction?: () => void; } export function useAADAuth(): ReturnType { @@ -28,6 +35,7 @@ export function useAADAuth(): ReturnType { const [tenantId, setTenantId] = React.useState(cachedTenantId); const [graphToken, setGraphToken] = React.useState(); const [armToken, setArmToken] = React.useState(); + const [authFailure, setAuthFailure] = React.useState(undefined); msalInstance.setActiveAccount(account); const login = React.useCallback(async () => { @@ -61,24 +69,60 @@ export function useAADAuth(): ReturnType { [account, tenantId], ); - React.useEffect(() => { - if (account && tenantId) { - Promise.all([ - msalInstance.acquireTokenSilent({ - authority: `${configContext.AAD_ENDPOINT}${tenantId}`, - scopes: [`${configContext.GRAPH_ENDPOINT}/.default`], - }), - msalInstance.acquireTokenSilent({ - authority: `${configContext.AAD_ENDPOINT}${tenantId}`, - scopes: [`${configContext.ARM_ENDPOINT}/.default`], - }), - ]).then(([graphTokenResponse, armTokenResponse]) => { - setGraphToken(graphTokenResponse.accessToken); - setArmToken(armTokenResponse.accessToken); + const acquireTokens = React.useCallback(async () => { + if (!(account && tenantId)) { + return; + } + + try { + const armToken = await acquireTokenWithMsal(msalInstance, { + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.ARM_ENDPOINT}/.default`], }); + + setArmToken(armToken); + setAuthFailure(null); + } catch (error) { + if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { + // This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively + // and user has popups disabled in browser. This fails as the popup is not the result of a explicit user + // action. In this case, we display the failure and a link to repeat the operation. Clicking on the + // link is a user action so it will work even if popups have been disabled. + // See: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/76#issuecomment-324787539 + setAuthFailure({ + failureMessage: + "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease click below to retry authorization without requiring popups being enabled.", + failureLinkTitle: "Retry Authorization", + failureLinkAction: acquireTokens, + }); + } else { + const errorJson = JSON.stringify(error); + setAuthFailure({ + failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`, + }); + } + } + + try { + const graphToken = await acquireTokenWithMsal(msalInstance, { + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.GRAPH_ENDPOINT}/.default`], + }); + + setGraphToken(graphToken); + } catch (error) { + // Graph token is used only for retrieving user photo at the moment, so + // it's not critical if this fails. + console.warn("Error acquiring graph token: " + error); } }, [account, tenantId]); + React.useEffect(() => { + if (account && tenantId && !authFailure) { + acquireTokens(); + } + }, [account, tenantId, acquireTokens, authFailure]); + return { account, tenantId, @@ -88,5 +132,6 @@ export function useAADAuth(): ReturnType { login, logout, switchTenant, + authFailure, }; } diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index e6338e80b..57fca4de0 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -6,6 +6,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap import { useSelectedNode } from "Explorer/useSelectedNode"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; @@ -35,7 +36,7 @@ import { import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; -import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; +import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; @@ -243,16 +244,19 @@ async function configureHostedWithAAD(config: AAD): Promise { let keys: DatabaseAccountListKeysResult = {}; if (account.properties?.documentEndpoint) { const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); - const msalInstance = getMsalInstance(); + const msalInstance = await getMsalInstance(); const cachedAccount = msalInstance.getAllAccounts()?.[0]; msalInstance.setActiveAccount(cachedAccount); const cachedTenantId = localStorage.getItem("cachedTenantId"); - const aadTokenResponse = await msalInstance.acquireTokenSilent({ - forceRefresh: true, - scopes: [hrefEndpoint], - authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`, - }); - aadToken = aadTokenResponse.accessToken; + try { + aadToken = await acquireTokenWithMsal(msalInstance, { + forceRefresh: true, + scopes: [hrefEndpoint], + authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`, + }); + } catch (authError) { + logConsoleError("Failed to acquire authorization token: " + authError); + } } try { if (!account.properties.disableLocalAuth) { From 533e9c887c6d16b0ada5fdf555f7b12596d3507f Mon Sep 17 00:00:00 2001 From: Vsevolod Kukol Date: Wed, 6 Mar 2024 01:41:50 +0100 Subject: [PATCH 031/102] Small fixes for Fabric PuPr (#1761) * Hide the RU Threshold Message in Fabric Fabric is RO and the Settings button is hidden, hence the message doesn't make sense. If customers hit the limits they can go to Portal and change the settings there. * Change the toolbar font size and icon color in Fabric --- less/Common/Constants.less | 1 + src/Explorer/Menus/CommandBar/CommandBarUtil.tsx | 16 +++++++++++++--- src/Explorer/Tabs/Tabs.tsx | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/less/Common/Constants.less b/less/Common/Constants.less index ea7cf9f9f..946426d37 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -163,6 +163,7 @@ /**********************************************************************************/ @FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; +@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)"; @FabricBoxBorderRadius: 8px; @FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 0b866cbbf..384bd06cd 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -37,7 +37,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol if (isDisabled) { return StyleConstants.GrayScale; } - return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined; + return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined; }; return btns @@ -96,7 +96,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol }, width: 16, }, - label: { fontSize: StyleConstants.mediumFontSize }, + label: { + fontSize: + configContext.platform == Platform.Fabric + ? StyleConstants.DefaultFontSize + : StyleConstants.mediumFontSize, + }, rootHovered: { backgroundColor: hoverColor }, rootPressed: { backgroundColor: hoverColor }, splitButtonMenuButtonExpanded: { @@ -133,7 +138,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol // TODO Figure out how to do it the proper way with subComponentStyles. // TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes selectors: { - ".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize }, + ".ms-ContextualMenu-itemText": { + fontSize: + configContext.platform == Platform.Fabric + ? StyleConstants.DefaultFontSize + : StyleConstants.mediumFontSize, + }, ".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor }, ".ms-ContextualMenu-icon": { width: 16, height: 16 }, }, diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index ff12d63f8..fc152aa92 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -1,7 +1,7 @@ import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { sendMessage } from "Common/MessageHandler"; -import { configContext, updateConfigContext } from "ConfigContext"; +import { Platform, configContext, updateConfigContext } from "ConfigContext"; import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; @@ -35,7 +35,7 @@ interface TabsProps { export const Tabs = ({ explorer }: TabsProps): JSX.Element => { const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs(); const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState( - userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(), + userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(), ); const [ showMongoAndCassandraProxiesNetworkSettingsWarningState, From b8457e3bf9afe6f3a3078be22b7609c043b5cea3 Mon Sep 17 00:00:00 2001 From: MokireddySampath <120497218+MokireddySampath@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:43:44 +0530 Subject: [PATCH 032/102] defect2278780 (#1472) * arialabel has been added to close button of invitational youtube video * heading role has been addedd and tag has been changed to h1 * outline has been restored to choose columns link in entities page * Update QuickstartCarousel.tsx * Update SplashScreen.tsx * Update TableEntity.tsx * outline for edit entity has been added on focus * keyboard accessibility added to rows in table entities * Update queryBuilder.less * Update TableEntity.tsx * Update PanelComponent.less * Update DataTableBindingManager.ts * Update DataTableBindingManager.ts * Update DataTableBindingManager.ts * Update DataTableBindingManager.ts * Update DataTableBindingManager.ts --- .../DataTable/DataTableBindingManager.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts index 6616a3af5..3ef0037aa 100644 --- a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts +++ b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts @@ -42,6 +42,11 @@ function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewM createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start. $(window).resize(updateTableScrollableRegionMetrics); + operationManager.focusTable(); // Also selects the first row if needed. + // Attach the arrow key event handler to the table element + $dataTable.on("keydown", (event: JQueryEventObject) => { + handleArrowKey(element, valueAccessor, allBindings, viewModel, bindingContext, event); + }); } function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) { @@ -210,6 +215,39 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi }); //selected = bindingContext.$data.selected(); } +function handleArrowKey( + element: any, + valueAccessor: any, + allBindings: any, + viewModel: any, + bindingContext: any, + event: JQueryEventObject, +) { + const isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow; + const isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow; + + if (isUpArrowKey || isDownArrowKey) { + const $dataTable = $(element); + let $selectedRow = $dataTable.find("tr.selected"); + + if ($selectedRow.length === 0) { + // No row is currently selected, select the first row + $selectedRow = $dataTable.find("tr:first"); + $selectedRow.addClass("selected"); + } else { + const $targetRow = isUpArrowKey ? $selectedRow.prev("tr") : $selectedRow.next("tr"); + + if ($targetRow.length > 0) { + // Remove the selected class from the current row and add it to the target row + $selectedRow.removeClass("selected").attr("tabindex", "-1"); + $targetRow.addClass("selected").attr("tabindex", "0"); + $targetRow.focus(); + } + } + + event.preventDefault(); + } +} function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { // do nothing for now From 47bdc9c42640d1f13fac4be7159cf2b99b84c42f Mon Sep 17 00:00:00 2001 From: MokireddySampath <120497218+MokireddySampath@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:47:57 +0530 Subject: [PATCH 033/102] styling changes have been made o remove the overlaping of focus outlines (#1721) --- .../Controls/ThroughputInput/ThroughputInput.less | 10 ++++++++-- src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less index 00c328077..ae7e873fa 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less @@ -14,7 +14,13 @@ .throughputInputSpacing > :not(:last-child) { margin-bottom: @DefaultSpace; } -.capacitycalculator-link:focus{ + +.capacitycalculator-link:focus { text-decoration: underline; outline-offset: 2px; -} \ No newline at end of file +} + +.copyQuery:focus::after, +.deleteQuery:focus::after { + outline: none !important; +} diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 557aee3d6..892100e6d 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -618,6 +618,7 @@ export const QueryCopilotPromptbar: React.FC = ({ = ({ Copy query { setShowDeletePopup(true); }} From 5d4e9d82bb9277165344797faac0345235244ad2 Mon Sep 17 00:00:00 2001 From: MokireddySampath <120497218+MokireddySampath@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:48:46 +0530 Subject: [PATCH 034/102] Bug 1240907: Aria-label is not descriptive enough for 'More(...)' button present under 'SQL API' section. (#1748) * screen reader name for the more button has been modified as suggested * e2e test have been updated * e2e tests updated --- src/Explorer/Controls/TreeComponent/TreeComponent.tsx | 2 +- .../TreeComponent/__snapshots__/TreeComponent.test.tsx.snap | 4 ++-- test/cassandra/container.spec.ts | 4 ++-- test/graph/container.spec.ts | 4 ++-- test/mongo/container.spec.ts | 4 ++-- test/mongo/container32.spec.ts | 4 ++-- test/sql/container.spec.ts | 4 ++-- test/tables/container.spec.ts | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index 362390b80..4cf601047 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -247,7 +247,7 @@ export class TreeNodeComponent extends React.Component { await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId); await explorer.click("#sidePanelOkButton"); await explorer.click(`.nodeItem >> text=${keyspaceId}`); - await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', keyspaceId); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 342c12d4b..3e6155f1c 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -21,11 +21,11 @@ test("Graph CRUD", async () => { await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Graph")'); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index d6a9271ed..88a311784 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -32,11 +32,11 @@ test("Mongo CRUD", async () => { await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[data-test="Save"]'); // Delete database and collection - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index ac6dbc39c..25466e266 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -21,11 +21,11 @@ test("Mongo CRUD", async () => { explorer.click(`.nodeItem >> text=${databaseId}`); explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and collection - explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 975687aaa..aead57b92 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -18,11 +18,11 @@ test("SQL CRUD", async () => { await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); await explorer.click(`.nodeItem >> text=${databaseId}`); - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index 932127bb1..d8daa0a16 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -17,7 +17,7 @@ test("Tables CRUD", async () => { await explorer.fill('[aria-label="Table id, Example Table1"]', tableId); await explorer.click("#sidePanelOkButton"); await explorer.click(`[data-test="TablesDB"]`); - await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); From 0039adf1c23ba7c5dee94f3f403f512b958fa710 Mon Sep 17 00:00:00 2001 From: MokireddySampath <120497218+MokireddySampath@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:54:44 +0530 Subject: [PATCH 035/102] Border has been added to distinguish clear notifications from text (#1750) --- .../NotificationConsoleComponent.tsx | 1 + .../NotificationConsoleComponent.test.tsx.snap | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index b66d344cc..7cefa9ac2 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -162,6 +162,7 @@ export class NotificationConsoleComponent extends React.Component< role="button" onKeyDown={(event: React.KeyboardEvent) => this.onClearNotificationsKeyPress(event)} tabIndex={0} + style={{ border: "1px solid black", borderRadius: "2px" }} > clear notifications image Clear Notifications diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 8b8b2bdeb..f702107b8 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -146,6 +146,12 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` onClick={[Function]} onKeyDown={[Function]} role="button" + style={ + Object { + "border": "1px solid black", + "borderRadius": "2px", + } + } tabIndex={0} > Date: Wed, 6 Mar 2024 12:56:07 +0530 Subject: [PATCH 036/102] Bug 1242529: [Usable - Azure Cosmos DB - Input Parameters]: Aria-label is not descriptive enough for the 'Close(x)' button of 'Input Parameters' blade. (#1760) * screen reader name for the button has been changed to read out the name of the dialog box * tests have been updated --- src/Explorer/Panes/PanelContainerComponent.tsx | 2 +- .../Panes/__snapshots__/PanelContainerComponent.test.tsx.snap | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Explorer/Panes/PanelContainerComponent.tsx b/src/Explorer/Panes/PanelContainerComponent.tsx index fa2db4f42..bad1719be 100644 --- a/src/Explorer/Panes/PanelContainerComponent.tsx +++ b/src/Explorer/Panes/PanelContainerComponent.tsx @@ -58,7 +58,7 @@ export class PanelContainerComponent extends React.Component Date: Wed, 6 Mar 2024 12:56:34 +0530 Subject: [PATCH 037/102] Asterisk has beenadded beside the heading of input to show that it is a mandatory input (#1749) --- .../StoredProcedureTab/StoredProcedureTabComponent.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 73736eaac..98e398e1b 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -1,8 +1,8 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Pivot, PivotItem } from "@fluentui/react"; import React from "react"; -import DiscardIcon from "../../../../images/discard.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; import { NormalizedEventKey } from "../../../Common/Constants"; import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure"; @@ -512,7 +512,12 @@ export default class StoredProcedureTabComponent extends React.Component< return (
-
Stored Procedure Id
+
+ Stored Procedure Id + + *  + +
Date: Wed, 6 Mar 2024 12:57:05 +0530 Subject: [PATCH 038/102] color of the link has been changed to get the approved color contrast ratio of 4.5:1 (#1710) --- src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 892100e6d..58e5988b9 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -511,7 +511,7 @@ export const QueryCopilotPromptbar: React.FC = ({ AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} - + Read preview terms {showErrorMessageBar && ( From 56c0049e9ad7f564407b68fe88989a1d462f1b62 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:33:21 -0600 Subject: [PATCH 039/102] add feedback policies integration with copilot (#1745) * add feedback policies integration with copilot * remove teaching bubble and welcome modal * force prod phoenix endpoint in MPAC * force prod phoenix endpoint in MPAC --- configs/mpac.json | 8 +- src/Contracts/ViewModels.ts | 1 + .../AddCollectionPanel.test.tsx.snap | 2 +- .../QueryCopilot/QueryCopilotPromptbar.tsx | 174 ++++++++---------- src/UserContext.ts | 16 ++ src/hooks/useKnockoutExplorer.ts | 1 + 6 files changed, 103 insertions(+), 99 deletions(-) diff --git a/configs/mpac.json b/configs/mpac.json index cd8b27e03..0a8e7eaba 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,5 +1,5 @@ { - "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", - "isTerminalEnabled" : true, - "isPhoenixEnabled" : true -} \ No newline at end of file + "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", + "isTerminalEnabled": true, + "isPhoenixEnabled": true +} diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 3bfb62ae1..bbf5455bb 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -407,6 +407,7 @@ export interface DataExplorerInputsFrame { features?: { [key: string]: string; }; + feedbackPolicies?: any; } export interface SelfServeFrameInputs { diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap index 0a19a654b..79485d550 100644 --- a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap @@ -486,4 +486,4 @@ exports[`AddCollectionPanel should render Default properly 1`] = ` isButtonDisabled={false} /> -`; +`; \ No newline at end of file diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 58e5988b9..fb6b79dd6 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -21,7 +21,6 @@ import { import { HttpStatusCodes } from "Common/Constants"; import { handleError } from "Common/ErrorHandlingUtils"; import { createUri } from "Common/UrlUtility"; -import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { @@ -272,28 +271,11 @@ export const QueryCopilotPromptbar: React.FC = ({ } }; - const showTeachingBubble = (): void => { - if (showPromptTeachingBubble && !inputEdited.current) { - setTimeout(() => { - if (!inputEdited.current && !isWelcomModalVisible()) { - setCopilotTeachingBubbleVisible(true); - inputEdited.current = true; - } - }, 30000); - } else { - toggleCopilotTeachingBubbleVisible(false); - } - }; - const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => { setCopilotTeachingBubbleVisible(visible); setShowPromptTeachingBubble(visible); }; - const isWelcomModalVisible = (): boolean => { - return localStorage.getItem("hideWelcomeModal") !== "true"; - }; - const clearFeedback = () => { resetButtonState(); resetQueryResults(); @@ -322,7 +304,6 @@ export const QueryCopilotPromptbar: React.FC = ({ }; React.useEffect(() => { - showTeachingBubble(); useTabs.getState().setIsQueryErrorThrown(false); }, []); @@ -539,84 +520,90 @@ export const QueryCopilotPromptbar: React.FC = ({ {showFeedbackBar && ( - - Provide feedback on the query generated - {showCallout && !hideFeedbackModalForLikedQueries && ( - { - setShowCallout(false); - SubmitFeedback({ - params: { - generatedQuery: generatedQuery, - likeQuery: likeQuery, - description: "", - userPrompt: userPrompt, - }, - explorer, - databaseId, - containerId, - mode: isSampleCopilotActive ? "Sample" : "User", - }); - }} - directionalHint={DirectionalHint.topCenter} - > - - Thank you. Need to give{" "} - { + + {userContext.feedbackPolicies?.policyAllowFeedback && ( + + Provide feedback on the query generated + {showCallout && !hideFeedbackModalForLikedQueries && ( + { setShowCallout(false); - openFeedbackModal(generatedQuery, true, userPrompt); + SubmitFeedback({ + params: { + generatedQuery: generatedQuery, + likeQuery: likeQuery, + description: "", + userPrompt: userPrompt, + }, + explorer, + databaseId, + containerId, + mode: isSampleCopilotActive ? "Sample" : "User", + }); }} + directionalHint={DirectionalHint.topCenter} > - more feedback? - - - + + Thank you. Need to give{" "} + { + setShowCallout(false); + openFeedbackModal(generatedQuery, true, userPrompt); + }} + > + more feedback? + + + + )} + { + setShowCallout(!likeQuery); + setLikeQuery(!likeQuery); + if (likeQuery === true) { + document.getElementById("likeStatus").innerHTML = "Unpressed"; + } + if (likeQuery === false) { + document.getElementById("likeStatus").innerHTML = "Liked"; + } + if (dislikeQuery) { + setDislikeQuery(!dislikeQuery); + } + }} + /> + { + let toggleStatusValue = "Unpressed"; + if (!dislikeQuery) { + openFeedbackModal(generatedQuery, false, userPrompt); + setLikeQuery(false); + toggleStatusValue = "Disliked"; + } + setDislikeQuery(!dislikeQuery); + setShowCallout(false); + document.getElementById("likeStatus").innerHTML = toggleStatusValue; + }} + /> + + + )} - { - setShowCallout(!likeQuery); - setLikeQuery(!likeQuery); - if (likeQuery === true) { - document.getElementById("likeStatus").innerHTML = "Unpressed"; - } - if (likeQuery === false) { - document.getElementById("likeStatus").innerHTML = "Liked"; - } - if (dislikeQuery) { - setDislikeQuery(!dislikeQuery); - } - }} - /> - { - let toggleStatusValue = "Unpressed"; - if (!dislikeQuery) { - openFeedbackModal(generatedQuery, false, userPrompt); - setLikeQuery(false); - toggleStatusValue = "Disliked"; - } - setDislikeQuery(!dislikeQuery); - setShowCallout(false); - document.getElementById("likeStatus").innerHTML = toggleStatusValue; - }} - /> - - - - = ({ )} - {isSamplePromptsOpen && } {query !== "" && query.trim().length !== 0 && ( Date: Mon, 11 Mar 2024 15:17:01 -0700 Subject: [PATCH 040/102] Add CassandraProxy support in DE (#1764) --- src/Common/Constants.ts | 13 +- src/ConfigContext.ts | 9 ++ src/Explorer/Tables/TableDataClient.ts | 200 ++++++++++++++++++++++++- src/Utils/EndpointUtils.ts | 8 + 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 3a00cb10e..804c8adea 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -124,7 +124,7 @@ export enum MongoBackendEndpointType { remote, } -// TODO: 435619 Add default endpoints per cloud and use regional only when available +//TODO: Remove this when new backend is migrated over export class CassandraBackend { public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; @@ -136,6 +136,17 @@ export class CassandraBackend { public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; } +export class CassandraProxyAPIs { + public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; + public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete"; + public static readonly queryApi: string = "api/cassandra/postquery"; + public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra"; + public static readonly keysApi: string = "api/cassandra/keys"; + public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys"; + public static readonly schemaApi: string = "api/cassandra/schema"; + public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema"; +} + export class Queries { public static CustomPageOption: string = "custom"; public static UnlimitedPageOption: string = "unlimited"; diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 3422b200f..f02fa260d 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -44,6 +44,8 @@ export interface ConfigContext { MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; NEW_MONGO_APIS?: string[]; CASSANDRA_PROXY_ENDPOINT?: string; + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean; + NEW_CASSANDRA_APIS?: string[]; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -99,6 +101,13 @@ let configContext: Readonly = { ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, + NEW_CASSANDRA_APIS: [ + // "postQuery", + // "createOrDelete", + // "getKeys", + // "getSchema", + ], + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, isTerminalEnabled: false, isPhoenixEnabled: false, }; diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 830a2544f..3d5f05c72 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -19,6 +19,7 @@ import Explorer from "../Explorer"; import * as TableConstants from "./Constants"; import * as Entities from "./Entities"; import * as TableEntityProcessor from "./TableEntityProcessor"; +import { CassandraProxyAPIs } from "../../Common/Constants"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -261,6 +262,57 @@ export class CassandraAPIDataClient extends TableDataClient { query: string, shouldNotify?: boolean, paginationToken?: string, + ): Promise { + if (!this.useCassandraProxyEndpoint("postQuery")) { + return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken); + } + const clearMessage = + shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); + try { + const { authType, databaseAccount } = userContext; + + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringQueryApi + : CassandraProxyAPIs.queryApi; + + const data: any = await $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + query, + paginationToken, + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }); + shouldNotify && + NotificationConsoleUtils.logConsoleInfo( + `Successfully fetched ${data.result.length} rows for table ${collection.id()}`, + ); + return { + Results: data.result, + ContinuationToken: data.paginationToken, + }; + } catch (error) { + shouldNotify && + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + throw error; + } finally { + clearMessage?.(); + } + } + + public async queryDocuments_ToBeDeprecated( + collection: ViewModels.Collection, + query: string, + shouldNotify?: boolean, + paginationToken?: string, ): Promise { const clearMessage = shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); @@ -294,7 +346,11 @@ export class CassandraAPIDataClient extends TableDataClient { }; } catch (error) { shouldNotify && - handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + handleError( + error, + "QueryDocuments_ToBeDeprecated_Cassandra", + `Failed to query rows for table ${collection.id()}`, + ); throw error; } finally { clearMessage?.(); @@ -402,6 +458,50 @@ export class CassandraAPIDataClient extends TableDataClient { } public getTableKeys(collection: ViewModels.Collection): Q.Promise { + if (!this.useCassandraProxyEndpoint("getTableKeys")) { + return this.getTableKeys_ToBeDeprecated(collection); + } + + if (!!collection.cassandraKeys) { + return Q.resolve(collection.cassandraKeys); + } + const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`); + const { authType, databaseAccount } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken ? CassandraProxyAPIs.connectionStringKeysApi : CassandraProxyAPIs.keysApi; + + let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + + $.ajax(endpoint, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }) + .then( + (data: CassandraTableKeys) => { + collection.cassandraKeys = data; + logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`); + deferred.resolve(data); + }, + (error: any) => { + handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(error); + }, + ) + .done(clearInProgressMessage); + return deferred.promise; + } + + public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraKeys) { return Q.resolve(collection.cassandraKeys); } @@ -442,6 +542,51 @@ export class CassandraAPIDataClient extends TableDataClient { } public getTableSchema(collection: ViewModels.Collection): Q.Promise { + if (!this.useCassandraProxyEndpoint("getSchema")) { + return this.getTableSchema_ToBeDeprecated(collection); + } + + if (!!collection.cassandraSchema) { + return Q.resolve(collection.cassandraSchema); + } + const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`); + const { databaseAccount, authType } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringSchemaApi + : CassandraProxyAPIs.schemaApi; + let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + + $.ajax(endpoint, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }) + .then( + (data: any) => { + collection.cassandraSchema = data.columns; + logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`); + deferred.resolve(data.columns); + }, + (error: any) => { + handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(error); + }, + ) + .done(clearInProgressMessage); + return deferred.promise; + } + + public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraSchema) { return Q.resolve(collection.cassandraSchema); } @@ -482,6 +627,44 @@ export class CassandraAPIDataClient extends TableDataClient { } private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise { + if (!this.useCassandraProxyEndpoint("createOrDelete")) { + return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query); + } + + const deferred = Q.defer(); + const { authType, databaseAccount } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringCreateOrDeleteApi + : CassandraProxyAPIs.createOrDeleteApi; + + $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint), + resourceId: resourceId, + query: query, + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }).then( + (data: any) => { + deferred.resolve(); + }, + (reason) => { + deferred.reject(reason); + }, + ); + return deferred.promise; + } + + private createOrDeleteQuery_ToBeDeprecated( + cassandraEndpoint: string, + resourceId: string, + query: string, + ): Q.Promise { const deferred = Q.defer(); const { authType, databaseAccount } = userContext; const apiEndpoint: string = @@ -547,4 +730,19 @@ export class CassandraAPIDataClient extends TableDataClient { private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { return collection.cassandraKeys.partitionKeys[0].property; } + + private useCassandraProxyEndpoint(api: string): boolean { + let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if (userContext.databaseAccount.properties.ipRules?.length > 0) { + canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + + return ( + canAccessCassandraProxy && + configContext.NEW_CASSANDRA_APIS?.includes(api) && + [Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes( + configContext.CASSANDRA_PROXY_ENDPOINT, + ) + ); + } } diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 3443d8c71..ee01f7f9a 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -98,6 +98,14 @@ export const allowedCassandraProxyEndpoints: ReadonlyArray = [ CassandraProxyEndpoints.Mooncake, ]; +export const allowedCassandraProxyEndpoints_ToBeDeprecated: ReadonlyArray = [ + "https://main.documentdb.ext.azure.com", + "https://main.documentdb.ext.azure.cn", + "https://main.documentdb.ext.azure.us", + "https://main.cosmos.ext.azure", + "https://localhost:12901", +]; + export const CassandraProxyOutboundIPs: { [key: string]: string[] } = { [CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"], [CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"], From 4c74525b5d46a40bdf31e124dd253ce4f2176963 Mon Sep 17 00:00:00 2001 From: vchske Date: Tue, 12 Mar 2024 14:55:14 -0700 Subject: [PATCH 041/102] Adding computed properties to Settings tab for containers (#1763) * Adding computed properties to Settings tab for containers * Fixing files for prettier and a test snapshot --- src/Contracts/DataModels.ts | 8 ++ src/Contracts/ViewModels.ts | 1 + .../Controls/Settings/SettingsComponent.tsx | 85 +++++++++++- .../Settings/SettingsRenderUtils.test.tsx | 2 - .../Controls/Settings/SettingsRenderUtils.tsx | 7 +- .../ComputedPropertiesComponent.test.tsx | 56 ++++++++ .../ComputedPropertiesComponent.tsx | 128 ++++++++++++++++++ .../IndexingPolicyComponent.tsx | 4 +- .../MongoIndexingPolicyComponent.tsx | 10 +- .../ComputedPropertiesComponent.test.tsx.snap | 36 +++++ .../Controls/Settings/SettingsUtils.tsx | 5 +- src/Explorer/Controls/Settings/TestUtils.tsx | 6 + .../SettingsComponent.test.tsx.snap | 37 +++++ .../SettingsRenderUtils.test.tsx.snap | 12 -- src/Explorer/Tree/Collection.ts | 2 + .../cosmos/cassandraResources.ts | 6 +- .../arm/generatedClients/cosmos/collection.ts | 6 +- .../cosmos/collectionPartition.ts | 6 +- .../cosmos/collectionPartitionRegion.ts | 6 +- .../cosmos/collectionRegion.ts | 6 +- .../arm/generatedClients/cosmos/database.ts | 6 +- .../cosmos/databaseAccountRegion.ts | 6 +- .../cosmos/databaseAccounts.ts | 6 +- .../generatedClients/cosmos/graphResources.ts | 6 +- .../cosmos/gremlinResources.ts | 6 +- .../arm/generatedClients/cosmos/locations.ts | 6 +- .../cosmos/mongoDBResources.ts | 6 +- .../arm/generatedClients/cosmos/operations.ts | 6 +- .../cosmos/partitionKeyRangeId.ts | 6 +- .../cosmos/partitionKeyRangeIdRegion.ts | 6 +- .../arm/generatedClients/cosmos/percentile.ts | 6 +- .../cosmos/percentileSourceTarget.ts | 6 +- .../cosmos/percentileTarget.ts | 6 +- .../generatedClients/cosmos/sqlResources.ts | 6 +- .../generatedClients/cosmos/tableResources.ts | 6 +- .../arm/generatedClients/cosmos/types.ts | 47 ++++--- utils/armClientGenerator/generator.ts | 2 +- 37 files changed, 460 insertions(+), 108 deletions(-) create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index ac7b9499e..3c8a5175d 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -159,6 +159,7 @@ export interface Collection extends Resource { geospatialConfig?: GeospatialConfig; schema?: ISchema; requestSchema?: () => void; + computedProperties?: ComputedProperties; } export interface CollectionsWithPagination { @@ -197,6 +198,13 @@ export interface IndexingPolicy { spatialIndexes?: any; } +export interface ComputedProperty { + name: string; + query: string; +} + +export type ComputedProperties = ComputedProperty[]; + export interface PartitionKey { paths: string[]; kind: "Hash" | "Range" | "MultiHash"; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index bbf5455bb..b79ddce4e 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -135,6 +135,7 @@ export interface Collection extends CollectionBase { changeFeedPolicy: ko.Observable; geospatialConfig: ko.Observable; documentIds: ko.ObservableArray; + computedProperties: ko.Observable; cassandraKeys: CassandraTableKeys; cassandraSchema: CassandraTableKey[]; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 18a0e1e9e..574d75603 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1,4 +1,8 @@ import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react"; +import { + ComputedPropertiesComponent, + ComputedPropertiesComponentProps, +} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; import { useDatabases } from "Explorer/useDatabases"; import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; @@ -108,6 +112,11 @@ export interface SettingsComponentState { indexesToAdd: AddMongoIndexProps[]; indexTransformationProgress: number; + computedPropertiesContent: DataModels.ComputedProperties; + computedPropertiesContentBaseline: DataModels.ComputedProperties; + shouldDiscardComputedProperties: boolean; + isComputedPropertiesDirty: boolean; + conflictResolutionPolicyMode: DataModels.ConflictResolutionMode; conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode; conflictResolutionPolicyPath: string; @@ -132,6 +141,7 @@ export class SettingsComponent extends React.Component this.setState({ isMongoIndexingPolicyDiscardable }); + private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void => + this.setState({ computedPropertiesContent: newComputedProperties }); + + private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false }); + + private logComputedPropertiesSuccessMessage = (): void => { + if (this.props.settingsTab.onLoadStartKey) { + traceSuccess( + Action.Tab, + { + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + }, + this.props.settingsTab.onLoadStartKey, + ); + this.props.settingsTab.onLoadStartKey = undefined; + } + }; + + private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void => + this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty }); + private calculateTotalThroughputUsed = (): void => { this.totalThroughputUsed = 0; (useDatabases.getState().databases || []).forEach(async (database) => { @@ -643,7 +689,6 @@ export class SettingsComponent extends React.Component => { const newCollection: DataModels.Collection = { ...this.collection.rawDataModel }; - if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) { + if ( + this.state.isSubSettingsSaveable || + this.state.isIndexingPolicyDirty || + this.state.isConflictResolutionDirty || + this.state.isComputedPropertiesDirty + ) { let defaultTtl: number; switch (this.state.timeToLive) { case TtlType.On: @@ -832,6 +890,10 @@ export class SettingsComponent extends React.Component, + }); + } + const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, selectedKey: SettingsV2TabTypes[this.state.selectedTab], diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index 2e66a86f9..06a9d4a0c 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx @@ -11,7 +11,6 @@ import { getThroughputApplyLongDelayMessage, getThroughputApplyShortDelayMessage, getToolTipContainer, - indexingPolicynUnsavedWarningMessage, manualToAutoscaleDisclaimerElement, mongoIndexTransformationRefreshingMessage, mongoIndexingPolicyAADError, @@ -39,7 +38,6 @@ class SettingsRenderUtilsTestComponent extends React.Component { {manualToAutoscaleDisclaimerElement} {ttlWarning} - {indexingPolicynUnsavedWarningMessage} {updateThroughputDelayedApplyWarningMessage} {getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)} diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index fefd2f6e5..1fe4536f0 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -61,6 +61,8 @@ export interface PriceBreakdown { currencySign: string; } +export type editorType = "indexPolicy" | "computedProperties"; + export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { @@ -254,9 +256,10 @@ export const ttlWarning: JSX.Element = ( ); -export const indexingPolicynUnsavedWarningMessage: JSX.Element = ( +export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => ( - You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. + You have not saved the latest changes made to your{" "} + {editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes. ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx new file mode 100644 index 000000000..811bc17ba --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx @@ -0,0 +1,56 @@ +import * as DataModels from "Contracts/DataModels"; +import { shallow } from "enzyme"; +import React from "react"; +import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent"; + +describe("ComputedPropertiesComponent", () => { + const initialComputedPropertiesContent: DataModels.ComputedProperties = [ + { + name: "prop1", + query: "query1", + }, + ]; + const baseProps: ComputedPropertiesComponentProps = { + computedPropertiesContent: initialComputedPropertiesContent, + computedPropertiesContentBaseline: initialComputedPropertiesContent, + logComputedPropertiesSuccessMessage: () => { + return; + }, + onComputedPropertiesContentChange: () => { + return; + }, + onComputedPropertiesDirtyChange: () => { + return; + }, + resetShouldDiscardComputedProperties: () => { + return; + }, + shouldDiscardComputedProperties: false, + }; + + it("renders", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("computed properties are reset", () => { + const wrapper = shallow(); + + const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent; + const resetComputedPropertiesEditorMockFn = jest.fn(); + computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn; + + wrapper.setProps({ shouldDiscardComputedProperties: true }); + wrapper.update(); + expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1); + }); + + it("dirty is set", () => { + let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps); + expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false); + + const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties }; + computedPropertiesComponent = new ComputedPropertiesComponent(newProps); + expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx new file mode 100644 index 000000000..ad3b12fec --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx @@ -0,0 +1,128 @@ +import { FontIcon, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react"; +import * as DataModels from "Contracts/DataModels"; +import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import { isDirty } from "Explorer/Controls/Settings/SettingsUtils"; +import { loadMonaco } from "Explorer/LazyMonaco"; +import * as monaco from "monaco-editor"; +import * as React from "react"; + +export interface ComputedPropertiesComponentProps { + computedPropertiesContent: DataModels.ComputedProperties; + computedPropertiesContentBaseline: DataModels.ComputedProperties; + logComputedPropertiesSuccessMessage: () => void; + onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void; + onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void; + resetShouldDiscardComputedProperties: () => void; + shouldDiscardComputedProperties: boolean; +} + +interface ComputedPropertiesComponentState { + computedPropertiesContentIsValid: boolean; +} + +export class ComputedPropertiesComponent extends React.Component< + ComputedPropertiesComponentProps, + ComputedPropertiesComponentState +> { + private shouldCheckComponentIsDirty = true; + private computedPropertiesDiv = React.createRef(); + private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor; + + constructor(props: ComputedPropertiesComponentProps) { + super(props); + this.state = { + computedPropertiesContentIsValid: true, + }; + } + + componentDidUpdate(): void { + if (this.props.shouldDiscardComputedProperties) { + this.resetComputedPropertiesEditor(); + this.props.resetShouldDiscardComputedProperties(); + } + this.onComponentUpdate(); + } + + componentDidMount(): void { + this.resetComputedPropertiesEditor(); + this.onComponentUpdate(); + } + + public resetComputedPropertiesEditor = (): void => { + if (!this.computedPropertiesEditor) { + this.createComputedPropertiesEditor(); + } else { + const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel(); + const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4); + indexingPolicyEditorModel.setValue(value); + } + this.onComponentUpdate(); + }; + + private onComponentUpdate = (): void => { + if (!this.shouldCheckComponentIsDirty) { + this.shouldCheckComponentIsDirty = true; + return; + } + this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty()); + this.shouldCheckComponentIsDirty = false; + }; + + public IsComponentDirty = (): boolean => { + if ( + isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && + this.state.computedPropertiesContentIsValid + ) { + return true; + } + + return false; + }; + + private async createComputedPropertiesEditor(): Promise { + const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4); + const monaco = await loadMonaco(); + this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, { + value: value, + language: "json", + ariaLabel: "Computed properties", + }); + if (this.computedPropertiesEditor) { + const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel(); + computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this)); + this.props.logComputedPropertiesSuccessMessage(); + } + } + + private onEditorContentChange = (): void => { + const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel(); + try { + const newComputedPropertiesContent = JSON.parse( + computedPropertiesEditorModel.getValue(), + ) as DataModels.ComputedProperties; + this.props.onComputedPropertiesContentChange(newComputedPropertiesContent); + this.setState({ computedPropertiesContentIsValid: true }); + } catch (e) { + this.setState({ computedPropertiesContentIsValid: false }); + } + }; + + public render(): JSX.Element { + return ( + + {isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && ( + + {unsavedEditorWarningMessage("computedProperties")} + + )} + + + {"Learn more"} + +   about how to define computed properties and how to use them. + +
+
+ ); + } +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 1f216b241..4d6ca765f 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -3,7 +3,7 @@ import * as monaco from "monaco-editor"; import * as React from "react"; import * as DataModels from "../../../../Contracts/DataModels"; import { loadMonaco } from "../../../LazyMonaco"; -import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; +import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils"; import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; @@ -120,7 +120,7 @@ export class IndexingPolicyComponent extends React.Component< refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} /> {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( - {indexingPolicynUnsavedWarningMessage} + {unsavedEditorWarningMessage("indexPolicy")} )}
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index c3b09286e..a55630532 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -19,7 +19,6 @@ import { addMongoIndexStackProps, createAndAddMongoIndexStackProps, customDetailsListStyles, - indexingPolicynUnsavedWarningMessage, infoAndToolTipTextStyle, mediumWidthStackStyles, mongoCompoundIndexNotSupportedMessage, @@ -27,15 +26,16 @@ import { onRenderRow, separatorStyles, subComponentStackProps, + unsavedEditorWarningMessage, } from "../../SettingsRenderUtils"; import { AddMongoIndexProps, - getMongoIndexType, - getMongoIndexTypeText, - isIndexTransforming, MongoIndexIdField, MongoIndexTypes, MongoNotificationType, + getMongoIndexType, + getMongoIndexTypeText, + isIndexTransforming, } from "../../SettingsUtils"; import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; @@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component + + + Learn more + + + +   about how to define computed properties and how to use them. + +
+ +`; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 930249d9d..c53974b88 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; -export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy; +export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; @@ -46,6 +46,7 @@ export enum SettingsV2TabTypes { SubSettingsTab, IndexingPolicyTab, PartitionKeyTab, + ComputedPropertiesTab, } export interface IsComponentDirtyResult { @@ -149,6 +150,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Indexing Policy"; case SettingsV2TabTypes.PartitionKeyTab: return "Partition Keys"; + case SettingsV2TabTypes.ComputedPropertiesTab: + return "Computed Properties (preview)"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 41b11ca68..d0c794025 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -40,6 +40,12 @@ export const collection = { version: 2, }, partitionKeyProperties: ["partitionKey"], + computedProperties: ko.observable([ + { + name: "queryName", + query: "query", + }, + ]), readSettings: () => { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 1ca3cddf1..3b9f76195 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -26,6 +26,7 @@ exports[`SettingsComponent renders 1`] = ` Object { "analyticalStorageTtl": [Function], "changeFeedPolicy": [Function], + "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, @@ -103,6 +104,7 @@ exports[`SettingsComponent renders 1`] = ` Object { "analyticalStorageTtl": [Function], "changeFeedPolicy": [Function], + "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, @@ -219,6 +221,7 @@ exports[`SettingsComponent renders 1`] = ` Object { "analyticalStorageTtl": [Function], "changeFeedPolicy": [Function], + "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, @@ -296,6 +299,40 @@ exports[`SettingsComponent renders 1`] = ` } /> + + +
diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 004862ffe..5a71353cd 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -99,18 +99,6 @@ exports[`SettingsUtils functions render 1`] = ` . - - You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. - ; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public usageSizeInKB: ko.Observable; + public computedProperties: ko.Observable; public offer: ko.Observable; public conflictResolutionPolicy: ko.Observable; @@ -121,6 +122,7 @@ export default class Collection implements ViewModels.Collection { this.schema = data.schema; this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); + this.computedProperties = ko.observable(data.computedProperties); this.partitionKeyPropertyHeaders = this.partitionKey?.paths; this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { diff --git a/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts index 2a7762328..461e516bf 100644 --- a/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */ export async function listCassandraKeyspaces( diff --git a/src/Utils/arm/generatedClients/cosmos/collection.ts b/src/Utils/arm/generatedClients/cosmos/collection.ts index 86b116f01..4a9a9c198 100644 --- a/src/Utils/arm/generatedClients/cosmos/collection.ts +++ b/src/Utils/arm/generatedClients/cosmos/collection.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and collection. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts index 8b3f407da..6fed487b6 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given collection, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts index ec0d5e0ea..b33c904d9 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts index 281d98289..984cb146a 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account, collection and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/database.ts b/src/Utils/arm/generatedClients/cosmos/database.ts index 1d01fcbd7..7b286c4ec 100644 --- a/src/Utils/arm/generatedClients/cosmos/database.ts +++ b/src/Utils/arm/generatedClients/cosmos/database.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and database. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts index 38fdbd89e..09f17c35b 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts index 32aeb05eb..4b52b631b 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the properties of an existing Azure Cosmos DB database account. */ export async function get( diff --git a/src/Utils/arm/generatedClients/cosmos/graphResources.ts b/src/Utils/arm/generatedClients/cosmos/graphResources.ts index 807380575..d51d44d77 100644 --- a/src/Utils/arm/generatedClients/cosmos/graphResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/graphResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the graphs under an existing Azure Cosmos DB database account. */ export async function listGraphs( diff --git a/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts index ffad0a38a..6e8656400 100644 --- a/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */ export async function listGremlinDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/locations.ts b/src/Utils/arm/generatedClients/cosmos/locations.ts index 5c28d60f0..6ec4cdc62 100644 --- a/src/Utils/arm/generatedClients/cosmos/locations.ts +++ b/src/Utils/arm/generatedClients/cosmos/locations.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* List Cosmos DB locations and their properties */ export async function list(subscriptionId: string): Promise { diff --git a/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts index b99584e1c..22b316904 100644 --- a/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */ export async function listMongoDBDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/operations.ts b/src/Utils/arm/generatedClients/cosmos/operations.ts index 5a01456ce..ae87fbb48 100644 --- a/src/Utils/arm/generatedClients/cosmos/operations.ts +++ b/src/Utils/arm/generatedClients/cosmos/operations.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists all of the available Cosmos DB Resource Provider operations. */ export async function list(): Promise { diff --git a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts index a66e6294c..d9b5d6dfa 100644 --- a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given partition key range id. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts index 3625b0859..6ec2ba50a 100644 --- a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given partition key range id and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentile.ts b/src/Utils/arm/generatedClients/cosmos/percentile.ts index b7a8d2841..cbbc751dc 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentile.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentile.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts index aa9432e8f..88c05dd11 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts index 43f102890..87359e9f0 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts index 321d68639..f85ddb636 100644 --- a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/tableResources.ts b/src/Utils/arm/generatedClients/cosmos/tableResources.ts index 3bc16cf0e..0da78793e 100644 --- a/src/Utils/arm/generatedClients/cosmos/tableResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/tableResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Tables under an existing Azure Cosmos DB database account. */ export async function listTables( diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 3252b38fe..4f69d223c 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -3,7 +3,7 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ /* The List operation response, that contains the client encryption keys and their properties. */ @@ -566,12 +566,14 @@ export interface DatabaseAccountGetProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Properties to create and update Azure Cosmos DB database accounts. */ @@ -663,12 +665,14 @@ export interface DatabaseAccountCreateUpdateProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Parameters to create and update Cosmos DB database accounts. */ @@ -763,12 +767,14 @@ export interface DatabaseAccountUpdateProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Parameters for patching Azure Cosmos DB database account properties. */ @@ -1256,6 +1262,9 @@ export interface SqlContainerResource { /* The configuration for defining Materialized Views. This must be specified only for creating a Materialized View container. */ materializedViewDefinition?: MaterializedViewDefinition; + + /* List of computed properties */ + computedProperties?: ComputedProperty[]; } /* Cosmos DB indexing policy */ @@ -1325,6 +1334,14 @@ export interface SpatialSpec { /* Indicates the spatial type of index. */ export type SpatialType = "Point" | "LineString" | "Polygon" | "MultiPolygon"; +/* The definition of a computed property */ +export interface ComputedProperty { + /* The name of a computed property, for example - "cp_lowerName" */ + name?: string; + /* The query that evaluates the value for computed property, for example - "SELECT VALUE LOWER(c.name) FROM c" */ + query?: string; +} + /* The configuration of the partition key to be used for partitioning data into multiple partitions */ export interface ContainerPartitionKey { /* List of paths using which data within the container can be partitioned */ @@ -1929,6 +1946,8 @@ export interface RestoreParametersBase { restoreSource?: string; /* Time to which the account has to be restored (ISO-8601 format). */ restoreTimestampInUtc?: string; + /* Specifies whether the restored account will have Time-To-Live disabled upon the successful restore. */ + restoreWithTtlDisabled?: boolean; } /* Parameters to indicate the information about the restore. */ @@ -2072,19 +2091,5 @@ export type ContinuousTier = "Continuous7Days" | "Continuous30Days"; /* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */ export type MinimalTlsVersion = "Tls" | "Tls11" | "Tls12"; -/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ -export type CustomerManagedKeyStatus = - | "Access to your account is currently revoked because the Azure Cosmos DB service is unable to obtain the AAD authentication token for the account's default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-active-directory-token-acquisition-error (4000)." - | "Access to your account is currently revoked because the Azure Cosmos DB account's key vault key URI does not follow the expected format; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#improper-syntax-detected-on-the-key-vault-uri-property (4006)." - | "Access to your account is currently revoked because the current default identity no longer has permission to the associated Key Vault key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#default-identity-is-unauthorized-to-access-the-azure-key-vault-key (4002)." - | "Access to your account is currently revoked because the Azure Key Vault DNS name specified by the account's keyvaultkeyuri property could not be resolved; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#unable-to-resolve-the-key-vaults-dns (4009)." - | "Access to your account is currently revoked because the correspondent key is not found on the specified Key Vault; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4003)." - | "Access to your account is currently revoked because the Azure Cosmos DB service is unable to wrap or unwrap the key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#internal-unwrapping-procedure-error (4005)." - | "Access to your account is currently revoked because the Azure Cosmos DB account has an undefined default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#invalid-azure-cosmos-db-default-identity (4015)." - | "Access to your account is currently revoked because the access rules are blocking outbound requests to the Azure Key Vault service; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide (4016)." - | "Access to your account is currently revoked because the correspondent Azure Key Vault was not found; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4017)." - | "Access to your account is currently revoked; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide" - | "Access to the configured customer managed key confirmed."; - /* Enum to indicate default priorityLevel of requests */ export type DefaultPriorityLevel = "High" | "Low"; diff --git a/utils/armClientGenerator/generator.ts b/utils/armClientGenerator/generator.ts index 01745830a..1a50e9082 100644 --- a/utils/armClientGenerator/generator.ts +++ b/utils/armClientGenerator/generator.ts @@ -16,7 +16,7 @@ Results of this file should be checked into the repo. */ // CHANGE THESE VALUES TO GENERATE NEW CLIENTS -const version = "2023-11-15-preview"; +const version = "2024-02-15-preview"; /* The following are legal options for resourceName but you generally will only use cosmos-db: "cosmos-db" | "managedCassandra" | "mongorbac" | "notebook" | "privateEndpointConnection" | "privateLinkResources" | "rbac" | "restorable" | "services" | "dataTransferService" From f881f7fd2f89bf125cb30ef6e285dea4fef8970d Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:32:59 -0400 Subject: [PATCH 042/102] Enabled the ability to close the home tab (#1765) --- images/Home_16.svg | 3 +++ .../CommandBarComponentButtonFactory.tsx | 17 +++++++++++++++++ src/Explorer/Tabs/Tabs.tsx | 8 +++----- src/hooks/useTabs.ts | 4 ++-- 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 images/Home_16.svg diff --git a/images/Home_16.svg b/images/Home_16.svg new file mode 100644 index 000000000..80facc866 --- /dev/null +++ b/images/Home_16.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index ecfd3fdb9..f55bef9d2 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -1,3 +1,4 @@ +import { ReactTabKind, useTabs } from "hooks/useTabs"; import * as React from "react"; import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; @@ -8,6 +9,7 @@ import AddUdfIcon from "../../../../images/AddUdf.svg"; import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg"; import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg"; +import HomeIcon from "../../../../images/Home_16.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; import GitHubIcon from "../../../../images/github.svg"; @@ -56,6 +58,9 @@ export function createStaticCommandBarButtons( } }; + const homeBtn = createHomeButton(); + buttons.push(homeBtn); + if (configContext.platform !== Platform.Fabric) { const newCollectionBtn = createNewCollectionGroup(container); buttons.push(newCollectionBtn); @@ -285,6 +290,18 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr }; } +function createHomeButton(): CommandButtonComponentProps { + const label = "Home"; + return { + iconSrc: HomeIcon, + iconAlt: label, + onCommandClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Home), + commandButtonLabel: label, + hasPopup: false, + ariaLabel: label, + }; +} + function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps { if (configContext.platform === Platform.Emulator) { return undefined; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index fc152aa92..d80706a83 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -182,11 +182,9 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind? )} {useObservable(tab?.tabTitle || getReactTabTitle())} - {tabKind !== ReactTabKind.Home && ( - - - - )} + + +
diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index 136e0ce2c..10bc3b144 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -105,7 +105,7 @@ export const useTabs: UseStore = create((set, get) => ({ return true; }); if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) { - set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); + set({ activeTab: undefined, activeReactTab: undefined }); } if (tab.tabId === activeTab.tabId && tabIndex !== -1) { @@ -143,7 +143,7 @@ export const useTabs: UseStore = create((set, get) => ({ }); if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) { - set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); + set({ activeTab: undefined, activeReactTab: undefined }); } } }, From 91d9e27049edf1f80b1c0f250e5b4a2aeadb83db Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Thu, 14 Mar 2024 20:56:26 +0000 Subject: [PATCH 043/102] Turn off fetching authorization token (#1766) --- src/Common/CosmosClient.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index f35efbd00..f75c47454 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,7 +1,6 @@ import * as Cosmos from "@azure/cosmos"; -import { sendCachedDataMessage } from "Common/MessageHandler"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; -import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes"; +import { AuthorizationToken } from "Contracts/MessageTypes"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { AuthType } from "../AuthType"; @@ -51,15 +50,23 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { case Cosmos.ResourceType.offer: case Cosmos.ResourceType.user: case Cosmos.ResourceType.permission: - // User master tokens - const authorizationToken = await sendCachedDataMessage( - MessageTypes.GetAuthorizationToken, - [requestInfo], - userContext.fabricContext.connectionId, - ); - console.log("Response from Fabric: ", authorizationToken); - headers[HttpHeaders.msDate] = authorizationToken.XDate; - return decodeURIComponent(authorizationToken.PrimaryReadWriteToken); + // For now, these operations aren't used, so fetching the authorization token is commented out. + // This provider must return a real token to pass validation by the client, so we return the cached resource token + // (which is a valid token, but won't work for these operations). + const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens; + return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId); + + /* ************** TODO: Uncomment this code if we need to support these operations ************** + // User master tokens + const authorizationToken = await sendCachedDataMessage( + MessageTypes.GetAuthorizationToken, + [requestInfo], + userContext.fabricContext.connectionId, + ); + console.log("Response from Fabric: ", authorizationToken); + headers[HttpHeaders.msDate] = authorizationToken.XDate; + return decodeURIComponent(authorizationToken.PrimaryReadWriteToken); + ***********************************************************************************************/ } } From ac22e88d9cbeca1212615ee7b5719e9f72a94d66 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:15:24 -0500 Subject: [PATCH 044/102] rebranding of inline copilot (#1767) * rebranding of inline copilot * rebranding of inline copilot * rebranding of inline copilot * fix styling --- .../Modal/QueryCopilotFeedbackModal.tsx | 9 +- .../QueryCopilotFeedbackModal.test.tsx.snap | 105 +++ .../QueryCopilot/QueryCopilotPromptbar.tsx | 694 ++++++++++-------- 3 files changed, 497 insertions(+), 311 deletions(-) diff --git a/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx b/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx index d8d68f4cc..c96f8d179 100644 --- a/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx +++ b/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx @@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({ }; return ( - +
@@ -68,9 +68,14 @@ export const QueryCopilotFeedbackModal = ({ rows={3} /> diff --git a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap index dc7f4c96f..356b86977 100644 --- a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap @@ -3,6 +3,14 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = ` = ({ const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); if (response.ok) { if (generateSQLQueryResponse?.sql !== "N/A") { - let query = `-- **Prompt:** ${userPrompt}\r\n`; - if (generateSQLQueryResponse.explanation) { - query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; - } - query += generateSQLQueryResponse.sql; - setQuery(query); + let currentGeneratedQuery = `-- **Prompt:** ${userPrompt}\r\n`; + currentGeneratedQuery += generateSQLQueryResponse.sql; + const lastQuery = generatedQuery && query ? `${query}\r\n` : ""; + setQuery(`${lastQuery}${currentGeneratedQuery}`); setGeneratedQuery(generateSQLQueryResponse.sql); setGeneratedQueryComments(generateSQLQueryResponse.explanation); setShowFeedbackBar(true); @@ -310,12 +307,388 @@ export const QueryCopilotPromptbar: React.FC = ({ return ( - - Copilot - Copilot + + + + { + inputEdited.current = true; + setShowSamplePrompts(true); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && userPrompt) { + inputEdited.current = true; + startGenerateQueryProcess(); + } + }} + style={{ lineHeight: 30 }} + styles={{ + root: { width: "100%" }, + suffix: { + background: "none", + padding: 0, + }, + fieldGroup: { + borderRadius: 4, + borderColor: "#D1D1D1", + "::after": { + border: "inherit", + borderWidth: 2, + borderBottomColor: "#464FEB", + borderRadius: 4, + }, + }, + }} + disabled={isGeneratingQuery} + autoComplete="off" + placeholder="Ask a question in natural language and we’ll generate the query for you." + aria-labelledby="copilot-textfield-label" + onRenderSuffix={() => { + return ( + startGenerateQueryProcess()} + aria-label="Send" + /> + ); + }} + /> + {showPromptTeachingBubble && copilotTeachingBubbleVisible && ( + toggleCopilotTeachingBubbleVisible(false)} + hasSmallHeadline={true} + headline="Write a prompt" + > + Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} + { + setShowSamplePrompts(true); + toggleCopilotTeachingBubbleVisible(false); + }} + style={{ color: "white", fontWeight: 600 }} + > + sample prompts + {" "} + or write your own query + + )} + {showSamplePrompts && ( + setShowSamplePrompts(false)} + directionalHintFixed={true} + directionalHint={DirectionalHint.bottomLeftEdge} + alignTargetEdge={true} + gapSpace={4} + > + + {filteredHistories?.length > 0 && ( + + + Recent + + {filteredHistories.map((history, i) => ( + { + setUserPrompt(history); + setShowSamplePrompts(false); + inputEdited.current = true; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {history} + + ))} + + )} + {filteredSuggestedPrompts?.length > 0 && ( + + + Suggested Prompts + + {filteredSuggestedPrompts.map((prompt) => ( + { + setUserPrompt(prompt.text); + setShowSamplePrompts(false); + inputEdited.current = true; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {prompt.text} + + ))} + + )} + {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( + + + + Learn about{" "} + + writing effective prompts + + + + )} + + + )} + + {!isGeneratingQuery && ( + + {!showFeedbackBar && ( + + AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} + + Read preview terms + + {showErrorMessageBar && ( + + {errorMessage ? errorMessage : "We ran into an error and were not able to execute query."} + + )} + {showInvalidQueryMessageBar && ( + + We were unable to generate a query based upon the prompt provided. Please modify the prompt and + try again. For examples of how to write a good prompt, please read + + this article. + {" "} + Our content guidelines can be found + + here. + + + )} + + )} + {showFeedbackBar && ( + + {userContext.feedbackPolicies?.policyAllowFeedback && ( + + Provide feedback + {showCallout && !hideFeedbackModalForLikedQueries && ( + { + setShowCallout(false); + SubmitFeedback({ + params: { + generatedQuery: generatedQuery, + likeQuery: likeQuery, + description: "", + userPrompt: userPrompt, + }, + explorer, + databaseId, + containerId, + mode: isSampleCopilotActive ? "Sample" : "User", + }); + }} + directionalHint={DirectionalHint.topCenter} + > + + Thank you. Need to give{" "} + { + setShowCallout(false); + openFeedbackModal(generatedQuery, true, userPrompt); + }} + > + more feedback? + + + + )} + { + setShowCallout(!likeQuery); + setLikeQuery(!likeQuery); + if (likeQuery === true) { + document.getElementById("likeStatus").innerHTML = "Unpressed"; + } + if (likeQuery === false) { + document.getElementById("likeStatus").innerHTML = "Liked"; + } + if (dislikeQuery) { + setDislikeQuery(!dislikeQuery); + } + }} + /> + { + let toggleStatusValue = "Unpressed"; + if (!dislikeQuery) { + openFeedbackModal(generatedQuery, false, userPrompt); + setLikeQuery(false); + toggleStatusValue = "Disliked"; + } + setDislikeQuery(!dislikeQuery); + setShowCallout(false); + document.getElementById("likeStatus").innerHTML = toggleStatusValue; + }} + /> + + + + )} + + Copy code + + { + setShowDeletePopup(true); + }} + iconProps={{ iconName: "Delete" }} + style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }} + styles={{ + root: { + backgroundColor: "inherit", + }, + }} + > + Clear editor + + + )} + + )} + {isGeneratingQuery && ( + + )} + { @@ -323,307 +696,10 @@ export const QueryCopilotPromptbar: React.FC = ({ clearFeedback(); resetMessageStates(); }} - styles={{ - root: { - marginLeft: "auto !important", - }, - }} ariaLabel="Close" title="Close copilot" /> - - { - inputEdited.current = true; - setShowSamplePrompts(true); - }} - onKeyDown={(e) => { - if (e.key === "Enter" && userPrompt) { - inputEdited.current = true; - startGenerateQueryProcess(); - } - }} - style={{ lineHeight: 30 }} - styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }} - disabled={isGeneratingQuery} - autoComplete="off" - placeholder="Ask a question in natural language and we’ll generate the query for you." - aria-labelledby="copilot-textfield-label" - /> - {showPromptTeachingBubble && copilotTeachingBubbleVisible && ( - toggleCopilotTeachingBubbleVisible(false)} - hasSmallHeadline={true} - headline="Write a prompt" - > - Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} - { - setShowSamplePrompts(true); - toggleCopilotTeachingBubbleVisible(false); - }} - style={{ color: "white", fontWeight: 600 }} - > - sample prompts - {" "} - or write your own query - - )} - startGenerateQueryProcess()} - aria-label="Send" - /> -
- {isGeneratingQuery && } -
- {showSamplePrompts && ( - setShowSamplePrompts(false)} - directionalHintFixed={true} - directionalHint={DirectionalHint.bottomLeftEdge} - alignTargetEdge={true} - gapSpace={4} - > - - {filteredHistories?.length > 0 && ( - - - Recent - - {filteredHistories.map((history, i) => ( - { - setUserPrompt(history); - setShowSamplePrompts(false); - inputEdited.current = true; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {history} - - ))} - - )} - {filteredSuggestedPrompts?.length > 0 && ( - - - Suggested Prompts - - {filteredSuggestedPrompts.map((prompt) => ( - { - setUserPrompt(prompt.text); - setShowSamplePrompts(false); - inputEdited.current = true; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {prompt.text} - - ))} - - )} - {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( - - - - Learn about{" "} - - writing effective prompts - - - - )} - - - )} -
- - - - AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} - - Read preview terms - - {showErrorMessageBar && ( - - {errorMessage ? errorMessage : "We ran into an error and were not able to execute query."} - - )} - {showInvalidQueryMessageBar && ( - - We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again. - For examples of how to write a good prompt, please read - - this article. - {" "} - Our content guidelines can be found - - here. - - - )} - - - - {showFeedbackBar && ( - - {userContext.feedbackPolicies?.policyAllowFeedback && ( - - Provide feedback on the query generated - {showCallout && !hideFeedbackModalForLikedQueries && ( - { - setShowCallout(false); - SubmitFeedback({ - params: { - generatedQuery: generatedQuery, - likeQuery: likeQuery, - description: "", - userPrompt: userPrompt, - }, - explorer, - databaseId, - containerId, - mode: isSampleCopilotActive ? "Sample" : "User", - }); - }} - directionalHint={DirectionalHint.topCenter} - > - - Thank you. Need to give{" "} - { - setShowCallout(false); - openFeedbackModal(generatedQuery, true, userPrompt); - }} - > - more feedback? - - - - )} - { - setShowCallout(!likeQuery); - setLikeQuery(!likeQuery); - if (likeQuery === true) { - document.getElementById("likeStatus").innerHTML = "Unpressed"; - } - if (likeQuery === false) { - document.getElementById("likeStatus").innerHTML = "Liked"; - } - if (dislikeQuery) { - setDislikeQuery(!dislikeQuery); - } - }} - /> - { - let toggleStatusValue = "Unpressed"; - if (!dislikeQuery) { - openFeedbackModal(generatedQuery, false, userPrompt); - setLikeQuery(false); - toggleStatusValue = "Disliked"; - } - setDislikeQuery(!dislikeQuery); - setShowCallout(false); - document.getElementById("likeStatus").innerHTML = toggleStatusValue; - }} - /> - - - - )} - - Copy query - - { - setShowDeletePopup(true); - }} - iconProps={{ iconName: "Delete" }} - style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} - > - Delete query - - - )} {isSamplePromptsOpen && } {query !== "" && query.trim().length !== 0 && ( Date: Mon, 18 Mar 2024 10:49:33 -0700 Subject: [PATCH 045/102] Fix API endpoint for CassandraProxy query API (#1769) --- src/Common/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 804c8adea..041ecf132 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -139,7 +139,7 @@ export class CassandraBackend { export class CassandraProxyAPIs { public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete"; - public static readonly queryApi: string = "api/cassandra/postquery"; + public static readonly queryApi: string = "api/cassandra"; public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra"; public static readonly keysApi: string = "api/cassandra/keys"; public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys"; From a524138ac98ff7f24f0d7dc874e446ec5c62f013 Mon Sep 17 00:00:00 2001 From: Vsevolod Kukol Date: Wed, 20 Mar 2024 00:28:24 +0100 Subject: [PATCH 046/102] Don't show the new Home button in Fabric (#1774) as the whole Home tab feature is not supported there. --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index f55bef9d2..c37ae41fb 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -58,10 +58,10 @@ export function createStaticCommandBarButtons( } }; - const homeBtn = createHomeButton(); - buttons.push(homeBtn); - if (configContext.platform !== Platform.Fabric) { + const homeBtn = createHomeButton(); + buttons.push(homeBtn); + const newCollectionBtn = createNewCollectionGroup(container); buttons.push(newCollectionBtn); if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { From da2e874ae6b5a6dab613a306add65f297250f69a Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:23:42 -0500 Subject: [PATCH 047/102] Fix bugs on data transfer and bring back query explanation and remove query prompt from editor (#1777) * Fix minor issues * add back preview tag * bring back query explanation and remove prompt in editor --- src/Common/dataAccess/dataTransfers.ts | 11 +++++++++-- .../PartitionKeyComponent.tsx | 14 +++++++------- src/Explorer/Controls/Settings/SettingsUtils.tsx | 2 +- .../__snapshots__/SettingsComponent.test.tsx.snap | 2 +- .../ChangePartitionKeyPane.tsx | 2 ++ .../QueryCopilot/QueryCopilotPromptbar.tsx | 6 ++++-- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Common/dataAccess/dataTransfers.ts b/src/Common/dataAccess/dataTransfers.ts index 138257c15..e639f9965 100644 --- a/src/Common/dataAccess/dataTransfers.ts +++ b/src/Common/dataAccess/dataTransfers.ts @@ -122,14 +122,21 @@ const pollDataTransferJobOperation = async ( updateDataTransferJob(body); - if (status === "Cancelled" || status === "Failed" || status === "Faulted") { + if (status === "Cancelled") { + removeFromPolling(jobName); + clearMessage && clearMessage(); + const cancelMessage = `Data transfer job ${jobName} cancelled`; + NotificationConsoleUtils.logConsoleError(cancelMessage); + throw new AbortError(cancelMessage); + } + if (status === "Failed" || status === "Faulted") { removeFromPolling(jobName); const errorMessage = body?.properties?.error ? JSON.stringify(body?.properties?.error) : "Operation could not be completed"; const error = new Error(errorMessage); clearMessage && clearMessage(); - NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`); + NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`); throw new AbortError(error); } if (status === "Completed") { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index 2efe7fb92..1930808a7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -136,15 +136,15 @@ export const PartitionKeyComponent: React.FC = ({ da }; const getPercentageComplete = () => { + const jobStatus = portalDataTransferJob?.properties?.status; + const isCompleted = jobStatus === "Completed"; + if (isCompleted) { + return 1; + } const processedCount = portalDataTransferJob?.properties?.processedCount; const totalCount = portalDataTransferJob?.properties?.totalCount; - const jobStatus = portalDataTransferJob?.properties?.status; - const isCancelled = jobStatus === "Cancelled"; - const isCompleted = jobStatus === "Completed"; - if (totalCount <= 0 && !isCompleted) { - return isCancelled ? 0 : null; - } - return isCompleted ? 1 : processedCount / totalCount; + const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob); + return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0; }; return ( diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index c53974b88..869ae323a 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -149,7 +149,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; case SettingsV2TabTypes.PartitionKeyTab: - return "Partition Keys"; + return "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties (preview)"; default: diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 3b9f76195..ab7abac11 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -207,7 +207,7 @@ exports[`SettingsComponent renders 1`] = ` /> = ({
{createNewContainer ? ( + All configurations except for unique keys will be copied from the source container diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 412b17051..6aaebc926 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -214,8 +214,10 @@ export const QueryCopilotPromptbar: React.FC = ({ const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); if (response.ok) { if (generateSQLQueryResponse?.sql !== "N/A") { - let currentGeneratedQuery = `-- **Prompt:** ${userPrompt}\r\n`; - currentGeneratedQuery += generateSQLQueryResponse.sql; + const queryExplanation = `-- **Explanation of query:** ${ + generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A" + }\r\n`; + const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql; const lastQuery = generatedQuery && query ? `${query}\r\n` : ""; setQuery(`${lastQuery}${currentGeneratedQuery}`); setGeneratedQuery(generateSQLQueryResponse.sql); From e09930d9d02e18fa6315e2d3c7f171a46809112c Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Fri, 22 Mar 2024 13:18:02 -0400 Subject: [PATCH 048/102] Support Token API in new Portal Backend (#1773) * added support for generate token * fix checks * fix checks * deactivate mongo proxy * fix tests * remove mongo proxy from mpac * change endpoints to prod * npm run format * add await --------- Co-authored-by: Asier Isayas --- src/Common/Constants.ts | 44 ++++++++++++------- src/Common/MongoProxyClient.ts | 3 +- src/ConfigContext.ts | 12 ++++- .../Hosted/Components/ConnectExplorer.tsx | 20 ++++++++- src/Utils/EndpointUtils.ts | 15 ++++++- 5 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 041ecf132..fe9c2672d 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -124,6 +124,34 @@ export enum MongoBackendEndpointType { remote, } +export enum BackendApi { + GenerateToken, +} + +export class PortalBackendEndpoints { + public static readonly Development: string = "https://localhost:7235"; + public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn"; +} + +export class MongoProxyEndpoints { + public static readonly Development: string = "https://localhost:7238"; + public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; +} + +export class CassandraProxyEndpoints { + public static readonly Development: string = "https://localhost:7240"; + public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn"; +} + //TODO: Remove this when new backend is migrated over export class CassandraBackend { public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; @@ -446,22 +474,6 @@ export class JunoEndpoints { public static readonly Stage = "https://tools-staging.cosmos.azure.com"; } -export class MongoProxyEndpoints { - public static readonly Development: string = "https://localhost:7238"; - public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; - public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; - public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; - public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; -} - -export class CassandraProxyEndpoints { - public static readonly Development: string = "https://localhost:7240"; - public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; - public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com"; - public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us"; - public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn"; -} - export class PriorityLevel { public static readonly High = "high"; public static readonly Low = "low"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 357ce1f50..e37b0eff5 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -690,6 +690,7 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { + const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; if (userContext.databaseAccount.properties.ipRules?.length > 0) { canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; @@ -698,6 +699,6 @@ function useMongoProxyEndpoint(api: string): boolean { return ( canAccessMongoProxy && configContext.NEW_MONGO_APIS?.includes(api) && - [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT) + activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) ); } diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index f02fa260d..e3350c7f6 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,4 +1,10 @@ -import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; +import { + BackendApi, + CassandraProxyEndpoints, + JunoEndpoints, + MongoProxyEndpoints, + PortalBackendEndpoints, +} from "Common/Constants"; import { allowedAadEndpoints, allowedArcadiaEndpoints, @@ -39,6 +45,8 @@ export interface ConfigContext { ARCADIA_ENDPOINT: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; BACKEND_ENDPOINT?: string; + PORTAL_BACKEND_ENDPOINT?: string; + NEW_BACKEND_APIS?: BackendApi[]; MONGO_BACKEND_ENDPOINT?: string; MONGO_PROXY_ENDPOINT?: string; MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; @@ -90,6 +98,8 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, + NEW_BACKEND_APIS: [BackendApi.GenerateToken], MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ // "resourcelist", diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index dd291f279..6b33d021b 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,10 +1,11 @@ import { useBoolean } from "@fluentui/react-hooks"; import { userContext } from "UserContext"; +import { usePortalBackendEndpoint } from "Utils/EndpointUtils"; import * as React from "react"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ErrorImage from "../../../../images/error.svg"; import { AuthType } from "../../../AuthType"; -import { HttpHeaders } from "../../../Common/Constants"; +import { BackendApi, HttpHeaders } from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; import { GenerateTokenResponse } from "../../../Contracts/DataModels"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; @@ -18,6 +19,23 @@ interface Props { } export const fetchEncryptedToken = async (connectionString: string): Promise => { + if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) { + return await fetchEncryptedToken_ToBeDeprecated(connectionString); + } + + const headers = new Headers(); + headers.append(HttpHeaders.connectionString, connectionString); + const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken"; + const response = await fetch(url, { headers, method: "POST" }); + if (!response.ok) { + throw response; + } + + const encryptedTokenResponse: string = await response.json(); + return decodeURIComponent(encryptedTokenResponse); +}; + +export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise => { const headers = new Headers(); headers.append(HttpHeaders.connectionString, connectionString); const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken"; diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index ee01f7f9a..4962c285b 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -1,4 +1,11 @@ -import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; +import { + BackendApi, + CassandraProxyEndpoints, + JunoEndpoints, + MongoProxyEndpoints, + PortalBackendEndpoints, +} from "Common/Constants"; +import { configContext } from "ConfigContext"; import * as Logger from "../Common/Logger"; export function validateEndpoint( @@ -137,3 +144,9 @@ export const allowedJunoOrigins: ReadonlyArray = [ ]; export const allowedNotebookServerUrls: ReadonlyArray = []; + +export function usePortalBackendEndpoint(backendApi: BackendApi): boolean { + const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development]; + const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false; + return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string); +} From 0df68c4967710baf485c6b53ecf1e00380c2746d Mon Sep 17 00:00:00 2001 From: Vsevolod Kukol Date: Tue, 26 Mar 2024 17:22:15 +0100 Subject: [PATCH 049/102] Fix initial container loading in Fabric (#1771) * Fix initial container loading in Fabric There is a rendering issue where the documents table doesn't resize properly if explorer is loaded in the beackground (invisible). To workaround this, track DE visibility from Fabric RPC and open the first container only once DE becomes visible. If DE has been already shown, open the container right away. * Preserve glitchy behavior if Fabric UX doesn't send isVisible * Preserve Fabric visibility in global status and fix a race condition where visibility might change during initialization. --- src/Contracts/FabricMessagesContract.ts | 3 ++- src/UserContext.ts | 1 + src/hooks/useKnockoutExplorer.ts | 22 +++++++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index d0b36f1c6..b381fd845 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -53,6 +53,7 @@ export type FabricMessageV2 = id: string; message: { connectionId: string; + isVisible: boolean; }; } | { @@ -72,7 +73,7 @@ export type FabricMessageV2 = }; } | { - type: "setToolbarStatus"; + type: "explorerVisible"; message: { visible: boolean; }; diff --git a/src/UserContext.ts b/src/UserContext.ts index daae0f052..2fa1bb946 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -51,6 +51,7 @@ interface FabricContext { connectionId: string; databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; isReadOnly: boolean; + isVisible: boolean; } export type AdminFeedbackControlPolicy = diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 22b5078c9..b04b16c72 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -2,7 +2,6 @@ import { createUri } from "Common/UrlUtility"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract"; import Explorer from "Explorer/Explorer"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; @@ -90,6 +89,7 @@ async function configureFabric(): Promise { // These are the versions of Fabric that Data Explorer supports. const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION]; + let firstContainerOpened = false; let explorer: Explorer; return new Promise((resolve) => { window.addEventListener( @@ -121,7 +121,10 @@ async function configureFabric(): Promise { await scheduleRefreshDatabaseResourceToken(true); resolve(explorer); await explorer.refreshAllDatabases(); - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + if (userContext.fabricContext.isVisible && !firstContainerOpened) { + firstContainerOpened = true; + openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + } break; } case "newContainer": @@ -132,8 +135,16 @@ async function configureFabric(): Promise { handleCachedDataMessage(data); break; } - case "setToolbarStatus": { - useCommandBar.getState().setIsHidden(data.message.visible === false); + case "explorerVisible": { + userContext.fabricContext.isVisible = data.message.visible; + if ( + userContext.fabricContext.isVisible && + !firstContainerOpened && + userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined + ) { + firstContainerOpened = true; + openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + } break; } default: @@ -327,12 +338,13 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer { return explorer; } -function createExplorerFabric(params: { connectionId: string }): Explorer { +function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer { updateUserContext({ fabricContext: { connectionId: params.connectionId, databaseConnectionInfo: undefined, isReadOnly: true, + isVisible: params.isVisible ?? true, }, authType: AuthType.ConnectionString, databaseAccount: { From 56408a97d7ff9354d2abb4b328f75314cf3ef436 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:36:04 -0400 Subject: [PATCH 050/102] Add container ids to tabs (#1772) * Added container ids to tabs * prettier run * Updated for undefined scenarios * prettier * added ellipsis to long container names * added slice * prettier * Added ellipsis to long DB names in tabs * Added undefined DB case * Replaced dots with ellipsis character * corrected undefined return value --- src/Explorer/Tabs/TabsBase.ts | 27 +++++++++++++++++++++++---- src/Explorer/Tree/Collection.ts | 4 ++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index a6f3a45dd..0425eac91 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -40,11 +40,10 @@ export default class TabsBase extends WaitsForTemplateViewModel { this.database = options.database; this.rid = options.rid || (this.collection && this.collection.rid) || ""; this.tabKind = options.tabKind; - this.tabTitle = ko.observable(options.title); + this.tabTitle = ko.observable(this.getTitle(options)); this.tabPath = - ko.observable(options.tabPath ?? "") || - (this.collection && - ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); + this.collection && + ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`); this.pendingNotification = ko.observable(undefined); this.onLoadStartKey = options.onLoadStartKey; this.closeTabButton = { @@ -143,6 +142,26 @@ export default class TabsBase extends WaitsForTemplateViewModel { return (this.collection && this.collection.container) || (this.database && this.database.container); } + public getTitle(options: ViewModels.TabOptions): string { + const coll = this.collection?.id(); + const db = this.database?.id(); + if (coll) { + if (coll.length > 8) { + return coll.slice(0, 5) + "…" + options.title; + } else { + return coll + "." + options.title; + } + } else if (db) { + if (db.length > 8) { + return db.slice(0, 5) + "…" + options.title; + } else { + return db + "." + options.title; + } + } else { + return options.title; + } + } + /** Renders a Javascript object to be displayed inside Monaco Editor */ public renderObjectForEditor(value: any, replacer: any, space: string | number): string { return JSON.stringify(value, replacer, space); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 649c560a9..d7c673620 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -308,7 +308,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.rawDataModel.id + " - Items", + tabTitle: "Items", }); this.documentIds([]); @@ -316,7 +316,7 @@ export default class Collection implements ViewModels.Collection { partitionKey: this.partitionKey, documentIds: ko.observableArray([]), tabKind: ViewModels.CollectionTabKind.Documents, - title: this.rawDataModel.id + " - Items", + title: "Items", collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Documents`, From f24b0bcf1b0116df79d72a0af08515060e8b8f25 Mon Sep 17 00:00:00 2001 From: Vsevolod Kukol Date: Thu, 28 Mar 2024 16:05:38 +0100 Subject: [PATCH 051/102] Show the Feedback button in Portal only (#1775) This feature is not supported on any other platform incl. Hosted. --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index c37ae41fb..869dd2383 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -240,7 +240,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt buttons.push(fullScreenButton); } - if (configContext.platform !== Platform.Emulator) { + if (configContext.platform === Platform.Portal) { const label = "Feedback"; const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, From 5aa6b0abe17163bf5f6bdd6cb6e3a4b30b2ba3eb Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:10:32 -0500 Subject: [PATCH 052/102] fix opening collections (#1780) * fix opening collections * fix opening collections * fix opening collections * fix opening collections --- src/Explorer/OpenActions/OpenActions.tsx | 192 ++++++++++++----------- 1 file changed, 104 insertions(+), 88 deletions(-) diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index 876f8e391..f3ef288c8 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -1,4 +1,5 @@ // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. +import { useDatabases } from "Explorer/useDatabases"; import React from "react"; import { ActionContracts } from "../../Contracts/ExplorerContracts"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -40,97 +41,112 @@ function openCollectionTab( databases: ViewModels.Database[], initialDatabaseIndex = 0, ) { - for (let i = initialDatabaseIndex; i < databases.length; i++) { - const database: ViewModels.Database = databases[i]; - if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) { - continue; - } - - const collectionActionHandler = (collections: ViewModels.Collection[]) => { - if (!action.collectionResourceId && collections.length === 0) { - subscription.dispose(); - openCollectionTab(action, databases, ++i); - return; - } - - for (let j = 0; j < collections.length; j++) { - const collection: ViewModels.Collection = collections[j]; - if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) { - continue; - } - - // select the collection - collection.expandCollection(); - - if ( - action.tabKind === ActionContracts.TabKind.SQLDocuments || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] - ) { - collection.onDocumentDBDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.MongoDocuments || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] - ) { - collection.onMongoDBDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] - ) { - collection.onSchemaAnalyzerClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.TableEntities || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] - ) { - collection.onTableEntitiesClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.Graph || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] - ) { - collection.onGraphDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.SQLQuery || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] - ) { - collection.onNewQueryClick( - collection, - undefined, - generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties), - ); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.ScaleSettings || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] - ) { - collection.onSettingsClick(); - break; - } - } - subscription.dispose(); + //if databases are not yet loaded, wait until loaded + if (!databases || databases.length === 0) { + const databaseActionHandler = (databases: ViewModels.Database[]) => { + databasesUnsubscription(); + openCollectionTab(action, databases, 0); + return; }; + const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases); + } else { + for (let i = initialDatabaseIndex; i < databases.length; i++) { + const database: ViewModels.Database = databases[i]; + if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) { + continue; + } - const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections)); - if (database.collections && database.collections() && database.collections().length) { - collectionActionHandler(database.collections()); + //expand database first if not expanded to load the collections + if (!database.isDatabaseExpanded?.()) { + database.expandDatabase?.(); + } + + const collectionActionHandler = (collections: ViewModels.Collection[]) => { + if (!action.collectionResourceId && collections.length === 0) { + subscription.dispose(); + openCollectionTab(action, databases, ++i); + return; + } + + for (let j = 0; j < collections.length; j++) { + const collection: ViewModels.Collection = collections[j]; + if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) { + continue; + } + + // select the collection + collection.expandCollection(); + + if ( + action.tabKind === ActionContracts.TabKind.SQLDocuments || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] + ) { + collection.onDocumentDBDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.MongoDocuments || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] + ) { + collection.onMongoDBDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] + ) { + collection.onSchemaAnalyzerClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.TableEntities || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] + ) { + collection.onTableEntitiesClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.Graph || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] + ) { + collection.onGraphDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.SQLQuery || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] + ) { + collection.onNewQueryClick( + collection, + undefined, + generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties), + ); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.ScaleSettings || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] + ) { + collection.onSettingsClick(); + break; + } + } + subscription.dispose(); + }; + + const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections)); + if (database.collections && database.collections() && database.collections().length) { + collectionActionHandler(database.collections()); + } + + break; } - - break; } } From cabedf4a290ca480796ac931a68d23fed998eefe Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 1 Apr 2024 07:54:04 -0700 Subject: [PATCH 053/102] Enable new backend endpoint to be passed to Data Explorer via message. (#1782) --- src/Contracts/ViewModels.ts | 1 + src/hooks/useKnockoutExplorer.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index b79ddce4e..66fd54ec4 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame { dnsSuffix?: string; serverId?: string; extensionEndpoint?: string; + portalBackendEndpoint?: string; mongoProxyEndpoint?: string; cassandraProxyEndpoint?: string; subscriptionType?: SubscriptionType; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index b04b16c72..4e80b3476 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -497,6 +497,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint, + PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint, }); updateUserContext({ From 86f2bc171f63c516ff37fbcb04c26e4eac377592 Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:44:42 -0700 Subject: [PATCH 054/102] Remove notebooks UI (#1779) * Fix API endpoint for CassandraProxy query API * Remove notebooks UI components * Fix tests * Address comments * Fix unit tests * Remove commented code --- .../CommandBarComponentButtonFactory.test.ts | 269 ------------------ .../CommandBarComponentButtonFactory.tsx | 143 ---------- src/Explorer/Tree/ResourceTree.tsx | 8 - src/Explorer/Tree/ResourceTreeAdapter.tsx | 5 - 4 files changed, 425 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 677df81de..0a4a805d2 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -2,10 +2,8 @@ import * as ko from "knockout"; import { AuthType } from "../../../AuthType"; import { DatabaseAccount } from "../../../Contracts/DataModels"; import { CollectionBase } from "../../../Contracts/ViewModels"; -import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; -import NotebookManager from "../../Notebook/NotebookManager"; import { useNotebook } from "../../Notebook/useNotebook"; import { useDatabases } from "../../useDatabases"; import { useSelectedNode } from "../../useSelectedNode"; @@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - describe("Enable notebook button", () => { - const enableNotebookBtnLabel = "Enable Notebooks (Preview)"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - portalEnv: "prod", - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - }); - - afterEach(() => { - updateUserContext({ - portalEnv: "prod", - }); - useNotebook.getState().setIsNotebookEnabled(false); - useNotebook.getState().setIsNotebooksEnabledForAccount(false); - }); - - it("Notebooks is already enabled - button should be hidden", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - expect(enableNotebookBtn).toBeUndefined(); - }); - - it("Account is running on one of the national clouds - button should be hidden", () => { - updateUserContext({ - portalEnv: "mooncake", - }); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - expect(enableNotebookBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled but is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - - //TODO: modify once notebooks are available - expect(enableNotebookBtn).toBeUndefined(); - //expect(enableNotebookBtn).toBeDefined(); - //expect(enableNotebookBtn.disabled).toBe(false); - //expect(enableNotebookBtn.tooltipText).toBe(""); - }); - - it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - - //TODO: modify once notebooks are available - expect(enableNotebookBtn).toBeUndefined(); - //expect(enableNotebookBtn).toBeDefined(); - //expect(enableNotebookBtn.disabled).toBe(true); - //expect(enableNotebookBtn.tooltipText).toBe( - // "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." - //); - }); - }); - - describe("Open Mongo shell button", () => { - const openMongoShellBtnLabel = "Open Mongo shell"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - }); - - afterAll(() => { - updateUserContext({ - apiType: "SQL", - }); - useNotebook.getState().setIsShellEnabled(false); - }); - - beforeEach(() => { - updateUserContext({ - apiType: "Mongo", - }); - useNotebook.getState().setIsShellEnabled(true); - }); - - afterEach(() => { - useNotebook.getState().setIsNotebookEnabled(false); - useNotebook.getState().setIsNotebooksEnabledForAccount(false); - }); - - it("Mongo Api not available - button should be hidden", () => { - updateUserContext({ - apiType: "SQL", - }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Running on a national cloud - button should be hidden", () => { - updateUserContext({ - portalEnv: "mooncake", - }); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled and is unavailable - button should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled and is available - button should be hidden", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openMongoShellBtn.disabled).toBe(true); - //expect(openMongoShellBtn.disabled).toBe(false); - //expect(openMongoShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openMongoShellBtn.disabled).toBe(true); - //expect(openMongoShellBtn.disabled).toBe(false); - //expect(openMongoShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - useNotebook.getState().setIsShellEnabled(false); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - }); - describe("Open Cassandra shell button", () => { const openCassandraShellBtnLabel = "Open Cassandra shell"; const selectedNodeState = useSelectedNode.getState(); @@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => { const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); - - it("Notebooks is not enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeUndefined(); - }); - - it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - - expect(openCassandraShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openCassandraShellBtn.disabled).toBe(true); - //expect(openCassandraShellBtn.disabled).toBe(false); - //expect(openCassandraShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openCassandraShellBtn.disabled).toBe(true); - //expect(openCassandraShellBtn.disabled).toBe(false); - //expect(openCassandraShellBtn.tooltipText).toBe(""); - }); }); describe("Open Postgres and vCore Mongo buttons", () => { @@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - describe("GitHub buttons", () => { - const connectToGitHubBtnLabel = "Connect to GitHub"; - const manageGitHubSettingsBtnLabel = "Manage GitHub settings"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - - mockExplorer.notebookManager = new NotebookManager(); - mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); - }); - - afterEach(() => { - jest.resetAllMocks(); - useNotebook.getState().setIsNotebookEnabled(false); - }); - - it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); - expect(connectToGitHubBtn).toBeDefined(); - }); - - it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => { - useNotebook.getState().setIsNotebookEnabled(true); - mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const manageGitHubSettingsBtn = buttons.find( - (button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel, - ); - expect(manageGitHubSettingsBtn).toBeDefined(); - }); - - it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - - const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); - expect(connectToGitHubBtn).toBeUndefined(); - - const manageGitHubSettingsBtn = buttons.find( - (button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel, - ); - expect(manageGitHubSettingsBtn).toBeUndefined(); - }); - }); - describe("Resource token", () => { const mockCollection = { id: ko.observable("test") } as CollectionBase; useSelectedNode.getState().setSelectedNode(mockCollection); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 869dd2383..9851dd668 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -7,14 +7,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg"; import AddTriggerIcon from "../../../../images/AddTrigger.svg"; import AddUdfIcon from "../../../../images/AddUdf.svg"; import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg"; -import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import HomeIcon from "../../../../images/Home_16.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; -import GitHubIcon from "../../../../images/github.svg"; -import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; -import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import SettingsIcon from "../../../../images/settings_15x15.svg"; import SynapseIcon from "../../../../images/synapse-link.svg"; @@ -22,7 +18,6 @@ import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { JunoClient } from "../../../Juno/JunoClient"; import { userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; @@ -33,7 +28,6 @@ import { useNotebook } from "../../Notebook/useNotebook"; import { OpenFullScreen } from "../../OpenFullScreen"; import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; -import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; import { useDatabases } from "../../useDatabases"; @@ -80,57 +74,6 @@ export function createStaticCommandBarButtons( } } - if (useNotebook.getState().isNotebookEnabled) { - addDivider(); - const notebookButtons: CommandButtonComponentProps[] = []; - - const newNotebookButton = createNewNotebookButton(container); - newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; - notebookButtons.push(newNotebookButton); - - if (container.notebookManager?.gitHubOAuthService) { - notebookButtons.push(createManageGitHubAccountButton(container)); - } - if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) { - notebookButtons.push(createOpenTerminalButton(container)); - } - if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) { - notebookButtons.push(createNotebookWorkspaceResetButton(container)); - } - if ( - (userContext.apiType === "Mongo" && - useNotebook.getState().isShellEnabled && - selectedNodeState.isDatabaseNodeOrNoneSelected()) || - userContext.apiType === "Cassandra" - ) { - notebookButtons.push(createDivider()); - if (userContext.apiType === "Cassandra") { - notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra)); - } else { - notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo)); - } - } - - notebookButtons.forEach((btn) => { - if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); - } - } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg); - } - } else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); - } - } else if (!useNotebook.getState().isPhoenixNotebooks) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); - } - buttons.push(btn); - }); - } - if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; @@ -449,40 +392,6 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) return buttons; } -function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void { - if (!buttonProps.isDivider) { - buttonProps.disabled = true; - buttonProps.tooltipText = tooltip; - } -} - -function createNewNotebookButton(container: Explorer): CommandButtonComponentProps { - const label = "New Notebook"; - return { - id: "newNotebookBtn", - iconSrc: NewNotebookIcon, - iconAlt: label, - onCommandClick: () => container.onNewNotebookClicked(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - -function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps { - const label = "Upload to Notebook Server"; - return { - iconSrc: NewNotebookIcon, - iconAlt: label, - onCommandClick: () => container.openUploadFilePanel(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createOpenQueryButton(container: Explorer): CommandButtonComponentProps { const label = "Open Query"; return { @@ -510,19 +419,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { }; } -function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps { - const label = "Open Terminal"; - return { - iconSrc: CosmosTerminalIcon, - iconAlt: label, - onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createOpenTerminalButtonByKind( container: Explorer, terminalKind: ViewModels.TerminalKind, @@ -562,45 +458,6 @@ function createOpenTerminalButtonByKind( }; } -function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps { - const label = "Reset Workspace"; - return { - iconSrc: ResetWorkspaceIcon, - iconAlt: label, - onCommandClick: () => container.resetNotebookWorkspace(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - -function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps { - const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn(); - const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub"; - const junoClient = new JunoClient(); - return { - iconSrc: GitHubIcon, - iconAlt: label, - onCommandClick: () => { - useSidePanel - .getState() - .openSidePanel( - label, - , - ); - }, - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createStaticCommandBarButtonsForResourceToken( container: Explorer, selectedNodeState: SelectedNodeState, diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b245f327f..b5f759534 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -373,11 +373,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc iconSrc: NewNotebookIcon, onClick: () => container.onCreateDirectory(item, isGithubTree), }, - { - label: "New Notebook", - iconSrc: NewNotebookIcon, - onClick: () => container.onNewNotebookClicked(item, isGithubTree), - }, { label: "Upload File", iconSrc: NewNotebookIcon, @@ -786,9 +781,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - - {/* {buildGalleryCallout()} */} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index abe05e85f..b22f0f916 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -800,11 +800,6 @@ export class ResourceTreeAdapter implements ReactAdapter { iconSrc: NewNotebookIcon, onClick: () => this.container.onCreateDirectory(item), }, - { - label: "New Notebook", - iconSrc: NewNotebookIcon, - onClick: () => this.container.onNewNotebookClicked(item), - }, { label: "Upload File", iconSrc: NewNotebookIcon, From 18cc2a419523760a9d437edc79f6c65452799ebd Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 2 Apr 2024 12:34:58 -0400 Subject: [PATCH 055/102] Activate Mongo and Cassandra Proxies in MPAC (#1776) * Fix API endpoint for CassandraProxy query API * activated mongo proxy * added mpac * Activate CassandraProxy API endpoints for MPAC * Run npm format * Set CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED when we detect new Cassandra Proxy endpoints in IP rules. * query documents API fix * simplify ip check --------- Co-authored-by: Senthamil Sindhu Co-authored-by: Asier Isayas Co-authored-by: Jade Welton --- src/Common/MongoProxyClient.ts | 6 ++--- src/ConfigContext.ts | 20 ++++++--------- src/Explorer/Tables/TableDataClient.ts | 9 +++---- .../Tabs/QueryTab/QueryTabComponent.tsx | 2 +- src/Explorer/Tabs/Tabs.tsx | 25 +++++++++++-------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index e37b0eff5..749248919 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -67,7 +67,7 @@ export function queryDocuments( query: string, continuationToken?: string, ): Promise { - if (!useMongoProxyEndpoint("resourcelist")) { + if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) { return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); } @@ -106,7 +106,7 @@ export function queryDocuments( headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken; } - const path = isResourceList ? "/resourcelist" : ""; + const path = isResourceList ? "/resourcelist" : "/queryDocuments"; return window .fetch(`${endpoint}${path}`, { @@ -690,7 +690,7 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development]; + const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; if (userContext.databaseAccount.properties.ipRules?.length > 0) { canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index e3350c7f6..3a8ef1cb5 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -102,21 +102,17 @@ let configContext: Readonly = { NEW_BACKEND_APIS: [BackendApi.GenerateToken], MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ - // "resourcelist", - // "createDocument", - // "readDocument", - // "updateDocument", - // "deleteDocument", - // "createCollectionWithProxy", + "resourcelist", + "queryDocuments", + "createDocument", + "readDocument", + "updateDocument", + "deleteDocument", + "createCollectionWithProxy", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, - NEW_CASSANDRA_APIS: [ - // "postQuery", - // "createOrDelete", - // "getKeys", - // "getSchema", - ], + NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, isTerminalEnabled: false, isPhoenixEnabled: false, diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 3d5f05c72..097ade395 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -19,7 +19,7 @@ import Explorer from "../Explorer"; import * as TableConstants from "./Constants"; import * as Entities from "./Entities"; import * as TableEntityProcessor from "./TableEntityProcessor"; -import { CassandraProxyAPIs } from "../../Common/Constants"; +import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -458,7 +458,7 @@ export class CassandraAPIDataClient extends TableDataClient { } public getTableKeys(collection: ViewModels.Collection): Q.Promise { - if (!this.useCassandraProxyEndpoint("getTableKeys")) { + if (!this.useCassandraProxyEndpoint("getKeys")) { return this.getTableKeys_ToBeDeprecated(collection); } @@ -732,6 +732,7 @@ export class CassandraAPIDataClient extends TableDataClient { } private useCassandraProxyEndpoint(api: string): boolean { + const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac]; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; if (userContext.databaseAccount.properties.ipRules?.length > 0) { canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; @@ -740,9 +741,7 @@ export class CassandraAPIDataClient extends TableDataClient { return ( canAccessCassandraProxy && configContext.NEW_CASSANDRA_APIS?.includes(api) && - [Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes( - configContext.CASSANDRA_PROXY_ENDPOINT, - ) + activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT) ); } } diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index ff44a4be3..c439cfc83 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -134,7 +134,7 @@ export default class QueryTabComponent extends React.Component { if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); - const ipRulesIncludeLegacyPortalBackend: boolean = - ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule)) - ?.length === legacyPortalBackendIPs.length; - + const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => + ipAddressesFromIPRules.includes(legacyPortalBackendIP), + ); if (!ipRulesIncludeLegacyPortalBackend) { return false; } @@ -344,9 +343,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { ? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]] : MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT]; - const ipRulesIncludeMongoProxy: boolean = - ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule)) - ?.length === mongoProxyOutboundIPs.length; + const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) => + ipAddressesFromIPRules.includes(mongoProxyOutboundIP), + ); if (ipRulesIncludeMongoProxy) { updateConfigContext({ @@ -368,9 +367,15 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { ] : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; - const ipRulesIncludeCassandraProxy: boolean = - ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule)) - ?.length === cassandraProxyOutboundIPs.length; + const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every( + (cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP), + ); + + if (ipRulesIncludeCassandraProxy) { + updateConfigContext({ + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, + }); + } return !ipRulesIncludeCassandraProxy; } From 1464745659419580831f1e080122a9569ea1a576 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:49:33 -0500 Subject: [PATCH 056/102] Add activate/close tab contracts and add to queryTab (#1783) --- src/Contracts/MessageTypes.ts | 1 + src/Explorer/Tabs/QueryTab/QueryTab.tsx | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Contracts/MessageTypes.ts b/src/Contracts/MessageTypes.ts index 1cbd86bb8..afe13a6a8 100644 --- a/src/Contracts/MessageTypes.ts +++ b/src/Contracts/MessageTypes.ts @@ -35,6 +35,7 @@ export enum MessageTypes { CreateWorkspace, CreateSparkPool, RefreshDatabaseAccount, + ActivateTab, CloseTab, OpenQuickstartBlade, OpenPostgreSQLPasswordReset, diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx index 47c2ce2ba..f297f91f4 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTab.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -1,3 +1,5 @@ +import { sendMessage } from "Common/MessageHandler"; +import { MessageTypes } from "Contracts/MessageTypes"; import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext"; import { userContext } from "UserContext"; import React from "react"; @@ -54,6 +56,11 @@ export class NewQueryTab extends TabsBase { ); } + public onActivate(): void { + this.propagateTabInformation(MessageTypes.ActivateTab); + super.onActivate(); + } + public onTabClick(): void { useTabs.getState().activateTab(this); this.iTabAccessor.onTabClickEvent(); @@ -61,6 +68,7 @@ export class NewQueryTab extends TabsBase { public onCloseTabButtonClick(): void { useTabs.getState().closeTab(this); + this.propagateTabInformation(MessageTypes.CloseTab); if (this.iTabAccessor) { this.iTabAccessor.onCloseClickEvent(true); } @@ -69,4 +77,15 @@ export class NewQueryTab extends TabsBase { public getContainer(): Explorer { return this.props.container; } + + private propagateTabInformation(type: MessageTypes): void { + sendMessage({ + type, + data: { + kind: this.tabKind, + databaseId: this.collection?.databaseId, + collectionId: this.collection?.id?.(), + }, + }); + } } From b44778b00ac47b7e35f24b72278e0ba67a5ea4ca Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 2 Apr 2024 10:51:19 -0700 Subject: [PATCH 057/102] fix #3061738 by unclobbering some Monaco styles we clobber (#1784) --- less/documentDB.less | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/less/documentDB.less b/less/documentDB.less index d8ae159db..357d159b3 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2296,6 +2296,17 @@ a:link { display: none !important; } +.monaco-editor .quick-input-list-label { + /* Restore some of Monaco's default styles that are clobbered by our global styles */ + padding: 0; + line-height: 22px; +} + +.monaco-editor .quick-input-list .highlight { + /* Padding in highlighted text within the quick input list breaks the flow of the text */ + padding: 0; +} + td a { color: #393939; } From 3c5d899e47d621163944e343c72057af7045ae76 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:54:53 -0500 Subject: [PATCH 058/102] add the new message to the bottom to avoid contract breaking (#1786) --- src/Contracts/MessageTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Contracts/MessageTypes.ts b/src/Contracts/MessageTypes.ts index afe13a6a8..a19b69e5f 100644 --- a/src/Contracts/MessageTypes.ts +++ b/src/Contracts/MessageTypes.ts @@ -35,7 +35,6 @@ export enum MessageTypes { CreateWorkspace, CreateSparkPool, RefreshDatabaseAccount, - ActivateTab, CloseTab, OpenQuickstartBlade, OpenPostgreSQLPasswordReset, @@ -48,6 +47,7 @@ export enum MessageTypes { GetAllResourceTokens, // Data Explorer -> Fabric Ready, // Data Explorer -> Fabric OpenCESCVAFeedbackBlade, + ActivateTab, } export interface AuthorizationToken { From f533eeb0fc541b7450eac75d8a210ebc6077d272 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 4 Apr 2024 09:16:23 -0700 Subject: [PATCH 059/102] add support for react dev tools in the cosmos explorer (#1788) --- src/Main.tsx | 3 +++ src/ReactDevTools.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Main.tsx b/src/Main.tsx index d62f9b45e..c6b79b139 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,3 +1,6 @@ +// Import this first, to ensure that the dev tools hook is copied before React is loaded. +import "./ReactDevTools"; + // CSS Dependencies import { initializeIcons, loadTheme } from "@fluentui/react"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; diff --git a/src/ReactDevTools.ts b/src/ReactDevTools.ts index 09947f934..2a12d81d5 100644 --- a/src/ReactDevTools.ts +++ b/src/ReactDevTools.ts @@ -1,3 +1,7 @@ if (window.parent !== window) { - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + try { + (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + } catch { + // No-op. We can throw here if the parent is not the same origin (such as in the Azure portal). + } } From db50f428328cf36c6d7e41915f02797caa0986f7 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 4 Apr 2024 09:17:09 -0700 Subject: [PATCH 060/102] [Task #3061771] Correct render order issues on undo (#1785) * fix #3061771 by correcting render order issues on undo * clarifying comment * fix lints * push an undo stop before executing edits * tidy up some unnecessary comments --- src/Explorer/Controls/Editor/EditorReact.tsx | 27 +++++++++++++++---- .../Tabs/QueryTab/QueryTabComponent.tsx | 13 +++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 956253a05..1d7a7381b 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -46,9 +46,21 @@ export class EditorReact extends React.Component { + // Hooking the model's onDidChangeContent event because of some event ordering issues. + // If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely), + // then there are some inconsistencies as to which event fires first. + // But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event. + // (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.) + // If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first. + this.editor.onDidChangeModelContent(() => { const queryEditorModel = this.editor.getModel(); this.props.onContentChanged(queryEditorModel.getValue()); }); diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index c439cfc83..fa849c212 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -496,13 +496,16 @@ export default class QueryTabComponent extends React.Component 0) { this.executeQueryButton = { @@ -544,7 +547,7 @@ export default class QueryTabComponent extends React.Component Date: Thu, 4 Apr 2024 10:18:50 -0700 Subject: [PATCH 061/102] Change copilot settings call to use new backend endpoint. (#1781) * Change copilot settings call to use new backend endpoint. * Refactor EndpointUtils function for new backend enablement. --- src/Common/Constants.ts | 5 +++-- src/ConfigContext.ts | 1 - .../QueryCopilot/Shared/QueryCopilotClient.ts | 8 ++++++- .../Hosted/Components/ConnectExplorer.tsx | 4 ++-- src/Utils/EndpointUtils.ts | 22 +++++++++++++++---- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index fe9c2672d..1eed03a4f 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -124,8 +124,9 @@ export enum MongoBackendEndpointType { remote, } -export enum BackendApi { - GenerateToken, +export class BackendApi { + public static readonly GenerateToken: string = "GenerateToken"; + public static readonly PortalSettings: string = "PortalSettings"; } export class PortalBackendEndpoints { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 3a8ef1cb5..435166b90 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -99,7 +99,6 @@ let configContext: Readonly = { JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, - NEW_BACKEND_APIS: [BackendApi.GenerateToken], MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ "resourcelist", diff --git a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts index 6d2cafdc5..59101cbb3 100644 --- a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts +++ b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts @@ -1,6 +1,7 @@ import { FeedOptions } from "@azure/cosmos"; import { Areas, + BackendApi, ConnectionStatusType, ContainerStatusType, HttpStatusCodes, @@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; +import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { useTabs } from "hooks/useTabs"; @@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis }; export const getCopilotEnabled = async (): Promise => { - const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; + const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings) + ? configContext.PORTAL_BACKEND_ENDPOINT + : configContext.BACKEND_ENDPOINT; + + const url = `${backendEndpoint}/api/portalsettings/querycopilot`; const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const headers = { [authorizationHeader.header]: authorizationHeader.token }; diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index 6b33d021b..749f248ba 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,6 +1,6 @@ import { useBoolean } from "@fluentui/react-hooks"; import { userContext } from "UserContext"; -import { usePortalBackendEndpoint } from "Utils/EndpointUtils"; +import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import * as React from "react"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ErrorImage from "../../../../images/error.svg"; @@ -19,7 +19,7 @@ interface Props { } export const fetchEncryptedToken = async (connectionString: string): Promise => { - if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) { + if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) { return await fetchEncryptedToken_ToBeDeprecated(connectionString); } diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 4962c285b..c59db4205 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -145,8 +145,22 @@ export const allowedJunoOrigins: ReadonlyArray = [ export const allowedNotebookServerUrls: ReadonlyArray = []; -export function usePortalBackendEndpoint(backendApi: BackendApi): boolean { - const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development]; - const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false; - return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string); +// +// Temporary function to determine if a portal backend API is supported by the +// new backend in this environment. +// +// TODO: Remove this function once new backend migration is completed for all environments. +// +export function useNewPortalBackendEndpoint(backendApi: string): boolean { + // This maps backend APIs to the environments supported by the new backend. + const newBackendApiEnvironmentMap: { [key: string]: string[] } = { + [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], + [BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac], + }; + + if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { + return false; + } + + return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT); } From 6925fa8e4e49766720137a83e3c6bff52e363b1b Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:55:08 -0700 Subject: [PATCH 062/102] Replace Entra app client secret auth with OpenID Connect in E2E tests. (#1792) * Use Az login with OpenID connection to get test credentials. * Set subscription id environment variable. * Update testExplorer and cleanup job. * Retrieve access token in test case and pass to testExplorer. * Add debug tracing for tests. * Set up other mongo test to use Az CLI creds. * Revert subscription id retrieval. * Add CLI credentials retrieval to rest of tests. * Fix missing imports. * Clean up redundant code. * Remove commented import statement. --- .github/workflows/ci.yml | 14 ++++++++++++-- .github/workflows/cleanup.yml | 15 +++++++++++++-- test/cassandra/container.spec.ts | 7 +++++-- test/graph/container.spec.ts | 7 +++++-- test/mongo/container.spec.ts | 7 +++++-- test/mongo/container32.spec.ts | 7 +++++-- test/selfServe/selfServeExample.spec.ts | 7 ++++++- test/sql/container.spec.ts | 7 +++++-- test/sql/resourceToken.spec.ts | 10 +++------- test/tables/container.spec.ts | 6 ++++-- test/testExplorer/TestExplorer.ts | 19 +------------------ test/utils/shared.ts | 11 +++++++++++ utils/cleanupDBs.js | 9 +++------ 13 files changed, 78 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a53b92645..fd097a194 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: pull_request: branches: - master +permissions: + id-token: write + contents: read jobs: codemetrics: runs-on: ubuntu-latest @@ -134,7 +137,7 @@ jobs: runs-on: ubuntu-latest env: NODE_TLS_REJECT_UNAUTHORIZED: 0 - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} strategy: fail-fast: false matrix: @@ -145,11 +148,18 @@ jobs: - ./test/mongo/container.spec.ts - ./test/mongo/container32.spec.ts - ./test/selfServe/selfServeExample.spec.ts - # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/sql/resourceToken.spec.ts - ./test/tables/container.spec.ts steps: - uses: actions/checkout@v4 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v4 with: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 229477f0b..6698951ae 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -9,6 +9,10 @@ on: # Once every hour - cron: "0 15 * * *" +permissions: + id-token: write + contents: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -16,10 +20,17 @@ jobs: name: "Cleanup Test Database Accounts" runs-on: ubuntu-latest env: - NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} steps: - uses: actions/checkout@v2 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 48acddb79..80d5df41d 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`); await page.waitForSelector("iframe"); const explorer = await waitForExplorer(); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 3e6155f1c..e7f288da5 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and graph diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 88a311784..baafefbbe 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 25466e266..c71f9d0cc 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/selfServe/selfServeExample.spec.ts b/test/selfServe/selfServeExample.spec.ts index 7e10c1ce2..3678f5b35 100644 --- a/test/selfServe/selfServeExample.spec.ts +++ b/test/selfServe/selfServeExample.spec.ts @@ -1,5 +1,10 @@ +import { getAzureCLICredentialsToken } from "../utils/shared"; + test("Self Serve", async () => { - await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); + + await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`); const handle = await page.waitForSelector("iframe"); const frame = await handle.contentFrame(); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index aead57b92..a1aacfd42 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`); const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts index f2e9d94ef..18228c7ed 100644 --- a/test/sql/resourceToken.spec.ts +++ b/test/sql/resourceToken.spec.ts @@ -1,19 +1,15 @@ import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosClient, PermissionMode } from "@azure/cosmos"; -import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentials } from "../utils/shared"; jest.setTimeout(120000); -const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const resourceGroupName = "runners"; test("Resource token", async () => { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index d8daa0a16..98687d60f 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,15 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`); const explorer = await waitForExplorer(); await page.waitForSelector('text="Querying databases"', { state: "detached" }); diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index 684673590..a415b5bd6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { ClientSecretCredential } from "@azure/identity"; import "../../less/hostedexplorer.less"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { updateUserContext } from "../../src/UserContext"; @@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search); const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const selfServeType = urlSearchParams.get("selfServeType") || "example"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; - -if (!process.env.AZURE_CLIENT_SECRET) { - throw new Error( - "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server", - ); -} - -// Azure SDK clients accept the credential as a parameter -const credentials = new ClientSecretCredential( - process.env.AZURE_TENANT_ID, - process.env.AZURE_CLIENT_ID, - process.env.AZURE_CLIENT_SECRET, - { - authorityHost: "https://localhost:1234", - }, -); +const token = urlSearchParams.get("token"); console.log("Resource Group:", resourceGroup); console.log("Subcription: ", subscriptionId); console.log("Account Name: ", accountName); const initTestExplorer = async (): Promise => { - const { token } = await credentials.getToken("https://management.azure.com//.default"); updateUserContext({ authorizationToken: `bearer ${token}`, }); diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 118736129..59ef0994c 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -1,3 +1,4 @@ +import { AzureCliCredentials } from "@azure/ms-rest-nodeauth"; import crypto from "crypto"; export function generateUniqueName(baseName = "", length = 4): string { @@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; } + +export async function getAzureCLICredentials(): Promise { + return await AzureCliCredentials.create(); +} + +export async function getAzureCLICredentialsToken(): Promise { + const credentials = await getAzureCLICredentials(); + const token = (await credentials.getToken()).accessToken; + return token; +} diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index 72fcfbafd..b2bbf0be8 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const ms = require("ms"); -const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const resourceGroupName = "runners"; const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); @@ -19,7 +16,7 @@ function friendlyTime(date) { } async function main() { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const client = new CosmosDBManagementClient(credentials, subscriptionId); const accounts = await client.databaseAccounts.list(resourceGroupName); for (const account of accounts) { @@ -38,7 +35,7 @@ async function main() { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( resourceGroupName, - account.name + account.name, ); for (const database of cassandraDatabases) { const timestamp = Number(database.resource._ts) * 1000; From dfcb7719392d77852e0987141aba703cdec90b65 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 9 Apr 2024 16:45:36 -0400 Subject: [PATCH 063/102] Activate Mongo Proxy and Cassandra Proxy in Prod (#1794) * activate Mongo Proxy and Cassandra Proxy in Prod * fix bug that blocked local mongo proxy and cassandra proxy development * fix pr check tests --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 11 +++++++++-- src/Explorer/Tables/TableDataClient.ts | 13 ++++++++++--- src/Explorer/Tabs/Tabs.tsx | 7 ++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 749248919..44400d874 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -690,9 +690,16 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac]; + const activeMongoProxyEndpoints: string[] = [ + MongoProxyEndpoints.Development, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + ]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if (userContext.databaseAccount.properties.ipRules?.length > 0) { + if ( + configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; } diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 097ade395..c6882bc33 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -3,6 +3,7 @@ import * as ko from "knockout"; import Q from "q"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; +import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; import { handleError } from "../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { createDocument } from "../../Common/dataAccess/createDocument"; @@ -19,7 +20,6 @@ import Explorer from "../Explorer"; import * as TableConstants from "./Constants"; import * as Entities from "./Entities"; import * as TableEntityProcessor from "./TableEntityProcessor"; -import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -732,9 +732,16 @@ export class CassandraAPIDataClient extends TableDataClient { } private useCassandraProxyEndpoint(api: string): boolean { - const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac]; + const activeCassandraProxyEndpoints: string[] = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ]; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if (userContext.databaseAccount.properties.ipRules?.length > 0) { + if ( + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; } diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 4af95118b..53678ed5a 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -324,7 +324,12 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; - if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { + if ( + ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || + (userContext.apiType === "Cassandra" && + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && + ipRules?.length + ) { const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => From 953bef404ba177e3030e811bf5c4c97e7bcda08c Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:43:46 -0700 Subject: [PATCH 064/102] Set backend endpoints in testExplorer to use MPAC. (#1797) --- test/testExplorer/TestExplorer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index a415b5bd6..4dbeb86b6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -35,6 +35,9 @@ const initTestExplorer = async (): Promise => { dnsSuffix: "documents.azure.com", serverId: "prod1", extensionEndpoint: "/proxy", + portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com", + mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com", + cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com", subscriptionType: 3, quotaId: "Internal_2014-09-01", isTryCosmosDBSubscription: false, From 00a816c4881a897a93959a124ab8a54e8fea6758 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:19:56 -0500 Subject: [PATCH 065/102] set the value in the editor for results (#1799) --- src/Explorer/Controls/Editor/EditorReact.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 1d7a7381b..47268c856 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -54,13 +54,17 @@ export class EditorReact extends React.Component Date: Mon, 15 Apr 2024 15:47:58 -0400 Subject: [PATCH 066/102] Cassandra API create table error messages swallowed by the Portal and shown as "undefined". (#1790) * changed error message variable * changed other error messages * Added check in case responseJSON is missing * created error const --- src/Explorer/Tables/TableDataClient.ts | 39 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index c6882bc33..a9d7c96a6 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(entity); }, (error) => { - handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { + const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError( - error, + errorText, "CreateKeyspaceCassandra", `Error while creating a keyspace with query ${createKeyspaceQuery}`, ); - deferred.reject(error); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { - handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError( + errorText, + "CreateTableCassandra", + `Error while creating a table with query ${createTableQuery}`, + ); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data); }, (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data); }, (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data.columns); }, (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data.columns); }, (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); From 6c9673975ad37df7343f889cfa21a2a2d0c83c85 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:24:14 -0400 Subject: [PATCH 067/102] Added hyphen to prohibited characters in keyspace name title (#1800) --- .../CassandraAddCollectionPane.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index dbef20b12..cd91a6628 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent Date: Tue, 16 Apr 2024 18:47:43 -0400 Subject: [PATCH 068/102] Is executing is false (#1801) --- src/Explorer/Panes/Tables/AddTableEntityPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 1f23a60bc..9ad999aa0 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent = setIsExecuting(true); const entity: Entities.ITableEntity = entityFromAttributes(entities); - const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); try { + const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); await tableEntityListViewModel.addEntityToCache(newEntity); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { tableEntityListViewModel.redrawTableThrottled(); From a44ed1f45c0ae6c977333be0cf96d53e1db29548 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 17 Apr 2024 11:19:09 -0700 Subject: [PATCH 069/102] [Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789) * keyboard shortcuts using tinykeys * refmt and fix lints * retarget keyboard shortcuts to the body instead of the root element of the React component tree * refmt * Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx Co-authored-by: Laurent Nguyen * add Save binding to New Item command bar --------- Co-authored-by: Laurent Nguyen --- package-lock.json | 6 ++ package.json | 1 + .../CommandButton/CommandButtonComponent.tsx | 10 ++- .../CommandBar/CommandBarComponentAdapter.tsx | 6 ++ .../CommandBarComponentButtonFactory.tsx | 5 ++ .../Menus/CommandBar/CommandBarUtil.tsx | 26 ++++++ src/Explorer/Tabs/DocumentsTab.ts | 3 + .../Tabs/QueryTab/QueryTabComponent.tsx | 4 + .../StoredProcedureTabComponent.tsx | 4 + src/Explorer/Tabs/TriggerTabContent.tsx | 3 + .../Tabs/UserDefinedFunctionTabContent.tsx | 5 +- src/KeyboardShortcuts.tsx | 89 +++++++++++++++++++ src/Main.tsx | 89 ++++++++++--------- 13 files changed, 206 insertions(+), 45 deletions(-) create mode 100644 src/KeyboardShortcuts.tsx diff --git a/package-lock.json b/package-lock.json index d3a36e3f3..edb334b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,7 @@ "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", + "tinykeys": "2.1.0", "underscore": "1.9.1", "utility-types": "3.10.0", "zustand": "3.5.0" @@ -37786,6 +37787,11 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "node_modules/tinykeys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-2.1.0.tgz", + "integrity": "sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==" + }, "node_modules/tinyqueue": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", diff --git a/package.json b/package.json index a7bfec4b8..cc195cd5f 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", + "tinykeys": "2.1.0", "underscore": "1.9.1", "utility-types": "3.10.0", "zustand": "3.5.0" diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index 1e5cfc171..6337f947e 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -1,6 +1,7 @@ /** * React component for Command button component. */ +import { KeyboardAction } from "KeyboardShortcuts"; import * as React from "react"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import { KeyCodes } from "../../../Common/Constants"; @@ -30,7 +31,7 @@ export interface CommandButtonComponentProps { /** * Click handler for command button click */ - onCommandClick: (e: React.SyntheticEvent) => void; + onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; /** * Label for the button @@ -107,10 +108,17 @@ export interface CommandButtonComponentProps { * Vertical bar to divide buttons */ isDivider?: boolean; + /** * Aria-label for the button */ ariaLabel: string; + + /** + * If specified, a keyboard action that should trigger this button's onCommandClick handler when activated. + * If not specified, the button will not be triggerable by keyboard shortcuts. + */ + keyboardAction?: KeyboardAction; } export class CommandButtonComponent extends React.Component { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 141bda577..eaa56591d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,6 +5,7 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { useKeyboardActionHandlers } from "KeyboardShortcuts"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; @@ -40,6 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; + const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -105,6 +107,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { }, }; + const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); + const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); + setKeyboardShortcutHandlers(keyboardHandlers); + return (
{ const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); @@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB id: "newQueryBtn", iconSrc: AddSqlQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_QUERY, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); @@ -397,6 +400,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps return { iconSrc: BrowseQueriesIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY, onCommandClick: () => useSidePanel.getState().openSidePanel("Open Saved Queries", ), commandButtonLabel: label, @@ -411,6 +415,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { return { iconSrc: OpenQueryFromDiskIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK, onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 384bd06cd..fc67ad894 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -7,6 +7,7 @@ import { IDropdownStyles, } from "@fluentui/react"; import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { KeyboardHandlerMap } from "KeyboardShortcuts"; import * as React from "react"; import _ from "underscore"; import ChevronDownIcon from "../../../../images/Chevron_down.svg"; @@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, onRender: () => , }; }; + +export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { + const handlers: KeyboardHandlerMap = {}; + + function createHandlers(buttons: CommandButtonComponentProps[]) { + buttons.forEach((button) => { + if (!button.disabled && button.keyboardAction) { + handlers[button.keyboardAction] = (e) => { + button.onCommandClick(e); + + // If the handler is bound, it means the button is visible and enabled, so we should prevent the default action + return true; + }; + } + + if (button.children && button.children.length > 0) { + createHandlers(button.children); + } + }); + } + + createHandlers(allButtons); + + return handlers; +} diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 3591378da..263fa77bd 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -1,6 +1,7 @@ import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { Platform, configContext } from "ConfigContext"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; +import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import * as ko from "knockout"; @@ -907,6 +908,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -936,6 +938,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveExistingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index fa849c212..e3f740a25 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component OnExecuteQueryClick(this.props.copilotStore) : this.onExecuteQueryClick, @@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 98e398e1b..dc179b002 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -1,5 +1,6 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Pivot, PivotItem } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React from "react"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import DiscardIcon from "../../../../images/discard.svg"; @@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveClick, commandButtonLabel: label, ariaLabel: label, @@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onUpdateClick, commandButtonLabel: label, ariaLabel: label, @@ -360,6 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: ExecuteQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.EXECUTE_ITEM, onCommandClick: () => { this.collection.container.openExecuteSprocParamsPanel(this.node); }, diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index bf756598e..2c7d32ab4 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -1,5 +1,6 @@ import { TriggerDefinition } from "@azure/cosmos"; import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; @@ -227,6 +228,7 @@ export class TriggerTabContent extends Component boolean | void; + +export type KeyboardHandlerMap = Partial>; + +/** + * The possible actions that can be triggered by keyboard shortcuts. + */ +export enum KeyboardAction { + NEW_QUERY = "NEW_QUERY", + EXECUTE_ITEM = "EXECUTE_ITEM", + CANCEL_QUERY = "CANCEL_QUERY", + SAVE_ITEM = "SAVE_ITEM", + OPEN_QUERY = "OPEN_QUERY", + OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK", +} + +/** + * The keyboard shortcuts for the application. + * This record maps each action to the keyboard shortcuts that trigger the action. + * Even if an action is specified here, it will not be triggered unless a handler is set for it. + */ +const bindings: Record = { + // NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. + // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. + + [KeyboardAction.NEW_QUERY]: ["$mod+J"], + [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], + [KeyboardAction.CANCEL_QUERY]: ["Escape"], + [KeyboardAction.SAVE_ITEM]: ["$mod+S"], + [KeyboardAction.OPEN_QUERY]: ["$mod+O"], + [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], +}; + +interface KeyboardShortcutState { + /** + * A set of all the keyboard shortcuts handlers. + */ + allHandlers: KeyboardHandlerMap; + + /** + * Sets the keyboard shortcut handlers. + */ + setHandlers: (handlers: KeyboardHandlerMap) => void; +} + +export const useKeyboardActionHandlers: UseStore = create((set) => ({ + allHandlers: {}, + setHandlers: (handlers: Partial>) => { + set({ allHandlers: handlers }); + }, +})); + +function createHandler(action: KeyboardAction): KeyboardActionHandler { + return (e) => { + const state = useKeyboardActionHandlers.getState(); + const handler = state.allHandlers[action]; + if (handler && handler(e)) { + e.preventDefault(); + e.stopPropagation(); + } + }; +} + +const allHandlers: KeyBindingMap = {}; +(Object.keys(bindings) as KeyboardAction[]).forEach((action) => { + const shortcuts = bindings[action]; + shortcuts.forEach((shortcut) => { + allHandlers[shortcut] = createHandler(action); + }); +}); + +export function KeyboardShortcutRoot({ children }: PropsWithChildren) { + useEffect(() => { + // We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component. + tinykeys(document.body, allHandlers); + }, []); + + return <>{children}; +} diff --git a/src/Main.tsx b/src/Main.tsx index c6b79b139..f79ee29f1 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js"; // Image Dependencies import { Platform } from "ConfigContext"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; +import { KeyboardShortcutRoot } from "KeyboardShortcuts"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import "../images/favicon.ico"; @@ -91,52 +92,54 @@ const App: React.FunctionComponent = () => { } return ( -
-
-
- {/* Main Command Bar - Start */} - - {/* Collections Tree and Tabs - Begin */} -
- {/* Collections Tree - Start */} - {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( -
-
- {/* Collections Tree Expanded - Start */} - - {/* Collections Tree Expanded - End */} - {/* Collections Tree Collapsed - Start */} - - {/* Collections Tree Collapsed - End */} + +
+
+
+ {/* Main Command Bar - Start */} + + {/* Collections Tree and Tabs - Begin */} +
+ {/* Collections Tree - Start */} + {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( +
+
+ {/* Collections Tree Expanded - Start */} + + {/* Collections Tree Expanded - End */} + {/* Collections Tree Collapsed - Start */} + + {/* Collections Tree Collapsed - End */} +
-
- )} - -
- {/* Collections Tree and Tabs - End */} - + {/* Collections Tree and Tabs - End */} +
+ + + {} + {} + {} + {}
- - - {} - {} - {} - {} -
+ ); }; From af664326ea499a0a69094398874e2da6973fe7bf Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 17 Apr 2024 15:57:29 -0700 Subject: [PATCH 070/102] Fix issues with the command bar when switching through React and Trigger tabs (#1804) * fix bug in trigger tab that takes over the command bar while open * clear context buttons when a react tab is active * restore unintentionally removed code * reformat --- src/Explorer/Tabs/Tabs.tsx | 4 ++++ src/Explorer/Tabs/TriggerTabContent.tsx | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 53678ed5a..d3732ac1c 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; +import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; @@ -297,6 +298,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => { }; const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { + // React tabs have no context buttons. + useCommandBar.getState().setContextButtons([]); + // eslint-disable-next-line no-console switch (activeReactTab) { case ReactTabKind.Connect: diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 2c7d32ab4..23ab9b2e1 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -219,6 +219,18 @@ export class TriggerTabContent extends Component From 98000a27f090ffc73b879b13e0cda75cd5358d0e Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 17 Apr 2024 19:01:12 -0400 Subject: [PATCH 071/102] Legacy Mongo Shell Mongo Proxy support (#1802) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test --------- Co-authored-by: Asier Isayas --- src/Common/Constants.ts | 2 +- src/Common/MongoProxyClient.ts | 42 ++--- src/ConfigContext.ts | 2 + .../MongoShellTab/MongoShellTabComponent.tsx | 17 +- .../MongoShellTab/getMongoShellOrigin.test.ts | 86 --------- .../Tabs/MongoShellTab/getMongoShellOrigin.ts | 10 - .../MongoShellTab/getMongoShellUrl.test.ts | 174 +----------------- .../Tabs/MongoShellTab/getMongoShellUrl.ts | 40 +--- src/Explorer/Tabs/Tabs.tsx | 2 +- src/Platform/Hosted/extractFeatures.ts | 12 +- src/Utils/EndpointUtils.ts | 2 +- web.config | 2 +- 12 files changed, 52 insertions(+), 339 deletions(-) delete mode 100644 src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts delete mode 100644 src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 1eed03a4f..69bd5ed49 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -138,7 +138,7 @@ export class PortalBackendEndpoints { } export class MongoProxyEndpoints { - public static readonly Development: string = "https://localhost:7238"; + public static readonly Local: string = "https://localhost:7238"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 44400d874..907b0305e 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -672,6 +672,27 @@ export function getEndpoint(endpoint: string): string { return url; } +export function useMongoProxyEndpoint(api: string): boolean { + const activeMongoProxyEndpoints: string[] = [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + ]; + let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if ( + configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { + canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + + return ( + canAccessMongoProxy && + configContext.NEW_MONGO_APIS?.includes(api) && + activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) + ); +} + // TODO: This function throws most of the time except on Forbidden which is a bit strange // It causes problems for TypeScript understanding the types async function errorHandling(response: Response, action: string, params: unknown): Promise { @@ -688,24 +709,3 @@ async function errorHandling(response: Response, action: string, params: unknown export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; } - -function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [ - MongoProxyEndpoints.Development, - MongoProxyEndpoints.Mpac, - MongoProxyEndpoints.Prod, - ]; - let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if ( - configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development && - userContext.databaseAccount.properties.ipRules?.length > 0 - ) { - canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; - } - - return ( - canAccessMongoProxy && - configContext.NEW_MONGO_APIS?.includes(api) && - activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) - ); -} diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 435166b90..061f25286 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -83,6 +83,7 @@ let configContext: Readonly = { `^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`, + `^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`, ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, hostedExplorerURL: "https://cosmos.azure.com/", @@ -108,6 +109,7 @@ let configContext: Readonly = { "updateDocument", "deleteDocument", "createCollectionWithProxy", + "legacyMongoShell", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index cb47a070d..a89ca5fc7 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -9,7 +9,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; import TabsBase from "../TabsBase"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; import { getMongoShellUrl } from "./getMongoShellUrl"; //eslint-disable-next-line @@ -35,7 +34,7 @@ export interface IMongoShellTabAccessor { } export interface IMongoShellTabComponentStates { - url: string; + url: URL; } export interface IMongoShellTabComponentProps { @@ -50,13 +49,16 @@ export default class MongoShellTabComponent extends Component< IMongoShellTabComponentStates > { private _logTraces: Map; + private _useMongoProxyEndpoint: boolean; constructor(props: IMongoShellTabComponentProps) { super(props); this._logTraces = new Map(); + this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell; + // this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); this.state = { - url: getMongoShellUrl(), + url: getMongoShellUrl(this._useMongoProxyEndpoint), }; props.onMongoShellTabAccessor({ @@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component< ) + Constants.MongoDBAccounts.defaultPort.toString(); const databaseId = this.props.collection.databaseId; const collectionId = this.props.collection.id(); - const apiEndpoint = configContext.BACKEND_ENDPOINT; + const apiEndpoint = this._useMongoProxyEndpoint + ? configContext.MONGO_PROXY_ENDPOINT + : configContext.BACKEND_ENDPOINT; const encryptedAuthToken: string = userContext.accessToken; - const targetOrigin = getMongoShellOrigin(); shellIframe.contentWindow.postMessage( { @@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component< apiEndpoint: apiEndpoint, }, }, - targetOrigin, + window.origin, ); } @@ -218,7 +221,7 @@ export default class MongoShellTabComponent extends Component< name="explorer" className="iframe" style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }} - src={this.state.url} + src={this.state.url.toString()} id={this.props.tabsBaseInstance.tabId} onLoad={(event) => this.setContentFocus(event)} title="Mongo Shell" diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts deleted file mode 100644 index 8f62b2a0c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { configContext } from "../../../ConfigContext"; -import { updateUserContext } from "../../../UserContext"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; - -describe("getMongoShellOrigin", () => { - (window as { origin: string }).origin = "window_origin"; - - beforeEach(() => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), - }); - }); - - it("should return by default", () => { - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT); - }); -}); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts deleted file mode 100644 index 774a4443c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { configContext } from "../../../ConfigContext"; -import { userContext } from "../../../UserContext"; - -export function getMongoShellOrigin(): string { - if (userContext.features.loadLegacyMongoShellFromBE === true) { - return configContext.BACKEND_ENDPOINT; - } - - return window.origin; -} diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts index 1d75f682d..0c138ff61 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts @@ -1,9 +1,9 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; +import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; import { updateUserContext, userContext } from "../../../UserContext"; -import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl"; +import { getMongoShellUrl } from "./getMongoShellUrl"; const mongoBackendEndpoint = "https://localhost:1234"; +const hostedExplorerURL = "https://cosmos.azure.com/"; describe("getMongoShellUrl", () => { let queryString = ""; @@ -13,6 +13,7 @@ describe("getMongoShellUrl", () => { updateConfigContext({ BACKEND_ENDPOINT: mongoBackendEndpoint, + hostedExplorerURL: hostedExplorerURL, platform: Platform.Hosted, }); @@ -32,175 +33,18 @@ describe("getMongoShellUrl", () => { cassandraEndpoint: "fakeCassandraEndpoint", }, }, - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), portalEnv: "prod", }); queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`; }); - it("should return /mongoshell/indexv2.html by default", () => { - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); + it("should return /indexv2.html by default", () => { + expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`); }); - it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => { - updateUserContext({ - portalEnv: "localhost", - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`); - }); - - describe("loadLegacyMongoShellFromBE===true", () => { - beforeEach(() => { - resetConfigContext(); - updateConfigContext({ - BACKEND_ENDPOINT: mongoBackendEndpoint, - platform: Platform.Hosted, - }); - - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - }); - - it("should return /mongoshell/index.html", () => { - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - BACKEND_ENDPOINT: mongoBackendEndpoint, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Hosted, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - }); -}); - -describe("getExtensionEndpoint", () => { - it("when platform === Platform.Hosted, backendEndpoint is undefined", () => { - expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "")).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Hosted, null)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo"); - }); - - it("when platform === Platform.Portal, backendEndpoint is udefined", () => { - expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "")).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Portal, null)).toBe(""); - }); - - it("when platform !== Platform.Portal, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo"); + it("should return /index.html when useMongoProxyEndpoint is true", () => { + const useMongoProxyEndpoint: boolean = true; + expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`); }); }); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index a029fe440..5c4c03bdb 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -1,45 +1,13 @@ -import { configContext, Platform } from "../../../ConfigContext"; +import { configContext } from "ConfigContext"; import { userContext } from "../../../UserContext"; -export function getMongoShellUrl(): string { +export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { const { databaseAccount: account } = userContext; const resourceId = account?.id; const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; + const path: string = useMongoProxyEndpoint ? `/index.html?${queryString}` : `/indexv2.html?${queryString}`; - if (userContext.features.enableLegacyMongoShellV1 === true) { - return `/mongoshell/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV1Debug === true) { - return `/mongoshell/debug/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2 === true) { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2Debug === true) { - return `/mongoshell/debug/indexv2.html?${queryString}`; - } - - if (userContext.portalEnv === "localhost") { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.loadLegacyMongoShellFromBE === true) { - const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`; - } - - return `/mongoshell/indexv2.html?${queryString}`; -} - -export function getExtensionEndpoint(platform: string, backendEndpoint: string): string { - const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : ""; - - const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || ""; - - return extensionEndpoint; + return new URL(path, configContext.hostedExplorerURL); } diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index d3732ac1c..78281bf62 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -329,7 +329,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; if ( - ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || + ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) || (userContext.apiType === "Cassandra" && configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && ipRules?.length diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 7bf3c8a3f..626b855bb 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -31,11 +31,6 @@ export type Features = { readonly mongoProxyAPIs?: string; readonly enableThroughputCap: boolean; readonly enableHierarchicalKeys: boolean; - readonly enableLegacyMongoShellV1: boolean; - readonly enableLegacyMongoShellV1Debug: boolean; - readonly enableLegacyMongoShellV2: boolean; - readonly enableLegacyMongoShellV2Debug: boolean; - readonly loadLegacyMongoShellFromBE: boolean; readonly enableCopilot: boolean; readonly copilotVersion?: string; readonly disableCopilotPhoenixGateaway: boolean; @@ -43,6 +38,7 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; + readonly enableLegacyMongoShell: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -106,11 +102,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear notebooksDownBanner: "true" === get("notebooksDownBanner"), enableThroughputCap: "true" === get("enablethroughputcap"), enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"), - enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"), - enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"), - enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"), - enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), - loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), enableCopilot: "true" === get("enablecopilot", "true"), copilotVersion: get("copilotversion") ?? "v2.0", disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), @@ -118,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), + enableLegacyMongoShell: "true" === get("enablelegacymongoshell"), }; } diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index c59db4205..f8398568c 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = { }; export const allowedMongoProxyEndpoints: ReadonlyArray = [ - MongoProxyEndpoints.Development, + MongoProxyEndpoints.Local, MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, MongoProxyEndpoints.Fairfax, diff --git a/web.config b/web.config index 9d9ff2619..4a967e52a 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - + From e3fab9b5bf4d5b2527aa4a7b2fe87dbe2d60002a Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 18 Apr 2024 15:39:13 -0400 Subject: [PATCH 072/102] Add 'mongoshell' to Legacy Mongo Shell path (#1806) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test * add mongoshell to path --------- Co-authored-by: Asier Isayas --- src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index 5c4c03bdb..0ecbcb83e 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -7,7 +7,9 @@ export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - const path: string = useMongoProxyEndpoint ? `/index.html?${queryString}` : `/indexv2.html?${queryString}`; + const path: string = useMongoProxyEndpoint + ? `/mongoshell/index.html?${queryString}` + : `/mongoshell/indexv2.html?${queryString}`; return new URL(path, configContext.hostedExplorerURL); } From a5a5a95973254cdc4f255f986e4ddc11054072b9 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 19 Apr 2024 09:43:27 -0700 Subject: [PATCH 073/102] [Task 3061766] Additional Keyboard Shortcuts (#1805) * [Task 3061766] Additional Keyboard Shortcuts refmt and fix lints shortcuts for: discard, new item/sproc/udf/trigger, delete item/sproc/udf/trigger copilot shortcut * remove 'Ctrl+I' due to conflict with Monaco Autocomplete --- .../CommandBar/CommandBarComponentAdapter.tsx | 4 ++-- .../CommandBarComponentButtonFactory.tsx | 6 +++++ src/Explorer/Tabs/DocumentsTab.ts | 4 ++++ .../Tabs/QueryTab/QueryTabComponent.tsx | 7 +++--- .../StoredProcedureTabComponent.tsx | 1 + src/Explorer/Tabs/TriggerTabContent.tsx | 1 + .../Tabs/UserDefinedFunctionTabContent.tsx | 1 + src/KeyboardShortcuts.tsx | 22 ++++++++++++++++--- 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index eaa56591d..a5db3826c 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -41,7 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; - const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers); + const setKeyboardActionHandlers = useKeyboardActionHandlers((state) => state.setHandlers); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -109,7 +109,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); - setKeyboardShortcutHandlers(keyboardHandlers); + setKeyboardActionHandlers(keyboardHandlers); return (
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 175b43d04..a1aa3e49b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -58,6 +58,7 @@ export function createStaticCommandBarButtons( buttons.push(homeBtn); const newCollectionBtn = createNewCollectionGroup(container); + newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below. buttons.push(newCollectionBtn); if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { const addSynapseLink = createOpenSynapseLinkDialogButton(container); @@ -95,6 +96,7 @@ export function createStaticCommandBarButtons( const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -278,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { return { iconSrc: AddDatabaseIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_DATABASE, onCommandClick: async () => { const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; if (throughputCap && throughputCap !== -1) { @@ -340,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -359,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newUserDefinedFunctionBtn: CommandButtonComponentProps = { iconSrc: AddUdfIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_UDF, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); @@ -378,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newTriggerBtn: CommandButtonComponentProps = { iconSrc: AddTriggerIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_TRIGGER, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 263fa77bd..1d13237d0 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -894,6 +894,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: NewDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_ITEM, onCommandClick: this.onNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -923,6 +924,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -953,6 +955,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -968,6 +971,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DeleteDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.DELETE_ITEM, onCommandClick: this.onDeleteExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index e3f740a25..07f0a9eba 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -440,7 +440,7 @@ export default class QueryTabComponent extends React.Component { this._toggleCopilot(!this.state.copilotActive); }, @@ -471,7 +472,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index dc179b002..ce8c6ac45 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -350,6 +350,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 23ab9b2e1..5fd28502a 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -271,6 +271,7 @@ export class TriggerTabContent extends Component = { // NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. - [KeyboardAction.NEW_QUERY]: ["$mod+J"], + [KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"], [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], - [KeyboardAction.CANCEL_QUERY]: ["Escape"], + [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], + [KeyboardAction.NEW_SPROC]: ["Alt+N P"], + [KeyboardAction.NEW_UDF]: ["Alt+N F"], + [KeyboardAction.NEW_TRIGGER]: ["Alt+N T"], + [KeyboardAction.NEW_DATABASE]: ["Alt+N D"], + [KeyboardAction.NEW_COLLECTION]: ["Alt+N C"], + [KeyboardAction.NEW_ITEM]: ["Alt+N I"], + [KeyboardAction.DELETE_ITEM]: ["Alt+D"], + [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], }; interface KeyboardShortcutState { From c220a8b070e9037710f9d1a8e6c3ad454ce31d6c Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 19 Apr 2024 13:44:30 -0700 Subject: [PATCH 074/102] [Task 3071878] Tab Navigation Keyboard Shortcuts (#1808) * [Task 3071878] Tab Navigation Keyboard Shortcuts * throw in development on duplicate handlers * refmt --- .../CommandBar/CommandBarComponentAdapter.tsx | 6 +- src/Explorer/Tabs/Tabs.tsx | 11 ++++ src/KeyboardShortcuts.tsx | 64 +++++++++++++++++-- src/hooks/useTabs.ts | 46 +++++++++++++ 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index a5db3826c..9a5f222a3 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,7 +5,7 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; -import { useKeyboardActionHandlers } from "KeyboardShortcuts"; +import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; @@ -41,7 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; - const setKeyboardActionHandlers = useKeyboardActionHandlers((state) => state.setHandlers); + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -109,7 +109,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); - setKeyboardActionHandlers(keyboardHandlers); + setKeyboardHandlers(keyboardHandlers); return (
diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 78281bf62..b33f3d4af 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -14,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; +import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { userContext } from "UserContext"; import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; @@ -42,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { showMongoAndCassandraProxiesNetworkSettingsWarningState, setShowMongoAndCassandraProxiesNetworkSettingsWarningState, ] = useState(showMongoAndCassandraProxiesNetworkSettingsWarning()); + + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS); + useEffect(() => { + setKeyboardHandlers({ + [KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(), + [KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(), + [KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(), + }); + }, [setKeyboardHandlers]); + return (
{networkSettingsWarning && ( diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index dbf6dddb7..66efd68b4 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -12,6 +12,15 @@ export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void; export type KeyboardHandlerMap = Partial>; +/** + * The groups of keyboard actions that can be managed by the application. + * Each group can be updated separately, but, when updated, must be completely replaced. + */ +export enum KeyboardActionGroup { + TABS = "TABS", + COMMAND_BAR = "COMMAND_BAR", +} + /** * The possible actions that can be triggered by keyboard shortcuts. */ @@ -30,6 +39,9 @@ export enum KeyboardAction { NEW_ITEM = "NEW_ITEM", DELETE_ITEM = "DELETE_ITEM", TOGGLE_COPILOT = "TOGGLE_COPILOT", + SELECT_LEFT_TAB = "SELECT_LEFT_TAB", + SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB", + CLOSE_TAB = "CLOSE_TAB", } /** @@ -55,6 +67,9 @@ const bindings: Record = { [KeyboardAction.NEW_ITEM]: ["Alt+N I"], [KeyboardAction.DELETE_ITEM]: ["Alt+D"], [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"], + [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], }; interface KeyboardShortcutState { @@ -64,15 +79,47 @@ interface KeyboardShortcutState { allHandlers: KeyboardHandlerMap; /** - * Sets the keyboard shortcut handlers. + * A set of all the groups of keyboard shortcuts handlers. */ - setHandlers: (handlers: KeyboardHandlerMap) => void; + groups: Partial>; + + /** + * Sets the keyboard shortcut handlers for the given group. + */ + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; } -export const useKeyboardActionHandlers: UseStore = create((set) => ({ +/** + * Defines the calling component as the manager of the keyboard actions for the given group. + * @param group The group of keyboard actions to manage. + * @returns A function that can be used to set the keyboard action handlers for the given group. + */ +export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => + useKeyboardActionHandlers.getState().setHandlers(group, handlers); + +const useKeyboardActionHandlers: UseStore = create((set, get) => ({ allHandlers: {}, - setHandlers: (handlers: Partial>) => { - set({ allHandlers: handlers }); + groups: {}, + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => { + const state = get(); + const groups = { ...state.groups, [group]: handlers }; + + // Combine all the handlers from all the groups in the correct order. + const allHandlers: KeyboardHandlerMap = {}; + eachKey(groups).forEach((group) => { + const groupHandlers = groups[group]; + if (groupHandlers) { + eachKey(groupHandlers).forEach((action) => { + // Check for duplicate handlers in development mode. + // We don't want to raise an error here in production, but having duplicate handlers is a mistake. + if (process.env.NODE_ENV === "development" && allHandlers[action]) { + throw new Error(`Duplicate handler for Keyboard Action "${action}".`); + } + allHandlers[action] = groupHandlers[action]; + }); + } + }); + set({ groups, allHandlers }); }, })); @@ -88,7 +135,7 @@ function createHandler(action: KeyboardAction): KeyboardActionHandler { } const allHandlers: KeyBindingMap = {}; -(Object.keys(bindings) as KeyboardAction[]).forEach((action) => { +eachKey(bindings).forEach((action) => { const shortcuts = bindings[action]; shortcuts.forEach((shortcut) => { allHandlers[shortcut] = createHandler(action); @@ -103,3 +150,8 @@ export function KeyboardShortcutRoot({ children }: PropsWithChildren) { return <>{children}; } + +/** A _typed_ version of `Object.keys` that preserves the original key type */ +function eachKey(record: Partial>): K[] { + return Object.keys(record) as K[]; +} diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index 10bc3b144..982768afa 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,3 +1,4 @@ +import { clamp } from "@fluentui/react"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels"; @@ -29,6 +30,11 @@ export interface TabsState { setQueryCopilotTabInitialInput: (input: string) => void; setIsTabExecuting: (state: boolean) => void; setIsQueryErrorThrown: (state: boolean) => void; + getCurrentTabIndex: () => number; + selectTabByIndex: (index: number) => void; + selectLeftTab: () => void; + selectRightTab: () => void; + closeActiveTab: () => void; } export enum ReactTabKind { @@ -175,4 +181,44 @@ export const useTabs: UseStore = create((set, get) => ({ setIsQueryErrorThrown: (state: boolean) => { set({ isQueryErrorThrown: state }); }, + getCurrentTabIndex: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + return state.openedReactTabs.indexOf(state.activeReactTab); + } else if (state.activeTab !== undefined) { + const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab); + if (nonReactTabIndex !== -1) { + return state.openedReactTabs.length + nonReactTabIndex; + } + } + + return -1; + }, + selectTabByIndex: (index: number) => { + const state = get(); + const totalTabCount = state.openedReactTabs.length + state.openedTabs.length; + const clampedIndex = clamp(index, totalTabCount - 1, 0); + + if (clampedIndex < state.openedReactTabs.length) { + set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] }); + } else { + set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined }); + } + }, + selectLeftTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() - 1); + }, + selectRightTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() + 1); + }, + closeActiveTab: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + state.closeReactTab(state.activeReactTab); + } else if (state.activeTab !== undefined) { + state.closeTab(state.activeTab); + } + }, })); From 2b15a4d43d05431045b750fe56bc59d44ebd182d Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:03:11 -0700 Subject: [PATCH 075/102] Update package.json (#1807) --- package-lock.json | 205 +++++++++++++++++++++++++++++++++------------- package.json | 8 +- 2 files changed, 156 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index edb334b7c..f0b65b98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -51,6 +51,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -74,12 +75,14 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", @@ -103,11 +106,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", "tinykeys": "2.1.0", - "underscore": "1.9.1", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, @@ -597,13 +601,13 @@ } }, "node_modules/@azure/ms-rest-nodeauth": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz", - "integrity": "sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.1.1.tgz", + "integrity": "sha512-UA/8dgLy3+ZiwJjAZHxL4MUB14fFQPkaAOZ94jsTW/Z6WmoOeny2+cLk0+dyIX/iH6qSrEWKwbStEeB970B9pA==", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", - "adal-node": "^0.1.28" + "adal-node": "^0.2.2" } }, "node_modules/@azure/msal-browser": { @@ -14184,6 +14188,14 @@ } } }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -14301,29 +14313,39 @@ } }, "node_modules/adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha512-98nQ5MQSyJR0ZY/R0Mue/cv4OkebRyKz4hS40GdkZU42Bq49ldHeup7UeAo/0vROMB57CX2et6IF0U/Pe1rY3A==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.4.tgz", + "integrity": "sha512-zIcvbwQFKMUtKxxj8YMHeTT1o/TPXfVNsTXVgXD8sxwV6h4AFQgK77dRciGhuEF9/Sdm3UQPJVPc/6XxrccSeA==", "deprecated": "This package is no longer supported. Please migrate to @azure/msal-node.", "dependencies": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", + "@xmldom/xmldom": "^0.8.3", + "async": "^2.6.3", + "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", - "request": ">= 2.52.0", "underscore": ">= 1.3.1", "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", "xpath.js": "~1.1.0" }, "engines": { "node": ">= 0.6.15" } }, - "node_modules/adal-node/node_modules/@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + "node_modules/adal-node/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/adal-node/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } }, "node_modules/adal-node/node_modules/jwa": { "version": "1.4.1", @@ -14940,7 +14962,9 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "peer": true }, "node_modules/async-hook-jl": { "version": "1.7.6", @@ -15084,6 +15108,32 @@ "webpack": ">=2" } }, + "node_modules/babel-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/babel-loader/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -21759,6 +21809,32 @@ "html-loader": "^0.5.1" } }, + "node_modules/html-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/html-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/html-minifier": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", @@ -22333,10 +22409,9 @@ } }, "node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", - "dev": true, + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -29209,29 +29284,16 @@ } }, "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "json5": "^2.1.2" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "node": ">=8.9.0" } }, "node_modules/locate-path": { @@ -34255,6 +34317,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -34321,6 +34393,12 @@ "node": ">= 6" } }, + "node_modules/react-dev-utils/node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "node_modules/react-dev-utils/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -36410,10 +36488,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" }, "node_modules/shellwords": { "version": "0.1.1", @@ -37255,6 +37332,32 @@ "node": ">= 0.12.0" } }, + "node_modules/style-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/style-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/style-loader/node_modules/schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -38443,9 +38546,9 @@ } }, "node_modules/underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "node_modules/unherit": { "version": "1.1.3", @@ -40149,14 +40252,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "devOptional": true }, - "node_modules/xmldom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", - "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/xpath.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", diff --git a/package.json b/package.json index cc195cd5f..2c7e2b7f5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -46,6 +46,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -69,12 +70,14 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", @@ -98,11 +101,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", "tinykeys": "2.1.0", - "underscore": "1.9.1", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, From c12eced120fddfa4c8cea5eedce89fae72e1fc50 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:10:16 -0700 Subject: [PATCH 076/102] Update node-fetch, react-dev-utils and azure/identity dependencies. (#1809) --- package-lock.json | 1125 +++++++++++++++++++++++++-------------------- package.json | 6 +- 2 files changed, 623 insertions(+), 508 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0b65b98a..590a1c049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@azure/arm-cosmosdb": "9.1.0", "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", + "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", @@ -180,13 +180,13 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", @@ -253,14 +253,6 @@ "tslib": "^1.10.0" } }, - "node_modules/@azure/core-asynciterator-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", - "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@azure/core-auth": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", @@ -279,66 +271,35 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", - "node-fetch": "^2.6.0", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@azure/core-http/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@azure/core-http/node_modules/tslib": { + "node_modules/@azure/core-client/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" @@ -459,42 +420,44 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/identity": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.2.1.tgz", - "integrity": "sha512-vCzV4Xg5hWJ2e4Et0waOmIEgYHsqtGF06kklnqblZg0hKDLKxTAX5FzKYuDMk1CctY2UdEmWFcA2li2uOXOLXQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.2.tgz", + "integrity": "sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==", "dependencies": { - "@azure/core-http": "^1.2.0", - "@azure/core-tracing": "1.0.0-preview.9", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", - "@azure/msal-node": "1.0.0-beta.1", - "@opentelemetry/api": "^0.10.2", + "@azure/msal-node": "1.0.0-beta.6", + "@types/stoppable": "^1.1.0", "axios": "^0.21.1", "events": "^3.0.0", "jws": "^4.0.0", "msal": "^1.0.2", "open": "^7.0.0", "qs": "^6.7.0", + "stoppable": "^1.1.0", "tslib": "^2.0.0", "uuid": "^8.3.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "optionalDependencies": { - "keytar": "^5.4.0" + "keytar": "^7.3.0" } }, "node_modules/@azure/identity/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.9", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", - "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "version": "1.0.0-preview.12", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.12.tgz", + "integrity": "sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" } }, "node_modules/@azure/identity/node_modules/tslib": { @@ -634,61 +597,17 @@ } }, "node_modules/@azure/msal-node": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.1.tgz", - "integrity": "sha512-dO/bgVScpl5loZfsfhHXmFLTNoDxGvUiZIsJCe1+HpHyFWXwGsBZ71P5ixbxRhhf/bPpZS3X+/rm1Fq2uUucJw==", + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz", + "integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==", "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", "dependencies": { - "@azure/msal-common": "^1.7.2", - "axios": "^0.19.2", + "@azure/msal-common": "^4.0.0", + "axios": "^0.21.1", "jsonwebtoken": "^8.5.1", "uuid": "^8.3.0" } }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.7.2.tgz", - "integrity": "sha512-3/voCdFKONENX+5tMrNOBSrVJb6NbE7YB8vc4FZ/4ZbjpK7GVtq9Bu1MW+HZhrmsUzSF/joHx0ZIJDYIequ/jg==", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node/node_modules/axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", - "dependencies": { - "follow-redirects": "1.5.10" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dependencies": { - "debug": "=3.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@azure/msal-node/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -7071,17 +6990,6 @@ "node": ">=10" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10184,29 +10092,10 @@ "@octokit/openapi-types": "^19.0.2" } }, - "node_modules/@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/@opentelemetry/api": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", - "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", - "dependencies": { - "@opentelemetry/context-base": "^0.10.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "engines": { "node": ">=8.0.0" } @@ -12839,6 +12728,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "devOptional": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, "node_modules/@types/post-robot": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@types/post-robot/-/post-robot-10.0.1.tgz", @@ -13618,6 +13513,14 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "devOptional": true }, + "node_modules/@types/stoppable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.3.tgz", + "integrity": "sha512-7wGKIBJGE4ZxFjk9NkjAxZMLlIXroETqP1FJCdoSvKmEznwmBxQFmTB1dsCkAvVcNemuSZM5qkkd9HE/NL2JTw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/styled-components": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.1.tgz", @@ -13644,14 +13547,6 @@ "@types/jest": "*" } }, - "node_modules/@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/underscore": { "version": "1.7.36", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.7.36.tgz", @@ -14661,7 +14556,9 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/archy": { "version": "1.0.0", @@ -14669,16 +14566,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -16469,7 +16356,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16746,7 +16635,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -17020,7 +16911,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "devOptional": true + "dev": true }, "node_modules/cosmiconfig": { "version": "5.2.1", @@ -17861,7 +17752,9 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -18150,7 +18043,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/denodeify": { "version": "1.2.1", @@ -18204,15 +18099,12 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -20325,9 +20217,9 @@ "optional": true }, "node_modules/filesize": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", "dev": true, "engines": { "node": ">= 0.4.0" @@ -20725,22 +20617,200 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", - "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.5.5", - "chalk": "^2.4.1", - "micromatch": "^3.1.10", + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", "minimatch": "^3.0.4", - "semver": "^5.6.0", - "tapable": "^1.0.0", - "worker-rpc": "^0.1.0" + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" }, "engines": { - "node": ">=6.11.5", + "node": ">=10", "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { @@ -20752,6 +20822,30 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -20987,43 +21081,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21375,16 +21432,18 @@ "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "node_modules/gzip-size": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "dependencies": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" + "duplexer": "^0.1.2" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/handle-thing": { @@ -21520,7 +21579,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/has-value": { "version": "1.0.0", @@ -22314,6 +22375,14 @@ "node-fetch": "2.6.1" } }, + "node_modules/i18next-http-backend/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -22863,7 +22932,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -28995,22 +29066,16 @@ "integrity": "sha512-yQa1dz+FilQ+w3JM6GH2V/wnFeQhfbkK9stvs3UiraW3GOEO7zrOBBh0ZuHsrzeN1xx6v7P5EpA2JtOUUnfN/w==" }, "node_modules/keytar": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-5.6.0.tgz", - "integrity": "sha512-ueulhshHSGoryfRXaIvTj0BV1yB0KddBGhGoqCxSN9LR1Ks1GKuuCdVhF+2/YOs5fMl6MlTI9On1a4DHDXoTow==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "hasInstallScript": true, "optional": true, "dependencies": { - "nan": "2.14.1", - "prebuild-install": "5.3.3" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, - "node_modules/keytar/node_modules/nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "optional": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -31389,12 +31454,6 @@ "node": ">=12" } }, - "node_modules/microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", - "dev": true - }, "node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -31470,7 +31529,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -31885,19 +31946,61 @@ } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.60.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.60.0.tgz", + "integrity": "sha512-zcGgwoXbzw9NczqbGzAWL/ToDYAxv1V8gL1D67ClbdkIfeeDBbY0GelZtC25ayLvVjr2q2cloHeQV1R0QAWqRQ==", "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, + "node_modules/node-abi/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -31912,11 +32015,22 @@ } }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-forge": { @@ -32015,12 +32129,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "optional": true - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -32074,18 +32182,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -32108,7 +32204,9 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -33582,44 +33680,81 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prebuild-install": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", - "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" + "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/prebuild-install/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "optional": true, "dependencies": { - "minimist": "^1.2.6" + "mimic-response": "^3.1.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, "node_modules/prelude-ls": { @@ -33803,7 +33938,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "devOptional": true + "dev": true }, "node_modules/process-on-spawn": { "version": "1.0.0", @@ -34210,106 +34345,110 @@ } }, "node_modules/react-dev-utils": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", - "integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", "dev": true, "dependencies": { - "@babel/code-frame": "7.10.4", - "address": "1.1.2", - "browserslist": "4.14.2", - "chalk": "2.4.2", - "cross-spawn": "7.0.3", - "detect-port-alt": "1.1.6", - "escape-string-regexp": "2.0.0", - "filesize": "6.1.0", - "find-up": "4.1.0", - "fork-ts-checker-webpack-plugin": "4.1.6", - "global-modules": "2.0.0", - "globby": "11.0.1", - "gzip-size": "5.1.1", - "immer": "8.0.1", - "is-root": "2.1.0", - "loader-utils": "2.0.0", - "open": "^7.0.2", - "pkg-up": "3.1.0", - "prompts": "2.4.0", - "react-error-overlay": "^6.0.9", - "recursive-readdir": "2.2.2", - "shell-quote": "1.7.2", - "strip-ansi": "6.0.0", - "text-table": "0.2.0" + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/react-dev-utils/node_modules/browserslist": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", - "integrity": "sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001125", - "electron-to-chromium": "^1.3.564", - "escalade": "^3.0.2", - "node-releases": "^1.1.61" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" }, "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, "engines": { "node": ">=10" }, @@ -34317,10 +34456,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/react-dev-utils/node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "dev": true, "funding": { "type": "opencollective", @@ -34328,47 +34476,74 @@ } }, "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, "engines": { - "node": ">=8.9.0" + "node": ">= 12.13.0" } }, "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-dev-utils/node_modules/node-releases": { - "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/react-dev-utils/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/path-exists": { @@ -34380,41 +34555,13 @@ "node": ">=8" } }, - "node_modules/react-dev-utils/node_modules/prompts": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/react-dev-utils/node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" @@ -34956,7 +35103,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "devOptional": true, + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -34971,7 +35118,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "devOptional": true + "dev": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -36560,7 +36707,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -37129,6 +37278,15 @@ "node": ">=0.10.0" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -37161,7 +37319,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -37175,7 +37335,9 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -37184,7 +37346,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -37994,6 +38158,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "devOptional": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -38008,6 +38173,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "devOptional": true, "engines": { "node": ">= 4.0.0" } @@ -39373,21 +39539,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-bundle-analyzer/node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", @@ -39893,15 +40044,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -39924,7 +40066,9 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -40063,15 +40207,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, - "node_modules/worker-rpc": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", - "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", - "dev": true, - "dependencies": { - "microevent.ts": "~0.1.1" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -40217,26 +40352,6 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", diff --git a/package.json b/package.json index 2c7e2b7f5..9ac61d243 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@azure/arm-cosmosdb": "9.1.0", "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", + "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", @@ -175,13 +175,13 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", From 7b81767ded7031167d57f1ae7f2075d8bd9eaad0 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:34:20 -0700 Subject: [PATCH 077/102] Enable new backend for Settings API in Prod. (#1791) --- src/Utils/EndpointUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index f8398568c..e4c036d75 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -155,7 +155,11 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean { // This maps backend APIs to the environments supported by the new backend. const newBackendApiEnvironmentMap: { [key: string]: string[] } = { [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], - [BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac], + [BackendApi.PortalSettings]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], }; if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { From acf5acfdb45c0609b8dbc83cddfb730ec3a25b66 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 23 Apr 2024 08:20:27 -0400 Subject: [PATCH 078/102] Remove Legacy Mongo Shell feature flag (#1810) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test * add mongoshell to path * remove LMS feature flag --------- Co-authored-by: Asier Isayas --- src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx | 4 ++-- src/Platform/Hosted/extractFeatures.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index a89ca5fc7..d3720cf41 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,3 +1,4 @@ +import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import React, { Component } from "react"; import * as Constants from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; @@ -54,8 +55,7 @@ export default class MongoShellTabComponent extends Component< constructor(props: IMongoShellTabComponentProps) { super(props); this._logTraces = new Map(); - this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell; - // this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); + this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); this.state = { url: getMongoShellUrl(this._useMongoProxyEndpoint), diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 626b855bb..5bd84516e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -38,7 +38,6 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; - readonly enableLegacyMongoShell: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), - enableLegacyMongoShell: "true" === get("enablelegacymongoshell"), }; } From c1a28793ba04c000b29af923051dff3435118bad Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 09:08:29 -0700 Subject: [PATCH 079/102] bind F5 to execute query (#1813) --- src/KeyboardShortcuts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 66efd68b4..023b7078e 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -54,7 +54,7 @@ const bindings: Record = { // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. [KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"], - [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], + [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"], [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], From d36e511b1873403a742bcdae1fd96f6963df9bdf Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:15:48 -0700 Subject: [PATCH 080/102] Update d3, webpack-dev-server, typedoc dependencies. (#1812) * Update d3, webpack-dev-server, typedoc dependencies. * Fix unit test failures. * Revert change to snapshot as it doesn't seem required when running in github. --- jest.config.js | 5 +- package-lock.json | 582 +++++++++++++++++++++++++++++++--------------- package.json | 6 +- 3 files changed, 405 insertions(+), 188 deletions(-) diff --git a/jest.config.js b/jest.config.js index c00efdac6..b4f660063 100644 --- a/jest.config.js +++ b/jest.config.js @@ -76,6 +76,10 @@ module.exports = { "^dnd-core$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", + "d3-force": "/node_modules/d3-force/dist/d3-force.min.js", + "d3-quadtree": "/node_modules/d3-quadtree/dist/d3-quadtree.min.js", + "d3-scale-chromatic": "/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js", + "d3-zoom": "/node_modules/d3-zoom/dist/d3-zoom.min.js", }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -130,7 +134,6 @@ 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 diff --git a/package-lock.json b/package-lock.json index 590a1c049..8dcfe5d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -191,14 +191,14 @@ "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" } }, "canvas": { @@ -17259,40 +17259,43 @@ } }, "node_modules/d3": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/d3/-/d3-6.1.1.tgz", - "integrity": "sha512-bJYW9wlS2uvP2EoMkcPptrUzLMHQKCbiSW+/la8iGSLZgs4KbI/f3Fch4RtnUA9PA+/nPlwyFYzTwDjX80Of8w==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", "dependencies": { - "d3-array": "2", - "d3-axis": "2", - "d3-brush": "2", - "d3-chord": "2", - "d3-color": "2", - "d3-contour": "2", - "d3-delaunay": "5", - "d3-dispatch": "2", - "d3-drag": "2", - "d3-dsv": "2", - "d3-ease": "2", - "d3-fetch": "2", - "d3-force": "2", - "d3-format": "2", - "d3-geo": "2", - "d3-hierarchy": "2", - "d3-interpolate": "2", - "d3-path": "2", - "d3-polygon": "2", - "d3-quadtree": "2", - "d3-random": "2", - "d3-scale": "3", - "d3-scale-chromatic": "2", - "d3-selection": "2", - "d3-shape": "2", - "d3-time": "2", - "d3-time-format": "3", - "d3-timer": "2", - "d3-transition": "2", - "d3-zoom": "2" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-array": { @@ -17304,9 +17307,12 @@ } }, "node_modules/d3-axis": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz", - "integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-brush": { "version": "2.1.0", @@ -17321,11 +17327,14 @@ } }, "node_modules/d3-chord": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz", - "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-collection": { @@ -17339,19 +17348,36 @@ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" }, "node_modules/d3-contour": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz", - "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dependencies": { - "d3-array": "2" + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-delaunay": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", - "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dependencies": { - "delaunator": "4" + "delaunator": "5" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-dispatch": { @@ -17369,24 +17395,46 @@ } }, "node_modules/d3-dsv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", - "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "dependencies": { - "commander": "2", - "iconv-lite": "0.4", + "commander": "7", + "iconv-lite": "0.6", "rw": "1" }, "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/d3-ease": { @@ -17395,21 +17443,27 @@ "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==" }, "node_modules/d3-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz", - "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "dependencies": { - "d3-dsv": "1 - 2" + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-force": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", - "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-quadtree": "1 - 2", - "d3-timer": "1 - 2" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-format": { @@ -17418,11 +17472,14 @@ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" }, "node_modules/d3-geo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", - "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "dependencies": { - "d3-array": "^2.5.0" + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-glyphedge": { @@ -17436,9 +17493,12 @@ "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==" }, "node_modules/d3-hierarchy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", - "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-interpolate": { "version": "2.0.1", @@ -17449,9 +17509,12 @@ } }, "node_modules/d3-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", - "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-path-arrows": { "version": "0.4.0", @@ -17473,19 +17536,28 @@ "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" }, "node_modules/d3-polygon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", - "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-quadtree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", - "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-random": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz", - "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-sankey-circular": { "version": "0.25.0", @@ -17515,12 +17587,15 @@ } }, "node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-selection": { @@ -17583,23 +17658,181 @@ "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" }, "node_modules/d3-zoom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", - "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" } }, "node_modules/d3/node_modules/d3-shape": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", - "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, "node_modules/dashdash": { @@ -18021,9 +18254,12 @@ } }, "node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -21452,36 +21688,6 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -28937,9 +29143,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -29740,15 +29946,15 @@ } }, "node_modules/marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/martinez-polygon-clipping": { @@ -35900,6 +36106,11 @@ "rimraf": "bin.js" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -36645,9 +36856,9 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/shiki": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.15.tgz", - "integrity": "sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", + "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", "dev": true, "dependencies": { "jsonc-parser": "^3.0.0", @@ -38572,19 +38783,16 @@ } }, "node_modules/typedoc": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.5.tgz", - "integrity": "sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw==", + "version": "0.22.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.15.tgz", + "integrity": "sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==", "dev": true, "dependencies": { - "glob": "^7.1.7", - "handlebars": "^4.7.7", + "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^2.1.1", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shiki": "^0.9.3", - "typedoc-default-themes": "^0.12.10" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" }, "bin": { "typedoc": "bin/typedoc" @@ -38593,16 +38801,28 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" } }, - "node_modules/typedoc-default-themes": { - "version": "0.12.10", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", - "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">= 8" + "node": ">=10" } }, "node_modules/typescript": { @@ -39615,9 +39835,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -39691,9 +39911,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -39724,7 +39944,7 @@ "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", + "webpack-dev-middleware": "^5.3.4", "ws": "^8.13.0" }, "bin": { @@ -40201,12 +40421,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 9ac61d243..3a2f8b742 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -186,14 +186,14 @@ "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" }, "scripts": { "postinstall": "patch-package", From 17207624a9e20d1d5d77ff0916f8176d9eca65f5 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 15:46:41 -0700 Subject: [PATCH 081/102] add more intl-friendly tab nav shortcuts (#1814) --- src/KeyboardShortcuts.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 023b7078e..5ce91b8f2 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -67,8 +67,8 @@ const bindings: Record = { [KeyboardAction.NEW_ITEM]: ["Alt+N I"], [KeyboardAction.DELETE_ITEM]: ["Alt+D"], [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], - [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["], - [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], }; From f4bcee54612b4821176609fec170e135d03e4ec7 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 15:47:04 -0700 Subject: [PATCH 082/102] initialize new documents with their partition key (#1815) * initialize new documents with their partition key * refmt --- src/Explorer/Tabs/DocumentsTab.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 1d13237d0..c79f7eb4f 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -463,7 +463,22 @@ export default class DocumentsTab extends TabsBase { private initializeNewDocument = (): void => { this.selectedDocumentId(null); - const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); + const newDocument: any = { + id: "replace_with_new_document_id", + }; + this.partitionKeyProperties.forEach((partitionKeyProperty) => { + let target = newDocument; + const keySegments = partitionKeyProperty.split("."); + const finalSegment = keySegments.pop(); + + // Initialize nested objects as needed + keySegments.forEach((segment) => { + target = target[segment] = target[segment] || {}; + }); + + target[finalSegment] = "replace_with_new_partition_key_value"; + }); + const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); From afc82845b5a5eb5fe4367400f6423f1a5a3c60d2 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 24 Apr 2024 15:04:01 -0400 Subject: [PATCH 083/102] activate Token Controller (#1820) Co-authored-by: Asier Isayas --- src/Utils/EndpointUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index e4c036d75..97e733b98 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -154,7 +154,11 @@ export const allowedNotebookServerUrls: ReadonlyArray = []; export function useNewPortalBackendEndpoint(backendApi: string): boolean { // This maps backend APIs to the environments supported by the new backend. const newBackendApiEnvironmentMap: { [key: string]: string[] } = { - [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], + [BackendApi.GenerateToken]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], [BackendApi.PortalSettings]: [ PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac, From 618c5ec0fee67545eb53bc471cb45449972b4ff2 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 24 Apr 2024 15:11:51 -0700 Subject: [PATCH 084/102] Add button (and keyboard shortcut) to download query (#1817) --- .../Tabs/QueryTab/QueryTabComponent.tsx | 46 +++++++++++++++---- src/KeyboardShortcuts.tsx | 2 + 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 07f0a9eba..d5f4bf697 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -22,6 +22,7 @@ import "react-splitter-layout/lib/index.css"; import { format } from "react-string-format"; import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; +import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; @@ -225,6 +226,20 @@ export default class QueryTabComponent extends React.Component { + const text = this.getCurrentEditorQuery(); + const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" }); + + // It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it + const blobUrl = URL.createObjectURL(queryFile); + const anchor = document.createElement("a"); + anchor.href = blobUrl; + anchor.download = queryFile.name; + document.body.appendChild(anchor); // Must put the anchor in the document. + anchor.click(); + document.body.removeChild(anchor); // Clean up the anchor. + }; + public onSaveQueryClick = (): void => { useSidePanel.getState().openSidePanel("Save Query", ); }; @@ -405,15 +420,28 @@ export default class QueryTabComponent extends React.Component 0; + useCommandBar.getState().setContextButtons(this.getTabsButtons()); } diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 5ce91b8f2..98f988038 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -29,6 +29,7 @@ export enum KeyboardAction { EXECUTE_ITEM = "EXECUTE_ITEM", CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD", SAVE_ITEM = "SAVE_ITEM", + DOWNLOAD_ITEM = "DOWNLOAD_ITEM", OPEN_QUERY = "OPEN_QUERY", OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK", NEW_SPROC = "NEW_SPROC", @@ -57,6 +58,7 @@ const bindings: Record = { [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"], [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], + [KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], [KeyboardAction.NEW_SPROC]: ["Alt+N P"], From cbd5e6bf761b5469c63712ec6a451122e3dddcc1 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Fri, 26 Apr 2024 14:55:47 -0400 Subject: [PATCH 085/102] open Legacy Mongo SHell with correct base URL in sovereign clouds (#1823) Co-authored-by: Asier Isayas --- .../Tabs/MongoShellTab/MongoShellTabComponent.tsx | 4 ++-- src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index d3720cf41..052c81ce6 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -35,7 +35,7 @@ export interface IMongoShellTabAccessor { } export interface IMongoShellTabComponentStates { - url: URL; + url: string; } export interface IMongoShellTabComponentProps { @@ -221,7 +221,7 @@ export default class MongoShellTabComponent extends Component< name="explorer" className="iframe" style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }} - src={this.state.url.toString()} + src={this.state.url} id={this.props.tabsBaseInstance.tabId} onLoad={(event) => this.setContentFocus(event)} title="Mongo Shell" diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index 0ecbcb83e..a3b49b373 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -1,15 +1,11 @@ -import { configContext } from "ConfigContext"; import { userContext } from "../../../UserContext"; -export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { +export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string { const { databaseAccount: account } = userContext; const resourceId = account?.id; const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - const path: string = useMongoProxyEndpoint - ? `/mongoshell/index.html?${queryString}` - : `/mongoshell/indexv2.html?${queryString}`; - return new URL(path, configContext.hostedExplorerURL); + return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`; } From f8f7ea34bdd156c8e7fd04c9945ae93652942d74 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:09:55 -0500 Subject: [PATCH 086/102] Copilot rewording (#1824) * Copilot rebranding to query advisor * fix the subquery link --- src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 4 ++-- src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx | 2 +- src/Explorer/QueryCopilot/QueryCopilotTab.tsx | 6 +++--- src/Explorer/SplashScreen/SplashScreen.tsx | 4 ++-- src/Explorer/Tabs/QueryTab/QueryResultSection.tsx | 10 +++++++--- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 6 +++--- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 1f0eda4f7..9b2412c4b 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -630,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ Enable sample database This is a sample database and collection with synthetic product data you can use to explore using - NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is + NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is created by, and maintained by Microsoft at no cost to you.
@@ -640,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ label: { padding: 0 }, }} className="padding" - ariaLabel="Enable sample db for Copilot" + ariaLabel="Enable sample db for Query Advisor" checked={copilotSampleDBEnabled} onChange={handleSampleDatabaseChange} /> diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 6aaebc926..6c54cd506 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC = ({ hasSmallHeadline={true} headline="Write a prompt" > - Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} + Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "} { setShowSamplePrompts(true); diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index 240881310..150f7ec5b 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query const toggleCopilotButton = { iconSrc: QueryCommandIcon, - iconAlt: "Copilot", + iconAlt: "Query Advisor", onCommandClick: () => { toggleCopilot(true); }, - commandButtonLabel: "Copilot", - ariaLabel: "Copilot", + commandButtonLabel: "Query Advisor", + ariaLabel: "Query Advisor", hasPopup: false, disabled: copilotActive, }; diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index aa7e7dcd9..f4ebb9cd0 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -151,9 +151,9 @@ export class SplashScreen extends React.Component { {useQueryCopilot.getState().copilotEnabled && ( { const copilotVersion = userContext.features.copilotVersion; diff --git a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx index fc93d98a1..30d2ed9c1 100644 --- a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx @@ -381,9 +381,13 @@ export const QueryResultSection: React.FC = ({ Error - We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. - - Please see Cosmos sub query documentation for further information + We detected you may be using a subquery. To learn more about subqueries effectively,{" "} + + visit the documentation
diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index d5f4bf697..532ce4662 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -483,13 +483,13 @@ export default class QueryTabComponent extends React.Component { this._toggleCopilot(!this.state.copilotActive); }, - commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", - ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", + commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", + ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", hasPopup: false, }; buttons.push(toggleCopilotButton); From b94ce28e96edd5b65896dec56a6baf833fba63cd Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:23:49 +0530 Subject: [PATCH 087/102] [accessibility-2724013]:[Screen reader - Cosmos DB - Data Explorer -> Entities -> Add entity]: Screen reader announces incorrect role when focus lands on the "Edit" and "Delete" buttons. (#1822) Co-authored-by: Satyapriya Bai --- src/Common/TableEntity.tsx | 4 ++-- .../GraphExplorerComponent/NodePropertiesComponent.tsx | 4 ++-- .../Graph/NewVertexComponent/NewVertexComponent.tsx | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Common/TableEntity.tsx b/src/Common/TableEntity.tsx index f3d0b5244..eece32ecb 100644 --- a/src/Common/TableEntity.tsx +++ b/src/Common/TableEntity.tsx @@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent = ({ editEntity = ({ delete entity - Delete + Delete ); } else { @@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component< aria-label="Edit properties" onActivated={expandClickHandler} > - Edit + Edit )} diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx index de357989a..6b20cfcb0 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx @@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent = ( className="rightPaneTrashIcon rightPaneBtns" tabIndex={0} role="button" + aria-label={`Delete ${data.key}`} onClick={(event: React.MouseEvent) => removeNewVertexProperty(event, index)} onKeyPress={(event: React.KeyboardEvent) => removeNewVertexPropertyKeyPress(event, index) } > - Remove property + Remove property
From a08415e7bcf9683cee3e37459005972a5693b49a Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:26:27 +0530 Subject: [PATCH 088/102] [3100018:[Programmatic Access - Azure Cosmos DB - Edit Property]: Text Area edit field does not have a label under 'Edit Property' pane. (#1819) Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/Tables/AddTableEntityPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 9ad999aa0..7d73ccc1f 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent = { entityChange(newInput, selectedRow, "value"); From 92246144f710b2dcdc4e9b8613c54344aca7f753 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 29 Apr 2024 16:25:58 -0400 Subject: [PATCH 089/102] Enable Legacy Mongo Shell in Fairfax (#1829) * enable Mongo Proxy and LMS in sovereign clouds * remove mooncake --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 1 + src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 907b0305e..d9aa0fb4c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -677,6 +677,7 @@ export function useMongoProxyEndpoint(api: string): boolean { MongoProxyEndpoints.Local, MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, ]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; if ( diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts index 0c138ff61..8b16816ab 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts @@ -3,7 +3,6 @@ import { updateUserContext, userContext } from "../../../UserContext"; import { getMongoShellUrl } from "./getMongoShellUrl"; const mongoBackendEndpoint = "https://localhost:1234"; -const hostedExplorerURL = "https://cosmos.azure.com/"; describe("getMongoShellUrl", () => { let queryString = ""; @@ -13,7 +12,6 @@ describe("getMongoShellUrl", () => { updateConfigContext({ BACKEND_ENDPOINT: mongoBackendEndpoint, - hostedExplorerURL: hostedExplorerURL, platform: Platform.Hosted, }); From b023250e67e834ee8485670a1605973d0db88936 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:46:24 -0700 Subject: [PATCH 090/102] First set of changes for Notebooks removal. (#1816) * First set of changes for Notebooks removal. * Fix unit test snapshots. --- src/Contracts/ActionContracts.ts | 5 - .../SettingsComponent.test.tsx.snap | 16 - src/Explorer/Explorer.tsx | 612 +----------------- src/Explorer/Notebook/NotebookManager.tsx | 27 +- src/Explorer/OpenActions/OpenActions.tsx | 12 - .../CopyNotebookPane/CopyNotebookPane.tsx | 154 ----- .../CopyNotebookPaneComponent.tsx | 120 ---- .../GitHubReposPanel.test.tsx.snap | 4 - .../StringInputPane.test.tsx.snap | 4 - .../QueryCopilotTab.test.tsx.snap | 4 - src/Explorer/SplashScreen/SplashScreen.tsx | 28 +- src/Explorer/Tabs/NotebookV2Tab.ts | 336 +--------- src/Explorer/Tree/ResourceTree.tsx | 432 +------------ src/Explorer/Tree/ResourceTreeAdapter.tsx | 497 +------------- src/Utils/GalleryUtils.ts | 2 - 15 files changed, 11 insertions(+), 2242 deletions(-) delete mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx delete mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index f8fc956e6..cf4b66ed6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -68,10 +68,6 @@ export interface OpenPane extends DataExplorerAction { paneKind: PaneKind | string; } -export interface OpenSampleNotebook extends DataExplorerAction { - path: string; -} - /** * The types of actions that the DataExplorer supports performing upon opening. */ @@ -80,5 +76,4 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, - OpenSampleNotebook, } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ab7abac11..b08834dd6 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -29,8 +29,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -47,10 +45,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -107,8 +103,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -125,10 +119,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -224,8 +216,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -242,10 +232,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -271,8 +259,6 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -289,10 +275,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..368168a4a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; @@ -16,7 +15,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; @@ -32,34 +31,23 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants" import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; -import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; -import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import * as FileSystemUtil from "./Notebook/FileSystemUtil"; -import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; -import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; +import { NotebookContentItem } from "./Notebook/NotebookContentItem"; import type NotebookManager from "./Notebook/NotebookManager"; -import { NotebookPaneContent } from "./Notebook/NotebookManager"; -import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; -import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -87,7 +75,6 @@ export default class Explorer { // Notebooks public notebookManager?: NotebookManager; - private _isInitializingNotebooks: boolean; private notebookToImport: { name: string; content: string; @@ -99,7 +86,6 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); - this._isInitializingNotebooks = false; this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); useNotebook.subscribe( @@ -205,12 +191,10 @@ export default class Explorer { container: this, resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList(), }); } this.refreshCommandBarButtons(); - this.refreshNotebookList(); } public openEnableSynapseLinkDialog(): void { @@ -373,7 +357,6 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); - this.refreshNotebookList(); }; // Facade @@ -381,19 +364,6 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - this.refreshNotebookList(); - this._isInitializingNotebooks = false; - } - public async allocateContainer(poolId: PoolIdType, mode?: string): Promise { const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const notebookServerInfo = shouldUseNotebookStates @@ -472,8 +442,6 @@ export default class Explorer { ? useNotebook.getState().setIsAllocating(false) : useQueryCopilot.getState().setIsAllocatingContainer(false); this.refreshCommandBarButtons(); - this.refreshNotebookList(); - this._isInitializingNotebooks = false; } } } @@ -510,104 +478,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -696,406 +566,6 @@ export default class Explorer { } } - public uploadFile( - name: string, - content: string, - parent: NotebookContentItem, - isGithubTree?: boolean, - ): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to upload notebook, but notebook is not enabled"; - handleError(error, "Explorer/uploadFile"); - throw new Error(error); - } - - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); - promise - .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); - return promise; - } - - public async importAndOpen(path: string): Promise { - const name = NotebookUtil.getName(path); - const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const content = await this.readFile(item); - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - return Promise.resolve(false); - } - - public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { - this.notebookToImport = undefined; // we don't want to try opening this notebook again - } - - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - this.notebookToImport = { name, content }; // we'll try opening this notebook later on - return Promise.resolve(false); - } - - public async publishNotebook( - name: string, - content: NotebookPaneContent, - notebookContentRef?: string, - onTakeSnapshot?: (request: SnapshotRequest) => void, - onClosePanel?: () => void, - ): Promise { - if (this.notebookManager) { - await this.notebookManager.openPublishNotebookPane( - name, - content, - notebookContentRef, - onTakeSnapshot, - onClosePanel, - ); - } - } - - public copyNotebook(name: string, content: string): void { - this.notebookManager?.openCopyNotebookPane(name, content); - } - - /** - * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. - * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. - * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder - * will not fetch its content if the children array exists (and has only one child which was manually created). - * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. - * - * @param name - * @param path - */ - public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { - return NotebookUtil.createNotebookContentItem(name, path, "file"); - } - - public async openNotebook(notebookContentItem: NotebookContentItem): Promise { - if (!notebookContentItem || !notebookContentItem.path) { - throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); - } - if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - const notebookTabs = useTabs - .getState() - .getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab) => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path), - ) as NotebookV2Tab[]; - let notebookTab = notebookTabs && notebookTabs[0]; - - if (notebookTab) { - useTabs.getState().activateTab(notebookTab); - } else { - const options: NotebookTabOptions = { - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookV2, - node: undefined, - title: notebookContentItem.name, - tabPath: notebookContentItem.path, - collection: undefined, - masterKey: userContext.masterKey || "", - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: undefined, - container: this, - notebookContentItem, - }; - - try { - const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); - notebookTab = new NotebookTabV2.default(options); - useTabs.getState().activateNewTab(notebookTab); - } catch (reason) { - console.error("Import NotebookV2Tab failed!", reason); - return false; - } - } - - return true; - } - - public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to rename notebook, but notebook is not enabled"; - handleError(error, "Explorer/renameNotebook"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); - } else { - useSidePanel.getState().openSidePanel( - "Rename Notebook", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - inputLabel="Enter new notebook name" - submitButtonLabel="Rename" - errorMessage="Could not rename notebook" - inProgressMessage="Renaming notebook to" - successMessage="Renamed notebook to" - paneTitle="Rename Notebook" - defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) - } - notebookFile={notebookFile} - />, - ); - } - } - - public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create notebook directory, but notebook is not enabled"; - handleError(error, "Explorer/onCreateDirectory"); - throw new Error(error); - } - - useSidePanel.getState().openSidePanel( - "Create new directory", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - errorMessage="Could not create directory " - inProgressMessage="Creating directory " - successMessage="Created directory " - inputLabel="Enter new directory name" - paneTitle="Create new directory" - submitButtonLabel="Create" - defaultInput="" - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) - } - notebookFile={parent} - />, - ); - } - - public readFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to read file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); - } - - public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to download file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( - (content: string) => { - const blob = stringToBlob(content, "text/plain"); - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob(blob, notebookFile.name); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.target = "_self"; - downloadLink.download = notebookFile.name; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - - clearMessage(); - }, - (error) => { - logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); - clearMessage(); - }, - ); - } - - private refreshNotebookList = async (): Promise => { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - return; - } - - await this.resourceTree.initialize(); - await useNotebook.getState().initializeNotebooksTree(this.notebookManager); - - this.notebookManager?.refreshPinnedRepos(); - if (this.notebookToImport) { - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - }; - - public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to delete notebook file, but notebook is not enabled"; - handleError(error, "Explorer/deleteNotebookFile"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); - return Promise.reject(); - } - - if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { - useDialog.getState().openDialog({ - isModal: true, - title: "Unable to delete file", - subText: "Directory is not empty.", - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => useDialog.getState().closeDialog(), - onSecondaryButtonClick: undefined, - }); - return Promise.reject(); - } - - return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( - () => logConsoleInfo(`Successfully deleted: ${item.path}`), - (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`), - ); - } - - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1252,32 +722,6 @@ export default class Explorer { } } - public async handleOpenFileAction(path: string): Promise { - if (useNotebook.getState().isPhoenixNotebooks === undefined) { - await useNotebook.getState().getPhoenixStatus(); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb - // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly - // calling GitHub. For now convert this url to a raw url and download content. - const gitHubInfo = fromContentUri(path); - if (gitHubInfo) { - const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); - const response = await fetch(rawUrl); - if (response.status === Constants.HttpStatusCodes.OK) { - this.notebookToImport = { - name: NotebookUtil.getName(path), - content: await response.text(), - }; - - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - } - } - public openUploadItemsPanePane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } @@ -1287,54 +731,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - - public getDownloadModalConent(fileName: string): JSX.Element { - if (useNotebook.getState().isPhoenixNotebooks) { - return ( - <> -

{Notebook.galleryNotebookDownloadContent1}

-
-

- {Notebook.galleryNotebookDownloadContent2} - - {Notebook.learnMore} - -

- - ); - } - return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; - } - public async refreshExplorer(): Promise { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken @@ -1359,10 +755,6 @@ export default class Explorer { dataExplorerArea: Constants.Areas.Notebook, }); - if (useNotebook.getState().isPhoenixNotebooks) { - await this.initNotebooks(userContext.databaseAccount); - } - await this.refreshSampleData(); } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 3ccbefcaf..45afe6061 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -12,15 +12,13 @@ import * as Logger from "../../Common/Logger"; import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; -import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; @@ -40,7 +38,6 @@ export interface NotebookManagerOptions { container: Explorer; resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; - refreshNotebookList: () => void; } export default class NotebookManager { @@ -81,10 +78,6 @@ export default class NotebookManager { contents.JupyterContentProvider, ); - this.notebookClient = new NotebookContainerClient(() => - this.params.container.initNotebooks(userContext?.databaseAccount), - ); - this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider); this.gitHubOAuthService.getTokenObservable().subscribe((token) => { @@ -106,11 +99,9 @@ export default class NotebookManager { } this.params.refreshCommandBarButtons(); - this.params.refreshNotebookList(); }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { - this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); @@ -149,22 +140,6 @@ export default class NotebookManager { ); } - public openCopyNotebookPane(name: string, content: string): void { - const { container } = this.params; - useSidePanel - .getState() - .openSidePanel( - "Copy Notebook", - , - ); - } - // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index f3ef288c8..e2059cdba 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -195,17 +195,5 @@ export function handleOpenAction( return true; } - if ( - action.actionType === ActionContracts.ActionType.OpenSampleNotebook || - action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] - ) { - openFile(action as ActionContracts.OpenSampleNotebook, explorer); - return true; - } - return false; } - -function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { - explorer.handleOpenFileAction(decodeURIComponent(action.path)); -} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx deleted file mode 100644 index 0f7927b3b..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { IDropdownOption } from "@fluentui/react"; -import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; -import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants"; -import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; -import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; -import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; -import { useSidePanel } from "../../../hooks/useSidePanel"; -import Explorer from "../../Explorer"; -import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; -import { useNotebook } from "../../Notebook/useNotebook"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; -import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} -export interface CopyNotebookPanelProps { - name: string; - content: string; - container: Explorer; - junoClient: JunoClient; - gitHubOAuthService: GitHubOAuthService; -} - -export const CopyNotebookPane: FunctionComponent = ({ - name, - content, - container, - junoClient, - gitHubOAuthService, -}: CopyNotebookPanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const [isExecuting, setIsExecuting] = useState(); - const [formError, setFormError] = useState(""); - const [pinnedRepos, setPinnedRepos] = useState(); - const [selectedLocation, setSelectedLocation] = useState(); - - useEffect(() => { - open(); - }, []); - - const open = async (): Promise => { - if (gitHubOAuthService.isLoggedIn()) { - const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); - } - - if (response.data?.length > 0) { - setPinnedRepos(response.data); - } - } - }; - - const submit = async (): Promise => { - let destination: string = selectedLocation?.type; - let clearMessage: () => void; - setIsExecuting(true); - - try { - if (!selectedLocation) { - throw new Error(`No location selected`); - } - - if (selectedLocation.type === "GitHub") { - destination = `${destination} - ${GitHubUtils.toRepoFullName( - selectedLocation.owner, - selectedLocation.repo, - )} - ${selectedLocation.branch}`; - } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { - destination = useNotebook.getState().notebookFolderName; - } - - clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); - - const notebookContentItem = await copyNotebook(selectedLocation); - if (!notebookContentItem) { - throw new Error(`Failed to upload ${name}`); - } - - NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); - closeSidePanel(); - } catch (error) { - const errorMessage = getErrorMessage(error); - setFormError(`Failed to copy ${name} to ${destination}`); - handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); - } finally { - clearMessage && clearMessage(); - setIsExecuting(false); - } - }; - - const copyNotebook = async (location: Location): Promise => { - let parent: NotebookContentItem; - let isGithubTree: boolean; - switch (location.type) { - case "MyNotebooks": - parent = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - isGithubTree = false; - if (useNotebook.getState().isPhoenixNotebooks) { - await container.allocateContainer(PoolIdType.DefaultPoolId); - } - break; - - case "GitHub": - parent = { - name: selectedLocation.branch, - path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), - type: NotebookContentItemType.Directory, - }; - isGithubTree = true; - break; - - default: - throw new Error(`Unsupported location type ${location.type}`); - } - - return container.uploadFile(name, content, parent, isGithubTree); - }; - - const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { - setSelectedLocation(option?.data); - }; - - const props: RightPaneFormProps = { - formError, - isExecuting: isExecuting, - submitButtonText: "OK", - onSubmit: () => submit(), - }; - - const copyNotebookPaneProps: CopyNotebookPaneProps = { - name, - pinnedRepos, - onDropDownChange: onDropDownChange, - }; - - return ( - - - - ); -}; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx deleted file mode 100644 index 5cd0cfdc1..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { - Dropdown, - IDropdownOption, - IDropdownProps, - IRenderFunction, - ISelectableOption, - Label, - SelectableOptionMenuItemType, - Stack, - Text, -} from "@fluentui/react"; -import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; -import React, { FormEvent, FunctionComponent } from "react"; -import { IPinnedRepo } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { useNotebook } from "../../Notebook/useNotebook"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} - -export interface CopyNotebookPaneProps { - name: string; - pinnedRepos: IPinnedRepo[]; - onDropDownChange: (_: FormEvent, option?: IDropdownOption) => void; -} - -export const CopyNotebookPaneComponent: FunctionComponent = ({ - name, - pinnedRepos, - onDropDownChange, -}: CopyNotebookPaneProps) => { - const BranchNameWhiteSpace = " "; - - const onRenderDropDownTitle: IRenderFunction = (options: IDropdownOption[]): JSX.Element => { - return {options.length && options[0].title}; - }; - - const onRenderDropDownOption: IRenderFunction = (option: ISelectableOption): JSX.Element => { - return {option.text}; - }; - - const getDropDownOptions = (): IDropdownOption[] => { - const options: IDropdownOption[] = []; - options.push({ - key: "MyNotebooks-Item", - text: useNotebook.getState().notebookFolderName, - title: useNotebook.getState().notebookFolderName, - data: { - type: "MyNotebooks", - } as Location, - }); - - if (pinnedRepos && pinnedRepos.length > 0) { - options.push({ - key: "GitHub-Header-Divider", - text: undefined, - itemType: SelectableOptionMenuItemType.Divider, - }); - - options.push({ - key: "GitHub-Header", - text: GitHubReposTitle, - itemType: SelectableOptionMenuItemType.Header, - }); - - pinnedRepos.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - options.push({ - key: `GitHub-Repo-${repoFullName}`, - text: repoFullName, - disabled: true, - }); - - pinnedRepo.branches.forEach((branch) => - options.push({ - key: `GitHub-Repo-${repoFullName}-${branch.name}`, - text: `${BranchNameWhiteSpace}${branch.name}`, - title: `${repoFullName} - ${branch.name}`, - data: { - type: "GitHub", - owner: pinnedRepo.owner, - repo: pinnedRepo.name, - branch: branch.name, - } as Location, - }), - ); - }); - } - - return options; - }; - const dropDownProps: IDropdownProps = { - label: "Location", - ariaLabel: "Location", - placeholder: "Select an option", - onRenderTitle: onRenderDropDownTitle, - onRenderOption: onRenderDropDownOption, - options: getDropDownOptions(), - onChange: onDropDownChange, - }; - - return ( -
- - - - {name} - - - - -
- ); -}; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..aa2137c42 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -17,8 +17,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` addRepoProps={ Object { "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -35,10 +33,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..566808f4b 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -7,8 +7,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` errorMessage="Could not create directory " explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -25,10 +23,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..6d875fe2e 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -22,8 +22,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = databaseId="CopilotSampleDb" explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -40,10 +38,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index f4ebb9cd0..e9477e088 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,11 +25,9 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import * as Constants from "../../Common/Constants"; import { userContext } from "../../UserContext"; @@ -410,14 +408,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -493,28 +483,12 @@ export class SplashScreen extends React.Component { }; } - private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { - return { - info: path, - iconSrc: NotebookIcon, - title: name, - description: "Notebook", - onClick: () => { - const notebookItem = this.container.createNotebookContentItemFile(name, path); - notebookItem && this.container.openNotebook(notebookItem); - }, - }; - } - private createRecentItems(): SplashScreenItem[] { return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { switch (activity.type) { default: { - const unknownActivity: never = activity; - throw new Error(`Unknown activity: ${unknownActivity}`); + throw new Error(`Unknown activity: ${activity}`); } - case MostRecentActivity.Type.OpenNotebook: - return this.decorateOpenNotebookActivity(activity); case MostRecentActivity.Type.OpenCollection: return this.decorateOpenCollectionActivity(activity); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 92e0e6958..fadb43258 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,31 +1,11 @@ -import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; -import { userContext } from "UserContext"; -import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import CutIcon from "../../../images/notebook/Notebook-cut.svg"; -import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; -import PasteIcon from "../../../images/notebook/Notebook-paste.svg"; -import RestartIcon from "../../../images/notebook/Notebook-restart.svg"; -import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; -import RunIcon from "../../../images/notebook/Notebook-run.svg"; -import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "../Controls/Dialog"; -import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; -import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; -import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; -import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; @@ -90,275 +70,7 @@ export default class NotebookTabV2 extends NotebookTabBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); - const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted(); - - const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined; - - const saveLabel = "Save"; - const copyToLabel = "Copy to ..."; - const publishLabel = "Publish to gallery"; - const kernelLabel = "No Kernel"; - const runLabel = "Run"; - const runActiveCellLabel = "Run Active Cell"; - const runAllLabel = "Run All"; - const interruptKernelLabel = "Interrupt Kernel"; - const killKernelLabel = "Halt Kernel"; - const restartKernelLabel = "Restart Kernel"; - const clearLabel = "Clear outputs"; - const newCellLabel = "New Cell"; - const cellTypeLabel = "Cell Type"; - const codeLabel = "Code"; - const markdownLabel = "Markdown"; - const rawLabel = "Raw"; - const copyLabel = "Copy"; - const cutLabel = "Cut"; - const pasteLabel = "Paste"; - const cellCodeType = "code"; - const cellMarkdownType = "markdown"; - const cellRawType = "raw"; - - const saveButtonChildren = []; - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - saveButtonChildren.push({ - iconName: copyToLabel, - onCommandClick: () => this.copyNotebook(), - commandButtonLabel: copyToLabel, - hasPopup: false, - disabled: false, - ariaLabel: copyToLabel, - }); - } - - if (userContext.features.publicGallery) { - saveButtonChildren.push({ - iconName: "PublishContent", - onCommandClick: async () => await this.publishToGallery(), - commandButtonLabel: publishLabel, - hasPopup: false, - disabled: false, - ariaLabel: publishLabel, - }); - } - - let buttons: CommandButtonComponentProps[] = [ - { - iconSrc: SaveIcon, - iconAlt: saveLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - children: saveButtonChildren.length && [ - { - iconName: "Save", - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - }, - ...saveButtonChildren, - ], - }, - { - iconSrc: null, - iconAlt: kernelLabel, - onCommandClick: () => {}, - commandButtonLabel: null, - hasPopup: false, - disabled: availableKernels.length < 1, - isDropdown: true, - dropdownPlaceholder: kernelLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName, - dropdownWidth: 100, - children: availableKernels.map( - (kernel: KernelSpecsDisplay) => - ({ - iconSrc: null, - iconAlt: kernel.displayName, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name), - commandButtonLabel: kernel.displayName, - dropdownItemKey: kernel.name, - hasPopup: false, - disabled: false, - ariaLabel: kernel.displayName, - }) as CommandButtonComponentProps, - ), - ariaLabel: kernelLabel, - }, - { - iconSrc: RunIcon, - iconAlt: runLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runLabel, - tooltipText: runBtnTooltip, - ariaLabel: runLabel, - hasPopup: false, - disabled: isNotebookUntrusted, - children: [ - { - iconSrc: RunIcon, - iconAlt: runActiveCellLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runActiveCellLabel, - hasPopup: false, - disabled: false, - ariaLabel: runActiveCellLabel, - }, - { - iconSrc: RunAllIcon, - iconAlt: runAllLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAll(); - this.traceTelemetry(Action.ExecuteAllCells); - }, - commandButtonLabel: runAllLabel, - hasPopup: false, - disabled: false, - ariaLabel: runAllLabel, - }, - { - iconSrc: InterruptKernelIcon, - iconAlt: interruptKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(), - commandButtonLabel: interruptKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: interruptKernelLabel, - }, - { - iconSrc: KillKernelIcon, - iconAlt: killKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(), - commandButtonLabel: killKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: killKernelLabel, - }, - { - iconSrc: RestartIcon, - iconAlt: restartKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(), - commandButtonLabel: restartKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: restartKernelLabel, - }, - ], - }, - { - iconSrc: ClearAllOutputsIcon, - iconAlt: clearLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(), - commandButtonLabel: clearLabel, - hasPopup: false, - disabled: false, - ariaLabel: clearLabel, - }, - { - iconSrc: NewCellIcon, - iconAlt: newCellLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(), - commandButtonLabel: newCellLabel, - ariaLabel: newCellLabel, - hasPopup: false, - disabled: false, - }, - CommandBarComponentButtonFactory.createDivider(), - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => {}, - commandButtonLabel: null, - ariaLabel: cellTypeLabel, - hasPopup: false, - disabled: false, - isDropdown: true, - dropdownPlaceholder: cellTypeLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(), - dropdownWidth: 110, - children: [ - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType), - commandButtonLabel: codeLabel, - ariaLabel: codeLabel, - dropdownItemKey: cellCodeType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType), - commandButtonLabel: markdownLabel, - ariaLabel: markdownLabel, - dropdownItemKey: cellMarkdownType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType), - commandButtonLabel: rawLabel, - ariaLabel: rawLabel, - dropdownItemKey: cellRawType, - hasPopup: false, - disabled: false, - }, - ], - }, - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - children: [ - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: CutIcon, - iconAlt: cutLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookCut(), - commandButtonLabel: cutLabel, - ariaLabel: cutLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: PasteIcon, - iconAlt: pasteLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookPaste(), - commandButtonLabel: pasteLabel, - ariaLabel: pasteLabel, - hasPopup: false, - disabled: false, - }, - ], - }, - // TODO: Uncomment when undo/redo is reimplemented in nteract - ]; - return buttons; + return []; } protected buildCommandBarOptions(): void { @@ -382,50 +94,4 @@ export default class NotebookTabV2 extends NotebookTabBase { sparkClusterConnectionInfo, ); } - - private publishToGallery = async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.CommandBarMenu, - }); - - const notebookReduxStore = NotebookTabV2.clientManager.getStore(); - const unsubscribe = notebookReduxStore.subscribe(() => { - const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb; - useNotebookSnapshotStore.setState({ - snapshot: cdbState.notebookSnapshot?.imageSrc, - error: cdbState.notebookSnapshotError, - }); - }); - - const notebookContent = this.notebookComponentAdapter.getContent(); - const notebookContentRef = this.notebookComponentAdapter.contentRef; - const onPanelClose = (): void => { - unsubscribe(); - useNotebookSnapshotStore.setState({ - snapshot: undefined, - error: undefined, - }); - notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined)); - }; - - await this.container.publishNotebook( - notebookContent.name, - notebookContent.content, - notebookContentRef, - (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)), - onPanelClose, - ); - }; - - private copyNotebook = () => { - const notebookContent = this.notebookComponentAdapter.getContent(); - let content: string; - if (typeof notebookContent.content === "string") { - content = notebookContent.content; - } else { - content = stringifyNotebook(toJS(notebookContent.content)); - } - - this.container.copyNotebook(notebookContent.name, content); - }; } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5f759534..0933eac7b 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,42 +1,23 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -45,391 +26,21 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; -export const MyNotebooksTitle = "My Notebooks"; -export const GitHubReposTitle = "GitHub repos"; - interface ResourceTreeProps { container: Explorer; } export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { const databases = useDatabases((state) => state.databases); - const { - isNotebookEnabled, - myNotebooksContentRoot, - galleryContentRoot, - gitHubNotebooksContentRoot, - updateNotebookItem, - } = useNotebook( + const { isNotebookEnabled } = useNotebook( (state) => ({ isNotebookEnabled: state.isNotebookEnabled, - myNotebooksContentRoot: state.myNotebooksContentRoot, - galleryContentRoot: state.galleryContentRoot, - gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, - updateNotebookItem: state.updateNotebookItem, }), shallow, ); - const { activeTab, refreshActiveTab } = useTabs(); + const { refreshActiveTab } = useTabs(); const showScriptNodes = configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); - const pseudoDirPath = "PsuedoDir"; - - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - - const buildChildNodes = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode[] => { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) - : buildNotebookFileNode(item, onFileClick, isGithubTree); - result.timestamp = item.timestamp; - return result; - }); - } - }; - - const buildNotebookFileNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu(container, item, isGithubTree), - data: item, - }; - }; - - const createFileContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => copyNotebook(container, item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await container.readFile(item); - if (content) { - await container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - }; - - const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { - const content = await container.readFile(item); - if (content) { - container.copyNotebook(item.name, content); - } - }; - - const createDirectoryContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => loadSubitems(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item, isGithubTree), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - }; - - const buildNotebookDirectoryNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - loadSubitems(item, isGithubTree); - } - }, - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, - data: item, - children: buildChildNodes(item, onFileClick, isGithubTree), - }; - }; const buildDataTree = (): TreeNode => { const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { @@ -757,11 +368,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { - const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem, isGithubTree); - }; - const dataRootNode = buildDataTree(); const isSampleDataEnabled = useQueryCopilot().copilotEnabled && @@ -775,46 +381,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc {!isNotebookEnabled && !isSampleDataEnabled && ( )} - {isNotebookEnabled && !isSampleDataEnabled && ( - <> - - - - - - - {/* {buildGalleryCallout()} */} - - )} {!isNotebookEnabled && isSampleDataEnabled && ( <> - + - - {/* {buildGalleryCallout()} */} - - )} - {isNotebookEnabled && isSampleDataEnabled && ( - <> - - - - - - - - - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b22f0f916..67fb54773 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,42 +1,21 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -46,19 +25,8 @@ import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { - public static readonly MyNotebooksTitle = "My Notebooks"; - public static readonly GitHubReposTitle = "GitHub repos"; - - private static readonly DataTitle = "DATA"; - private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PsuedoDir"; - public parameters: ko.Observable; - public galleryContentRoot: NotebookContentItem; - public myNotebooksContentRoot: NotebookContentItem; - public gitHubNotebooksContentRoot: NotebookContentItem; - public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -76,111 +44,9 @@ export class ResourceTreeAdapter implements ReactAdapter { this.triggerRender(); } - private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.myNotebooksContentRoot; - if (myNotebooksTree.children) { - // Count 1st generation children (tree is lazy-loaded) - const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach((treeNode) => { - switch ((treeNode as NotebookContentItem).type) { - case NotebookContentItemType.File: - nodeCounts.files++; - break; - case NotebookContentItemType.Directory: - nodeCounts.directories++; - break; - case NotebookContentItemType.Notebook: - nodeCounts.notebooks++; - break; - default: - break; - } - }); - TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); - } - } - public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } - } - - public async initialize(): Promise { - const refreshTasks: Promise[] = []; - - this.galleryContentRoot = { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }; - this.myNotebooksContentRoot = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - - // Only if notebook server is available we can refresh - if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { - refreshTasks.push( - this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { - this.triggerRender(); - this.traceMyNotebookTreeInfo(); - }), - ); - } - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - - return Promise.all(refreshTasks); - } - - public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.gitHubNotebooksContentRoot) { - this.gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - children: [], - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - }); - }); - - this.gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - this.triggerRender(); - } + return ; } private buildDataTree(): TreeNode { @@ -504,365 +370,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - - private buildChildNodes( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode[] { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) - : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); - result.timestamp = item.timestamp; - return result; - }); - } - } - - private buildNotebookFileNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu && this.createFileContextMenu(item), - data: item, - }; - } - - private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => this.copyNotebook(item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => this.container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await this.container.readFile(item); - if (content) { - await this.container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - } - - private copyNotebook = async (item: NotebookContentItem) => { - const content = await this.container.readFile(item); - if (content) { - this.container.copyNotebook(item.name, content); - } - }; - - private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => this.container.onCreateDirectory(item), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - } - - private buildNotebookDirectoryNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - this.container.refreshContentItem(item).then(() => this.triggerRender()); - } - }, - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: - createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath - ? this.createDirectoryContextMenu(item) - : undefined, - data: item, - children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), - }; - } - public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index f11b6df5c..1314b2b3c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,7 +245,6 @@ export function downloadItem( }, "Cancel", undefined, - container.getDownloadModalConent(name), ); } export async function downloadNotebookItem( @@ -278,7 +277,6 @@ export async function downloadNotebookItem( metadata.untrusted = true; } - await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); From 81a5b7cb6d6011c9be922770581dcfe2021ee80d Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 30 Apr 2024 10:03:27 -0700 Subject: [PATCH 091/102] add shortcuts for the Items tab (#1827) * add shortcuts for the Items tab * Add shortcut to clear Items tab filter. --- src/Explorer/Tabs/DocumentsTab.html | 3 ++- src/Explorer/Tabs/DocumentsTab.ts | 33 ++++++++++++++++++++++++++++- src/Explorer/Tabs/TabsBase.ts | 2 ++ src/KeyboardShortcuts.tsx | 28 ++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 4283a661c..cfe1b2039 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -80,7 +80,8 @@ placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' }, css: { placeholderVisible: filterContent().length === 0 }, - textInput: filterContent" + textInput: filterContent, + event: { keydown: onFilterKeyDown }" /> { super.onActivate(); + this.setKeyboardActions({ + [KeyboardAction.SEARCH]: () => { + this.onShowFilterClick(); + return true; + }, + [KeyboardAction.CLEAR_SEARCH]: () => { + this.filterContent(""); + this.refreshDocumentsGrid(true); + return true; + }, + }); + if (!this._documentsIterator) { try { await this.autoPopulateContent(); diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 0425eac91..8b017f6bc 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -1,3 +1,4 @@ +import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as ThemeUtility from "../../Common/ThemeUtility"; @@ -107,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onActivate(): void { + clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); this.updateSelectedNode(); this.collection?.selectedSubnodeKind(this.tabKind); this.database?.selectedSubnodeKind(this.tabKind); diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 98f988038..2041662ee 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -17,8 +17,17 @@ export type KeyboardHandlerMap = Partial = { [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], + [KeyboardAction.SEARCH]: ["$mod+Shift+F"], + [KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"], }; interface KeyboardShortcutState { @@ -91,13 +104,24 @@ interface KeyboardShortcutState { setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; } +export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void; + /** * Defines the calling component as the manager of the keyboard actions for the given group. * @param group The group of keyboard actions to manage. * @returns A function that can be used to set the keyboard action handlers for the given group. */ -export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => - useKeyboardActionHandlers.getState().setHandlers(group, handlers); +export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter = + (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => + useKeyboardActionHandlers.getState().setHandlers(group, handlers); + +/** + * Clears the keyboard action handlers for the given group. + * @param group The group of keyboard actions to clear. + */ +export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => { + useKeyboardActionHandlers.getState().setHandlers(group, {}); +}; const useKeyboardActionHandlers: UseStore = create((set, get) => ({ allHandlers: {}, From 298197b1b80e07282c93d0ee2b3608848d0c3af2 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Wed, 1 May 2024 07:21:50 -0700 Subject: [PATCH 092/102] Revert "First set of changes for Notebooks removal. (#1816)" (#1830) This reverts commit b023250e67e834ee8485670a1605973d0db88936. --- src/Contracts/ActionContracts.ts | 5 + .../SettingsComponent.test.tsx.snap | 16 + src/Explorer/Explorer.tsx | 612 +++++++++++++++++- src/Explorer/Notebook/NotebookManager.tsx | 27 +- src/Explorer/OpenActions/OpenActions.tsx | 12 + .../CopyNotebookPane/CopyNotebookPane.tsx | 154 +++++ .../CopyNotebookPaneComponent.tsx | 120 ++++ .../GitHubReposPanel.test.tsx.snap | 4 + .../StringInputPane.test.tsx.snap | 4 + .../QueryCopilotTab.test.tsx.snap | 4 + src/Explorer/SplashScreen/SplashScreen.tsx | 28 +- src/Explorer/Tabs/NotebookV2Tab.ts | 336 +++++++++- src/Explorer/Tree/ResourceTree.tsx | 432 ++++++++++++- src/Explorer/Tree/ResourceTreeAdapter.tsx | 497 +++++++++++++- src/Utils/GalleryUtils.ts | 2 + 15 files changed, 2242 insertions(+), 11 deletions(-) create mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx create mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index cf4b66ed6..f8fc956e6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -68,6 +68,10 @@ export interface OpenPane extends DataExplorerAction { paneKind: PaneKind | string; } +export interface OpenSampleNotebook extends DataExplorerAction { + path: string; +} + /** * The types of actions that the DataExplorer supports performing upon opening. */ @@ -76,4 +80,5 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, + OpenSampleNotebook, } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b08834dd6..ab7abac11 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -29,6 +29,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -45,8 +47,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -103,6 +107,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -119,8 +125,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -216,6 +224,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -232,8 +242,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -259,6 +271,8 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -275,8 +289,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 368168a4a..4af478475 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,3 +1,4 @@ +import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; @@ -15,7 +16,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; @@ -31,23 +32,34 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants" import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; +import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; +import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import { NotebookContentItem } from "./Notebook/NotebookContentItem"; +import * as FileSystemUtil from "./Notebook/FileSystemUtil"; +import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; +import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import type NotebookManager from "./Notebook/NotebookManager"; +import { NotebookPaneContent } from "./Notebook/NotebookManager"; +import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; +import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; +import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; +import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -75,6 +87,7 @@ export default class Explorer { // Notebooks public notebookManager?: NotebookManager; + private _isInitializingNotebooks: boolean; private notebookToImport: { name: string; content: string; @@ -86,6 +99,7 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); + this._isInitializingNotebooks = false; this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); useNotebook.subscribe( @@ -191,10 +205,12 @@ export default class Explorer { container: this, resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), + refreshNotebookList: () => this.refreshNotebookList(), }); } this.refreshCommandBarButtons(); + this.refreshNotebookList(); } public openEnableSynapseLinkDialog(): void { @@ -357,6 +373,7 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); + this.refreshNotebookList(); }; // Facade @@ -364,6 +381,19 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; + public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + throw new Error("No database account specified"); + } + + if (this._isInitializingNotebooks) { + return; + } + this._isInitializingNotebooks = true; + this.refreshNotebookList(); + this._isInitializingNotebooks = false; + } + public async allocateContainer(poolId: PoolIdType, mode?: string): Promise { const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const notebookServerInfo = shouldUseNotebookStates @@ -442,6 +472,8 @@ export default class Explorer { ? useNotebook.getState().setIsAllocating(false) : useQueryCopilot.getState().setIsAllocatingContainer(false); this.refreshCommandBarButtons(); + this.refreshNotebookList(); + this._isInitializingNotebooks = false; } } } @@ -478,6 +510,104 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } + public resetNotebookWorkspace(): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { + handleError( + "Attempt to reset notebook workspace, but notebook is not enabled", + "Explorer/resetNotebookWorkspace", + ); + return; + } + const dialogContent = useNotebook.getState().isPhoenixNotebooks + ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" + : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; + + const resetConfirmationDialogProps: DialogProps = { + isModal: true, + title: "Reset Workspace", + subText: dialogContent, + primaryButtonText: "OK", + secondaryButtonText: "Cancel", + onPrimaryButtonClick: this._resetNotebookWorkspace, + onSecondaryButtonClick: () => useDialog.getState().closeDialog(), + }; + useDialog.getState().openDialog(resetConfirmationDialogProps); + } + + private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + return false; + } + try { + const { value: workspaces } = await listByDatabaseAccount( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); + return false; + } + } + + private _resetNotebookWorkspace = async () => { + useDialog.getState().closeDialog(); + const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); + let connectionStatus: ContainerConnectionInfo; + try { + const notebookServerInfo = useNotebook.getState().notebookServerInfo; + if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { + const error = "No server endpoint detected"; + Logger.logError(error, "NotebookContainerClient/resetWorkspace"); + logConsoleError(error); + return; + } + TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); + if (useNotebook.getState().isPhoenixNotebooks) { + useTabs.getState().closeAllNotebookTabs(true); + connectionStatus = { + status: ConnectionStatusType.Connecting, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); + if (connectionInfo?.status !== HttpStatusCodes.OK) { + throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); + } + if (!connectionInfo?.data?.phoenixServiceUrl) { + throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); + } + if (useNotebook.getState().isPhoenixNotebooks) { + await this.setNotebookInfo(true, connectionInfo, connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + logConsoleInfo("Successfully reset notebook workspace"); + TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); + } catch (error) { + logConsoleError(`Failed to reset notebook workspace: ${error}`); + TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }); + if (useNotebook.getState().isPhoenixNotebooks) { + connectionStatus = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().resetContainerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + throw error; + } finally { + clearInProgressMessage(); + } + }; + private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -566,6 +696,406 @@ export default class Explorer { } } + public uploadFile( + name: string, + content: string, + parent: NotebookContentItem, + isGithubTree?: boolean, + ): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to upload notebook, but notebook is not enabled"; + handleError(error, "Explorer/uploadFile"); + throw new Error(error); + } + + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); + promise + .then(() => this.resourceTree.triggerRender()) + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); + return promise; + } + + public async importAndOpen(path: string): Promise { + const name = NotebookUtil.getName(path); + const item = NotebookUtil.createNotebookContentItem(name, path, "file"); + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const content = await this.readFile(item); + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + return Promise.resolve(false); + } + + public async importAndOpenContent(name: string, content: string): Promise { + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { + if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { + this.notebookToImport = undefined; // we don't want to try opening this notebook again + } + + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + this.notebookToImport = { name, content }; // we'll try opening this notebook later on + return Promise.resolve(false); + } + + public async publishNotebook( + name: string, + content: NotebookPaneContent, + notebookContentRef?: string, + onTakeSnapshot?: (request: SnapshotRequest) => void, + onClosePanel?: () => void, + ): Promise { + if (this.notebookManager) { + await this.notebookManager.openPublishNotebookPane( + name, + content, + notebookContentRef, + onTakeSnapshot, + onClosePanel, + ); + } + } + + public copyNotebook(name: string, content: string): void { + this.notebookManager?.openCopyNotebookPane(name, content); + } + + /** + * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. + * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. + * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder + * will not fetch its content if the children array exists (and has only one child which was manually created). + * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. + * + * @param name + * @param path + */ + public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { + return NotebookUtil.createNotebookContentItem(name, path, "file"); + } + + public async openNotebook(notebookContentItem: NotebookContentItem): Promise { + if (!notebookContentItem || !notebookContentItem.path) { + throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); + } + if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + } + + const notebookTabs = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab) => + (tab as NotebookV2Tab).notebookPath && + FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path), + ) as NotebookV2Tab[]; + let notebookTab = notebookTabs && notebookTabs[0]; + + if (notebookTab) { + useTabs.getState().activateTab(notebookTab); + } else { + const options: NotebookTabOptions = { + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookV2, + node: undefined, + title: notebookContentItem.name, + tabPath: notebookContentItem.path, + collection: undefined, + masterKey: userContext.masterKey || "", + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: undefined, + container: this, + notebookContentItem, + }; + + try { + const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); + notebookTab = new NotebookTabV2.default(options); + useTabs.getState().activateNewTab(notebookTab); + } catch (reason) { + console.error("Import NotebookV2Tab failed!", reason); + return false; + } + } + + return true; + } + + public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to rename notebook, but notebook is not enabled"; + handleError(error, "Explorer/renameNotebook"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); + }); + if (openedNotebookTabs.length > 0) { + useDialog + .getState() + .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + } else { + useSidePanel.getState().openSidePanel( + "Rename Notebook", + { + useSidePanel.getState().closeSidePanel(); + this.resourceTree.triggerRender(); + }} + inputLabel="Enter new notebook name" + submitButtonLabel="Rename" + errorMessage="Could not rename notebook" + inProgressMessage="Renaming notebook to" + successMessage="Renamed notebook to" + paneTitle="Rename Notebook" + defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} + onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => + this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) + } + notebookFile={notebookFile} + />, + ); + } + } + + public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create notebook directory, but notebook is not enabled"; + handleError(error, "Explorer/onCreateDirectory"); + throw new Error(error); + } + + useSidePanel.getState().openSidePanel( + "Create new directory", + { + useSidePanel.getState().closeSidePanel(); + this.resourceTree.triggerRender(); + }} + errorMessage="Could not create directory " + inProgressMessage="Creating directory " + successMessage="Created directory " + inputLabel="Enter new directory name" + paneTitle="Create new directory" + submitButtonLabel="Create" + defaultInput="" + onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => + this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) + } + notebookFile={parent} + />, + ); + } + + public readFile(notebookFile: NotebookContentItem): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to read file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); + } + + public downloadFile(notebookFile: NotebookContentItem): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to download file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( + (content: string) => { + const blob = stringToBlob(content, "text/plain"); + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob(blob, notebookFile.name); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.target = "_self"; + downloadLink.download = notebookFile.name; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + + clearMessage(); + }, + (error) => { + logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); + clearMessage(); + }, + ); + } + + private refreshNotebookList = async (): Promise => { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + return; + } + + await this.resourceTree.initialize(); + await useNotebook.getState().initializeNotebooksTree(this.notebookManager); + + this.notebookManager?.refreshPinnedRepos(); + if (this.notebookToImport) { + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + }; + + public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to delete notebook file, but notebook is not enabled"; + handleError(error, "Explorer/deleteNotebookFile"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); + }); + if (openedNotebookTabs.length > 0) { + useDialog + .getState() + .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + return Promise.reject(); + } + + if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { + useDialog.getState().openDialog({ + isModal: true, + title: "Unable to delete file", + subText: "Directory is not empty.", + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => useDialog.getState().closeDialog(), + onSecondaryButtonClick: undefined, + }); + return Promise.reject(); + } + + return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( + () => logConsoleInfo(`Successfully deleted: ${item.path}`), + (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`), + ); + } + + /** + * This creates a new notebook file, then opens the notebook + */ + public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create new notebook, but notebook is not enabled"; + handleError(error, "Explorer/onNewNotebookClicked"); + throw new Error(error); + } + if (useNotebook.getState().isPhoenixNotebooks) { + if (isGithubTree) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + } else { + useDialog.getState().showOkCancelModalDialog( + Notebook.newNotebookModalTitle, + undefined, + "Create", + async () => { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + }, + "Cancel", + undefined, + this.getNewNoteWarningText(), + ); + } + } else { + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + } + } + + private getNewNoteWarningText(): JSX.Element { + return ( + <> +

{Notebook.newNotebookModalContent1}

+
+

+ {Notebook.newNotebookModalContent2} + + {Notebook.learnMore} + +

+ + ); + } + + private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { + const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { + dataExplorerArea: Constants.Areas.Notebook, + }); + + this.notebookManager?.notebookContentClient + .createNewNotebookFile(parent, isGithubTree) + .then((newFile: NotebookContentItem) => { + logConsoleInfo(`Successfully created: ${newFile.name}`); + TelemetryProcessor.traceSuccess( + Action.CreateNewNotebook, + { + dataExplorerArea: Constants.Areas.Notebook, + }, + startKey, + ); + return this.openNotebook(newFile); + }) + .then(() => this.resourceTree.triggerRender()) + .catch((error) => { + const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; + logConsoleError(errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateNewNotebook, + { + dataExplorerArea: Constants.Areas.Notebook, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }) + .finally(clearInProgressMessage); + } + // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -722,6 +1252,32 @@ export default class Explorer { } } + public async handleOpenFileAction(path: string): Promise { + if (useNotebook.getState().isPhoenixNotebooks === undefined) { + await useNotebook.getState().getPhoenixStatus(); + } + if (useNotebook.getState().isPhoenixNotebooks) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + } + + // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb + // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly + // calling GitHub. For now convert this url to a raw url and download content. + const gitHubInfo = fromContentUri(path); + if (gitHubInfo) { + const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); + const response = await fetch(rawUrl); + if (response.status === Constants.HttpStatusCodes.OK) { + this.notebookToImport = { + name: NotebookUtil.getName(path), + content: await response.text(), + }; + + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + } + } + public openUploadItemsPanePane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } @@ -731,6 +1287,54 @@ export default class Explorer { .openSidePanel("Input parameters", ); } + public openUploadFilePanel(parent?: NotebookContentItem): void { + if (useNotebook.getState().isPhoenixNotebooks) { + useDialog.getState().showOkCancelModalDialog( + Notebook.newNotebookUploadModalTitle, + undefined, + "Upload", + async () => { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + }, + "Cancel", + undefined, + this.getNewNoteWarningText(), + ); + } else { + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + } + } + + private uploadFilePanel(parent?: NotebookContentItem): void { + useSidePanel + .getState() + .openSidePanel( + "Upload file to notebook server", + this.uploadFile(name, content, parent)} />, + ); + } + + public getDownloadModalConent(fileName: string): JSX.Element { + if (useNotebook.getState().isPhoenixNotebooks) { + return ( + <> +

{Notebook.galleryNotebookDownloadContent1}

+
+

+ {Notebook.galleryNotebookDownloadContent2} + + {Notebook.learnMore} + +

+ + ); + } + return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; + } + public async refreshExplorer(): Promise { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken @@ -755,6 +1359,10 @@ export default class Explorer { dataExplorerArea: Constants.Areas.Notebook, }); + if (useNotebook.getState().isPhoenixNotebooks) { + await this.initNotebooks(userContext.databaseAccount); + } + await this.refreshSampleData(); } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 45afe6061..3ccbefcaf 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -12,13 +12,15 @@ import * as Logger from "../../Common/Logger"; import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; +import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; @@ -38,6 +40,7 @@ export interface NotebookManagerOptions { container: Explorer; resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; + refreshNotebookList: () => void; } export default class NotebookManager { @@ -78,6 +81,10 @@ export default class NotebookManager { contents.JupyterContentProvider, ); + this.notebookClient = new NotebookContainerClient(() => + this.params.container.initNotebooks(userContext?.databaseAccount), + ); + this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider); this.gitHubOAuthService.getTokenObservable().subscribe((token) => { @@ -99,9 +106,11 @@ export default class NotebookManager { } this.params.refreshCommandBarButtons(); + this.params.refreshNotebookList(); }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { + this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); @@ -140,6 +149,22 @@ export default class NotebookManager { ); } + public openCopyNotebookPane(name: string, content: string): void { + const { container } = this.params; + useSidePanel + .getState() + .openSidePanel( + "Copy Notebook", + , + ); + } + // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index e2059cdba..f3ef288c8 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -195,5 +195,17 @@ export function handleOpenAction( return true; } + if ( + action.actionType === ActionContracts.ActionType.OpenSampleNotebook || + action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] + ) { + openFile(action as ActionContracts.OpenSampleNotebook, explorer); + return true; + } + return false; } + +function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { + explorer.handleOpenFileAction(decodeURIComponent(action.path)); +} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx new file mode 100644 index 000000000..0f7927b3b --- /dev/null +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -0,0 +1,154 @@ +import { IDropdownOption } from "@fluentui/react"; +import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants"; +import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; +import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; +import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { useSidePanel } from "../../../hooks/useSidePanel"; +import Explorer from "../../Explorer"; +import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; +import { useNotebook } from "../../Notebook/useNotebook"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; +import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; + +interface Location { + type: "MyNotebooks" | "GitHub"; + + // GitHub + owner?: string; + repo?: string; + branch?: string; +} +export interface CopyNotebookPanelProps { + name: string; + content: string; + container: Explorer; + junoClient: JunoClient; + gitHubOAuthService: GitHubOAuthService; +} + +export const CopyNotebookPane: FunctionComponent = ({ + name, + content, + container, + junoClient, + gitHubOAuthService, +}: CopyNotebookPanelProps) => { + const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const [isExecuting, setIsExecuting] = useState(); + const [formError, setFormError] = useState(""); + const [pinnedRepos, setPinnedRepos] = useState(); + const [selectedLocation, setSelectedLocation] = useState(); + + useEffect(() => { + open(); + }, []); + + const open = async (): Promise => { + if (gitHubOAuthService.isLoggedIn()) { + const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); + } + + if (response.data?.length > 0) { + setPinnedRepos(response.data); + } + } + }; + + const submit = async (): Promise => { + let destination: string = selectedLocation?.type; + let clearMessage: () => void; + setIsExecuting(true); + + try { + if (!selectedLocation) { + throw new Error(`No location selected`); + } + + if (selectedLocation.type === "GitHub") { + destination = `${destination} - ${GitHubUtils.toRepoFullName( + selectedLocation.owner, + selectedLocation.repo, + )} - ${selectedLocation.branch}`; + } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { + destination = useNotebook.getState().notebookFolderName; + } + + clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); + + const notebookContentItem = await copyNotebook(selectedLocation); + if (!notebookContentItem) { + throw new Error(`Failed to upload ${name}`); + } + + NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); + closeSidePanel(); + } catch (error) { + const errorMessage = getErrorMessage(error); + setFormError(`Failed to copy ${name} to ${destination}`); + handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); + } finally { + clearMessage && clearMessage(); + setIsExecuting(false); + } + }; + + const copyNotebook = async (location: Location): Promise => { + let parent: NotebookContentItem; + let isGithubTree: boolean; + switch (location.type) { + case "MyNotebooks": + parent = { + name: useNotebook.getState().notebookFolderName, + path: useNotebook.getState().notebookBasePath, + type: NotebookContentItemType.Directory, + }; + isGithubTree = false; + if (useNotebook.getState().isPhoenixNotebooks) { + await container.allocateContainer(PoolIdType.DefaultPoolId); + } + break; + + case "GitHub": + parent = { + name: selectedLocation.branch, + path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), + type: NotebookContentItemType.Directory, + }; + isGithubTree = true; + break; + + default: + throw new Error(`Unsupported location type ${location.type}`); + } + + return container.uploadFile(name, content, parent, isGithubTree); + }; + + const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { + setSelectedLocation(option?.data); + }; + + const props: RightPaneFormProps = { + formError, + isExecuting: isExecuting, + submitButtonText: "OK", + onSubmit: () => submit(), + }; + + const copyNotebookPaneProps: CopyNotebookPaneProps = { + name, + pinnedRepos, + onDropDownChange: onDropDownChange, + }; + + return ( + + + + ); +}; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx new file mode 100644 index 000000000..5cd0cfdc1 --- /dev/null +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -0,0 +1,120 @@ +import { + Dropdown, + IDropdownOption, + IDropdownProps, + IRenderFunction, + ISelectableOption, + Label, + SelectableOptionMenuItemType, + Stack, + Text, +} from "@fluentui/react"; +import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; +import React, { FormEvent, FunctionComponent } from "react"; +import { IPinnedRepo } from "../../../Juno/JunoClient"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { useNotebook } from "../../Notebook/useNotebook"; + +interface Location { + type: "MyNotebooks" | "GitHub"; + + // GitHub + owner?: string; + repo?: string; + branch?: string; +} + +export interface CopyNotebookPaneProps { + name: string; + pinnedRepos: IPinnedRepo[]; + onDropDownChange: (_: FormEvent, option?: IDropdownOption) => void; +} + +export const CopyNotebookPaneComponent: FunctionComponent = ({ + name, + pinnedRepos, + onDropDownChange, +}: CopyNotebookPaneProps) => { + const BranchNameWhiteSpace = " "; + + const onRenderDropDownTitle: IRenderFunction = (options: IDropdownOption[]): JSX.Element => { + return {options.length && options[0].title}; + }; + + const onRenderDropDownOption: IRenderFunction = (option: ISelectableOption): JSX.Element => { + return {option.text}; + }; + + const getDropDownOptions = (): IDropdownOption[] => { + const options: IDropdownOption[] = []; + options.push({ + key: "MyNotebooks-Item", + text: useNotebook.getState().notebookFolderName, + title: useNotebook.getState().notebookFolderName, + data: { + type: "MyNotebooks", + } as Location, + }); + + if (pinnedRepos && pinnedRepos.length > 0) { + options.push({ + key: "GitHub-Header-Divider", + text: undefined, + itemType: SelectableOptionMenuItemType.Divider, + }); + + options.push({ + key: "GitHub-Header", + text: GitHubReposTitle, + itemType: SelectableOptionMenuItemType.Header, + }); + + pinnedRepos.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + options.push({ + key: `GitHub-Repo-${repoFullName}`, + text: repoFullName, + disabled: true, + }); + + pinnedRepo.branches.forEach((branch) => + options.push({ + key: `GitHub-Repo-${repoFullName}-${branch.name}`, + text: `${BranchNameWhiteSpace}${branch.name}`, + title: `${repoFullName} - ${branch.name}`, + data: { + type: "GitHub", + owner: pinnedRepo.owner, + repo: pinnedRepo.name, + branch: branch.name, + } as Location, + }), + ); + }); + } + + return options; + }; + const dropDownProps: IDropdownProps = { + label: "Location", + ariaLabel: "Location", + placeholder: "Select an option", + onRenderTitle: onRenderDropDownTitle, + onRenderOption: onRenderDropDownOption, + options: getDropDownOptions(), + onChange: onDropDownChange, + }; + + return ( +
+ + + + {name} + + + + +
+ ); +}; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index aa2137c42..4a3a8942e 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -17,6 +17,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` addRepoProps={ Object { "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -33,8 +35,10 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 566808f4b..8054abe19 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -7,6 +7,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` errorMessage="Could not create directory " explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -23,8 +25,10 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 6d875fe2e..26b52ff90 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -22,6 +22,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = databaseId="CopilotSampleDb" explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -38,8 +40,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index e9477e088..f4ebb9cd0 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,9 +25,11 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; +import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import * as Constants from "../../Common/Constants"; import { userContext } from "../../UserContext"; @@ -408,6 +410,14 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); + } else if (useNotebook.getState().isPhoenixNotebooks) { + const newNotebookBtn = { + iconSrc: NotebookColorIcon, + title: "New notebook", + description: "Visualize your data stored in Azure Cosmos DB", + onClick: () => this.container.onNewNotebookClicked(), + }; + heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -483,12 +493,28 @@ export class SplashScreen extends React.Component { }; } + private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { + return { + info: path, + iconSrc: NotebookIcon, + title: name, + description: "Notebook", + onClick: () => { + const notebookItem = this.container.createNotebookContentItemFile(name, path); + notebookItem && this.container.openNotebook(notebookItem); + }, + }; + } + private createRecentItems(): SplashScreenItem[] { return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { switch (activity.type) { default: { - throw new Error(`Unknown activity: ${activity}`); + const unknownActivity: never = activity; + throw new Error(`Unknown activity: ${unknownActivity}`); } + case MostRecentActivity.Type.OpenNotebook: + return this.decorateOpenNotebookActivity(activity); case MostRecentActivity.Type.OpenCollection: return this.decorateOpenCollectionActivity(activity); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index fadb43258..92e0e6958 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,11 +1,31 @@ +import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; +import { userContext } from "UserContext"; +import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import CutIcon from "../../../images/notebook/Notebook-cut.svg"; +import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; +import PasteIcon from "../../../images/notebook/Notebook-paste.svg"; +import RestartIcon from "../../../images/notebook/Notebook-restart.svg"; +import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; +import RunIcon from "../../../images/notebook/Notebook-run.svg"; +import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "../Controls/Dialog"; +import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; +import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; +import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; +import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; @@ -70,7 +90,275 @@ export default class NotebookTabV2 extends NotebookTabBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - return []; + const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); + const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted(); + + const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined; + + const saveLabel = "Save"; + const copyToLabel = "Copy to ..."; + const publishLabel = "Publish to gallery"; + const kernelLabel = "No Kernel"; + const runLabel = "Run"; + const runActiveCellLabel = "Run Active Cell"; + const runAllLabel = "Run All"; + const interruptKernelLabel = "Interrupt Kernel"; + const killKernelLabel = "Halt Kernel"; + const restartKernelLabel = "Restart Kernel"; + const clearLabel = "Clear outputs"; + const newCellLabel = "New Cell"; + const cellTypeLabel = "Cell Type"; + const codeLabel = "Code"; + const markdownLabel = "Markdown"; + const rawLabel = "Raw"; + const copyLabel = "Copy"; + const cutLabel = "Cut"; + const pasteLabel = "Paste"; + const cellCodeType = "code"; + const cellMarkdownType = "markdown"; + const cellRawType = "raw"; + + const saveButtonChildren = []; + if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + saveButtonChildren.push({ + iconName: copyToLabel, + onCommandClick: () => this.copyNotebook(), + commandButtonLabel: copyToLabel, + hasPopup: false, + disabled: false, + ariaLabel: copyToLabel, + }); + } + + if (userContext.features.publicGallery) { + saveButtonChildren.push({ + iconName: "PublishContent", + onCommandClick: async () => await this.publishToGallery(), + commandButtonLabel: publishLabel, + hasPopup: false, + disabled: false, + ariaLabel: publishLabel, + }); + } + + let buttons: CommandButtonComponentProps[] = [ + { + iconSrc: SaveIcon, + iconAlt: saveLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookSave(), + commandButtonLabel: saveLabel, + hasPopup: false, + disabled: false, + ariaLabel: saveLabel, + children: saveButtonChildren.length && [ + { + iconName: "Save", + onCommandClick: () => this.notebookComponentAdapter.notebookSave(), + commandButtonLabel: saveLabel, + hasPopup: false, + disabled: false, + ariaLabel: saveLabel, + }, + ...saveButtonChildren, + ], + }, + { + iconSrc: null, + iconAlt: kernelLabel, + onCommandClick: () => {}, + commandButtonLabel: null, + hasPopup: false, + disabled: availableKernels.length < 1, + isDropdown: true, + dropdownPlaceholder: kernelLabel, + dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName, + dropdownWidth: 100, + children: availableKernels.map( + (kernel: KernelSpecsDisplay) => + ({ + iconSrc: null, + iconAlt: kernel.displayName, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name), + commandButtonLabel: kernel.displayName, + dropdownItemKey: kernel.name, + hasPopup: false, + disabled: false, + ariaLabel: kernel.displayName, + }) as CommandButtonComponentProps, + ), + ariaLabel: kernelLabel, + }, + { + iconSrc: RunIcon, + iconAlt: runLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAndAdvance(); + this.traceTelemetry(Action.ExecuteCell); + }, + commandButtonLabel: runLabel, + tooltipText: runBtnTooltip, + ariaLabel: runLabel, + hasPopup: false, + disabled: isNotebookUntrusted, + children: [ + { + iconSrc: RunIcon, + iconAlt: runActiveCellLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAndAdvance(); + this.traceTelemetry(Action.ExecuteCell); + }, + commandButtonLabel: runActiveCellLabel, + hasPopup: false, + disabled: false, + ariaLabel: runActiveCellLabel, + }, + { + iconSrc: RunAllIcon, + iconAlt: runAllLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAll(); + this.traceTelemetry(Action.ExecuteAllCells); + }, + commandButtonLabel: runAllLabel, + hasPopup: false, + disabled: false, + ariaLabel: runAllLabel, + }, + { + iconSrc: InterruptKernelIcon, + iconAlt: interruptKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(), + commandButtonLabel: interruptKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: interruptKernelLabel, + }, + { + iconSrc: KillKernelIcon, + iconAlt: killKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(), + commandButtonLabel: killKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: killKernelLabel, + }, + { + iconSrc: RestartIcon, + iconAlt: restartKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(), + commandButtonLabel: restartKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: restartKernelLabel, + }, + ], + }, + { + iconSrc: ClearAllOutputsIcon, + iconAlt: clearLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(), + commandButtonLabel: clearLabel, + hasPopup: false, + disabled: false, + ariaLabel: clearLabel, + }, + { + iconSrc: NewCellIcon, + iconAlt: newCellLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(), + commandButtonLabel: newCellLabel, + ariaLabel: newCellLabel, + hasPopup: false, + disabled: false, + }, + CommandBarComponentButtonFactory.createDivider(), + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => {}, + commandButtonLabel: null, + ariaLabel: cellTypeLabel, + hasPopup: false, + disabled: false, + isDropdown: true, + dropdownPlaceholder: cellTypeLabel, + dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(), + dropdownWidth: 110, + children: [ + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType), + commandButtonLabel: codeLabel, + ariaLabel: codeLabel, + dropdownItemKey: cellCodeType, + hasPopup: false, + disabled: false, + }, + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType), + commandButtonLabel: markdownLabel, + ariaLabel: markdownLabel, + dropdownItemKey: cellMarkdownType, + hasPopup: false, + disabled: false, + }, + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType), + commandButtonLabel: rawLabel, + ariaLabel: rawLabel, + dropdownItemKey: cellRawType, + hasPopup: false, + disabled: false, + }, + ], + }, + { + iconSrc: CopyIcon, + iconAlt: copyLabel, + onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), + commandButtonLabel: copyLabel, + ariaLabel: copyLabel, + hasPopup: false, + disabled: false, + children: [ + { + iconSrc: CopyIcon, + iconAlt: copyLabel, + onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), + commandButtonLabel: copyLabel, + ariaLabel: copyLabel, + hasPopup: false, + disabled: false, + }, + { + iconSrc: CutIcon, + iconAlt: cutLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookCut(), + commandButtonLabel: cutLabel, + ariaLabel: cutLabel, + hasPopup: false, + disabled: false, + }, + { + iconSrc: PasteIcon, + iconAlt: pasteLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookPaste(), + commandButtonLabel: pasteLabel, + ariaLabel: pasteLabel, + hasPopup: false, + disabled: false, + }, + ], + }, + // TODO: Uncomment when undo/redo is reimplemented in nteract + ]; + return buttons; } protected buildCommandBarOptions(): void { @@ -94,4 +382,50 @@ export default class NotebookTabV2 extends NotebookTabBase { sparkClusterConnectionInfo, ); } + + private publishToGallery = async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.CommandBarMenu, + }); + + const notebookReduxStore = NotebookTabV2.clientManager.getStore(); + const unsubscribe = notebookReduxStore.subscribe(() => { + const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb; + useNotebookSnapshotStore.setState({ + snapshot: cdbState.notebookSnapshot?.imageSrc, + error: cdbState.notebookSnapshotError, + }); + }); + + const notebookContent = this.notebookComponentAdapter.getContent(); + const notebookContentRef = this.notebookComponentAdapter.contentRef; + const onPanelClose = (): void => { + unsubscribe(); + useNotebookSnapshotStore.setState({ + snapshot: undefined, + error: undefined, + }); + notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined)); + }; + + await this.container.publishNotebook( + notebookContent.name, + notebookContent.content, + notebookContentRef, + (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)), + onPanelClose, + ); + }; + + private copyNotebook = () => { + const notebookContent = this.notebookComponentAdapter.getContent(); + let content: string; + if (typeof notebookContent.content === "string") { + content = notebookContent.content; + } else { + content = stringifyNotebook(toJS(notebookContent.content)); + } + + this.container.copyNotebook(notebookContent.name, content); + }; } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 0933eac7b..b5f759534 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,23 +1,42 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; +import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; +import { useDialog } from "../Controls/Dialog"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -26,21 +45,391 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; +export const MyNotebooksTitle = "My Notebooks"; +export const GitHubReposTitle = "GitHub repos"; + interface ResourceTreeProps { container: Explorer; } export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { const databases = useDatabases((state) => state.databases); - const { isNotebookEnabled } = useNotebook( + const { + isNotebookEnabled, + myNotebooksContentRoot, + galleryContentRoot, + gitHubNotebooksContentRoot, + updateNotebookItem, + } = useNotebook( (state) => ({ isNotebookEnabled: state.isNotebookEnabled, + myNotebooksContentRoot: state.myNotebooksContentRoot, + galleryContentRoot: state.galleryContentRoot, + gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, + updateNotebookItem: state.updateNotebookItem, }), shallow, ); - const { refreshActiveTab } = useTabs(); + const { activeTab, refreshActiveTab } = useTabs(); const showScriptNodes = configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); + const pseudoDirPath = "PsuedoDir"; + + const buildGalleryCallout = (): JSX.Element => { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + container.openGallery(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + }; + + const buildNotebooksTree = (): TreeNode => { + const notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (!useNotebook.getState().isPhoenixNotebooks) { + notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); + } else { + if (galleryContentRoot) { + notebooksTree.children.push(buildGalleryNotebooksTree()); + } + + if ( + myNotebooksContentRoot && + useNotebook.getState().isPhoenixNotebooks && + useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected + ) { + notebooksTree.children.push(buildMyNotebooksTree()); + } + if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(buildGitHubNotebooksTree(true)); + } + } + return notebooksTree; + }; + + const buildNotebooksTemporarilyDownTree = (): TreeNode => { + return { + label: Notebook.temporarilyDownMsg, + className: "clickDisabled", + }; + }; + + const buildGalleryNotebooksTree = (): TreeNode => { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => container.openGallery(), + isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, + }; + }; + + const buildMyNotebooksTree = (): TreeNode => { + const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( + myNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item); + }, + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + }; + + const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { + const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( + gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item); + }, + true, + ); + const manageGitContextMenu: TreeNodeMenuItem[] = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + , + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + gitHubNotebooksTree.contextMenu = manageGitContextMenu; + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + }; + + const buildChildNodes = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode[] => { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) + : buildNotebookFileNode(item, onFileClick, isGithubTree); + result.timestamp = item.timestamp; + return result; + }); + } + }; + + const buildNotebookFileNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode => { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu(container, item, isGithubTree), + data: item, + }; + }; + + const createFileContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean, + ): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item, isGithubTree), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item, isGithubTree), + "Cancel", + undefined, + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => copyNotebook(container, item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await container.readFile(item); + if (content) { + await container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + }; + + const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { + const content = await container.readFile(item); + if (content) { + container.copyNotebook(item.name, content); + } + }; + + const createDirectoryContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean, + ): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => loadSubitems(item, isGithubTree), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item, isGithubTree), + "Cancel", + undefined, + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item, isGithubTree), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => container.onCreateDirectory(item, isGithubTree), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => container.openUploadFilePanel(item), + }, + ]; + + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File", + ); + } + + return items; + }; + + const buildNotebookDirectoryNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode => { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + loadSubitems(item, isGithubTree); + } + }, + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, + data: item, + children: buildChildNodes(item, onFileClick, isGithubTree), + }; + }; const buildDataTree = (): TreeNode => { const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { @@ -368,6 +757,11 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; + const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { + const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); + updateNotebookItem(updatedItem, isGithubTree); + }; + const dataRootNode = buildDataTree(); const isSampleDataEnabled = useQueryCopilot().copilotEnabled && @@ -381,16 +775,46 @@ export const ResourceTree: React.FC = ({ container }: Resourc {!isNotebookEnabled && !isSampleDataEnabled && ( )} + {isNotebookEnabled && !isSampleDataEnabled && ( + <> + + + + + + + {/* {buildGalleryCallout()} */} + + )} {!isNotebookEnabled && isSampleDataEnabled && ( <> - + + + {/* {buildGalleryCallout()} */} + + )} + {isNotebookEnabled && isSampleDataEnabled && ( + <> + + + + + + + + + + + + + {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 67fb54773..b22f0f916 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,21 +1,42 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; +import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { IPinnedRepo } from "../../Juno/JunoClient"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; +import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -25,8 +46,19 @@ import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { + public static readonly MyNotebooksTitle = "My Notebooks"; + public static readonly GitHubReposTitle = "GitHub repos"; + + private static readonly DataTitle = "DATA"; + private static readonly NotebooksTitle = "NOTEBOOKS"; + private static readonly PseudoDirPath = "PsuedoDir"; + public parameters: ko.Observable; + public galleryContentRoot: NotebookContentItem; + public myNotebooksContentRoot: NotebookContentItem; + public gitHubNotebooksContentRoot: NotebookContentItem; + public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -44,9 +76,111 @@ export class ResourceTreeAdapter implements ReactAdapter { this.triggerRender(); } + private traceMyNotebookTreeInfo() { + const myNotebooksTree = this.myNotebooksContentRoot; + if (myNotebooksTree.children) { + // Count 1st generation children (tree is lazy-loaded) + const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; + myNotebooksTree.children.forEach((treeNode) => { + switch ((treeNode as NotebookContentItem).type) { + case NotebookContentItemType.File: + nodeCounts.files++; + break; + case NotebookContentItemType.Directory: + nodeCounts.directories++; + break; + case NotebookContentItemType.Notebook: + nodeCounts.notebooks++; + break; + default: + break; + } + }); + TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); + } + } + public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - return ; + const notebooksRootNode = this.buildNotebooksTrees(); + + if (useNotebook.getState().isNotebookEnabled) { + return ( + <> + + + + + + + + + + {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} + + ); + } else { + return ; + } + } + + public async initialize(): Promise { + const refreshTasks: Promise[] = []; + + this.galleryContentRoot = { + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }; + this.myNotebooksContentRoot = { + name: useNotebook.getState().notebookFolderName, + path: useNotebook.getState().notebookBasePath, + type: NotebookContentItemType.Directory, + }; + + // Only if notebook server is available we can refresh + if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { + refreshTasks.push( + this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { + this.triggerRender(); + this.traceMyNotebookTreeInfo(); + }), + ); + } + this.gitHubNotebooksContentRoot = { + name: ResourceTreeAdapter.GitHubReposTitle, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + }; + + return Promise.all(refreshTasks); + } + + public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { + if (this.gitHubNotebooksContentRoot) { + this.gitHubNotebooksContentRoot.children = []; + pinnedRepos?.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + const repoTreeItem: NotebookContentItem = { + name: repoFullName, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + children: [], + }; + + pinnedRepo.branches.forEach((branch) => { + repoTreeItem.children.push({ + name: branch.name, + path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), + type: NotebookContentItemType.Directory, + }); + }); + + this.gitHubNotebooksContentRoot.children.push(repoTreeItem); + }); + + this.triggerRender(); + } } private buildDataTree(): TreeNode { @@ -370,6 +504,365 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } + private buildNotebooksTrees(): TreeNode { + let notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (this.galleryContentRoot) { + notebooksTree.children.push(this.buildGalleryNotebooksTree()); + } + + if (this.myNotebooksContentRoot) { + notebooksTree.children.push(this.buildMyNotebooksTree()); + } + + if (this.gitHubNotebooksContentRoot) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(this.buildGitHubNotebooksTree()); + } + + return notebooksTree; + } + + private buildGalleryCallout(): JSX.Element { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + this.triggerRender(); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + this.container.openGallery(); + this.triggerRender(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + } + + private buildGalleryNotebooksTree(): TreeNode { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => this.container.openGallery(), + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; + }, + }; + } + + private buildMyNotebooksTree(): TreeNode { + const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( + this.myNotebooksContentRoot, + (item: NotebookContentItem) => { + this.container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + }, + true, + true, + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + } + + private buildGitHubNotebooksTree(): TreeNode { + const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( + this.gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + this.container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + }, + true, + true, + ); + + gitHubNotebooksTree.contextMenu = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + , + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + this.container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + } + + private buildChildNodes( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createDirectoryContextMenu: boolean, + createFileContextMenu: boolean, + ): TreeNode[] { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) + : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); + result.timestamp = item.timestamp; + return result; + }); + } + } + + private buildNotebookFileNode( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createFileContextMenu: boolean, + ): TreeNode { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu && this.createFileContextMenu(item), + data: item, + }; + } + + private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => this.container.renameNotebook(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined, + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => this.copyNotebook(item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => this.container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await this.container.readFile(item); + if (content) { + await this.container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + } + + private copyNotebook = async (item: NotebookContentItem) => { + const content = await this.container.readFile(item); + if (content) { + this.container.copyNotebook(item.name, content); + } + }; + + private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined, + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => this.container.renameNotebook(item), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => this.container.onCreateDirectory(item), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => this.container.openUploadFilePanel(item), + }, + ]; + + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File", + ); + } + + return items; + } + + private buildNotebookDirectoryNode( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createDirectoryContextMenu: boolean, + createFileContextMenu: boolean, + ): TreeNode { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + this.container.refreshContentItem(item).then(() => this.triggerRender()); + } + }, + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: + createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath + ? this.createDirectoryContextMenu(item) + : undefined, + data: item, + children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), + }; + } + public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 1314b2b3c..f11b6df5c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,6 +245,7 @@ export function downloadItem( }, "Cancel", undefined, + container.getDownloadModalConent(name), ); } export async function downloadNotebookItem( @@ -277,6 +278,7 @@ export async function downloadNotebookItem( metadata.untrusted = true; } + await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); From 6ebc48ad28a280a96152aaab968b990ff8b5bef0 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 2 May 2024 07:14:31 -0700 Subject: [PATCH 093/102] Remove some Notebooks code (#1832) * Remove onNewNotebookClicked, openUploadFilePanel functions and UploadFilePane. * Remove resetNotebookWorkspace function. * Remove Notebooks related resource tree node generation. * Fix test snapshots. --- .../SettingsComponent.test.tsx.snap | 4 - src/Explorer/Explorer.tsx | 220 ------------------ .../GitHubReposPanel.test.tsx.snap | 1 - .../StringInputPane.test.tsx.snap | 1 - .../Panes/UploadFilePane/UploadFilePane.tsx | 91 -------- .../QueryCopilotTab.test.tsx.snap | 1 - src/Explorer/SplashScreen/SplashScreen.tsx | 9 - src/Explorer/Tree/ResourceTree.tsx | 166 ------------- src/Explorer/Tree/ResourceTreeAdapter.tsx | 183 +-------------- 9 files changed, 1 insertion(+), 675 deletions(-) delete mode 100644 src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ab7abac11..65eb765e5 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -225,7 +223,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -272,7 +269,6 @@ exports[`SettingsComponent renders 1`] = ` explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..fe648918b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; @@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; @@ -510,104 +508,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -1010,92 +910,6 @@ export default class Explorer { ); } - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1130,10 +944,6 @@ export default class Explorer { let title: string; switch (kind) { - case ViewModels.TerminalKind.Default: - title = "Terminal"; - break; - case ViewModels.TerminalKind.Mongo: title = "Mongo Shell"; break; @@ -1287,36 +1097,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - public getDownloadModalConent(fileName: string): JSX.Element { if (useNotebook.getState().isPhoenixNotebooks) { return ( diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..19f98d5c6 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` Object { "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..3e8f7c92d 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx b/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx deleted file mode 100644 index d9b1d9792..000000000 --- a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Upload } from "Common/Upload/Upload"; -import { useSidePanel } from "hooks/useSidePanel"; -import React, { ChangeEvent, FunctionComponent, useState } from "react"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; -import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; - -export interface UploadFilePanelProps { - uploadFile: (name: string, content: string) => Promise; -} - -export const UploadFilePane: FunctionComponent = ({ uploadFile }: UploadFilePanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const extensions: string = undefined; //ex. ".ipynb" - const errorMessage = "Could not upload file"; - const inProgressMessage = "Uploading file to notebook server"; - const successMessage = "Successfully uploaded file to notebook server"; - - const [files, setFiles] = useState(); - const [formErrors, setFormErrors] = useState(""); - const [isExecuting, setIsExecuting] = useState(false); - - const submit = () => { - setFormErrors(""); - if (!files || files.length === 0) { - setFormErrors("No file specified. Please input a file."); - logConsoleError(`${errorMessage} -- No file specified. Please input a file.`); - return; - } - - const file: File = files.item(0); - - const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`); - - setIsExecuting(true); - - onSubmit(files.item(0)) - .then( - () => { - logConsoleInfo(`${successMessage} ${file.name}`); - closeSidePanel(); - }, - (error: string) => { - setFormErrors(errorMessage); - logConsoleError(`${errorMessage} ${file.name}: ${error}`); - }, - ) - .finally(() => { - setIsExecuting(false); - clearMessage(); - }); - }; - - const updateSelectedFiles = (event: ChangeEvent): void => { - setFiles(event.target.files); - }; - - const onSubmit = async (file: File): Promise => { - const readFileAsText = (inputFile: File): Promise => { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onerror = () => { - reader.abort(); - reject(`Problem parsing file: ${inputFile}`); - }; - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsText(inputFile); - }); - }; - - const fileContent = await readFileAsText(file); - return uploadFile(file.name, fileContent); - }; - - const props: RightPaneFormProps = { - formError: formErrors, - isExecuting: isExecuting, - submitButtonText: "Upload", - onSubmit: submit, - }; - - return ( - -
- -
-
- ); -}; diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..bb942bf55 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index f4ebb9cd0..0374edd23 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,7 +25,6 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; @@ -410,14 +409,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5f759534..a14c202e8 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,11 +1,9 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -14,17 +12,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; @@ -36,7 +31,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -75,152 +69,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); const pseudoDirPath = "PsuedoDir"; - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - const buildChildNodes = ( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -373,11 +221,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc iconSrc: NewNotebookIcon, onClick: () => container.onCreateDirectory(item, isGithubTree), }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace @@ -782,8 +625,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - {/* {buildGalleryCallout()} */} )} {!isNotebookEnabled && isSampleDataEnabled && ( @@ -796,8 +637,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - {/* {buildGalleryCallout()} */} )} {isNotebookEnabled && isSampleDataEnabled && ( @@ -809,12 +648,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b22f0f916..2fd8d6bd1 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,9 +1,7 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -13,21 +11,17 @@ import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; @@ -36,7 +30,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -102,26 +95,7 @@ export class ResourceTreeAdapter implements ReactAdapter { public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } + return ; } public async initialize(): Promise { @@ -504,156 +478,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - private buildChildNodes( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -800,11 +624,6 @@ export class ResourceTreeAdapter implements ReactAdapter { iconSrc: NewNotebookIcon, onClick: () => this.container.onCreateDirectory(item), }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace From 5c3f18f5f8dcb47884b063c7411d65e2ed304854 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 7 May 2024 12:30:46 -0700 Subject: [PATCH 094/102] add link to keyboard shortcuts doc to home tab (#1836) --- src/Explorer/SplashScreen/SplashScreen.tsx | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 0374edd23..0ed9351ae 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -680,11 +680,20 @@ export class SplashScreen extends React.Component { title: "Learn the Fundamentals", description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.", }; - let items: item[]; + + const commonItems: item[] = [ + { + link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts", + title: "Data Explorer keyboard shortcuts", + description: "Learn keyboard shortcuts to navigate Data Explorer.", + }, + ]; + + let apiItems: item[]; switch (userContext.apiType) { case "SQL": case "Postgres": - items = [ + apiItems = [ { link: "https://aka.ms/msl-sdk-connect", title: "Get Started using an SDK", @@ -699,7 +708,7 @@ export class SplashScreen extends React.Component { ]; break; case "Mongo": - items = [ + apiItems = [ { link: "https://aka.ms/mongonodejs", title: "Build an app with Node.js", @@ -714,7 +723,7 @@ export class SplashScreen extends React.Component { ]; break; case "Cassandra": - items = [ + apiItems = [ { link: "https://aka.ms/cassandracontainer", title: "Create a Container", @@ -729,7 +738,7 @@ export class SplashScreen extends React.Component { ]; break; case "Gremlin": - items = [ + apiItems = [ { link: "https://aka.ms/graphquickstart", title: "Get Started ", @@ -744,7 +753,7 @@ export class SplashScreen extends React.Component { ]; break; case "Tables": - items = [ + apiItems = [ { link: "https://aka.ms/tabledotnet", title: "Build a .NET App", @@ -761,6 +770,9 @@ export class SplashScreen extends React.Component { default: break; } + + const items = [...commonItems, ...apiItems]; + return ( {items.map((item, i) => ( From 14e5efcebf5cd56d04a6627184a437b750c644ac Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 9 May 2024 13:42:59 -0400 Subject: [PATCH 095/102] Point Mongo requests to old backend (#1838) * point mongo requests to old backend * point mongo requests to old backend --------- Co-authored-by: Asier Isayas --- src/ConfigContext.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 061f25286..ed8eaab0e 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -102,13 +102,13 @@ let configContext: Readonly = { PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ - "resourcelist", - "queryDocuments", - "createDocument", - "readDocument", - "updateDocument", - "deleteDocument", - "createCollectionWithProxy", + // "resourcelist", + // "queryDocuments", + // "createDocument", + // "readDocument", + // "updateDocument", + // "deleteDocument", + // "createCollectionWithProxy", "legacyMongoShell", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, From f56e5e64b90c0790a00b372fd621e4e9ffb0157f Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 14 May 2024 09:56:16 +0530 Subject: [PATCH 096/102] =?UTF-8?q?[accessibility-3102916]:[Keyboard=20Nav?= =?UTF-8?q?igation=20-=20Azure=20CosmosDB=20-=20Data=20=E2=80=A6=20(#1834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [accessibility-3102916]:[Keyboard Navigation - Azure CosmosDB - Data Explorer]: Keyboard focus is moving to non-interactive control after checkbox control of Advanced button. * Updated Snap. --------- Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/AddCollectionPanel.tsx | 7 +++---- .../Panes/__snapshots__/AddCollectionPanel.test.tsx.snap | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 2a99a56a7..dee78d7bf 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -924,10 +924,9 @@ export class AddCollectionPanel extends React.Component - To ensure compatibility with - older SDKs, the created container will use a legacy partitioning scheme that supports partition - key values of size only up to 101 bytes. If this is enabled, you will not be able to use - hierarchical partition keys.{" "} + To ensure compatibility with older SDKs, the + created container will use a legacy partitioning scheme that supports partition key values of size + only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "} Learn more diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap index 79485d550..d2fd03913 100644 --- a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap @@ -466,7 +466,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = ` To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys. From 9e9d270b65ed43d1772044368ac30dae00950599 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 14 May 2024 09:56:53 +0530 Subject: [PATCH 097/102] =?UTF-8?q?[accessibility-3102877]:[Programmatic?= =?UTF-8?q?=20Access=20-=20Azure=20CosmosDB=20=E2=80=93=20Data=20Explorer]?= =?UTF-8?q?:=20Ensures=20every=20ARIA=20input=20field=20has=20an=20accessi?= =?UTF-8?q?ble=20name.=20(#1835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/AddCollectionPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index dee78d7bf..8c1cfef5b 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -382,6 +382,7 @@ export class AddCollectionPanel extends React.Component Date: Tue, 14 May 2024 09:57:50 +0530 Subject: [PATCH 098/102] Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane. (#1837) * [accessibility-3100026]: [Screen Reader - Azure Cosmos DB - Add Table Row]: Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane. * Fixed format. * Snap update. --------- Co-authored-by: Satyapriya Bai --- less/Common/Constants.less | 7 +++++++ .../DeleteCollectionConfirmationPane.test.tsx.snap | 5 +++++ .../__snapshots__/ExecuteSprocParamsPane.test.tsx.snap | 5 +++++ src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx | 6 ++++++ .../__snapshots__/RightPaneForm.test.tsx.snap | 5 +++++ .../__snapshots__/StringInputPane.test.tsx.snap | 5 +++++ .../__snapshots__/TableQuerySelectPanel.test.tsx.snap | 5 +++++ .../Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap | 5 +++++ .../__snapshots__/EditTableEntityPanel.test.tsx.snap | 5 +++++ .../DeleteDatabaseConfirmationPanel.test.tsx.snap | 5 +++++ src/Explorer/Tables/Constants.ts | 5 +++++ 11 files changed, 58 insertions(+) diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 946426d37..5b80d67f8 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -335,4 +335,11 @@ width: 0; height: 0; border-color: @InfoPointerColor transparent; +} +/********************************************************************************************************* + Screen Reader Only +**********************************************************************************************************/ +.screenReaderOnly { + position: absolute; + left: -9999px; } \ No newline at end of file diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index e63cb45bd..8add09097 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2140,6 +2140,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
+ `; diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap index 6e0cbbeab..6c5cb5dfb 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap @@ -7073,6 +7073,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx index 27ba8e7f6..575403ccf 100644 --- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx +++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, FunctionComponent, ReactNode } from "react"; import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelLoadingScreen } from "../PanelLoadingScreen"; +import { labelToLoadingItemName } from "Explorer/Tables/Constants"; export interface RightPaneFormProps { formError: string; @@ -27,6 +28,10 @@ export const RightPaneForm: FunctionComponent = ({ const handleOnSubmit = (event: React.FormEvent) => { event.preventDefault(); onSubmit(); + const screenReaderStatusElement = document.getElementById("screenReaderStatus"); + if (screenReaderStatusElement) { + screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading"; + } }; return ( @@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent = ({ /> )} + {isExecuting && } ); diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap index bcfa8a9e6..7691f6ed4 100644 --- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap +++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap @@ -1782,5 +1782,10 @@ exports[`Right Pane Form should render Default properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 3e8f7c92d..e8553fa4e 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -2449,6 +2449,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap index ca5c6c926..4a913958d 100644 --- a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap @@ -3019,6 +3019,11 @@ exports[`Table query select Panel should render Default properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap index 80d494808..f2ee599f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap @@ -2130,6 +2130,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap index da5c0c722..648f797f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap @@ -2136,6 +2136,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 4ac4a9e20..6e32d62c0 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -2810,6 +2810,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` + `; diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 76c050f5d..58d5b8e87 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -225,3 +225,8 @@ export const InputType = { DateTime: "datetime-local", Number: "number", }; + +export const labelToLoadingItemName: Record = { + "Add Row": "Adding row to table", + "Add Entity": "Adding entity to table", +}; From ff4bc78d6cf5c7047f91ee1a13e03d512aec27aa Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Fri, 17 May 2024 06:57:35 -0700 Subject: [PATCH 099/102] Remove preview label from Computed Properties. (#1842) --- src/Explorer/Controls/Settings/SettingsUtils.tsx | 2 +- .../Settings/__snapshots__/SettingsComponent.test.tsx.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 869ae323a..24ef14681 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -151,7 +151,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.PartitionKeyTab: return "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: - return "Computed Properties (preview)"; + return "Computed Properties"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 65eb765e5..b536bed8a 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -296,7 +296,7 @@ exports[`SettingsComponent renders 1`] = ` /> Date: Fri, 17 May 2024 12:19:23 -0500 Subject: [PATCH 100/102] add capacityMode (#1826) * add capacityMode * add check for capacityMode for serverless --- src/Common/Constants.ts | 5 +++++ src/Contracts/DataModels.ts | 3 ++- src/Utils/CapabilityUtils.ts | 8 +++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 69bd5ed49..0658d28c5 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -90,6 +90,11 @@ export class CapabilityNames { public static readonly EnableServerless: string = "EnableServerless"; } +export enum CapacityMode { + Provisioned = "Provisioned", + Serverless = "Serverless", +} + // flight names returned from the portal are always lowercase export class Flights { public static readonly SettingsV2 = "settingsv2"; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3c8a5175d..e85f56964 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,4 +1,4 @@ -import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; +import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; export interface ArmEntity { id: string; @@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties { ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; capacity?: { totalThroughputLimit: number }; + capacityMode?: CapacityMode; locations?: DatabaseAccountResponseLocation[]; postgresqlEndpoint?: string; publicNetworkAccess?: string; diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index fc2de8149..216eff13d 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -9,4 +9,10 @@ export const isCapabilityEnabled = (capabilityName: string): boolean => { return false; }; -export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); +export const isServerlessAccount = (): boolean => { + const { databaseAccount } = userContext; + return ( + databaseAccount?.properties?.capacityMode === Constants.CapacityMode.Serverless || + isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) + ); +}; From ceeead8458ceb85e0ee998ad545f456271a6f2eb Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 20 May 2024 13:30:30 -0500 Subject: [PATCH 101/102] Vector search for NoSQL accounts (#1843) * Add container vector policy and indexing policy support * Add vector search capability * hide vector settings for shared throughput DB * update package-lock * fix pipeline * remove comments * Address comments * Address comments --- package-lock.json | 8 +- package.json | 2 +- src/Common/Constants.ts | 1 + .../__snapshots__/queryDocuments.test.ts.snap | 2 + src/Common/dataAccess/createCollection.ts | 5 +- src/Common/dataAccess/queryDocuments.ts | 2 + src/Contracts/DataModels.ts | 23 ++- .../CollapsibleSectionComponent.tsx | 6 +- src/Explorer/Controls/Editor/EditorReact.tsx | 11 +- .../Controls/Settings/SettingsComponent.less | 21 ++- .../Controls/Settings/SettingsComponent.tsx | 19 +++ .../ComputedPropertiesComponent.tsx | 2 +- .../ContainerVectorPolicyComponent.tsx | 30 ++++ .../IndexingPolicyComponent.tsx | 8 +- .../ComputedPropertiesComponent.test.tsx.snap | 2 +- .../IndexingPolicyComponent.test.tsx.snap | 2 +- .../Controls/Settings/SettingsUtils.tsx | 3 + .../SettingsComponent.test.tsx.snap | 1 + src/Explorer/Panes/AddCollectionPanel.tsx | 146 +++++++++++++++++- src/Explorer/Panes/PanelComponent.less | 7 + .../AddCollectionPanel.test.tsx.snap | 5 +- src/Utils/CapabilityUtils.ts | 4 + .../generatedClients/cosmos/sqlResources.ts | 2 +- .../arm/generatedClients/cosmos/types.ts | 21 +++ 24 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx diff --git a/package-lock.json b/package-lock.json index 8dcfe5d50..26c513d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.2", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", @@ -362,9 +362,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/cosmos": { - "version": "4.0.1-beta.2", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.2.tgz", - "integrity": "sha512-iuqg/QwLQlxgRi4pnXU8JUYv+f24wkRvJ9ZZI4/sYk+DxSgkuQ194Cc2IpckpeO8z7ZpcBkVQFa82wcZVVZ8Zg==", + "version": "4.0.1-beta.3", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.3.tgz", + "integrity": "sha512-CpRGt+S5jnvtGUi4TmlS79YvxpbNc8/5/QHgIvvQ9D2ZFUqO0MjbMCU3lVZV2NAJT02BsbLfRAFe+FPn8nMRQw==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", diff --git a/package.json b/package.json index 3a2f8b742..e1d544d1d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.2", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 0658d28c5..d56f5087a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -88,6 +88,7 @@ export class CapabilityNames { public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableServerless: string = "EnableServerless"; + public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; } export enum CapacityMode { diff --git a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap index 66e740abf..ec64a72b4 100644 --- a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap +++ b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap @@ -2,6 +2,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` Object { + "disableNonStreamingOrderByQuery": true, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 0, @@ -12,6 +13,7 @@ Object { exports[`getCommonQueryOptions reads from localStorage 1`] = ` Object { + "disableNonStreamingOrderByQuery": true, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 17, diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 73ccff3f4..1d9f41844 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getCollectionName } from "../../Utils/APITypeUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; -import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; import { createMongoCollectionWithProxy } from "../MongoProxyClient"; @@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr if (params.uniqueKeyPolicy) { resource.uniqueKeyPolicy = params.uniqueKeyPolicy; } + if (params.vectorEmbeddingPolicy) { + resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; + } const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { properties: { diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 0b8ebd29d..223fe987d 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,4 +1,5 @@ import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Queries } from "../Constants"; import { client } from "../CosmosClient"; @@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || Queries.itemsPerPage; options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); + options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled(); return options; }; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index e85f56964..3f7e3a7db 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -158,6 +158,7 @@ export interface Collection extends Resource { changeFeedPolicy?: ChangeFeedPolicy; analyticalStorageTtl?: number; geospatialConfig?: GeospatialConfig; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; schema?: ISchema; requestSchema?: () => void; computedProperties?: ComputedProperties; @@ -195,8 +196,14 @@ export interface IndexingPolicy { indexingMode: "consistent" | "lazy" | "none"; includedPaths: any; excludedPaths: any; - compositeIndexes?: any; - spatialIndexes?: any; + compositeIndexes?: any[]; + spatialIndexes?: any[]; + vectorIndexes?: VectorIndex[]; +} + +export interface VectorIndex { + path: string; + type: "flat" | "diskANN" | "quantizedFlat"; } export interface ComputedProperty { @@ -334,6 +341,18 @@ export interface CreateCollectionParams { partitionKey?: PartitionKey; uniqueKeyPolicy?: UniqueKeyPolicy; createMongoWildcardIndex?: boolean; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; +} + +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + dataType: "float16" | "float32" | "uint8" | "int8"; + dimensions: number; + distanceFunction: "euclidean" | "cosine" | "dotproduct"; + path: string; } export interface ReadDatabaseOfferParams { diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index d943f7cf0..207642d84 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component { @@ -75,9 +78,11 @@ export class EditorReact extends React.Component - {!this.state.showEditor && } + {!this.state.showEditor && ( + + )}
this.setRef(elt)} /> @@ -119,7 +124,7 @@ export class EditorReact extends React.Component, }); + if (this.isVectorSearchEnabled) { + tabs.push({ + tab: SettingsV2TabTypes.ContainerVectorPolicyTab, + content: , + }); + } + if (this.shouldShowIndexingPolicyEditor) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx index ad3b12fec..c8650b988 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx @@ -121,7 +121,7 @@ export class ComputedPropertiesComponent extends React.Component<   about how to define computed properties and how to use them. -
+
); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx new file mode 100644 index 000000000..23ccdc84c --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx @@ -0,0 +1,30 @@ +import { Stack } from "@fluentui/react"; +import { VectorEmbeddingPolicy } from "Contracts/DataModels"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import React from "react"; + +export interface ContainerVectorPolicyComponentProps { + vectorEmbeddingPolicy: VectorEmbeddingPolicy; +} + +export const ContainerVectorPolicyComponent: React.FC = ({ + vectorEmbeddingPolicy, +}) => { + return ( + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 4d6ca765f..bc5de93a4 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps { logIndexingPolicySuccessMessage: () => void; indexTransformationProgress: number; refreshIndexTransformationProgress: () => Promise; + isVectorSearchEnabled?: boolean; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; } @@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component< indexTransformationProgress={this.props.indexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} /> + {this.props.isVectorSearchEnabled && ( + + Container vector policies and vector indexes are not modifiable after container creation + + )} {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {unsavedEditorWarningMessage("indexPolicy")} )} -
+
); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap index 86a9bda74..2f67d7d70 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap @@ -29,7 +29,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `   about how to define computed properties and how to use them.
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap index 1f66324f5..93516dd7e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap @@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = ` refreshIndexTransformationProgress={[Function]} />
diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 24ef14681..cff7d1f74 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -47,6 +47,7 @@ export enum SettingsV2TabTypes { IndexingPolicyTab, PartitionKeyTab, ComputedPropertiesTab, + ContainerVectorPolicyTab, } export interface IsComponentDirtyResult { @@ -152,6 +153,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; + case SettingsV2TabTypes.ContainerVectorPolicyTab: + return "Container Vector Policy (preview)"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b536bed8a..561368bdd 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -196,6 +196,7 @@ exports[`SettingsComponent renders 1`] = ` "indexingMode": "consistent", } } + isVectorSearchEnabled={false} logIndexingPolicySuccessMessage={[Function]} onIndexingPolicyContentChange={[Function]} onIndexingPolicyDirtyChange={[Function]} diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 8c1cfef5b..7bb0f553d 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; import { SubscriptionType } from "Contracts/SubscriptionType"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import React from "react"; @@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { getCollectionName } from "Utils/APITypeUtils"; -import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"; +import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { getUpsellMessage } from "Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; @@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = { excludedPaths: [], }; +const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + }, + ], + excludedPaths: [ + { + path: '/"_etag"/?', + }, + ], + vectorIndexes: [], +}; + +export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { + vectorEmbeddings: [], +}; + export interface AddCollectionPanelState { createNewDatabase: boolean; newDatabaseId: string; @@ -101,6 +122,8 @@ export interface AddCollectionPanelState { isExecuting: boolean; isThroughputCapExceeded: boolean; teachingBubbleStep: number; + vectorIndexingPolicy: string; + vectorEmbeddingPolicy: string; } export class AddCollectionPanel extends React.Component { @@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component +
{this.state.errorMessage && ( )} - + {this.shouldShowVectorSearchParameters() && ( + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + this.setVectorIndexingPolicy(newIndexingPolicy)} + /> + + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + + this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy) + } + /> + + + + )} {userContext.apiType !== "Tables" && ( { TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); - this.scrollToAdvancedSection(); + this.scrollToSection("collapsibleAdvancedSectionContent"); }} > - + {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && ( @@ -1070,6 +1160,18 @@ export class AddCollectionPanel extends React.Component
-`; \ No newline at end of file +`; diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 216eff13d..8b6976666 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -16,3 +16,7 @@ export const isServerlessAccount = (): boolean => { isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) ); }; + +export const isVectorSearchEnabled = (): boolean => { + return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch); +}; diff --git a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts index f85ddb636..049e265e9 100644 --- a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -9,7 +9,7 @@ import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -const apiVersion = "2024-02-15-preview"; +const apiVersion = "2024-05-15-preview"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 4f69d223c..5272f215b 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -1235,6 +1235,9 @@ export interface SqlDatabaseResource { export interface SqlContainerResource { /* Name of the Cosmos DB SQL container */ id: string; + + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + /* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */ indexingPolicy?: IndexingPolicy; @@ -1267,6 +1270,17 @@ export interface SqlContainerResource { computedProperties?: ComputedProperty[]; } +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + path?: string; + dataType?: string; + dimensions?: number; + distanceFunction?: string; +} + /* Cosmos DB indexing policy */ export interface IndexingPolicy { /* Indicates if the indexing policy is automatic */ @@ -1285,6 +1299,13 @@ export interface IndexingPolicy { /* List of spatial specifics */ spatialIndexes?: SpatialSpec[]; + + vectorIndexes?: VectorIndex[]; +} + +export interface VectorIndex { + path?: string; + type?: string; } /* undocumented */ From 19d1e0d1df5f4206dc40301201a812c3946740d6 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 20 May 2024 19:24:04 -0500 Subject: [PATCH 102/102] allow serverless accounts to have vector search embeddings (#1844) --- .../SettingsSubComponents/ContainerVectorPolicyComponent.tsx | 2 +- src/Explorer/Panes/AddCollectionPanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx index 23ccdc84c..ca4d63a04 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx @@ -15,7 +15,7 @@ export const ContainerVectorPolicyComponent: React.FC