Compare commits

..

29 Commits

Author SHA1 Message Date
MokireddySampath
c32b859f83 Update QuickstartCarousel.tsx 2023-09-27 19:55:47 +05:30
MokireddySampath
5358333384 Update QuickstartCarousel.tsx 2023-09-27 19:50:18 +05:30
Sampath
e45abde0bf File name changed, since the video is not playing in the portal's data explorer blade 2023-09-26 09:45:28 +05:30
Sampath
551bc0f973 "Dummy video has been added and instead of using the reactyoutube library using hte native
video player "
2023-09-25 12:16:06 +05:30
Armando Trejo Oliver
bb82915cc6 Add wildcard to allow any fabric test origin (#1617) 2023-09-24 17:53:55 -07:00
bogercraig
c59d31f4c0 File Upload Tooltip (#1621)
* Added tooltip to the filename and status columns.  There is an InfoTooltip function that can reduce the boilerplate.

* Changed tooltip to be constructed with the DetailsList column and changed orientation to rightCenter.
That orientation prevents issues with topCenter orientation getting off center from the width of the cell.
Also looks a bit nicer, but will get feedback.

* Updated formatting.

* Removing uneccessary column formatting.

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-09-20 19:34:22 -04:00
sunghyunkang1111
8fa2721eab Update graph endpoint (#1623)
* Update graph endpoint

* Update graph endpoint
2023-09-20 10:39:37 -05:00
vchske
754354dbf9 Adds unit tests for vcore quickstart feature code (#1618)
* Safety checkin

* Adding vcoremongo to Main

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Adding message

* Integrated mongo shell

* Moving Juno endpoint back to prod

* Fixed command bar unit tests

* Fixing spelling

* Adds unit tests for vcore quickstart feature code

* Fix lint
2023-09-19 17:12:41 -07:00
vchske
ae5811306b Minor QOL changes (#1619) 2023-09-19 17:06:40 -07:00
Armando Trejo Oliver
f84deea9bc Fix query copilot help links (#1620) 2023-09-19 14:40:57 -07:00
Predrag Klepic
260c99e15c [Query Copilot V2] Explanation bubble added buttons (#1609)
* Fixing naming convention

* Additional implementation for Explanation

* Added snaps

* Removing snapshots

* re-updated snapshots

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-09-19 10:17:51 +02:00
v-darkora
c1c12019da Add feature flag for fixed output editor height or calculated height (#1606) 2023-09-18 11:30:17 +02:00
Armando Trejo Oliver
4e5358185f Add Fabric edog origin to allowed ancestors (#1616) 2023-09-16 19:45:07 -07:00
Vsevolod Kukol
b4bc93ac03 Fix active tabs border on Fabric (#1614) 2023-09-15 22:15:33 +02:00
Vsevolod Kukol
c61788198f Allow iframe within Fabric Extension iframe (#1613) 2023-09-15 21:44:44 +02:00
Vsevolod Kukol
379395567c Initial Fabric support (#1607)
* Add Platform.Fabric to run in context of Fabric

* Use separate StyleConstants

We want to have more flexibility with Styles at runtime
but Constants depend on ConfigContext and therefore
get loaded very early at startup.

* Add Fabric specific styles and Fluent theme

documentDBFabric.less contains all styles for Fabric.
We use React.lazy to import them conditionally at
runtime preventing webpack from preprocessing
them into main.css.

* Restyle CommandBar for Fabric
with more roundness and native colors.

* Disable Notebooks when running in Fabric

* Disable Synapse and Scripts commands for Fabric

* Fix code formatting issues

* Fetch encrypted token from sessionStorage for fabric platform

* Fix Tabs style

* Dark refresh icons for Fabric

* Use new ResourceTree2 for Fabric

* Fluent tree should have a fixed width
otherwise the action buttons jump around on hover.

* Disable remaining Script actions in Fabric

* Revert accidentally committed change
which broke a test

* Fix cross-origin error second try

* Adjust @FabrixBoxMargin css

* Hide Database Scale node on Fabric

* Remove all Collection child nodes on Fabric

* Add a comment about why we need FabricPlatform.tsx

* Fix equality checks

* Fix more Colors for Fabric

* Switch resource tree to "medium" size

* Fix formatting again

* Fix formatting again

* Disable no-var-requires error on some intended var import.

* Increase memory limit for build

* Use standard Segoe UI font for Fabric

* Improve Tabs design for Fabric

* Fix active Tab style bug in Portal
introduced with 39a7765aef

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2023-09-15 17:33:27 +02:00
Laurent Nguyen
c2d2ff3dee Update CSP header in web.config to allow iframe within fabric (#1611) 2023-09-15 06:58:15 +02:00
sindhuba
53fd738982 Add additional checks for NPS survey (#1610)
* Add additional checks for NPS survey

* Fix variable name

* Fix lint errors
2023-09-14 11:53:42 -07:00
vchske
c07000a5c2 Adding vcore mongo quickstart (#1600)
* Safety checkin

* Adding vcoremongo to Main

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Adding message

* Integrated mongo shell

* Moving Juno endpoint back to prod

* Fixed command bar unit tests

* Fixing spelling
2023-09-12 18:03:59 -07:00
Armando Trejo Oliver
135a409f0c Enable copilot by default (#1608) 2023-09-12 15:40:26 -07:00
Laurent Nguyen
93b0101d4c Create new ResourceTree based on FluentUI Tree (#1603)
* Alternate tree running fluentui v9 Tree component

* Fix tree update after sp, udf and trigger load

* Enable scrolling for subtrees

* Clean up duplicates

* Restore current tree

* Reformat

* Update package-lock.json
2023-09-12 17:23:13 +02:00
v-darkora
0408a53121 Change deafult schema feature flag (#1604) 2023-09-12 07:17:10 -07:00
Predrag Klepic
12ed591634 Adjusted Extecute Query logic (#1605)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-09-11 17:22:30 +02:00
Predrag Klepic
76408e2f98 [Query Copilot V2] Wire and adjust Output bubble with backend communication (#1599)
* Initial wiring of copilot backend and bubble

* Additional changes in explanation bubbles

* Changes based on checks

* test snapshots updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-09-06 19:49:27 +02:00
sindhuba
8c2ca4ab8e Support NPS for PostGres (#1598) 2023-09-06 08:22:54 -07:00
vchske
98bf84d09d Adding contract to open vcore mongo connection string blade (#1601)
* Adding a contract to open vcore mongo networking blade

* prettier fix

* Adding contract to open vcore mongo connection string blade
2023-09-01 16:37:24 -07:00
bogercraig
d4c831ff91 Users/bogercraig/blank ttl default value (#1596)
* Initial rough of keeping textfield blank.  Still need to disable save button.

* Rough implementation with state moved up to settings component.  Allows for discarding of new TTL when TTL is already enabled.

* Updating unit tests to include new display variable.

* Brought formatting back from master.

* Updating unit test snapshots.

* Ran prettier and renormalized modified files.

* Correct lint issues.

* Undo prettier changes to add collection code and testing snapshot.

* Restoring AddCollectionPanel to match master.  Not modifying snapshot.

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-09-01 14:46:49 -04:00
vchske
1eb566ab57 Adding a contract to open vcore mongo networking blade (#1597)
* Adding a contract to open vcore mongo networking blade

* prettier fix
2023-08-31 17:50:15 -07:00
Predrag Klepic
d155407b58 [Query Copilot] Phoenix Gateway flag true by default (#1595)
* Phoenix Gateway flag true by default

* Additional changes for clearness

* removal of console log

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-31 17:01:11 -07:00
114 changed files with 3337 additions and 982 deletions

View File

@@ -85,6 +85,8 @@ jobs:
path: .cache
key: ${{ runner.os }}-build-cache
- run: npm run pack:prod
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v2

View File

@@ -23,5 +23,6 @@
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.preferences.importModuleSpecifier": "non-relative"
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

20
images/Table.svg Normal file
View File

@@ -0,0 +1,20 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.11108 12.8623H38.8889V33.9245C38.8889 34.2593 38.7559 34.5803 38.5192 34.8171C38.2825 35.0538 37.9614 35.1867 37.6266 35.1867H2.37331C2.03854 35.1867 1.71749 35.0538 1.48078 34.8171C1.24407 34.5803 1.11108 34.2593 1.11108 33.9245V12.8623Z" fill="url(#paint0_linear_307_12833)"/>
<path d="M2.37997 4.81349H37.62C37.7857 4.81349 37.9499 4.84614 38.103 4.90958C38.2561 4.97301 38.3953 5.06598 38.5125 5.18319C38.6297 5.3004 38.7227 5.43955 38.7861 5.59269C38.8495 5.74582 38.8822 5.90996 38.8822 6.07572V12.8624H1.11108V6.07572C1.11108 5.9094 1.14395 5.74472 1.2078 5.59114C1.27165 5.43756 1.36522 5.29812 1.48313 5.18083C1.60105 5.06353 1.74098 4.97069 1.89489 4.90766C2.0488 4.84462 2.21366 4.81262 2.37997 4.81349V4.81349Z" fill="#0078D4"/>
<path opacity="0.9" d="M12.6021 15.8242H5.74876C5.40144 15.8242 5.11987 16.1058 5.11987 16.4531V19.3064C5.11987 19.6538 5.40144 19.9353 5.74876 19.9353H12.6021C12.9494 19.9353 13.231 19.6538 13.231 19.3064V16.4531C13.231 16.1058 12.9494 15.8242 12.6021 15.8242Z" fill="white"/>
<path opacity="0.9" d="M23.4821 15.7666H16.6288C16.2814 15.7666 15.9999 16.0482 15.9999 16.3955V19.2488C15.9999 19.5961 16.2814 19.8777 16.6288 19.8777H23.4821C23.8294 19.8777 24.111 19.5961 24.111 19.2488V16.3955C24.111 16.0482 23.8294 15.7666 23.4821 15.7666Z" fill="white"/>
<path opacity="0.9" d="M34.3621 15.7666H27.5088C27.1614 15.7666 26.8799 16.0482 26.8799 16.3955V19.2488C26.8799 19.5961 27.1614 19.8777 27.5088 19.8777H34.3621C34.7094 19.8777 34.991 19.5961 34.991 19.2488V16.3955C34.991 16.0482 34.7094 15.7666 34.3621 15.7666Z" fill="white"/>
<path opacity="0.9" d="M12.7221 21.7051H5.86876C5.52143 21.7051 5.23987 21.9866 5.23987 22.334V25.1873C5.23987 25.5346 5.52143 25.8162 5.86876 25.8162H12.7221C13.0694 25.8162 13.351 25.5346 13.351 25.1873V22.334C13.351 21.9866 13.0694 21.7051 12.7221 21.7051Z" fill="white"/>
<path d="M23.6021 21.6465H16.7488C16.4014 21.6465 16.1199 21.928 16.1199 22.2754V25.1287C16.1199 25.476 16.4014 25.7576 16.7488 25.7576H23.6021C23.9494 25.7576 24.231 25.476 24.231 25.1287V22.2754C24.231 21.928 23.9494 21.6465 23.6021 21.6465Z" fill="#ECF4FD"/>
<path d="M34.4821 21.6465H27.6288C27.2814 21.6465 26.9999 21.928 26.9999 22.2754V25.1287C26.9999 25.476 27.2814 25.7576 27.6288 25.7576H34.4821C34.8294 25.7576 35.111 25.476 35.111 25.1287V22.2754C35.111 21.928 34.8294 21.6465 34.4821 21.6465Z" fill="#ECF4FD"/>
<path d="M12.7221 27.6426H5.86876C5.52143 27.6426 5.23987 27.9241 5.23987 28.2715V31.1248C5.23987 31.4721 5.52143 31.7537 5.86876 31.7537H12.7221C13.0694 31.7537 13.351 31.4721 13.351 31.1248V28.2715C13.351 27.9241 13.0694 27.6426 12.7221 27.6426Z" fill="#ECF4FD"/>
<path d="M23.6021 27.585H16.7488C16.4014 27.585 16.1199 27.8665 16.1199 28.2139V31.0672C16.1199 31.4145 16.4014 31.6961 16.7488 31.6961H23.6021C23.9494 31.6961 24.231 31.4145 24.231 31.0672V28.2139C24.231 27.8665 23.9494 27.585 23.6021 27.585Z" fill="#ECF4FD"/>
<path d="M34.4821 27.585H27.6288C27.2814 27.585 26.9999 27.8665 26.9999 28.2139V31.0672C26.9999 31.4145 27.2814 31.6961 27.6288 31.6961H34.4821C34.8294 31.6961 35.111 31.4145 35.111 31.0672V28.2139C35.111 27.8665 34.8294 27.585 34.4821 27.585Z" fill="#ECF4FD"/>
<defs>
<linearGradient id="paint0_linear_307_12833" x1="20" y1="35.1867" x2="20" y2="12.8623" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="0.502" stop-color="#4093E6"/>
<stop offset="0.775" stop-color="#5EA0EF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -10,6 +10,7 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@NoColor: "brightness(0) saturate(100%)";
@xSmallFontSize: 4px;
@smallFontSize: 8px;
@@ -147,14 +148,41 @@
// CommandBar
@CommandBarButtonHeight: 40px;
/**********************************************************************************
Portal Consts
/**********************************************************************************/
@PortalAccentMediumHigh: #0058ad;
@PortalAccentMedium: #004e87;
@PortalAccentLight: #eef7ff;
@PortalAccentAccentExtra: #ddf0ff;
/**********************************************************************************
Fabric Consts
/**********************************************************************************/
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
@FabricBoxMargin: 4px 3px 4px 3px;
@FabricAccentMediumHigh: #0c695a;
@FabricAccentMedium: #117865;
@FabricAccentLight: #f5f5f5;
@FabricAccentExtra: #ebebeb;
@FabricButtonBorderRadius: 4px;
/**********************************************************************************
Common Flex Property
/**********************************************************************************/
.flex-display(@display: flex) {
display: ~"-webkit-@{display}";
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display: ~"-ms-@{display}"; // IE11
display:~"-webkit-@{display}";
display:~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display:~"-ms-@{display}"; // IE11
display: @display;
}
@@ -168,13 +196,15 @@
High contrast mode active
**************************************************************************************/
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
@media all and (-ms-high-contrast: none),
(-ms-high-contrast: active) {
.selectedRadio,
.selectedRadio:hover,
.selectedRadio:active,
.selectedRadio.dirty,
.tab [type="radio"]:checked ~ label,
.tab [type="radio"]:checked ~ label:hover {
.tab [type="radio"]:checked~label,
.tab [type="radio"]:checked~label:hover {
-ms-high-contrast-adjust: none;
-webkit-text-fill-color: HighlightText;
color: HighlightText;
@@ -183,6 +213,7 @@
}
.queryMetricsSummaryTuple {
th,
td {
&:nth-child(2) {
@@ -302,4 +333,4 @@
width: 0;
height: 0;
border-color: @InfoPointerColor transparent;
}
}

View File

@@ -2646,6 +2646,11 @@ a:link {
width: @ActiveTabWidth;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0,120,212,1);
}
.nav-tabs > li.active:focus > .tabNavContentContainer {
.focus();
}

211
less/documentDBFabric.less Normal file
View File

@@ -0,0 +1,211 @@
@import "./Common/Constants";
html {
font-family: @FabricFont;
}
body {
font-family: @FabricFont;
background-color: #f5f5f5;
}
a {
color: @FabricAccentMedium;
text-decoration: none;
}
a:hover,
a:focus {
color: @FabricAccentMediumHigh;
text-decoration: underline;
}
#divExplorer {
background-color: #f5f5f5;
}
.resourceTreeAndTabs {
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 4px;
background-color: #ffffff;
}
.tabsManagerContainer {
background-color: #fafafa
}
.nav-tabs-margin {
padding-top: 8px;
background-color: #fafafa
}
.commandBarContainer {
background-color: #ffffff;
border-bottom: none;
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
padding-top: 2px;
}
.dividerContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
.flex-display();
span {
border-left: @ButtonBorderWidth solid @BaseMedium;
margin: 0 10px 0 10px;
}
}
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
border-bottom: 2px solid #e0e0e0;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
border-bottom: 2px solid @FabricAccentMedium;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText {
border-bottom: 0px none transparent;
}
.tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
&:hover {
background-color: transparent;
border-color: transparent;
}
.tab_Content {
border-right: 0px none transparent;
margin: 0px @SmallSpace 0px @SmallSpace;
width: calc(@TabsWidth - (@SmallSpace * 2));
padding-bottom: @SmallSpace;
.statusIconContainer {
margin-left: 0px;
}
.tabIconSection {
.cancelButton {
padding: 0px 0px 0px @SmallSpace;
&:hover {
background-color: transparent;
}
&:focus {
background-color: transparent;
}
&:active {
background-color: transparent;
}
}
}
}
}
.resourceTree {
padding: 12px;
}
.accordion {
.accordionItemContainer {
.accordionItemHeader {
border-radius: 4px;
}
}
}
.treeComponent {
.nodeItem {
&:focus {
outline: 2px @FabricAccentMedium;
}
.treeNodeHeader {
padding: 5px 5px;
border-radius: 4px;
&:hover {
background-color: @FabricAccentLight;
.treeMenuEllipsis {
opacity: 1;
}
}
&.showingMenu {
background-color: #eee;
}
}
.selected {
&>.treeNodeHeader {
background-color: @FabricAccentExtra;
}
}
}
}
.dataExplorerErrorConsoleContainer {
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
width: auto;
align-self: auto;
}
.filterbtnstyle {
background: #fff;
color: #000;
border: solid 1px #d1d1d1;
border-radius: 4px;
}
.filterbtnstyle:hover {
background: @FabricAccentLight;
color: #000;
border: solid 1px #d1d1d1;
}
.filterbtnstyle:active {
background: @FabricAccentLight;
color: #000;
border: solid 1px #d1d1d1;
}
.filterbtnstyle:focus {
background: #fff;
color: #000;
border: solid 1px #d1d1d1;
}
.gridRowSelected .tabdocumentsGridElement:hover {
background-color: @FabricAccentLight !important;
}
.refreshcol {
filter: brightness(0) saturate(100%);
}
.refreshcol1 {
filter: brightness(0) saturate(100%);
}
.fileImportImg img {
filter: brightness(0) saturate(100%);
}

8
package-lock.json generated
View File

@@ -196,6 +196,14 @@
"uuid": "^8.3.0"
},
"dependencies": {
"@azure/core-tracing": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz",
"integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==",
"requires": {
"tslib": "^2.2.0"
}
},
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",

View File

@@ -199,6 +199,7 @@
"pack:fast": "webpack --mode development --progress",
"copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest",
"test:debug": "jest --runInBand",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
"watch": "npm run start",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",

View File

@@ -365,9 +365,6 @@ export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;

View File

@@ -1,12 +1,14 @@
import { ResourceTree } from "Explorer/Tree/ResourceTree";
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { Platform, configContext } from "./../ConfigContext";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
@@ -76,6 +78,8 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : configContext.platform === Platform.Fabric ? (
<ResourceTree2 container={container} />
) : (
<ResourceTree container={container} />
)}

View File

@@ -0,0 +1,18 @@
import { Platform, configContext } from "../ConfigContext";
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export function updateStyles(): void {
if (configContext.platform === Platform.Fabric) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
StyleConstants.AccentAccentExtra = StyleConstants.FabricAccentMediumHigh;
} else {
StyleConstants.AccentMediumHigh = StyleConstants.PortalAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.PortalAccentMedium;
StyleConstants.AccentLight = StyleConstants.PortalAccentLight;
StyleConstants.AccentAccentExtra = StyleConstants.PortalAccentMediumHigh;
}
}

View File

@@ -16,6 +16,7 @@ export enum Platform {
Portal = "Portal",
Hosted = "Hosted",
Emulator = "Emulator",
Fabric = "Fabric",
}
export interface ConfigContext {
@@ -67,7 +68,7 @@ let configContext: Readonly<ConfigContext> = {
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.windows.net",
GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
@@ -187,6 +188,7 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
console.error(`Invalid platform query parameter: ${platform}`);
break;
case Platform.Portal:
case Platform.Fabric:
case Platform.Hosted:
case Platform.Emulator:
updateConfigContext({ platform });

View File

@@ -1,11 +1,14 @@
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
export interface DatabaseAccount {
export interface ArmEntity {
id: string;
name: string;
location: string;
type: string;
kind: string;
}
export interface DatabaseAccount extends ArmEntity {
properties: DatabaseAccountExtendedProperties;
systemData?: DatabaseAccountSystemData;
}
@@ -35,6 +38,7 @@ export interface DatabaseAccountExtendedProperties {
locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
publicNetworkAccess?: string;
vcoreMongoEndpoint?: string;
}
export interface DatabaseAccountResponseLocation {
@@ -575,7 +579,7 @@ export interface ContainerConnectionInfo {
//need to add ram and rom info
}
export interface PostgresFirewallRule {
export interface FirewallRule {
id: string;
name: string;
type: string;

View File

@@ -39,6 +39,8 @@ export enum MessageTypes {
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
}
export { Versions, ActionContracts, Diagnostics };
export { ActionContracts, Diagnostics, Versions };

View File

@@ -372,6 +372,7 @@ export enum TerminalKind {
Mongo = 1,
Cassandra = 2,
Postgres = 3,
VCoreMongo = 4,
}
export interface DataExplorerInputsFrame {
@@ -397,6 +398,7 @@ export interface DataExplorerInputsFrame {
defaultCollectionThroughput?: CollectionCreationDefaults;
isPostgresAccount?: boolean;
isReplica?: boolean;
isVCoreMongoAccount?: boolean;
clientIpAddress?: string;
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
connectionStringParams?: any;

View File

@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { StyleConstants } from "../../Common/Constants";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { StyleConstants } from "../../Common/StyleConstants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";

View File

@@ -18,6 +18,7 @@ import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
@@ -99,11 +100,13 @@ export const createCollectionContextMenuButton = (
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
if (
configContext.platform !== Platform.Fabric &&
(userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
) {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
@@ -112,7 +115,6 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: "New UDF",
@@ -121,7 +123,6 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
@@ -130,13 +131,15 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () =>
onClick: () => {
useSelectedNode.getState().setSelectedNode(selectedCollection);
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
);
},
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});

View File

@@ -32,6 +32,20 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
},
};
const testPostgresAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
postgresqlEndpoint: "https://testPostgresEndpoint.azure.com/",
},
};
const testVCoreMongoAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
vcoreMongoEndpoint: "https://testVCoreMongoEndpoint.azure.com/",
},
};
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
@@ -50,6 +64,18 @@ const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInf
forwardingId: "Id",
};
const testPostgresNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/postgresql",
forwardingId: "Id",
};
const testVCoreMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongovcore",
forwardingId: "Id",
};
describe("NotebookTerminalComponent", () => {
it("renders terminal", () => {
const props: NotebookTerminalComponentProps = {
@@ -94,4 +120,27 @@ describe("NotebookTerminalComponent", () => {
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders Postgres shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testPostgresAccount,
notebookServerInfo: testPostgresNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders vCore Mongo shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testVCoreMongoAccount,
notebookServerInfo: testVCoreMongoNotebookServerInfo,
tabId: undefined,
username: "username",
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -14,6 +14,7 @@ export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount;
tabId: string;
username?: string;
}
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
@@ -50,7 +51,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return;
}
const props: TerminalProps = {
let props: TerminalProps = {
terminalEndpoint: this.tryGetTerminalEndpoint(),
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
authToken: this.props.notebookServerInfo?.authToken,
@@ -61,6 +62,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
tabId: this.props.tabId,
};
if (this.props.username) {
props = {
...props,
username: this.props.username,
};
}
postRobot.send(this.terminalWindow, "props", props, {
domain: window.location.origin,
});
@@ -78,6 +86,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return this.props.databaseAccount?.properties.postgresqlEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
}
if (terminalEndpoint) {

View File

@@ -1,5 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookTerminalComponent renders Postgres shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
<div
className="notebookTerminalContainer"
@@ -47,3 +59,15 @@ exports[`NotebookTerminalComponent renders terminal 1`] = `
/>
</div>
`;
exports[`NotebookTerminalComponent renders vCore Mongo shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;

View File

@@ -23,9 +23,9 @@ import * as React from "react";
import * as _ from "underscore";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { QueriesClient } from "../../../Common/QueriesClient";
import { StyleConstants } from "../../../Common/StyleConstants";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -5,18 +5,18 @@ import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
@@ -37,15 +37,15 @@ import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
MongoIndexTypes,
SettingsV2TabTypes,
TtlType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDirty,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType,
} from "./SettingsUtils";
interface SettingsV2TabInfo {
@@ -78,6 +78,8 @@ export interface SettingsComponentState {
timeToLiveBaseline: TtlType;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
displayedTtlSeconds: string;
displayedTtlSecondsBaseline: string;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
analyticalStorageTtlSelection: TtlType;
@@ -164,6 +166,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
timeToLiveBaseline: undefined,
timeToLiveSeconds: undefined,
timeToLiveSecondsBaseline: undefined,
displayedTtlSeconds: undefined,
displayedTtlSecondsBaseline: undefined,
geospatialConfigType: undefined,
geospatialConfigTypeBaseline: undefined,
analyticalStorageTtlSelection: undefined,
@@ -369,6 +373,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
throughput: this.state.throughputBaseline,
timeToLive: this.state.timeToLiveBaseline,
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
indexesToAdd: [],
@@ -479,6 +484,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
private onDisplayedTtlChange = (newDisplayedTtlSeconds: string): void =>
this.setState({ displayedTtlSeconds: newDisplayedTtlSeconds });
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
@@ -608,6 +616,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
break;
}
const displayedTtlSeconds: string = timeToLive === TtlType.On ? timeToLiveSeconds.toString() : "";
let analyticalStorageTtlSelection: TtlType;
let analyticalStorageTtlSeconds: number;
if (this.isAnalyticalStorageEnabled) {
@@ -645,6 +655,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
timeToLiveBaseline: timeToLive,
timeToLiveSeconds: timeToLiveSeconds,
timeToLiveSecondsBaseline: timeToLiveSeconds,
displayedTtlSeconds: displayedTtlSeconds,
displayedTtlSecondsBaseline: displayedTtlSeconds,
analyticalStorageTtlSelection: analyticalStorageTtlSelection,
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
@@ -986,6 +998,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
timeToLiveSeconds: this.state.timeToLiveSeconds,
timeToLiveSecondsBaseline: this.state.timeToLiveSecondsBaseline,
onTimeToLiveSecondsChange: this.onTimeToLiveSecondsChange,
displayedTtlSeconds: this.state.displayedTtlSeconds,
onDisplayedTtlSecondsChange: this.onDisplayedTtlChange,
geospatialConfigType: this.state.geospatialConfigType,
geospatialConfigTypeBaseline: this.state.geospatialConfigTypeBaseline,
onGeoSpatialConfigTypeChange: this.onGeoSpatialConfigTypeChange,

View File

@@ -23,7 +23,8 @@ import {
Text,
} from "@fluentui/react";
import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { Urls } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { hoursInAMonth } from "../../../Shared/Constants";
import {
computeRUUsagePriceHourly,

View File

@@ -20,6 +20,10 @@ describe("SubSettingsComponent", () => {
onTimeToLiveSecondsChange: () => {
return;
},
displayedTtlSeconds: "1000",
onDisplayedTtlSecondsChange: () => {
return;
},
geospatialConfigType: GeospatialConfigType.Geography,
geospatialConfigTypeBaseline: GeospatialConfigType.Geography,

View File

@@ -15,13 +15,13 @@ import {
import {
ChangeFeedPolicyState,
GeospatialConfigType,
getSanitizedInputValue,
IsComponentDirtyResult,
isDirty,
TtlOff,
TtlOn,
TtlOnNoDefault,
TtlType,
getSanitizedInputValue,
isDirty,
} from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
@@ -34,6 +34,8 @@ export interface SubSettingsComponentProps {
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
displayedTtlSeconds: string;
onDisplayedTtlSecondsChange: (newDisplayedTtlSeconds: string) => void;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
@@ -73,7 +75,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
this.onComponentUpdate();
}
componentDidUpdate(): void {
componentDidUpdate(prevProps: SubSettingsComponentProps): void {
if (
(prevProps.timeToLive === TtlType.Off || prevProps.timeToLive === TtlType.OnNoDefault) &&
this.props.timeToLive === TtlType.On &&
this.props.timeToLiveBaseline !== TtlType.On
) {
this.props.onDisplayedTtlSecondsChange("");
}
this.onComponentUpdate();
}
@@ -93,7 +102,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public IsComponentDirty = (): IsComponentDirtyResult => {
if (
(this.props.timeToLive === TtlType.On && !this.props.timeToLiveSeconds) ||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds)
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds) ||
(this.props.timeToLive === TtlType.On && this.props.displayedTtlSeconds === "")
) {
return { isSaveable: false, isDiscardable: true };
} else if (
@@ -138,6 +148,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
newValue?: string
): void => {
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
this.props.onDisplayedTtlSecondsChange(newTimeToLiveSeconds.toString());
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
};
@@ -204,7 +215,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
value={this.props.displayedTtlSeconds}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/>

View File

@@ -150,12 +150,14 @@ exports[`SettingsComponent renders 1`] = `
"usageSizeInKB": [Function],
}
}
displayedTtlSeconds="5"
geospatialConfigType="Geometry"
geospatialConfigTypeBaseline="Geometry"
isAnalyticalStorageEnabled={false}
onAnalyticalStorageTtlSecondsChange={[Function]}
onAnalyticalStorageTtlSelectionChange={[Function]}
onChangeFeedPolicyChange={[Function]}
onDisplayedTtlSecondsChange={[Function]}
onGeoSpatialConfigTypeChange={[Function]}
onSubSettingsDiscardableChange={[Function]}
onSubSettingsSaveableChange={[Function]}

View File

@@ -1,6 +1,6 @@
import { shallow } from "enzyme";
import React from "react";
import { TreeComponent, TreeNode, TreeNodeComponent_old } from "./TreeComponent";
import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent";
const buildChildren = (): TreeNode[] => {
const grandChild11: TreeNode = {
@@ -98,7 +98,7 @@ describe("TreeNodeComponent", () => {
generation: 12,
paddingLeft: 23,
};
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -113,7 +113,7 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -128,7 +128,7 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -156,7 +156,7 @@ describe("TreeNodeComponent", () => {
generation: 12,
paddingLeft: 23,
};
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -172,7 +172,7 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -12,16 +12,16 @@ import {
IContextualMenuItemProps,
IContextualMenuProps,
} from "@fluentui/react";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Spinner, Tree, TreeItem, TreeItemLayout } from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
@@ -59,90 +59,13 @@ export interface TreeComponentProps {
export class TreeComponent extends React.Component<TreeComponentProps> {
public render(): JSX.Element {
return (
<Tree>
<TreeNodeComponent node={this.props.rootNode} />
</Tree>
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
</div>
);
}
}
export const TreeNodeComponent: React.FC<{ node: TreeNode, className?: string }> = ({ node }): JSX.Element => {
const { children } = node;
const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode) => child.label) : undefined;
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
return (
<TreeItem key={node.label} value={node.label} itemType={node.children?.length > 0 || node.isLoading ? "branch" : "leaf"}>
<TreeItemLayout
className={node.className}
onClick={() => node.onClick && node.onClick(false)}
actions={node.contextMenu &&
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
<MenuPopover>
<MenuList>{node.contextMenu.map(menuItem => (
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>{menuItem.label}</MenuItem>
))}</MenuList>
</MenuPopover>
</Menu>
}
expandIcon={node.isLoading ? <Spinner size="tiny" /> : undefined}
>{node.label}</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && <Tree defaultOpenItems={defaultOpenItems}>
{getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent key={childNode.id} node={childNode} />
))}
</Tree>}
</TreeItem>
);
};
// ------------------------------------------------------------
/* Tree node is a react component */
interface TreeNodeComponentProps {
node: TreeNode;
@@ -154,7 +77,7 @@ interface TreeNodeComponentState {
isExpanded: boolean;
isMenuShowing: boolean;
}
export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
private static readonly paddingPerGenerationPx = 16;
private static readonly iconOffset = 22;
private static readonly transitionDurationMS = 200;
@@ -175,9 +98,9 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
// Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) {
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent_old.callbackDelayMS);
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
} else {
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent_old.callbackDelayMS);
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
}
}
if (this.props.node.isExpanded !== this.isExpanded) {
@@ -223,13 +146,13 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
}
private renderNode(node: TreeNode, generation: number): JSX.Element {
let paddingLeft = generation * TreeNodeComponent_old.paddingPerGenerationPx;
let paddingLeft = generation * TreeNodeComponent.paddingPerGenerationPx;
let additionalOffsetPx = 15;
if (node.children) {
const childrenWithSubChildren = node.children.filter((child: TreeNode) => !!child.children);
if (childrenWithSubChildren.length > 0) {
additionalOffsetPx = TreeNodeComponent_old.iconOffset;
additionalOffsetPx = TreeNodeComponent.iconOffset;
}
}
@@ -237,10 +160,10 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
const showSelected =
this.props.node.isSelected &&
this.props.node.isSelected() &&
!TreeNodeComponent_old.isAnyDescendantSelected(this.props.node);
!TreeNodeComponent.isAnyDescendantSelected(this.props.node);
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
if (TreeNodeComponent_old.isNodeHeaderBlank(node)) {
if (TreeNodeComponent.isNodeHeaderBlank(node)) {
headerStyle.height = 0;
headerStyle.padding = 0;
}
@@ -272,10 +195,10 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div>
{node.children && (
<AnimateHeight duration={TreeNodeComponent_old.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label} role="group">
{TreeNodeComponent_old.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent_old
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
node={childNode}
generation={generation + 1}
@@ -298,7 +221,7 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
node.children &&
node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent_old.isAnyDescendantSelected(child),
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
false
)
);
@@ -309,13 +232,13 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
}
private onRightClick = (): void => {
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent_old.createClickEvent());
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
};
private renderContextMenuButton(node: TreeNode): JSX.Element {
const menuItemLabel = "More";
const buttonStyles: Partial<IButtonStyles> = {
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` },
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
};
return (
@@ -342,7 +265,7 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
<div
data-test={`treeComponentMenuItemContainer`}
className="treeComponentMenuItemContainer"
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent_old.createClickEvent())}
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
>
{props.item.onRenderIcon()}
<span
@@ -396,7 +319,7 @@ export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProp
if (node.children) {
const isExpanded = !this.state.isExpanded;
// Prevent collapsing if node header is blank
if (!(TreeNodeComponent_old.isNodeHeaderBlank(node) && !isExpanded)) {
if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
this.setState({ isExpanded });
}
}

View File

@@ -0,0 +1,142 @@
import {
Button,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
Spinner,
Tree,
TreeItem,
TreeItemLayout,
} from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import * as React from "react";
export interface TreeNode2MenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode2 {
label: string;
id?: string;
children?: TreeNode2[];
contextMenu?: TreeNode2MenuItem[];
iconSrc?: string;
// isExpanded?: boolean;
className?: string;
isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isScrollable?: boolean;
isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
}
export interface TreeNode2ComponentProps {
node: TreeNode2;
className?: string;
treeNodeId: string;
globalOpenIds: string[];
}
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />;
export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
node,
treeNodeId,
globalOpenIds,
}: TreeNode2ComponentProps): JSX.Element => {
// const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode2) => child.label) : undefined;
const [isExpanded, setIsExpanded] = React.useState<boolean>(false);
// Compute whether node is expanded
React.useEffect(() => {
const isNowExpanded = globalOpenIds && globalOpenIds.includes(treeNodeId);
if (!isExpanded && isNowExpanded) {
// Catch the transition non-expanded to expanded
node.onExpanded?.();
}
setIsExpanded(isNowExpanded);
}, [globalOpenIds, treeNodeId, node, isExpanded]);
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode2, b: TreeNode2) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode2[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode2[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
return (
<TreeItem value={treeNodeId} itemType={node.children !== undefined ? "branch" : "leaf"} style={{ height: "100%" }}>
<TreeItemLayout
className={node.className}
actions={
node.contextMenu && (
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
<MenuPopover>
<MenuList>
{node.contextMenu.map((menuItem) => (
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>
{menuItem.label}
</MenuItem>
))}
</MenuList>
</MenuPopover>
</Menu>
)
}
expandIcon={node.isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
>
<span onClick={() => node.onClick?.()}>{node.label}</span>
</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && (
<Tree
// defaultOpenItems={defaultOpenItems}
style={{ overflow: node.isScrollable ? "auto" : undefined }}
>
{getSortedChildren(node).map((childNode: TreeNode2) => (
<TreeNode2Component
key={childNode.label}
node={childNode}
treeNodeId={`${treeNodeId}/${childNode.label}`}
globalOpenIds={globalOpenIds}
/>
))}
</Tree>
)}
</TreeItem>
);
};

View File

@@ -1,7 +1,7 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform } from "ConfigContext";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { IGalleryItem } from "Juno/JunoClient";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
@@ -277,6 +277,20 @@ export default class Explorer {
userContext.databaseAccount?.systemData?.createdAt || "",
NINETY_DAYS_IN_MS
);
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
if (lastSubmitted !== null) {
let lastSubmittedDate: number = parseInt(lastSubmitted);
if (isNaN(lastSubmittedDate)) {
lastSubmittedDate = 0;
}
const nowMs: number = Date.now();
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
return;
}
}
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) {
@@ -285,17 +299,20 @@ export default class Explorer {
this.getRandomInt(100) < 25
) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
}
} else {
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
if (isAccountNewerThanNinetyDays) {
if (this.getRandomInt(100) < 10) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
}
} else {
// An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
if (this.getRandomInt(100) < 25) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
}
}
}
@@ -1127,6 +1144,10 @@ export default class Explorer {
title = "PSQL Shell";
break;
case ViewModels.TerminalKind.VCoreMongo:
title = "VCoreMongo Shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
@@ -1313,7 +1334,7 @@ export default class Explorer {
}
public async refreshExplorer(): Promise<void> {
if (userContext.apiType !== "Postgres") {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
@@ -1322,9 +1343,10 @@ export default class Explorer {
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures;
configContext.platform !== Platform.Fabric &&
(userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures);
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()

View File

@@ -8,7 +8,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType, StyleConstants } from "../../../Common/Constants";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -34,8 +36,11 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons);
const backgroundColor = StyleConstants.BaseLight;
if (userContext.apiType === "Postgres") {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
userContext.apiType === "Postgres"
? CommandBarComponentButtonFactory.createPostgreButtons(container)
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
return (
<div className="commandBarContainer">
<FluentCommandBar
@@ -81,15 +86,27 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
}
const rootStyle =
configContext.platform === Platform.Fabric
? {
root: {
backgroundColor: "transparent",
padding: "0px 14px 0px 14px",
},
}
: {
root: {
backgroundColor: backgroundColor,
},
};
return (
<div className="commandBarContainer">
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
farItems={uiFabricControlButtons}
styles={{
root: { backgroundColor: backgroundColor },
}}
styles={rootStyle}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
</div>

View File

@@ -142,8 +142,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Open Mongo Shell button", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
describe("Open Mongo shell button", () => {
const openMongoShellBtnLabel = "Open Mongo shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
@@ -247,8 +247,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
describe("Open Cassandra shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
@@ -343,6 +343,31 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Open Postgres and vCore Mongo buttons", () => {
const openPostgresShellButtonLabel = "Open PSQL shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (vcore) shell";
beforeAll(() => {
mockExplorer = {} as Explorer;
});
it("creates Postgres shell button", () => {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
const openPostgresShellButton = buttons.find(
(button) => button.commandButtonLabel === openPostgresShellButtonLabel
);
expect(openPostgresShellButton).toBeDefined();
});
it("creates vCore Mongo shell button", () => {
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
const openVCoreMongoShellButton = buttons.find(
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel
);
expect(openVCoreMongoShellButton).toBeDefined();
});
});
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";

View File

@@ -54,7 +54,11 @@ export function createStaticCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
if (
configContext.platform !== Platform.Fabric &&
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
@@ -94,9 +98,9 @@ export function createStaticCommandBarButtons(
) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container));
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
} else {
notebookButtons.push(createOpenMongoTerminalButton(container));
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
}
}
@@ -257,7 +261,9 @@ export function createDivider(): CommandButtonComponentProps {
}
function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
return (
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
@@ -499,8 +505,25 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
};
}
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
function createOpenTerminalButtonByKind(
container: Explorer,
terminalKind: ViewModels.TerminalKind
): CommandButtonComponentProps {
const terminalFriendlyName = (): string => {
switch (terminalKind) {
case ViewModels.TerminalKind.Cassandra:
return "Cassandra";
case ViewModels.TerminalKind.Mongo:
return "Mongo";
case ViewModels.TerminalKind.Postgres:
return "PSQL";
case ViewModels.TerminalKind.VCoreMongo:
return "MongoDB (vcore)";
default:
return "";
}
};
const label = `Open ${terminalFriendlyName()} shell`;
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
@@ -510,7 +533,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
container.openNotebookTerminal(terminalKind);
}
},
commandButtonLabel: label,
@@ -521,51 +544,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
};
}
function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open PSQL Shell";
const disableButton =
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton
? ""
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
};
}
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
@@ -630,7 +608,13 @@ function createStaticCommandBarButtonsForResourceToken(
}
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
return [openPostgreShellBtn];
}
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
return [openVCoreMongoTerminalButton];
}

View File

@@ -9,7 +9,9 @@ import {
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { PoolIdType, StyleConstants } from "../../../Common/Constants";
import { PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -24,11 +26,14 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const hoverColor =
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
};
return btns
@@ -68,6 +73,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
height: buttonHeightPx,
paddingRight: 0,
paddingLeft: 0,
borderRadius: configContext.platform == Platform.Fabric ? StyleConstants.FabricButtonBorderRadius : "0px",
minWidth: 24,
marginLeft: isSplit ? 0 : 5,
marginRight: isSplit ? 0 : 5,
@@ -79,17 +85,17 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
splitButtonMenuButton: {
backgroundColor: backgroundColor,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight },
":hover": { backgroundColor: hoverColor },
},
width: 16,
},
label: { fontSize: StyleConstants.mediumFontSize },
rootHovered: { backgroundColor: StyleConstants.AccentLight },
rootPressed: { backgroundColor: StyleConstants.AccentLight },
rootHovered: { backgroundColor: hoverColor },
rootPressed: { backgroundColor: hoverColor },
splitButtonMenuButtonExpanded: {
backgroundColor: StyleConstants.AccentExtra,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight },
":hover": { backgroundColor: hoverColor },
},
},
splitButtonDivider: {
@@ -120,7 +126,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
selectors: {
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
".ms-ContextualMenu-icon": { width: 16, height: 16 },
},
},

View File

@@ -3,7 +3,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { StyleConstants } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
interface Props {
lastSaved?: Date | null;

View File

@@ -1,6 +1,6 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
@@ -10,13 +10,13 @@ import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { useTabs } from "../../hooks/useTabs";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
@@ -124,7 +124,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
}
const firstWriteLocation =
userContext.apiType === "Postgres"
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
@@ -316,8 +316,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
isPhoenixFeatures =
isPublicInternetAllowed &&
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
(userContext.features.phoenixFeatures === true ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo");
} else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
}

View File

@@ -9,11 +9,11 @@ import {
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";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location {
type: "MyNotebooks" | "GitHub";
@@ -65,7 +65,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
options.push({
key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle,
text: GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header,
});

View File

@@ -1,8 +1,15 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react";
import {
DetailsList,
DetailsListLayoutMode,
DirectionalHint,
IColumn,
SelectionMode,
TooltipHost,
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -74,12 +81,21 @@ export const UploadItemsPane: FunctionComponent = () => {
];
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
let fieldContent: string;
const tooltipId = `tooltip-${index}-${column.key}`;
switch (column.key) {
case "status":
return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
break;
default:
return item.fileName;
fieldContent = item.fileName;
}
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
{fieldContent}
</TooltipHost>
);
};
return (

View File

@@ -11,8 +11,9 @@ import {
Stack,
Text,
} from "@fluentui/react";
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
import { QueryCopilotSampleDatabaseId } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { StyleConstants } from "Common/StyleConstants";
import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";

View File

@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
<Link href="https://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -97,7 +97,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>

View File

@@ -102,7 +102,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
Ask Copilot to generate a query by describing the query in your words.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
href="https://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>
@@ -138,7 +138,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
href="https://aka.ms/cdb-copilot-preview-terms"
>
Read preview terms
</StyledLinkBase>
@@ -174,7 +174,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
href="https://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>

View File

@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos";
import {
Callout,
CommandBarButton,
@@ -20,16 +19,11 @@ import { useBoolean } from "@fluentui/react-hooks";
import {
ContainerStatusType,
PoolIdType,
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -37,15 +31,11 @@ import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { OnExecuteQueryClick, SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useRef, useState } from "react";
@@ -83,8 +73,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
setSelectedQuery,
isGeneratingQuery,
setIsGeneratingQuery,
isExecuting,
setIsExecuting,
likeQuery,
setLikeQuery,
dislikeQuery,
@@ -93,12 +81,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
setShowCallout,
showSamplePrompts,
setShowSamplePrompts,
queryIterator,
setQueryIterator,
queryResults,
setQueryResults,
errorMessage,
setErrorMessage,
isSamplePromptsOpen,
setIsSamplePromptsOpen,
showDeletePopup,
@@ -109,7 +91,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
setshowCopyPopup,
showErrorMessageBar,
setShowErrorMessageBar,
generatedQueryComments,
setGeneratedQueryComments,
} = useQueryCopilot();
@@ -184,7 +165,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
useTabs.getState().setIsQueryErrorThrown(false);
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
userContext.features.enableCopilotPhoenixGateaway
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
@@ -196,9 +177,9 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
};
useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const queryUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
const response = await fetch(queryUri, {
method: "POST",
headers: {
@@ -238,64 +219,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
}
};
const onExecuteQueryClick = async (): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: userPrompt,
generatedQuery: generatedQuery,
generatedQueryComments: generatedQueryComments,
executedQuery: selectedQuery || query,
});
const queryToExecute = selectedQuery || query;
const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
} as FeedOptions);
setQueryIterator(queryIterator);
setTimeout(async () => {
await queryDocumentsPerPage(0, queryIterator);
}, 100);
};
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
try {
setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) =>
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
);
setQueryResults(queryResults);
setErrorMessage("");
setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) {
const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage: errorMessage,
});
setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
} finally {
setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false);
}
};
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = {
iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel,
onCommandClick: () => onExecuteQueryClick(),
onCommandClick: () => OnExecuteQueryClick(),
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
@@ -505,7 +434,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
}}
>
Learn about{" "}
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
@@ -519,7 +448,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
Read preview terms
</Link>
{showErrorMessageBar && (
@@ -622,16 +551,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
/>
<QueryResultSection
isMongoDB={false}
queryEditorContent={selectedQuery || query}
error={errorMessage}
queryResults={queryResults}
isExecuting={isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
queryDocumentsPerPage(firstItemIndex, queryIterator)
}
/>
<QueryCopilotResults />
</SplitterLayout>
</Stack>
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />

View File

@@ -50,9 +50,9 @@ describe("Query Copilot Client", () => {
forwardingId: "mocked-forwarding-id",
};
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "feedback");
it("should call fetch with the payload with like", async () => {
const mockFetch = jest.fn().mockResolvedValueOnce({});

View File

@@ -1,14 +1,24 @@
import { FeedOptions } from "@azure/cosmos";
import {
ContainerStatusType,
PoolIdType,
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
@@ -20,19 +30,17 @@ export const SendQueryRequest = async ({
explorer: Explorer;
}): Promise<void> => {
if (userPrompt.trim() !== "") {
useQueryCopilot.getState().setIsGeneratingQuery(true);
useQueryCopilot.getState().setShouldIncludeInMessages(true);
useQueryCopilot.getState().setShowQueryExplanation(false);
useQueryCopilot.getState().setShowExplanationBubble(false);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
useQueryCopilot
.getState()
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
useQueryCopilot.getState().setIsGeneratingQuery(true);
useQueryCopilot.getState().setShouldIncludeInMessages(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
userContext.features.enableCopilotPhoenixGateaway
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
@@ -40,9 +48,9 @@ export const SendQueryRequest = async ({
useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const queryUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
@@ -62,15 +70,17 @@ export const SendQueryRequest = async ({
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.ok) {
if (generateSQLQueryResponse?.sql) {
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
query += `\r\n${generateSQLQueryResponse.sql}`;
const bubbleMessage = `Here is a query which will help you with provided prompt.\r\n **Prompt:** "${userPrompt}"`;
if (useQueryCopilot.getState().shouldIncludeInMessages) {
useQueryCopilot
.getState()
.setChatMessages([
...useQueryCopilot.getState().chatMessages,
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
]);
useQueryCopilot.getState().setChatMessages([
...useQueryCopilot.getState().chatMessages,
{
source: 1,
message: bubbleMessage,
sqlQuery: generateSQLQueryResponse.sql,
explanation: generateSQLQueryResponse.explanation,
},
]);
useQueryCopilot.getState().setShowExplanationBubble(true);
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
@@ -113,14 +123,14 @@ export const SubmitFeedback = async ({
};
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
userContext.features.enableCopilotPhoenixGateaway
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "feedback")
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(serverInfo.notebookServerEndpoint, "feedback");
await fetch(feedbackUri, {
method: "POST",
headers: {
@@ -133,3 +143,57 @@ export const SubmitFeedback = async ({
handleError(error, "copilotSubmitFeedback");
}
};
export const OnExecuteQueryClick = async (): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: useQueryCopilot.getState().userPrompt,
generatedQuery: useQueryCopilot.getState().generatedQuery,
generatedQueryComments: useQueryCopilot.getState().generatedQueryComments,
executedQuery: useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query,
});
const queryToExecute = useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query;
const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
} as FeedOptions);
useQueryCopilot.getState().setQueryIterator(queryIterator);
setTimeout(async () => {
await QueryDocumentsPerPage(0, queryIterator);
}, 100);
};
export const QueryDocumentsPerPage = async (
firstItemIndex: number,
queryIterator: MinimalQueryIterator
): Promise<void> => {
try {
useQueryCopilot.getState().setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
);
useQueryCopilot.getState().setQueryResults(queryResults);
useQueryCopilot.getState().setErrorMessage("");
useQueryCopilot.getState().setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) {
const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage: errorMessage,
});
useQueryCopilot.getState().setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
useQueryCopilot.getState().setShowErrorMessageBar(true);
} finally {
useQueryCopilot.getState().setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false);
}
};

View File

@@ -11,11 +11,13 @@ export interface GenerateSQLQueryResponse {
enum MessageSource {
User,
AI,
AIExplanation,
}
export interface CopilotMessage {
source: MessageSource;
message: string;
sqlQuery?: string;
explanation?: string;
}

View File

@@ -0,0 +1,19 @@
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const QueryCopilotResults: React.FC = (): JSX.Element => {
return (
<QueryResultSection
isMongoDB={false}
queryEditorContent={useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query}
error={useQueryCopilot.getState().errorMessage}
queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator)
}
/>
);
};

View File

@@ -0,0 +1,69 @@
import { Text } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { ExplanationButton } from "./ExplanationButton";
describe("Explanation Button", () => {
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
useQueryCopilot.getState().showExplanationBubble = true;
useQueryCopilot.getState().shouldIncludeInMessages = false;
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it("should render explanation bubble with generated comments", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.find("Stack")).toHaveLength(1);
expect(wrapper.find("Text")).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
it("should render 'Explain this query' link", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const mockSetChatMessages = jest.fn();
const mockSetIsGeneratingExplanation = jest.fn();
const mockSetShouldIncludeInMessages = jest.fn();
const mockSetShowExplanationBubble = jest.fn();
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
useQueryCopilot.getState().setShowExplanationBubble = mockSetShowExplanationBubble;
const wrapper = shallow(<ExplanationButton />);
const textElement = wrapper.find(Text);
textElement.simulate("click");
expect(mockSetChatMessages).toHaveBeenCalledWith([
...initialStoreState.chatMessages,
{ source: 0, message: "Explain this query to me" },
]);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
expect(mockSetShowExplanationBubble).toHaveBeenCalledWith(false);
jest.advanceTimersByTime(3000);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockSetChatMessages).toHaveBeenCalled();
});
it("should render nothing when conditions are not met", () => {
useQueryCopilot.getState().showExplanationBubble = false;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -2,57 +2,44 @@ import { Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const ExplanationBubble: React.FC = (): JSX.Element => {
export const ExplanationButton: React.FC = (): JSX.Element => {
const {
showExplanationBubble,
isGeneratingQuery,
showQueryExplanation,
setShowQueryExplanation,
chatMessages,
setChatMessages,
generatedQuery,
generatedQueryComments,
isGeneratingExplanation,
setIsGeneratingExplanation,
shouldIncludeInMessages,
setShouldIncludeInMessages,
setShowExplanationBubble,
} = useQueryCopilot();
const showExplanation = () => {
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
setIsGeneratingExplanation(true);
setShouldIncludeInMessages(true);
setShowExplanationBubble(false);
setTimeout(() => {
setIsGeneratingExplanation(false);
setShowQueryExplanation(true);
if (useQueryCopilot.getState().shouldIncludeInMessages) {
setIsGeneratingExplanation(false);
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments, sqlQuery: generatedQuery }]);
}
}, 3000);
};
return (
showExplanationBubble &&
!isGeneratingQuery &&
!isGeneratingExplanation &&
(showQueryExplanation && shouldIncludeInMessages ? (
<Stack
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{generatedQueryComments}
</Stack>
) : (
!isGeneratingExplanation && (
<Stack
style={{
display: "flex",
alignItems: "center",
padding: "5px 5px 5px 40px",
padding: "5px 5px 5px 50px",
margin: "5px",
width: "100%",
}}
>
<Text
@@ -69,6 +56,6 @@ export const ExplanationBubble: React.FC = (): JSX.Element => {
Explain this query to me
</Text>
</Stack>
))
)
);
};

View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Button should render explanation bubble with generated comments 1`] = `
<Stack
style={
Object {
"alignItems": "center",
"display": "flex",
"margin": "5px",
"padding": "5px 5px 5px 50px",
}
}
>
<Text
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Explain this query to me
</Text>
</Stack>
`;
exports[`Explanation Button should render nothing when conditions are not met 1`] = `""`;

View File

@@ -0,0 +1,26 @@
import { Stack, Text } from "@fluentui/react";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
import React from "react";
export const ExplanationBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
return (
<Stack
horizontalAlign="start"
verticalAlign="start"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
<Text>{copilotMessage.message}</Text>
<FeedbackButtons sqlQuery={copilotMessage.sqlQuery} />
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
AI-generated content may be incorrect
</Text>
</Stack>
);
};

View File

@@ -1,69 +1,17 @@
import { Text } from "@fluentui/react";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { ExplanationBubble } from "./ExplanationBubble";
describe("Explanation Bubble", () => {
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
useQueryCopilot.getState().showExplanationBubble = true;
useQueryCopilot.getState().shouldIncludeInMessages = false;
jest.useFakeTimers();
});
describe("Explanation Bubble snapshot tests", () => {
it("should render", () => {
const mockCopilotMessage: CopilotMessage = {
source: 2,
message: "Mock message",
};
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
const wrapper = shallow(<ExplanationBubble copilotMessage={mockCopilotMessage} />);
it("should render explanation bubble with generated comments", () => {
useQueryCopilot.getState().showQueryExplanation = true;
useQueryCopilot.getState().shouldIncludeInMessages = true;
const wrapper = shallow(<ExplanationBubble />);
expect(wrapper.find("Stack")).toHaveLength(1);
expect(wrapper.find("Text")).toHaveLength(0);
expect(wrapper).toMatchSnapshot();
});
it("should render 'Explain this query' link", () => {
const mockSetChatMessages = jest.fn();
const mockSetIsGeneratingExplanation = jest.fn();
const mockSetShouldIncludeInMessages = jest.fn();
const mockSetShowQueryExplanation = jest.fn();
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
useQueryCopilot.getState().setShowQueryExplanation = mockSetShowQueryExplanation;
const wrapper = shallow(<ExplanationBubble />);
const textElement = wrapper.find(Text);
textElement.simulate("click");
expect(mockSetChatMessages).toHaveBeenCalledWith([
...initialStoreState.chatMessages,
{ source: 0, message: "Explain this query to me" },
]);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
expect(mockSetShowQueryExplanation).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockSetShowQueryExplanation).toHaveBeenCalledWith(true);
});
it("should render nothing when conditions are not met", () => {
useQueryCopilot.getState().showExplanationBubble = false;
const wrapper = shallow(<ExplanationBubble />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Bubble should render explanation bubble with generated comments 1`] = `
exports[`Explanation Bubble snapshot tests should render 1`] = `
<Stack
horizontalAlign="center"
horizontalAlign="start"
style={
Object {
"backgroundColor": "white",
@@ -17,7 +17,22 @@ exports[`Explanation Bubble should render explanation bubble with generated comm
"padding": 8,
}
}
/>
verticalAlign="start"
>
<Text>
Mock message
</Text>
<FeedbackButtons />
<Text
style={
Object {
"fontSize": "10px",
"fontWeight": 400,
"lineHeight": "14px",
}
}
>
AI-generated content may be incorrect
</Text>
</Stack>
`;
exports[`Explanation Bubble should render nothing when conditions are not met 1`] = `""`;

View File

@@ -10,7 +10,7 @@ describe("Copy button snapshot tests", () => {
it("should render and click copy", async () => {
const testInput = "test input query";
useQueryCopilot.getState().setGeneratedQuery(testInput);
const wrapper = shallow(<CopyButton />);
const wrapper = shallow(<CopyButton sqlQuery={""} />);
const button = wrapper.find(IconButton).first();
button.simulate("click", {});

View File

@@ -1,12 +1,11 @@
import { IconButton } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import CopilotCopy from "../../../../../../../../images/CopilotCopy.svg";
export const CopyButton: React.FC = (): JSX.Element => {
export const CopyButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
const copyGeneratedCode = (): void => {
const queryElement = document.createElement("textarea");
queryElement.value = useQueryCopilot.getState().generatedQuery;
queryElement.value = sqlQuery;
document.body.appendChild(queryElement);
queryElement.select();
document.execCommand("copy");

View File

@@ -20,7 +20,7 @@ beforeEach(() => {
describe("Feedback buttons snapshot tests", () => {
it("should click like and show callout", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
const dislikeButton = wrapper.find(IconButton).last();
@@ -35,7 +35,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should click like and dismiss callout", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
@@ -49,7 +49,7 @@ describe("Feedback buttons snapshot tests", () => {
it("should click like and submit feedback", () => {
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
@@ -61,7 +61,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over like", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
@@ -72,7 +72,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over rest like and leave", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
@@ -84,7 +84,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over pressed like and leave", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
@@ -97,7 +97,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over like and click", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
@@ -109,7 +109,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should dobule click on like", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
@@ -124,7 +124,7 @@ describe("Feedback buttons snapshot tests", () => {
it("should click dislike and show popup", () => {
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
let dislikeButton = wrapper.find(IconButton).last();
@@ -140,7 +140,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over dislike", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
@@ -151,7 +151,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over rest dislike and leave", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
@@ -163,7 +163,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over pressed dislike and leave", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("click");
@@ -178,7 +178,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should hover over dislike and click", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
@@ -190,7 +190,7 @@ describe("Feedback buttons snapshot tests", () => {
});
it("should dobule click on dislike", () => {
const wrapper = shallow(<FeedbackButtons />);
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("click");

View File

@@ -6,8 +6,8 @@ import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
export const FeedbackButtons: React.FC = (): JSX.Element => {
const { generatedQuery, userPrompt } = useQueryCopilot();
export const FeedbackButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
const { userPrompt } = useQueryCopilot();
const [likeQuery, setLikeQuery] = useState<boolean>(false);
const [dislikeQuery, setDislikeQuery] = useState<boolean>(false);
@@ -35,7 +35,7 @@ export const FeedbackButtons: React.FC = (): JSX.Element => {
<Link
onClick={() => {
setCalloutVisible(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
useQueryCopilot.getState().openFeedbackModal(sqlQuery, true, userPrompt);
}}
>
more feedback?
@@ -82,7 +82,7 @@ export const FeedbackButtons: React.FC = (): JSX.Element => {
setLikeQuery(false);
setDislikeImageLink(LikePressed);
setLikeImageLink(LikeRest);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
useQueryCopilot.getState().openFeedbackModal(sqlQuery, false, userPrompt);
}
}}
onMouseOver={() => setDislikeImageLink(LikeHover)}

View File

@@ -1,19 +1,10 @@
import { ActionButton } from "@fluentui/react";
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
describe("Insert button snapshot tests", () => {
it("should click and update state", () => {
const testQuery = "test query";
useQueryCopilot.getState().setGeneratedQuery(testQuery);
const wrapper = shallow(<InsertButton />);
const button = wrapper.find(ActionButton).first();
button.simulate("click");
expect(useQueryCopilot.getState().query).toEqual(testQuery);
const wrapper = shallow(<InsertButton sqlQuery={""} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -3,12 +3,12 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import CopilotInsert from "../../../../../../../../images/CopilotInsert.svg";
export const InsertButton: React.FC = (): JSX.Element => {
export const InsertButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
return (
<ActionButton
iconProps={{ imageProps: { src: CopilotInsert } }}
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
onClick={() => useQueryCopilot.getState().setQuery(useQueryCopilot.getState().generatedQuery)}
onClick={() => useQueryCopilot.getState().setQuery(sqlQuery)}
>
Insert
</ActionButton>

View File

@@ -4,7 +4,7 @@ import React from "react";
describe("Output Bubble Buttons snapshot tests", () => {
it("should render", () => {
const wrapper = shallow(<OutputBubbleButtons />);
const wrapper = shallow(<OutputBubbleButtons sqlQuery={""} />);
expect(wrapper).toMatchSnapshot();
});

View File

@@ -5,17 +5,17 @@ import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/In
import { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
import React from "react";
export const OutputBubbleButtons: React.FC = (): JSX.Element => {
export const OutputBubbleButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
return (
<Stack horizontal>
<Stack.Item style={{ paddingTop: "5px" }}>
<InsertButton />
<InsertButton sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<CopyButton />
<CopyButton sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<FeedbackButtons />
<FeedbackButtons sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<MoreButton />

View File

@@ -11,13 +11,19 @@ exports[`Output Bubble Buttons snapshot tests should render 1`] = `
}
}
>
<InsertButton />
<InsertButton
sqlQuery=""
/>
</StackItem>
<StackItem>
<CopyButton />
<CopyButton
sqlQuery=""
/>
</StackItem>
<StackItem>
<FeedbackButtons />
<FeedbackButtons
sqlQuery=""
/>
</StackItem>
<StackItem>
<MoreButton />

View File

@@ -1,16 +1,17 @@
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
describe("Output Bubble snapshot tests", () => {
it("should render and update height", () => {
withHooks(() => {
useQueryCopilot.getState().setGeneratedQuery("test query");
useQueryCopilot.getState().setGeneratedQueryComments("test comments");
const wrapper = shallow(<OutputBubble />);
const wrapper = shallow(
<OutputBubble
copilotMessage={{ message: "testMessage", source: 1, explanation: "testExplanation", sqlQuery: "testSQL" }}
/>
);
const editor = wrapper.find(EditorReact).first();

View File

@@ -1,19 +1,32 @@
import { Stack, Text } from "@fluentui/react";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { userContext } from "UserContext";
import React, { useState } from "react";
export const OutputBubble: React.FC = (): JSX.Element => {
export const OutputBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
const [windowHeight, setWindowHeight] = useState<string>();
const textHeightWithPadding = 16;
const calculateQueryWindowHeight = (): string => {
const calculatedHeight = document.getElementById("outputBubble")?.clientHeight * (3 / 5);
return `${calculatedHeight}px`;
const outputWidth = document.getElementById("outputBubble")?.clientWidth;
const responseLength = copilotMessage.sqlQuery.length;
if (outputWidth > responseLength) {
return `${textHeightWithPadding * 3}px`;
} else {
const neededLines = Math.ceil(responseLength / outputWidth);
return `${neededLines * textHeightWithPadding}px`;
}
};
React.useEffect(() => {
setWindowHeight(calculateQueryWindowHeight());
if (userContext.features.copilotChatFixedMonacoEditorHeight) {
setWindowHeight(`${textHeightWithPadding * 5}px`);
} else {
setWindowHeight(calculateQueryWindowHeight());
}
}, []);
return (
@@ -29,13 +42,11 @@ export const OutputBubble: React.FC = (): JSX.Element => {
}}
tokens={{ padding: 8, childrenGap: 8 }}
>
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>
{useQueryCopilot.getState()?.generatedQueryComments}
</Stack.Item>
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>{copilotMessage.message}</Stack.Item>
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
<EditorReact
language={"sql"}
content={useQueryCopilot.getState()?.generatedQuery}
content={copilotMessage.sqlQuery}
isReadOnly={true}
ariaLabel={"AI Response"}
wordWrap="on"
@@ -48,7 +59,7 @@ export const OutputBubble: React.FC = (): JSX.Element => {
/>
</Stack.Item>
<Stack.Item style={{ alignSelf: "flex-start" }}>
<OutputBubbleButtons />
<OutputBubbleButtons sqlQuery={copilotMessage.sqlQuery} />
</Stack.Item>
<Stack.Item>
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>

View File

@@ -28,7 +28,7 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
}
}
>
test comments
testMessage
</StackItem>
<StackItem
style={
@@ -40,7 +40,7 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
>
<EditorReact
ariaLabel="AI Response"
content="test query"
content="testSQL"
isReadOnly={true}
language="sql"
lineDecorationsWidth={0}
@@ -68,7 +68,9 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
}
}
>
<OutputBubbleButtons />
<OutputBubbleButtons
sqlQuery="testSQL"
/>
</StackItem>
<StackItem>
<Text

View File

@@ -65,7 +65,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>
@@ -87,7 +87,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
<Link href="https://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
</Stack>
@@ -109,7 +109,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
Copilot is setup on a sample database we have configured for you at no cost
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>

View File

@@ -1,6 +1,7 @@
import { Stack } from "@fluentui/react";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplanationBubble";
import { ExplanationButton } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/Button/ExplanationButton";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
@@ -42,41 +43,34 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
}}
>
<WelcomeBubble />
{chatMessages.map((message, index) =>
message.source === 0 ? (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "#E0E7FF",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
) : (
// This part should be wired with OutputBubble
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
)
)}
<OutputBubble />
{chatMessages.map((message, index) => {
switch (message.source) {
case 0:
return (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "#E0E7FF",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
);
case 1:
return <OutputBubble key={index} copilotMessage={message} />;
case 2:
return <ExplanationBubble key={index} copilotMessage={message} />;
default:
return <></>;
}
})}
<RetrievingBubble />
<ExplanationBubble />
<ExplanationButton />
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
</Stack>

View File

@@ -102,7 +102,7 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
>
Read preview terms
@@ -125,25 +125,26 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
>
<EditorReact
ariaLabel="Editing Query"
content=""
content="SELECT * FROM c"
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
/>
<QueryResultSection
error=""
executeQueryDocumentsPage={[Function]}
isExecuting={false}
isMongoDB={false}
queryEditorContent=""
/>
<QueryCopilotResults />
</t>
</Stack>
<WelcomeModal
visible={true}
/>
<DeletePopup
clearFeedback={[Function]}
setQuery={[Function]}
setShowDeletePopup={[Function]}
showDeletePopup={false}
showFeedbackBar={[Function]}
/>
<CopyPopup
setShowCopyPopup={[Function]}
showCopyPopup={false}

View File

@@ -4,7 +4,6 @@ import { traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react";
import Youtube from "react-youtube";
import Image1 from "../../../images/CarouselImage1.svg";
import Image2 from "../../../images/CarouselImage2.svg";
@@ -79,7 +78,12 @@ const getHeaderText = (page: number): string => {
const getContent = (page: number): JSX.Element => {
switch (page) {
case 1:
return <Youtube videoId="Jvgh64rvdXU" onPlay={() => traceSuccess(Action.PlayCarouselVideo)} />;
return (
<video controls width="640" height="360" controlsList="nofullscreen nodownload ">
<source src="src/Explorer/Quickstart/Videos/Cosmos-db-turorial.mp4" type="video/mp4"></source>
Your browser does not support the video tag.
</video>
);
case 2:
return <Image style={{ width: 640 }} src={Image1} />;
case 3:

View File

@@ -2,20 +2,26 @@ import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import React from "react";
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
export const QuickstartFirewallNotification: React.FC = (): JSX.Element => (
export interface QuickstartFirewallNotificationProps {
shellName: string;
screenshot: string;
messageType: MessageTypes;
}
export const QuickstartFirewallNotification: React.FC<QuickstartFirewallNotificationProps> = ({
shellName,
screenshot,
messageType,
}: QuickstartFirewallNotificationProps): JSX.Element => (
<Stack style={{ padding: "16px 20px" }}>
<Text block>
To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses
To use the {shellName} shell, you need to add a firewall rule to allow access from all IP addresses
(0.0.0.0-255.255.255).
</Text>
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
<PrimaryButton
style={{ width: 150 }}
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
>
<Text block>We strongly recommend removing this rule once you finish using the {shellName} shell.</Text>
<Image style={{ margin: "20px 0" }} src={screenshot} />
<PrimaryButton style={{ width: 150 }} onClick={() => sendMessage({ type: messageType })}>
Add firewall rule
</PrimaryButton>
</Stack>

View File

@@ -1,9 +1,7 @@
import {
DefaultButton,
Icon,
IconButton,
Image,
IPivotItemProps,
Pivot,
PivotItem,
PrimaryButton,
@@ -21,18 +19,10 @@ import {
queryCommand,
queryCommandForDisplay,
} from "Explorer/Quickstart/PostgreQuickstartCommands";
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
import { useTerminal } from "hooks/useTerminal";
import React, { useState } from "react";
import Youtube from "react-youtube";
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
import Pivot2Icon from "../../../images/Pivot2.svg";
import Pivot2SelectedIcon from "../../../images/Pivot2_selected.svg";
import Pivot3Icon from "../../../images/Pivot3.svg";
import Pivot3SelectedIcon from "../../../images/Pivot3_selected.svg";
import Pivot4Icon from "../../../images/Pivot4.svg";
import Pivot4SelectedIcon from "../../../images/Pivot4_selected.svg";
import Pivot5Icon from "../../../images/Pivot5.svg";
import Pivot5SelectedIcon from "../../../images/Pivot5_selected.svg";
import CompleteIcon from "../../../images/QuickstartComplete.svg";
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
@@ -53,44 +43,6 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
document.execCommand("copy");
};
const getPivotHeaderIcon = (step: number): string => {
switch (step) {
case 0:
return Pivot1SelectedIcon;
case 1:
return step === currentStep ? Pivot2SelectedIcon : Pivot2Icon;
case 2:
return step === currentStep ? Pivot3SelectedIcon : Pivot3Icon;
case 3:
return step === currentStep ? Pivot4SelectedIcon : Pivot4Icon;
case 4:
return step === currentStep ? Pivot5SelectedIcon : Pivot5Icon;
default:
return "";
}
};
const customPivotHeaderRenderer = (
link: IPivotItemProps,
defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null,
step: number
): JSX.Element | null => {
if (!link || !defaultRenderer) {
return null;
}
return (
<Stack horizontal verticalAlign="center">
{currentStep > step ? (
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
) : (
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(step)} />
)}
{defaultRenderer({ ...link, itemIcon: undefined })}
</Stack>
);
};
return (
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
@@ -103,7 +55,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
>
<PivotItem
headerText="Login"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
}
itemKey={GuideSteps[0]}
onClick={() => {
setCurrentStep(0);
@@ -125,7 +79,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
</PivotItem>
<PivotItem
headerText="New table"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 1)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
}
itemKey={GuideSteps[1]}
onClick={() => setCurrentStep(1)}
>
@@ -165,7 +121,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
</PivotItem>
<PivotItem
headerText="Distribute table"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 2)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
}
itemKey={GuideSteps[2]}
onClick={() => setCurrentStep(2)}
>
@@ -210,7 +168,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
</PivotItem>
<PivotItem
headerText="Load data"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 3)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
}
itemKey={GuideSteps[3]}
onClick={() => setCurrentStep(3)}
>
@@ -250,7 +210,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
</PivotItem>
<PivotItem
headerText="Query"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 4)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
}
itemKey={GuideSteps[4]}
onClick={() => setCurrentStep(4)}
>

View File

@@ -0,0 +1,50 @@
import { Icon, Image, IPivotItemProps, Stack } from "@fluentui/react";
import React from "react";
import Pivot1SelectedIcon from "../../../../images/Pivot1_selected.svg";
import Pivot2Icon from "../../../../images/Pivot2.svg";
import Pivot2SelectedIcon from "../../../../images/Pivot2_selected.svg";
import Pivot3Icon from "../../../../images/Pivot3.svg";
import Pivot3SelectedIcon from "../../../../images/Pivot3_selected.svg";
import Pivot4Icon from "../../../../images/Pivot4.svg";
import Pivot4SelectedIcon from "../../../../images/Pivot4_selected.svg";
import Pivot5Icon from "../../../../images/Pivot5.svg";
import Pivot5SelectedIcon from "../../../../images/Pivot5_selected.svg";
const getPivotHeaderIcon = (currentStep: number, newStep: number): string => {
switch (newStep) {
case 0:
return Pivot1SelectedIcon;
case 1:
return newStep === currentStep ? Pivot2SelectedIcon : Pivot2Icon;
case 2:
return newStep === currentStep ? Pivot3SelectedIcon : Pivot3Icon;
case 3:
return newStep === currentStep ? Pivot4SelectedIcon : Pivot4Icon;
case 4:
return newStep === currentStep ? Pivot5SelectedIcon : Pivot5Icon;
default:
return "";
}
};
export const customPivotHeaderRenderer = (
link: IPivotItemProps,
defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null,
currentStep: number,
newStep: number
): JSX.Element | null => {
if (!link || !defaultRenderer) {
return null;
}
return (
<Stack horizontal verticalAlign="center">
{currentStep > newStep ? (
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
) : (
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(currentStep, newStep)} />
)}
{defaultRenderer({ ...link, itemIcon: undefined })}
</Stack>
);
};

View File

@@ -0,0 +1,34 @@
export const newDbAndCollectionCommand = `use quickstartDB
db.createCollection('sampleCollection')`;
export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists
db.createCollection('sampleCollection') // Create new collection named 'sampleCollection'`;
export const loadDataCommand = `db.sampleCollection.insertMany([
{title: "The Great Gatsby", author: "F. Scott Fitzgerald", pages: 180},
{title: "To Kill a Mockingbird", author: "Harper Lee", pages: 324},
{title: "1984", author: "George Orwell", pages: 328},
{title: "The Catcher in the Rye", author: "J.D. Salinger", pages: 277},
{title: "Moby-Dick", author: "Herman Melville", pages: 720},
{title: "Pride and Prejudice", author: "Jane Austen", pages: 279},
{title: "The Hobbit", author: "J.R.R. Tolkien", pages: 310},
{title: "War and Peace", author: "Leo Tolstoy", pages: 1392},
{title: "The Odyssey", author: "Homer", pages: 374},
{title: "Ulysses", author: "James Joyce", pages: 730}
])`;
export const queriesCommand = `db.sampleCollection.find({author: "George Orwell"})
db.sampleCollection.find({pages: {$gt: 500}})
db.sampleCollection.find({}).sort({pages: 1})`;
export const queriesCommandForDisplay = `// Query to find all books written by "George Orwell"
db.sampleCollection.find({author: "George Orwell"})
// Query to find all books with more than 500 pages
db.sampleCollection.find({pages: {$gt: 500}})
// Query to find all books and sort them by the number of pages in ascending order
db.sampleCollection.find({}).sort({pages: 1})`;

View File

@@ -0,0 +1,310 @@
import {
DefaultButton,
IconButton,
Link,
Pivot,
PivotItem,
PrimaryButton,
Stack,
Text,
TextField,
} from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
import {
loadDataCommand,
newDbAndCollectionCommand,
newDbAndCollectionCommandForDisplay,
queriesCommand,
queriesCommandForDisplay,
} from "Explorer/Quickstart/VCoreMongoQuickstartCommands";
import { useTerminal } from "hooks/useTerminal";
import React, { useState } from "react";
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
enum GuideSteps {
Login,
NewTable,
DistributeTable,
LoadData,
Query,
}
export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
const [currentStep, setCurrentStep] = useState<number>(0);
const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector);
textfield.select();
document.execCommand("copy");
};
return (
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
<Text variant="xxLarge">Quick start guide</Text>
<Text variant="small">Getting started in Cosmos DB Mongo DB (vCore)</Text>
{currentStep < 5 && (
<Pivot
style={{ marginTop: 10, width: "100%" }}
selectedKey={GuideSteps[currentStep]}
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
>
<PivotItem
headerText="Connect"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
}
itemKey={GuideSteps[0]}
onClick={() => {
setCurrentStep(0);
}}
>
<Stack style={{ marginTop: 20 }}>
<Text>
A hosted mongosh (mongo shell) is provided for this quick start. You are automatically logged in to
mongosh, allowing you to interact with your database directly.
<br />
<br />
When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
using your connection string.
<br />
<br />
<Link
aria-label="View connection string"
href=""
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
>
View connection string
</Link>
<br />
<br />
This string contains placeholders for &lt;user&gt; and &lt;password&gt;. Replace them with your chosen
username and password to establish a secure connection to your cluster. Depending on your environment,
you may need to adjust firewall rules or configure private endpoints in the &lsquo;Networking&rsquo;
tab of your database settings, or modify your own network&apos;s firewall settings, to successfully
connect.
</Text>
</Stack>
</PivotItem>
<PivotItem
headerText="New collection"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
}
itemKey={GuideSteps[1]}
onClick={() => setCurrentStep(1)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
In MongoDB, data is stored in collections, which are analogous to tables in relational databases.
Collections contain documents, each of which consists of field and value pairs. The fields in
documents are similar to the columns in a relational database table. One key advantage of MongoDB is
that these documents within a collection can have different fields.
<br />
You&apos;re now going to create a new database and a collection within that database using the Mongo
shell. In MongoDB, creating a database or a collection is implicit. This means that databases and
collections are created when you first reference them in a command, so no explicit creation command is
necessary.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 270 }}
onClick={() => useTerminal.getState().sendMessage(newDbAndCollectionCommand)}
>
Create new database and collection
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="newDbAndCollectionCommand"
multiline
rows={5}
readOnly
defaultValue={newDbAndCollectionCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#newDbAndCollectionCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Load data"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
}
itemKey={GuideSteps[2]}
onClick={() => setCurrentStep(2)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
Now that you&apos;ve created your database and collection, it&apos;s time to populate your collection
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
<br />
<br />
Let&apos;s populate your sampleCollection with data. We&apos;ll add 10 documents representing books,
each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 200 }}
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
>
Create distributed table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="loadDataCommand"
multiline
rows={5}
readOnly
defaultValue={loadDataCommand}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#loadDataCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Queries"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
}
itemKey={GuideSteps[3]}
onClick={() => setCurrentStep(3)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
Once youve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB
queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit
results.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
>
Load data
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="queriesCommand"
multiline
rows={5}
readOnly
defaultValue={queriesCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#queriesCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Integrations"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
}
itemKey={GuideSteps[4]}
onClick={() => setCurrentStep(4)}
>
<Stack>
<Text>
Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable
Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified
experience, that just works.
</Text>
<Stack horizontal>
<Stack style={{ marginTop: 20, marginRight: 20 }}>
<Text>
<b>First party integrations</b>
<br />
<br />
<b>Azure Monitor</b>
<br />
Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn
more
<br />
<br />
<b>Azure Networking</b>
<br />
Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data
access. Learn more
<br />
<br />
<b>PowerShell/CLI/ARM</b>
<br />
PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient
management and automation. Learn more
</Text>
</Stack>
<Stack style={{ marginTop: 20, marginLeft: 20 }}>
<Text>
<b>Application platforms integrations</b>
<br />
<br />
<b>Vercel</b>
<br />
Vercel is a cloud platform for hosting static front ends and serverless functions, with instant
deployments, automated scaling, and Next.js integration. Learn more
</Text>
</Stack>
</Stack>
</Stack>
</PivotItem>
</Pivot>
)}
</Stack>
<Stack horizontal style={{ padding: "16px 20px", boxShadow: "inset 0px 1px 0px rgba(204, 204, 204, 0.8)" }}>
<DefaultButton disabled={currentStep === 0} onClick={() => setCurrentStep(currentStep - 1)}>
Previous
</DefaultButton>
{currentStep < 4 && (
<PrimaryButton onClick={() => setCurrentStep(currentStep + 1)} style={{ marginLeft: 8 }}>
Next
</PrimaryButton>
)}
{currentStep === 4 && (
<PrimaryButton
onClick={() => useTabs.getState().closeReactTab(ReactTabKind.Quickstart)}
style={{ marginLeft: 8 }}
>
Done
</PrimaryButton>
)}
</Stack>
</Stack>
);
};

Binary file not shown.

View File

@@ -260,30 +260,33 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
public render(): JSX.Element {
let title: string;
let subtitle: string;
switch (userContext.apiType) {
case "Postgres":
title = "Welcome to Azure Cosmos DB for PostgreSQL";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
break;
case "VCoreMongo":
title = "Welcome to Azure Cosmos DB for MongoDB (vCore)";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
break;
default:
title = "Welcome to Azure Cosmos DB";
subtitle = "Globally distributed, multi-model database service for any scale";
}
return (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<h1
className="title"
role="heading"
aria-label={
userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"
}
>
{userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"}
<h1 className="title" role="heading" aria-label={title}>
{title}
<FeaturePanelLauncher />
</h1>
<div className="subtitle">
{userContext.apiType === "Postgres"
? "Get started with our sample datasets, documentation, and additional tools."
: "Globally distributed, multi-model database service for any scale"}
</div>
<div className="subtitle">{subtitle}</div>
{this.getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && (
<Coachmark
@@ -313,7 +316,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
</TeachingBubbleContent>
</Coachmark>
)}
{userContext.apiType === "Postgres" ? (
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
<Stack style={{ width: "33%" }}>
<Text
@@ -380,7 +383,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
if (
userContext.apiType === "SQL" ||
userContext.apiType === "Mongo" ||
(userContext.apiType === "Postgres" && !userContext.isReplica)
(userContext.apiType === "Postgres" && !userContext.isReplica) ||
userContext.apiType === "VCoreMongo"
) {
const launchQuickstartBtn = {
id: "quickstartDescription",
@@ -388,9 +392,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Launch quick start",
description: "Launch a quick start tutorial to get started with sample data",
onClick: () => {
userContext.apiType === "Postgres"
? useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart)
: this.container.onNewCollectionClicked({ isQuickstart: true });
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
} else {
this.container.onNewCollectionClicked({ isQuickstart: true });
}
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
},
};
@@ -405,39 +411,65 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
heroes.push(newNotebookBtn);
}
heroes.push(this.getShellCard());
heroes.push(this.getThirdCard());
return heroes;
}
private getShellCard() {
if (userContext.apiType === "Postgres") {
const postgreShellBtn = {
return {
iconSrc: PowerShellIcon,
title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQLs shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
};
heroes.push(postgreShellBtn);
} else {
const newContainerBtn = {
iconSrc: ContainersIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => {
this.container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
},
};
heroes.push(newContainerBtn);
}
const connectBtn = {
iconSrc: ConnectIcon,
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
description:
userContext.apiType === "Postgres"
? "Prefer pgAdmin? Find your connection strings here"
: "Prefer using your own choice of tooling? Find the connection string you need to connect",
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
};
heroes.push(connectBtn);
if (userContext.apiType === "VCoreMongo") {
return {
iconSrc: PowerShellIcon,
title: "Mongo Shell",
description: "Create a collection and interact with data using MongoDB's shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.VCoreMongo),
};
}
return heroes;
return {
iconSrc: ContainersIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => {
this.container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
},
};
}
private getThirdCard() {
let icon = ConnectIcon;
let title = "Connect";
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
if (userContext.apiType === "Postgres") {
title = "Connect with pgAdmin";
description = "Prefer pgAdmin? Find your connection strings here";
}
if (userContext.apiType === "VCoreMongo") {
icon = ContainersIcon;
title = "Connect with Studio 3T";
description = "Prefer Studio 3T? Find your connection strings here";
onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
}
return {
iconSrc: icon,
title: title,
description: description,
onClick: onClick,
};
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
@@ -587,6 +619,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
break;
default:
break;
}
return (
<Stack>
@@ -724,6 +758,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
cdbLiveTv,
];
break;
default:
break;
}
return (
<Stack>
@@ -749,24 +785,46 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
private postgresNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
title: "Data Modeling",
description: "",
},
{
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
title: "How to choose a Distribution Column",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
title: "Build Apps with Python/Java/Django",
description: "",
},
];
private vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
{
link:
"https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import",
title: "Migrate Data",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai",
title: "Build AI apps with Vector Search",
description: "",
},
{
link:
"https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces",
title: "Build Apps with Nodejs",
description: "",
},
];
private getNextStepItems(): JSX.Element {
const items: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
title: "Data Modeling",
description: "",
},
{
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
title: "How to choose a Distribution Column",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
title: "Build Apps with Python/Java/Django",
description: "",
},
];
const items = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
@@ -785,24 +843,44 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
title: "Performance Tuning",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
title: "Useful Diagnostic Queries",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
title: "Distributed SQL Reference",
description: "",
},
];
private vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
title: "Vector Search",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index",
title: "Text Indexing",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues",
title: "Troubleshoot common issues",
description: "",
},
];
private getTipsAndLearnMoreItems(): JSX.Element {
const items: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
title: "Performance Tuning",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
title: "Useful Diagnostic Queries",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
title: "Distributed SQL Reference",
description: "",
},
];
const items = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>

View File

@@ -1,4 +1,6 @@
import { FeedOptions } from "@azure/cosmos";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
@@ -276,7 +278,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: this.onExecuteQueryClick,
onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
@@ -365,11 +367,18 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
selectedContent: "",
});
}
if (this.isCopilotTabActive) {
selectedContent.trim().length > 0
? useQueryCopilot.getState().setSelectedQuery(selectedContent)
: useQueryCopilot.getState().setSelectedQuery("");
}
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
public setEditorContent(): string {
if (this.state.queryCopilotGeneratedQuery) {
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
return this.state.queryCopilotGeneratedQuery;
}
@@ -416,14 +425,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
/>
</div>
</Fragment>
<QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
error={this.state.error}
queryResults={this.state.queryResults}
isExecuting={this.state.isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
/>
{this.isCopilotTabActive ? (
<QueryCopilotResults />
) : (
<QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
error={this.state.error}
queryResults={this.state.queryResults}
isExecuting={this.state.isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
this._executeQueryDocumentsPage(firstItemIndex)
}
/>
)}
</SplitterLayout>
</div>
</div>

View File

@@ -1,16 +1,16 @@
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { PoolIdType } from "Common/Constants";
import { configContext } from "ConfigContext";
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React, { useEffect, useState } from "react";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
import React, { useEffect, useState } from "react";
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
interface QuickstartTabProps {
explorer: Explorer;
@@ -26,29 +26,12 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
forwardingId: notebookServerInfo.forwardingId,
});
const checkFirewallRules = async (): Promise<void> => {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2022-11-08",
});
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
const isEnabled = firewallRules.some(
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
);
setIsAllPublicIPAddressEnabled(isEnabled);
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) {
setTimeout(checkFirewallRules, 30000);
}
};
useEffect(() => {
checkFirewallRules();
checkFirewallRules(
"2022-11-08",
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
setIsAllPublicIPAddressEnabled
);
});
useEffect(() => {
@@ -61,7 +44,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
<QuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="PostgreSQL"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}

View File

@@ -0,0 +1,91 @@
import { DatabaseAccount, FirewallRule } from "Contracts/DataModels";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { updateUserContext } from "UserContext";
import { mockFunction } from "Utils/JestUtils";
import { armRequest } from "Utils/arm/request";
import React from "react";
jest.mock("Utils/arm/request");
const armRequestMock = mockFunction(armRequest);
describe("CheckFirewallRule tests", () => {
const apiVersion = "2023-03-15-preview";
const rulePredicate = (rule: FirewallRule) =>
rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255";
let isAllPublicIPAddressEnabled: boolean;
const setIsAllPublicIPAddressEnabled = jest.fn((value) => (isAllPublicIPAddressEnabled = value));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const useStateMock: any = (initState: any) => [initState, setIsAllPublicIPAddressEnabled];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
beforeAll(() => {
updateUserContext({
databaseAccount: {
id: "testResourceId",
} as DatabaseAccount,
});
});
it("returns 'all public IP addresses' is enabled for account with the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce({
value: [
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "0.0.0.0",
endIpAddress: "255.255.255.255",
},
},
],
});
await checkFirewallRules(apiVersion, rulePredicate, setIsAllPublicIPAddressEnabled);
expect(isAllPublicIPAddressEnabled).toBe(true);
});
it("returns 'all public IP addresses' is NOT enabled for account without the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce([
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "10.10.10.10",
endIpAddress: "10.10.10.10",
},
},
]);
await checkFirewallRules(apiVersion, rulePredicate, setIsAllPublicIPAddressEnabled);
expect(isAllPublicIPAddressEnabled).toBe(false);
});
it("sets message for account without the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce([
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "0.0.0.0",
endIpAddress: "255.255.255.255",
},
},
]);
const warningMessage = "This is a warning message";
let warningMessageResult: string;
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
await checkFirewallRules(apiVersion, rulePredicate, undefined, warningMessageFunc, warningMessage);
expect(warningMessageResult).toEqual(warningMessage);
});
});

View File

@@ -0,0 +1,44 @@
import { configContext } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
export async function checkFirewallRules(
apiVersion: string,
firewallRulesPredicate: (rule: DataModels.FirewallRule) => unknown,
isAllPublicIPAddressesEnabled?: ko.Observable<boolean> | React.Dispatch<React.SetStateAction<boolean>>,
setMessageFunc?: (message: string) => void,
message?: string
): Promise<void> {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: apiVersion,
});
const firewallRules: DataModels.FirewallRule[] = response?.data?.value || response?.value || [];
const isEnabled = firewallRules.some(firewallRulesPredicate);
if (isAllPublicIPAddressesEnabled) {
isAllPublicIPAddressesEnabled(isEnabled);
}
if (setMessageFunc) {
if (!isEnabled) {
setMessageFunc(message);
} else {
setMessageFunc(undefined);
}
}
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
if (!isEnabled) {
setTimeout(
() =>
checkFirewallRules(apiVersion, firewallRulesPredicate, isAllPublicIPAddressesEnabled, setMessageFunc, message),
30000
);
}
}

View File

@@ -8,6 +8,8 @@ import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
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 { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTeachingBubble } from "hooks/useTeachingBubble";
@@ -35,7 +37,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
<MessageBar
messageBarType={MessageBarType.warning}
actions={
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
<MessageBarButton
onClick={() =>
sendMessage({
type:
userContext.apiType === "VCoreMongo"
? MessageTypes.OpenVCoreMongoNetworkingBlade
: MessageTypes.OpenPostgresNetworkingBlade,
})
}
>
Change network settings
</MessageBarButton>
}
@@ -127,12 +138,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
/>
)}
</span>
<span
className="tabNavText"
style={active ? { fontWeight: "bolder", borderBottom: "2px solid rgba(0,120,212,1)" } : {}}
>
{useObservable(tab?.tabTitle || getReactTabTitle())}
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
{tabKind !== ReactTabKind.Home && (
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
@@ -252,11 +258,21 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
switch (activeReactTab) {
case ReactTabKind.Connect:
return userContext.apiType === "Postgres" ? <PostgresConnectTab /> : <ConnectTab />;
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoConnectTab />
) : userContext.apiType === "Postgres" ? (
<PostgresConnectTab />
) : (
<ConnectTab />
);
case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />;
case ReactTabKind.Quickstart:
return <QuickstartTab explorer={explorer} />;
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} />
) : (
<QuickstartTab explorer={explorer} />
);
case ReactTabKind.QueryCopilot:
return <QueryCopilotTab explorer={explorer} />;
default:

View File

@@ -1,9 +1,10 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import { configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import * as ko from "knockout";
import * as React from "react";
import { armRequest } from "Utils/arm/request";
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -18,6 +19,7 @@ export interface TerminalTabOptions extends ViewModels.TabOptions {
account: DataModels.DatabaseAccount;
container: Explorer;
kind: ViewModels.TerminalKind;
username?: string;
}
/**
@@ -30,12 +32,19 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
private getDatabaseAccount: () => DataModels.DatabaseAccount,
private getTabId: () => string,
private getUsername: () => string,
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
) {}
public renderComponent(): JSX.Element {
if (!this.isAllPublicIPAddressesEnabled()) {
return <QuickstartFirewallNotification />;
return (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="PostgreSQL"
/>
);
}
return this.parameters() ? (
@@ -43,6 +52,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
notebookServerInfo={this.getNotebookServerInfo()}
databaseAccount={this.getDatabaseAccount()}
tabId={this.getTabId()}
username={this.getUsername()}
/>
) : (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
@@ -64,6 +74,7 @@ export default class TerminalTab extends TabsBase {
() => this.getNotebookServerInfo(options),
() => userContext?.databaseAccount,
() => this.tabId,
() => this.getUsername(),
this.isAllPublicIPAddressesEnabled
);
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
@@ -79,7 +90,21 @@ export default class TerminalTab extends TabsBase {
});
if (options.kind === ViewModels.TerminalKind.Postgres) {
this.checkPostgresFirewallRules();
checkFirewallRules(
"2022-11-08",
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
this.isAllPublicIPAddressesEnabled
);
}
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
checkFirewallRules(
"2023-03-01-preview",
(rule) =>
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
this.isAllPublicIPAddressesEnabled
);
}
}
@@ -115,6 +140,10 @@ export default class TerminalTab extends TabsBase {
endpointSuffix = "postgresql";
break;
case ViewModels.TerminalKind.VCoreMongo:
endpointSuffix = "mongovcore";
break;
default:
throw new Error(`Terminal kind: ${options.kind} not supported`);
}
@@ -127,24 +156,11 @@ export default class TerminalTab extends TabsBase {
};
}
private async checkPostgresFirewallRules(): Promise<void> {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2022-11-08",
});
const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || [];
const isEnabled = firewallRules.some(
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
);
this.isAllPublicIPAddressesEnabled(isEnabled);
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
if (!isEnabled) {
setTimeout(() => this.checkPostgresFirewallRules(), 30000);
private getUsername(): string {
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
return undefined;
}
return userContext.vcoreMongoConnectionParams.adminLogin;
}
}

View File

@@ -0,0 +1,37 @@
import { IconButton, ITextFieldStyles, Stack, TextField } from "@fluentui/react";
import React from "react";
import { userContext } from "UserContext";
export const VcoreMongoConnectTab: React.FC = (): JSX.Element => {
const { adminLogin, connectionString } = userContext.vcoreMongoConnectionParams;
const displayConnectionString = connectionString.replace("<user>", adminLogin);
const textfieldStyles: Partial<ITextFieldStyles> = {
root: { width: "100%" },
field: { backgroundColor: "rgb(230, 230, 230)" },
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
subComponentStyles: { label: { fontWeight: 400 } },
description: { fontWeight: 400 },
};
const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector);
textfield.select();
document.execCommand("copy");
};
return (
<div style={{ width: "100%", padding: 16 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="MongoDB SRV connection URL"
id="mongoSrvConnectionURL"
readOnly
value={displayConnectionString}
styles={textfieldStyles}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#mongoSrvConnectionURL")} />
</Stack>
</div>
);
};

View File

@@ -0,0 +1,80 @@
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { PoolIdType } from "Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
import React, { useEffect, useState } from "react";
import FirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
interface VCoreMongoQuickstartTabProps {
explorer: Explorer;
}
export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = ({
explorer,
}: VCoreMongoQuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongovcore`,
forwardingId: notebookServerInfo.forwardingId,
});
useEffect(() => {
checkFirewallRules(
"2023-03-01-preview",
(rule) =>
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
setIsAllPublicIPAddressEnabled
);
});
useEffect(() => {
explorer.allocateContainer(PoolIdType.DefaultPoolId);
}, []);
return (
<Stack style={{ width: "100%" }} horizontal>
<Stack style={{ width: "50%" }}>
<VcoreMongoQuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="MongoDB"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount}
tabId="QuickstartVcoreMongoShell"
username={userContext.vcoreMongoConnectionParams.adminLogin}
/>
)}
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
<Stack style={{ margin: "auto 0" }}>
<Text block style={{ margin: "auto" }}>
Connecting to the Mongo shell.
</Text>
<Text block style={{ margin: "auto" }}>
If the cluster was just created, this could take up to a minute.
</Text>
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
</Stack>
)}
</Stack>
</Stack>
);
};

View File

@@ -37,6 +37,7 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
@@ -205,7 +206,8 @@ export default class Collection implements ViewModels.Collection {
.map((node) => <Trigger>node);
});
const showScriptsMenus: boolean = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const showScriptsMenus: boolean =
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);

View File

@@ -1,6 +1,5 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { BrandVariants, FluentProvider, Theme, Tree, createLightTheme } from "@fluentui/react-components";
import { buildSampleDataTree } from "Explorer/Tree/SampleDataTree";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils";
import * as React from "react";
import shallow from "zustand/shallow";
@@ -27,8 +26,9 @@ 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 { TreeNode, TreeNodeComponent, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
@@ -39,6 +39,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@@ -50,30 +51,6 @@ interface ResourceTreeProps {
container: Explorer;
}
const cosmosdb: BrandVariants = {
10: "#020305",
20: "#111723",
30: "#16263D",
40: "#193253",
50: "#1B3F6A",
60: "#1B4C82",
70: "#18599B",
80: "#1267B4",
90: "#3174C2",
100: "#4F82C8",
110: "#6790CF",
120: "#7D9ED5",
130: "#92ACDC",
140: "#A6BAE2",
150: "#BAC9E9",
160: "#CDD8EF"
};
const lightTheme: Theme = {
...createLightTheme(cosmosdb),
};
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
const databases = useDatabases((state) => state.databases);
const {
@@ -93,7 +70,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
shallow
);
const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const showScriptNodes =
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
const pseudoDirPath = "PsuedoDir";
const buildGalleryCallout = (): JSX.Element => {
@@ -142,8 +120,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildNotebooksTree = (): TreeNode => {
const notebooksTree: TreeNode = {
id: "notebooks",
label: "NOTEBOOKS",
label: undefined,
isExpanded: true,
children: [],
};
@@ -527,8 +504,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
});
return {
id: "data",
label: "DATA",
label: undefined,
isExpanded: true,
children: databaseTreeNodes,
};
@@ -794,30 +770,56 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
const treeNodes = React.useMemo(() => {
if (!isNotebookEnabled && !isSampleDataEnabled) {
return dataRootNode.children;
}
return (
<>
{!isNotebookEnabled && !isSampleDataEnabled && (
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
)}
{isNotebookEnabled && !isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent>
const nodes: TreeNode[] = [dataRootNode];
if (isSampleDataEnabled) {
nodes.push(buildSampleDataTree(sampleDataResourceTokenCollection));
}
{buildGalleryCallout()}
</>
)}
{!isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
</AccordionComponent>
if (isNotebookEnabled) {
nodes.push(buildNotebooksTree());
}
{buildGalleryCallout()}
</>
)}
{isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent>
return nodes;
}, [isNotebookEnabled, isSampleDataEnabled]);
return (<>
<FluentProvider theme={lightTheme}>
<Tree openItems={treeNodes.map(node => node.label)}>
{treeNodes.map(node => <TreeNodeComponent key={node.id} className="dataResourceTree" node={node} />)}
</Tree>
</FluentProvider>
{(isNotebookEnabled || isSampleDataEnabled) && buildGalleryCallout()}
</>);
{buildGalleryCallout()}
</>
)}
</>
);
};

View File

@@ -40,6 +40,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@@ -249,7 +250,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
* @param container
*/
private static showScriptNodes(container: Explorer): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
return (
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {

View File

@@ -1,83 +0,0 @@
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import TabsBase from "Explorer/Tabs/TabsBase";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { useTabs } from "hooks/useTabs";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { TreeNode } from "../Controls/TreeComponent/TreeComponent";
export const buildSampleDataTree = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode => {
const updatedSampleTree: TreeNode = {
label: sampleDataResourceTokenCollection.databaseId,
isExpanded: false,
iconSrc: CosmosDBIcon,
className: "databaseHeader",
children: [
{
label: sampleDataResourceTokenCollection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
onClick: () => {
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
useCommandBar.getState().setContextButtons([]);
useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
children: [
{
label: "Items",
onClick: () => sampleDataResourceTokenCollection.onDocumentDBDocumentsClick(),
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(
sampleDataResourceTokenCollection.databaseId,
sampleDataResourceTokenCollection.id(),
[ViewModels.CollectionTabKind.Documents]
),
},
],
},
],
};
// TODO handle case non-initialized
return updatedSampleTree;
// export const SampleDataTree = ({
// sampleDataResourceTokenCollection,
// }: {
// sampleDataResourceTokenCollection: ViewModels.CollectionBase;
// }): JSX.Element => {
// return {
// id: "sampleData",
// label: "SAMPLE DATA",
// isExpanded: true,
// children: [updatedSampleTree],
// };
// };
// return (
// <TreeNodeComponent
// className="dataResourceTree"
// node={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
// />
// );
};

View File

@@ -0,0 +1,78 @@
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import TabsBase from "Explorer/Tabs/TabsBase";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { useTabs } from "hooks/useTabs";
import React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
export const SampleDataTree = ({
sampleDataResourceTokenCollection,
}: {
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
}): JSX.Element => {
const buildSampleDataTree = (): TreeNode => {
const updatedSampleTree: TreeNode = {
label: sampleDataResourceTokenCollection.databaseId,
isExpanded: false,
iconSrc: CosmosDBIcon,
className: "databaseHeader",
children: [
{
label: sampleDataResourceTokenCollection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
onClick: () => {
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
useCommandBar.getState().setContextButtons([]);
useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
children: [
{
label: "Items",
onClick: () => sampleDataResourceTokenCollection.onDocumentDBDocumentsClick(),
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(
sampleDataResourceTokenCollection.databaseId,
sampleDataResourceTokenCollection.id(),
[ViewModels.CollectionTabKind.Documents]
),
},
],
},
],
};
return {
label: undefined,
isExpanded: true,
children: [updatedSampleTree],
};
};
return (
<TreeComponent
className="dataResourceTree"
rootNode={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
/>
);
};

View File

@@ -0,0 +1,108 @@
import {
BrandVariants,
FluentProvider,
Theme,
Tree,
TreeItemValue,
TreeOpenChangeData,
TreeOpenChangeEvent,
createLightTheme,
} from "@fluentui/react-components";
import { TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes";
import * as React from "react";
import shallow from "zustand/shallow";
import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
export const MyNotebooksTitle = "My Notebooks";
export const GitHubReposTitle = "GitHub repos";
interface ResourceTreeProps {
container: Explorer;
}
const cosmosdb: BrandVariants = {
10: "#020305",
20: "#111723",
30: "#16263D",
40: "#193253",
50: "#1B3F6A",
60: "#1B4C82",
70: "#18599B",
80: "#1267B4",
90: "#3174C2",
100: "#4F82C8",
110: "#6790CF",
120: "#7D9ED5",
130: "#92ACDC",
140: "#A6BAE2",
150: "#BAC9E9",
160: "#CDD8EF",
};
const lightTheme: Theme = {
...createLightTheme(cosmosdb),
};
export const DATA_TREE_LABEL = "DATA";
/**
* Top-level tree that has no label, but contains all subtrees
*/
export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
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 { activeTab } = useTabs();
const databaseTreeNodes = useDatabaseTreeNodes(container, isNotebookEnabled);
const dataNodeTree = {
id: "data",
label: DATA_TREE_LABEL,
isExpanded: true,
className: "accordionItemHeader",
children: databaseTreeNodes,
isScrollable: true,
};
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([DATA_TREE_LABEL]);
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => setOpenItems(data.openItems);
return (
<>
<FluentProvider theme={lightTheme} style={{ overflow: "hidden" }}>
<Tree
aria-label="CosmosDB resources"
openItems={openItems}
onOpenChange={handleOpenChange}
size="medium"
style={{ height: "100%", width: "290px" }}
>
{[dataNodeTree].map((node) => (
<TreeNode2Component
key={node.label}
className="dataResourceTree"
node={node}
treeNodeId={node.label}
globalOpenIds={[...openItems].map((item) => item.toString())}
/>
))}
</Tree>
</FluentProvider>
</>
);
};

View File

@@ -0,0 +1,318 @@
import { TreeNode2 } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
import { useDatabases } from "Explorer/useDatabases";
import { getItemName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import CollectionIcon from "../../../images/tree-collection.svg";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
export const buildCollectionNode = (
database: ViewModels.Database,
collection: ViewModels.Collection,
isNotebookEnabled: boolean,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void
): TreeNode2 => {
let children: TreeNode2[];
// Flat Tree for Fabric
if (configContext.platform !== Platform.Fabric) {
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
}
return {
label: collection.id(),
iconSrc: CollectionIcon,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => {
useSelectedNode.getState().setSelectedNode(collection);
collection.openTab();
// push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
onExpanded: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
};
const buildCollectionNodeChildren = (
database: ViewModels.Database,
collection: ViewModels.Collection,
isNotebookEnabled: boolean,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void
): TreeNode2[] => {
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const children: TreeNode2[] = [];
children.push({
label: getItemName(),
id: collection.isSampleCollection ? "sampleItems" : "",
onClick: () => {
collection.openTab();
// push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
});
if (
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
useNotebook.getState().isPhoenixFeatures
) {
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
});
}
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
let id = "";
if (collection.isSampleCollection) {
id = database.isDatabaseShared() ? "sampleSettings" : "sampleScaleSettings";
}
children.push({
id,
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.CollectionSettingsV2,
]),
});
}
const schemaNode: TreeNode2 = buildSchemaNode(collection, container, refreshActiveTab);
if (schemaNode) {
children.push(schemaNode);
}
const onUpdateDatabase = () => useDatabases.getState().updateDatabase(database);
if (showScriptNodes) {
children.push(buildStoredProcedureNode(collection, container, refreshActiveTab, onUpdateDatabase));
children.push(buildUserDefinedFunctionsNode(collection, container, refreshActiveTab, onUpdateDatabase));
children.push(buildTriggerNode(collection, container, refreshActiveTab, onUpdateDatabase));
}
// This is a rewrite of showConflicts
const showConflicts =
userContext?.databaseAccount?.properties.enableMultipleWriteLocations &&
collection.rawDataModel &&
!!collection.rawDataModel.conflictResolutionPolicy;
if (showConflicts) {
children.push({
label: "Conflicts",
onClick: collection.onConflictsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
});
}
return children;
};
const buildStoredProcedureNode = (
collection: ViewModels.Collection,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
onUpdateDatabase: () => void
): TreeNode2 => {
return {
label: "Stored Procedures",
children: collection.storedProcedures().map((sp: StoredProcedure) => ({
label: sp.id(),
onClick: sp.open.bind(sp),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.StoredProcedures]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
})),
onExpanded: async () => {
await collection.loadStoredProcedures();
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
onUpdateDatabase();
},
};
};
const buildUserDefinedFunctionsNode = (
collection: ViewModels.Collection,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
onUpdateDatabase: () => void
): TreeNode2 => {
return {
label: "User Defined Functions",
children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({
label: udf.id(),
onClick: udf.open.bind(udf),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.UserDefinedFunctions,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
})),
onExpanded: async () => {
await collection.loadUserDefinedFunctions();
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
onUpdateDatabase();
},
};
};
const buildTriggerNode = (
collection: ViewModels.Collection,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
onUpdateDatabase: () => void
): TreeNode2 => {
return {
label: "Triggers",
children: collection.triggers().map((trigger: Trigger) => ({
label: trigger.id(),
onClick: trigger.open.bind(trigger),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
})),
onExpanded: async () => {
await collection.loadTriggers();
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
onUpdateDatabase();
},
};
};
const buildSchemaNode = (
collection: ViewModels.Collection,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void
): TreeNode2 => {
if (collection.analyticalStorageTtl() === undefined) {
return undefined;
}
if (!collection.schema || !collection.schema.fields) {
return undefined;
}
return {
label: "Schema",
children: getSchemaNodes(collection.schema.fields),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid);
},
};
};
const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode2[] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema: any = {};
//unflatten
fields.forEach((field: DataModels.IDataField) => {
const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) {
if (schema[name] === undefined) {
if (pathIndex === path.length - 1) {
schema[name] = fieldProperties;
} else {
schema[name] = {};
}
}
current = schema[name];
} else {
if (current[name] === undefined) {
if (pathIndex === path.length - 1) {
current[name] = fieldProperties;
} else {
current[name] = {};
}
}
current = current[name];
}
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const traverse = (obj: any): TreeNode2[] => {
const children: TreeNode2[] = [];
if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) });
});
} else if (Array.isArray(obj)) {
return [{ label: obj[0] }, { label: obj[1] }];
}
return children;
};
return traverse(schema);
};

View File

@@ -0,0 +1,84 @@
import { TreeNode2 } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
import TabsBase from "Explorer/Tabs/TabsBase";
import { buildCollectionNode } from "Explorer/Tree2/containerTreeNodeUtil";
import { useDatabases } from "Explorer/useDatabases";
import { useTabs } from "hooks/useTabs";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boolean): TreeNode2[] => {
const databases = useDatabases((state) => state.databases);
const { refreshActiveTab } = useTabs();
const databaseTreeNodes: TreeNode2[] = databases.map((database: ViewModels.Database) => {
const databaseNode: TreeNode2 = {
label: database.id(),
iconSrc: CosmosDBIcon,
className: "databaseHeader",
children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
onExpanded: async () => {
useSelectedNode.getState().setSelectedNode(database);
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
databaseNode.isLoading = false;
useCommandBar.getState().setContextButtons([]);
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
},
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
};
if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) {
databaseNode.children.push({
id: database.isSampleDB ? "sampleScaleSettings" : "",
label: "Scale",
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]),
onClick: database.onSettingsClick.bind(database),
});
}
// Find collections
database
.collections()
.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(
buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab)
)
);
if (database.collectionsContinuationToken) {
const loadMoreNode: TreeNode2 = {
label: "load more",
className: "loadMoreHeader",
onClick: async () => {
await database.loadCollections();
useDatabases.getState().updateDatabase(database);
},
};
databaseNode.children.push(loadMoreNode);
}
database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(
buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab)
)
);
});
return databaseNode;
});
return databaseTreeNodes;
};

View File

@@ -1,5 +1,5 @@
// CSS Dependencies
import { initializeIcons } from "@fluentui/react";
import { initializeIcons, loadTheme } from "@fluentui/react";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
@@ -26,6 +26,8 @@ import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import * as StyleConstants from "./Common/StyleConstants";
import { configContext, Platform } from "ConfigContext";
import "../less/documentDB.less";
import "../less/forms.less";
import "../less/infobox.less";
@@ -57,6 +59,7 @@ import "./Libs/jquery";
import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
initializeIcons();
@@ -67,6 +70,10 @@ const App: React.FunctionComponent = () => {
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
const config = useConfig();
if (config?.platform === Platform.Fabric) {
loadTheme(appThemeFabric);
}
StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform);
const toggleLeftPaneExpanded = () => {
@@ -84,6 +91,7 @@ const App: React.FunctionComponent = () => {
return (
<div className="flexContainer" aria-hidden="false">
<LoadFabricOverrides />
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
@@ -91,7 +99,7 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
{userContext.apiType !== "Postgres" && (
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */}
@@ -135,6 +143,19 @@ const App: React.FunctionComponent = () => {
ReactDOM.render(<App />, document.body);
function LoadFabricOverrides(): JSX.Element {
if (configContext.platform === Platform.Fabric) {
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
return (
<React.Suspense fallback={<div></div>}>
<FabricStyle />
</React.Suspense>
);
} else {
return <></>;
}
}
function LoadingExplorer(): JSX.Element {
return (
<div className="splashLoaderContainer">

View File

@@ -0,0 +1,7 @@
import React from "react";
import "../../../less/documentDBFabric.less";
// This is a dummy export, allowing us to conditionally import documentDBFabric.less
// by lazy-importing this in Main.tsx (see LoadFabricOverrides() there)
export default function InitFabric() {
return <></>;
}

View File

@@ -0,0 +1,208 @@
import { Theme, createTheme } from "@fluentui/react";
export const appThemeFabric: Theme = createTheme({
palette: {
/**
* Color code for themeDarker.
*/
themeDarker: "#033f38",
/**
* Color code for themeDark.
*/
themeDark: "#0a5c50",
/**
* Color code for themeDarkAlt.
*/
themeDarkAlt: "#0c695a",
/**
* Color code for themePrimary.
*/
themePrimary: "#117865",
/**
* Color code for themeSecondary.
*/
themeSecondary: "#1f937e",
/**
* Color code for themeTertiary.
*/
themeTertiary: "#52c7aa",
/**
* Color code for themeLight.
*/
themeLight: "#9ee0cb",
/**
* Color code for themeLighter.
*/
themeLighter: "#c0ecdd",
/**
* Color code for themeLighterAlt.
*/
themeLighterAlt: "#e3f7ef",
/**
* Color code for the strongest color, which is black in the default theme.
* This is a very light color in inverted themes.
*/
black: "#000000",
/**
* Color code for blackTranslucent40.
*/
blackTranslucent40: "rgba(0, 0, 0, 0.4)",
/**
* Color code for neutralDark.
*/
neutralDark: "#141414",
/**
* Color code for neutralPrimary.
*/
neutralPrimary: "#242424",
/**
* Color code for neutralPrimaryAlt.
*/
neutralPrimaryAlt: "#383838",
/**
* Color code for neutralSecondary.
*/
neutralSecondary: "#5c5c5c",
/**
* Color code for neutralSecondaryAlt.
*/
neutralSecondaryAlt: "#858585",
/**
* Color code for neutralTertiary.
*/
neutralTertiary: "#9e9e9e",
/**
* Color code for neutralTertiaryAlt.
*/
neutralTertiaryAlt: "#c7c7c7",
/**
* Color code for neutralQuaternary.
*/
neutralQuaternary: "#d1d1d1",
/**
* Color code for neutralQuaternaryAlt.
*/
neutralQuaternaryAlt: "#e0e0e0",
/**
* Color code for neutralLight.
*/
neutralLight: "#ebebeb",
/**
* Color code for neutralLighter.
*/
neutralLighter: "#f5f5f5",
/**
* Color code for neutralLighterAlt.
*/
neutralLighterAlt: "#fafafa",
/**
* Color code for the accent.
*/
accent: "#117865",
/**
* Color code for the softest color, which is white in the default theme. This is a very dark color in dark themes.
* This is the page background.
*/
white: "#ffffff",
/**
* Color code for whiteTranslucent40
*/
whiteTranslucent40: "rgba(255, 255, 255, 0.4)",
/**
* Color code for yellowDark.
*/
yellowDark: "#d39300",
/**
* Color code for yellow.
*/
yellow: "#fde300",
/**
* Color code for yellowLight.
*/
yellowLight: "#fef7b2",
/**
* Color code for orange.
*/
orange: "#f7630c",
/**
* Color code for orangeLight.
*/
orangeLight: "#f98845",
/**
* Color code for orangeLighter.
*/
orangeLighter: "#fdcfb4",
/**
* Color code for redDark.
*/
redDark: "#750b1c",
/**
* Color code for red.
*/
red: "#d13438",
/**
* Color code for magentaDark.
*/
magentaDark: "#6b0043",
/**
* Color code for magenta.
*/
magenta: "#bf0077",
/**
* Color code for magentaLight.
*/
magentaLight: "#d957a8",
/**
* Color code for purpleDark.
*/
purpleDark: "#401b6c",
/**
* Color code for purple.
*/
purple: "#5c2e91",
/**
* Color code for purpleLight.
*/
purpleLight: "#c6b1de",
/**
* Color code for blueDark.
*/
blueDark: "#003966",
/**
* Color code for blueMid.
*/
blueMid: "#004e8c",
/**
* Color code for blue.
*/
blue: "#0078d4",
/**
* Color code for blueLight.
*/
blueLight: "#3a96dd",
/**
* Color code for tealDark.
*/
tealDark: "#006666",
/**
* Color code for teal.
*/
teal: "#038387",
/**
* Color code for tealLight.
*/
tealLight: "#00b7c3",
/**
* Color code for greenDark.
*/
greenDark: "#0b6a0b",
/**
* Color code for green.
*/
green: "#107c10",
/**
* Color code for greenLight.
*/
greenLight: "#13a10e",
},
});

View File

@@ -4,7 +4,7 @@
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react";
import * as React from "react";
import { FunctionComponent, useEffect, useState } from "react";
import { StyleConstants } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../hooks/useSubscriptions";

View File

@@ -1,7 +1,7 @@
import { useBoolean } from "@fluentui/react-hooks";
import * as React from "react";
import ErrorImage from "../../../../images/error.svg";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg";
import { AuthType } from "../../../AuthType";
import { HttpHeaders } from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext";
@@ -16,6 +16,19 @@ interface Props {
setAuthType: (authType: AuthType) => void;
}
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
// This API has a quirk where it must be parsed twice
const result: GenerateTokenResponse = JSON.parse(await response.json());
return decodeURIComponent(result.readWrite || result.read);
};
export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken,
login,
@@ -44,16 +57,8 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
return;
}
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
// This API has a quirk where it must be parsed twice
const result: GenerateTokenResponse = JSON.parse(await response.json());
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
const encryptedToken = await fetchEncryptedToken(connectionString);
setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString);
}}
>

View File

@@ -38,8 +38,9 @@ export type Features = {
readonly enableCopilot: boolean;
readonly enablePriorityBasedThrottling: boolean;
readonly copilotVersion?: string;
readonly enableCopilotPhoenixGateaway: boolean;
readonly disableCopilotPhoenixGateaway: boolean;
readonly enableCopilotFullSchema: boolean;
readonly copilotChatFixedMonacoEditorHeight: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -108,10 +109,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enablePriorityBasedThrottling: "true" === get("enableprioritybasedthrottling"),
enableCopilot: "true" === get("enablecopilot"),
enableCopilot: "true" === get("enablecopilot", "true"),
copilotVersion: get("copilotversion") ?? "v1.0",
enableCopilotPhoenixGateaway: "true" === get("enablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema"),
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
};
}

View File

@@ -1,5 +1,5 @@
import { IButtonStyles, ICommandBarStyles, ISeparatorStyles, IStackTokens } from "@fluentui/react";
import { StyleConstants } from "../Common/Constants";
import { StyleConstants } from "../Common/StyleConstants";
export const commandBarItemStyles: IButtonStyles = { root: { paddingLeft: 20 } };

Some files were not shown because too many files have changed in this diff Show More