mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-21 11:47:14 +00:00
Merge branch 'master' into users/v-prklepic/V2deleteHistoryButton
This commit is contained in:
commit
863fdb4f7e
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -85,6 +85,8 @@ jobs:
|
|||||||
path: .cache
|
path: .cache
|
||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
- run: npm run pack:prod
|
- run: npm run pack:prod
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
- run: cp -r ./configs ./dist/configs
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
20
images/Table.svg
Normal file
20
images/Table.svg
Normal 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 |
BIN
images/vcoreMongoFirewallRule.png
Normal file
BIN
images/vcoreMongoFirewallRule.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -10,6 +10,7 @@
|
|||||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
@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;
|
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||||
@GrayScale: "grayscale()";
|
@GrayScale: "grayscale()";
|
||||||
|
@NoColor: "brightness(0) saturate(100%)";
|
||||||
|
|
||||||
@xSmallFontSize: 4px;
|
@xSmallFontSize: 4px;
|
||||||
@smallFontSize: 8px;
|
@smallFontSize: 8px;
|
||||||
@ -147,14 +148,41 @@
|
|||||||
// CommandBar
|
// CommandBar
|
||||||
@CommandBarButtonHeight: 40px;
|
@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
|
Common Flex Property
|
||||||
/**********************************************************************************/
|
/**********************************************************************************/
|
||||||
|
|
||||||
.flex-display(@display: flex) {
|
.flex-display(@display: flex) {
|
||||||
display: ~"-webkit-@{display}";
|
display:~"-webkit-@{display}";
|
||||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
display:~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||||
display: ~"-ms-@{display}"; // IE11
|
display:~"-ms-@{display}"; // IE11
|
||||||
display: @display;
|
display: @display;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,13 +196,15 @@
|
|||||||
High contrast mode active
|
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,
|
||||||
.selectedRadio:hover,
|
.selectedRadio:hover,
|
||||||
.selectedRadio:active,
|
.selectedRadio:active,
|
||||||
.selectedRadio.dirty,
|
.selectedRadio.dirty,
|
||||||
.tab [type="radio"]:checked ~ label,
|
.tab [type="radio"]:checked~label,
|
||||||
.tab [type="radio"]:checked ~ label:hover {
|
.tab [type="radio"]:checked~label:hover {
|
||||||
-ms-high-contrast-adjust: none;
|
-ms-high-contrast-adjust: none;
|
||||||
-webkit-text-fill-color: HighlightText;
|
-webkit-text-fill-color: HighlightText;
|
||||||
color: HighlightText;
|
color: HighlightText;
|
||||||
@ -183,6 +213,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queryMetricsSummaryTuple {
|
.queryMetricsSummaryTuple {
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
|
@ -2646,6 +2646,11 @@ a:link {
|
|||||||
width: @ActiveTabWidth;
|
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 {
|
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
||||||
.focus();
|
.focus();
|
||||||
}
|
}
|
||||||
|
211
less/documentDBFabric.less
Normal file
211
less/documentDBFabric.less
Normal 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%);
|
||||||
|
}
|
33010
package-lock.json
generated
33010
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@
|
|||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@fluentui/react": "8.14.3",
|
"@fluentui/react": "8.14.3",
|
||||||
|
"@fluentui/react-components": "9.30.1",
|
||||||
"@jupyterlab/services": "6.0.2",
|
"@jupyterlab/services": "6.0.2",
|
||||||
"@jupyterlab/terminal": "3.0.3",
|
"@jupyterlab/terminal": "3.0.3",
|
||||||
"@microsoft/applicationinsights-web": "2.6.1",
|
"@microsoft/applicationinsights-web": "2.6.1",
|
||||||
|
@ -365,9 +365,6 @@ export const EmulatorMasterKey =
|
|||||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
"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 {
|
export class Notebook {
|
||||||
public static readonly defaultBasePath = "./notebooks";
|
public static readonly defaultBasePath = "./notebooks";
|
||||||
public static readonly heartbeatDelayMs = 60000;
|
public static readonly heartbeatDelayMs = 60000;
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
import { NormalizedEventKey } from "./Constants";
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface ResourceTreeContainerProps {
|
export interface ResourceTreeContainerProps {
|
||||||
@ -76,6 +78,8 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
<ResourceTokenTree />
|
<ResourceTokenTree />
|
||||||
) : userContext.features.enableKoResourceTree ? (
|
) : userContext.features.enableKoResourceTree ? (
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||||
|
) : configContext.platform === Platform.Fabric ? (
|
||||||
|
<ResourceTree2 container={container} />
|
||||||
) : (
|
) : (
|
||||||
<ResourceTree container={container} />
|
<ResourceTree container={container} />
|
||||||
)}
|
)}
|
||||||
|
18
src/Common/StyleConstants.ts
Normal file
18
src/Common/StyleConstants.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ export enum Platform {
|
|||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
Hosted = "Hosted",
|
Hosted = "Hosted",
|
||||||
Emulator = "Emulator",
|
Emulator = "Emulator",
|
||||||
|
Fabric = "Fabric",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
@ -187,6 +188,7 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
|||||||
console.error(`Invalid platform query parameter: ${platform}`);
|
console.error(`Invalid platform query parameter: ${platform}`);
|
||||||
break;
|
break;
|
||||||
case Platform.Portal:
|
case Platform.Portal:
|
||||||
|
case Platform.Fabric:
|
||||||
case Platform.Hosted:
|
case Platform.Hosted:
|
||||||
case Platform.Emulator:
|
case Platform.Emulator:
|
||||||
updateConfigContext({ platform });
|
updateConfigContext({ platform });
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface DatabaseAccount {
|
export interface ArmEntity {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
location: string;
|
||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatabaseAccount extends ArmEntity {
|
||||||
properties: DatabaseAccountExtendedProperties;
|
properties: DatabaseAccountExtendedProperties;
|
||||||
systemData?: DatabaseAccountSystemData;
|
systemData?: DatabaseAccountSystemData;
|
||||||
}
|
}
|
||||||
@ -35,6 +38,7 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
locations?: DatabaseAccountResponseLocation[];
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
postgresqlEndpoint?: string;
|
postgresqlEndpoint?: string;
|
||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
|
vcoreMongoEndpoint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
@ -575,7 +579,7 @@ export interface ContainerConnectionInfo {
|
|||||||
//need to add ram and rom info
|
//need to add ram and rom info
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostgresFirewallRule {
|
export interface FirewallRule {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -372,6 +372,7 @@ export enum TerminalKind {
|
|||||||
Mongo = 1,
|
Mongo = 1,
|
||||||
Cassandra = 2,
|
Cassandra = 2,
|
||||||
Postgres = 3,
|
Postgres = 3,
|
||||||
|
VCoreMongo = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataExplorerInputsFrame {
|
export interface DataExplorerInputsFrame {
|
||||||
@ -397,6 +398,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||||
isPostgresAccount?: boolean;
|
isPostgresAccount?: boolean;
|
||||||
isReplica?: boolean;
|
isReplica?: boolean;
|
||||||
|
isVCoreMongoAccount?: boolean;
|
||||||
clientIpAddress?: string;
|
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
|
// 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;
|
connectionStringParams?: any;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
import * as Plotly from "plotly.js-cartesian-dist-min";
|
||||||
import { StyleConstants } from "../../Common/Constants";
|
|
||||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
||||||
|
import { StyleConstants } from "../../Common/StyleConstants";
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||||
import "./Heatmap.less";
|
import "./Heatmap.less";
|
||||||
|
@ -18,6 +18,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||||
import Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
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({
|
items.push({
|
||||||
iconSrc: AddStoredProcedureIcon,
|
iconSrc: AddStoredProcedureIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||||
},
|
},
|
||||||
label: "New Stored Procedure",
|
label: "New Stored Procedure",
|
||||||
@ -112,7 +115,6 @@ export const createCollectionContextMenuButton = (
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: AddUdfIcon,
|
iconSrc: AddUdfIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
|
||||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||||
},
|
},
|
||||||
label: "New UDF",
|
label: "New UDF",
|
||||||
@ -121,7 +123,6 @@ export const createCollectionContextMenuButton = (
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: AddTriggerIcon,
|
iconSrc: AddTriggerIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
|
||||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||||
},
|
},
|
||||||
label: "New Trigger",
|
label: "New Trigger",
|
||||||
@ -130,13 +131,15 @@ export const createCollectionContextMenuButton = (
|
|||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: () =>
|
onClick: () => {
|
||||||
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
"Delete " + getCollectionName(),
|
"Delete " + getCollectionName(),
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
||||||
),
|
);
|
||||||
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ export interface NotebookTerminalComponentProps {
|
|||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||||
@ -50,7 +51,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props: TerminalProps = {
|
let props: TerminalProps = {
|
||||||
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
||||||
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
||||||
authToken: this.props.notebookServerInfo?.authToken,
|
authToken: this.props.notebookServerInfo?.authToken,
|
||||||
@ -61,6 +62,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
tabId: this.props.tabId,
|
tabId: this.props.tabId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.props.username) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
username: this.props.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
postRobot.send(this.terminalWindow, "props", props, {
|
postRobot.send(this.terminalWindow, "props", props, {
|
||||||
domain: window.location.origin,
|
domain: window.location.origin,
|
||||||
});
|
});
|
||||||
@ -78,6 +86,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
||||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
||||||
|
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
|
||||||
|
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminalEndpoint) {
|
if (terminalEndpoint) {
|
||||||
|
@ -23,9 +23,9 @@ import * as React from "react";
|
|||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||||
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
@ -23,7 +23,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as React from "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 { hoursInAMonth } from "../../../Shared/Constants";
|
||||||
import {
|
import {
|
||||||
computeRUUsagePriceHourly,
|
computeRUUsagePriceHourly,
|
||||||
|
@ -18,6 +18,7 @@ import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squa
|
|||||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
@ -237,7 +238,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||||
const menuItemLabel = "More";
|
const menuItemLabel = "More";
|
||||||
const buttonStyles: Partial<IButtonStyles> = {
|
const buttonStyles: Partial<IButtonStyles> = {
|
||||||
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` },
|
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
142
src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx
Normal file
142
src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
@ -277,6 +277,20 @@ export default class Explorer {
|
|||||||
userContext.databaseAccount?.systemData?.createdAt || "",
|
userContext.databaseAccount?.systemData?.createdAt || "",
|
||||||
NINETY_DAYS_IN_MS
|
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.
|
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
|
||||||
if (userContext.isTryCosmosDBSubscription) {
|
if (userContext.isTryCosmosDBSubscription) {
|
||||||
@ -285,17 +299,20 @@ export default class Explorer {
|
|||||||
this.getRandomInt(100) < 25
|
this.getRandomInt(100) < 25
|
||||||
) {
|
) {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
|
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
|
||||||
if (isAccountNewerThanNinetyDays) {
|
if (isAccountNewerThanNinetyDays) {
|
||||||
if (this.getRandomInt(100) < 10) {
|
if (this.getRandomInt(100) < 10) {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
|
// 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) {
|
if (this.getRandomInt(100) < 25) {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1127,6 +1144,10 @@ export default class Explorer {
|
|||||||
title = "PSQL Shell";
|
title = "PSQL Shell";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ViewModels.TerminalKind.VCoreMongo:
|
||||||
|
title = "VCoreMongo Shell";
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Terminal kind: ${kind} not supported");
|
throw new Error("Terminal kind: ${kind} not supported");
|
||||||
}
|
}
|
||||||
@ -1313,7 +1334,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshExplorer(): Promise<void> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
if (userContext.apiType !== "Postgres") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
@ -1322,9 +1343,10 @@ export default class Explorer {
|
|||||||
|
|
||||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
const isNotebookEnabled =
|
const isNotebookEnabled =
|
||||||
userContext.features.notebooksDownBanner ||
|
configContext.platform !== Platform.Fabric &&
|
||||||
|
(userContext.features.notebooksDownBanner ||
|
||||||
useNotebook.getState().isPhoenixNotebooks ||
|
useNotebook.getState().isPhoenixNotebooks ||
|
||||||
useNotebook.getState().isPhoenixFeatures;
|
useNotebook.getState().isPhoenixFeatures);
|
||||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||||
useNotebook
|
useNotebook
|
||||||
.getState()
|
.getState()
|
||||||
|
@ -8,7 +8,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
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 { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@ -34,8 +36,11 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container);
|
const buttons =
|
||||||
|
userContext.apiType === "Postgres"
|
||||||
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer">
|
||||||
<FluentCommandBar
|
<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 (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer">
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
farItems={uiFabricControlButtons}
|
farItems={uiFabricControlButtons}
|
||||||
styles={{
|
styles={rootStyle}
|
||||||
root: { backgroundColor: backgroundColor },
|
|
||||||
}}
|
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,8 +142,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Open Mongo Shell button", () => {
|
describe("Open Mongo shell button", () => {
|
||||||
const openMongoShellBtnLabel = "Open Mongo Shell";
|
const openMongoShellBtnLabel = "Open Mongo shell";
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -247,8 +247,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Open Cassandra Shell button", () => {
|
describe("Open Cassandra shell button", () => {
|
||||||
const openCassandraShellBtnLabel = "Open Cassandra Shell";
|
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
@ -54,7 +54,11 @@ export function createStaticCommandBarButtons(
|
|||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
buttons.push(newCollectionBtn);
|
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);
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
|
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
@ -94,9 +98,9 @@ export function createStaticCommandBarButtons(
|
|||||||
) {
|
) {
|
||||||
notebookButtons.push(createDivider());
|
notebookButtons.push(createDivider());
|
||||||
if (userContext.apiType === "Cassandra") {
|
if (userContext.apiType === "Cassandra") {
|
||||||
notebookButtons.push(createOpenCassandraTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
|
||||||
} else {
|
} else {
|
||||||
notebookButtons.push(createOpenMongoTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +261,9 @@ export function createDivider(): CommandButtonComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function areScriptsSupported(): boolean {
|
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 {
|
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
|
||||||
@ -499,8 +505,25 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenTerminalButtonByKind(
|
||||||
const label = "Open Mongo Shell";
|
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 =
|
const tooltip =
|
||||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||||
const disableButton =
|
const disableButton =
|
||||||
@ -510,7 +533,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
|||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
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 {
|
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Reset Workspace";
|
const label = "Reset Workspace";
|
||||||
return {
|
return {
|
||||||
@ -630,7 +608,13 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
|
return [openVCoreMongoTerminalButton];
|
||||||
|
}
|
||||||
|
@ -9,7 +9,9 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
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 { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
@ -24,11 +26,14 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||||
|
|
||||||
|
const hoverColor =
|
||||||
|
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
|
||||||
|
|
||||||
const getFilter = (isDisabled: boolean): string => {
|
const getFilter = (isDisabled: boolean): string => {
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return StyleConstants.GrayScale;
|
return StyleConstants.GrayScale;
|
||||||
}
|
}
|
||||||
return undefined;
|
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return btns
|
return btns
|
||||||
@ -68,6 +73,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
height: buttonHeightPx,
|
height: buttonHeightPx,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
|
borderRadius: configContext.platform == Platform.Fabric ? StyleConstants.FabricButtonBorderRadius : "0px",
|
||||||
minWidth: 24,
|
minWidth: 24,
|
||||||
marginLeft: isSplit ? 0 : 5,
|
marginLeft: isSplit ? 0 : 5,
|
||||||
marginRight: isSplit ? 0 : 5,
|
marginRight: isSplit ? 0 : 5,
|
||||||
@ -79,17 +85,17 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
splitButtonMenuButton: {
|
splitButtonMenuButton: {
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
selectors: {
|
selectors: {
|
||||||
":hover": { backgroundColor: StyleConstants.AccentLight },
|
":hover": { backgroundColor: hoverColor },
|
||||||
},
|
},
|
||||||
width: 16,
|
width: 16,
|
||||||
},
|
},
|
||||||
label: { fontSize: StyleConstants.mediumFontSize },
|
label: { fontSize: StyleConstants.mediumFontSize },
|
||||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
rootHovered: { backgroundColor: hoverColor },
|
||||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
rootPressed: { backgroundColor: hoverColor },
|
||||||
splitButtonMenuButtonExpanded: {
|
splitButtonMenuButtonExpanded: {
|
||||||
backgroundColor: StyleConstants.AccentExtra,
|
backgroundColor: StyleConstants.AccentExtra,
|
||||||
selectors: {
|
selectors: {
|
||||||
":hover": { backgroundColor: StyleConstants.AccentLight },
|
":hover": { backgroundColor: hoverColor },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
splitButtonDivider: {
|
splitButtonDivider: {
|
||||||
@ -120,7 +126,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||||
selectors: {
|
selectors: {
|
||||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
".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 },
|
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lastSaved?: Date | null;
|
lastSaved?: Date | null;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { cloneDeep } from "lodash";
|
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@ -10,13 +10,13 @@ import * as Logger from "../../Common/Logger";
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import NotebookManager from "./NotebookManager";
|
import NotebookManager from "./NotebookManager";
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const firstWriteLocation =
|
const firstWriteLocation =
|
||||||
userContext.apiType === "Postgres"
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
? databaseAccount?.location
|
? databaseAccount?.location
|
||||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
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;
|
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||||
isPhoenixFeatures =
|
isPhoenixFeatures =
|
||||||
isPublicInternetAllowed &&
|
isPublicInternetAllowed &&
|
||||||
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
|
// 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.features.phoenixFeatures === true ||
|
||||||
|
userContext.apiType === "Postgres" ||
|
||||||
|
userContext.apiType === "VCoreMongo");
|
||||||
} else {
|
} else {
|
||||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
|
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
|
||||||
import React, { FormEvent, FunctionComponent } from "react";
|
import React, { FormEvent, FunctionComponent } from "react";
|
||||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
type: "MyNotebooks" | "GitHub";
|
type: "MyNotebooks" | "GitHub";
|
||||||
@ -65,7 +65,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
|||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
key: "GitHub-Header",
|
key: "GitHub-Header",
|
||||||
text: ResourceTreeAdapter.GitHubReposTitle,
|
text: GitHubReposTitle,
|
||||||
itemType: SelectableOptionMenuItemType.Header,
|
itemType: SelectableOptionMenuItemType.Header,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
|
import { QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { StyleConstants } from "Common/StyleConstants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions } from "@azure/cosmos";
|
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
@ -20,16 +19,11 @@ import { useBoolean } from "@fluentui/react-hooks";
|
|||||||
import {
|
import {
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
PoolIdType,
|
PoolIdType,
|
||||||
QueryCopilotSampleContainerId,
|
|
||||||
QueryCopilotSampleContainerSchema,
|
QueryCopilotSampleContainerSchema,
|
||||||
ShortenedQueryCopilotSampleContainerSchema,
|
ShortenedQueryCopilotSampleContainerSchema,
|
||||||
} from "Common/Constants";
|
} from "Common/Constants";
|
||||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
|
||||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
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 { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { OnExecuteQueryClick, SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
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 { 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 { userContext } from "UserContext";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
@ -83,8 +73,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
setSelectedQuery,
|
setSelectedQuery,
|
||||||
isGeneratingQuery,
|
isGeneratingQuery,
|
||||||
setIsGeneratingQuery,
|
setIsGeneratingQuery,
|
||||||
isExecuting,
|
|
||||||
setIsExecuting,
|
|
||||||
likeQuery,
|
likeQuery,
|
||||||
setLikeQuery,
|
setLikeQuery,
|
||||||
dislikeQuery,
|
dislikeQuery,
|
||||||
@ -93,12 +81,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
setShowCallout,
|
setShowCallout,
|
||||||
showSamplePrompts,
|
showSamplePrompts,
|
||||||
setShowSamplePrompts,
|
setShowSamplePrompts,
|
||||||
queryIterator,
|
|
||||||
setQueryIterator,
|
|
||||||
queryResults,
|
|
||||||
setQueryResults,
|
|
||||||
errorMessage,
|
|
||||||
setErrorMessage,
|
|
||||||
isSamplePromptsOpen,
|
isSamplePromptsOpen,
|
||||||
setIsSamplePromptsOpen,
|
setIsSamplePromptsOpen,
|
||||||
showDeletePopup,
|
showDeletePopup,
|
||||||
@ -109,7 +91,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
setshowCopyPopup,
|
setshowCopyPopup,
|
||||||
showErrorMessageBar,
|
showErrorMessageBar,
|
||||||
setShowErrorMessageBar,
|
setShowErrorMessageBar,
|
||||||
generatedQueryComments,
|
|
||||||
setGeneratedQueryComments,
|
setGeneratedQueryComments,
|
||||||
} = useQueryCopilot();
|
} = useQueryCopilot();
|
||||||
|
|
||||||
@ -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 getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
const executeQueryBtn = {
|
const executeQueryBtn = {
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: executeQueryBtnLabel,
|
iconAlt: executeQueryBtnLabel,
|
||||||
onCommandClick: () => onExecuteQueryClick(),
|
onCommandClick: () => OnExecuteQueryClick(),
|
||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
@ -622,16 +551,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
<QueryCopilotResults />
|
||||||
isMongoDB={false}
|
|
||||||
queryEditorContent={selectedQuery || query}
|
|
||||||
error={errorMessage}
|
|
||||||
queryResults={queryResults}
|
|
||||||
isExecuting={isExecuting}
|
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
|
||||||
queryDocumentsPerPage(firstItemIndex, queryIterator)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SplitterLayout>
|
</SplitterLayout>
|
||||||
</Stack>
|
</Stack>
|
||||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
PoolIdType,
|
PoolIdType,
|
||||||
|
QueryCopilotSampleContainerId,
|
||||||
QueryCopilotSampleContainerSchema,
|
QueryCopilotSampleContainerSchema,
|
||||||
ShortenedQueryCopilotSampleContainerSchema,
|
ShortenedQueryCopilotSampleContainerSchema,
|
||||||
} from "Common/Constants";
|
} 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 { createUri } from "Common/UrlUtility";
|
||||||
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||||
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
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 { userContext } from "UserContext";
|
||||||
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
|
||||||
@ -20,15 +30,13 @@ export const SendQueryRequest = async ({
|
|||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
if (userPrompt.trim() !== "") {
|
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
|
useQueryCopilot
|
||||||
.getState()
|
.getState()
|
||||||
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
|
.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 {
|
try {
|
||||||
if (
|
if (
|
||||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||||
@ -62,14 +70,16 @@ export const SendQueryRequest = async ({
|
|||||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (generateSQLQueryResponse?.sql) {
|
if (generateSQLQueryResponse?.sql) {
|
||||||
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
|
const bubbleMessage = `Here is a query which will help you with provided prompt.\r\n **Prompt:** "${userPrompt}"`;
|
||||||
query += `\r\n${generateSQLQueryResponse.sql}`;
|
|
||||||
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
||||||
useQueryCopilot
|
useQueryCopilot.getState().setChatMessages([
|
||||||
.getState()
|
|
||||||
.setChatMessages([
|
|
||||||
...useQueryCopilot.getState().chatMessages,
|
...useQueryCopilot.getState().chatMessages,
|
||||||
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
|
{
|
||||||
|
source: 1,
|
||||||
|
message: bubbleMessage,
|
||||||
|
sqlQuery: generateSQLQueryResponse.sql,
|
||||||
|
explanation: generateSQLQueryResponse.explanation,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
useQueryCopilot.getState().setShowExplanationBubble(true);
|
useQueryCopilot.getState().setShowExplanationBubble(true);
|
||||||
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
@ -133,3 +143,57 @@ export const SubmitFeedback = async ({
|
|||||||
handleError(error, "copilotSubmitFeedback");
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -11,11 +11,13 @@ export interface GenerateSQLQueryResponse {
|
|||||||
enum MessageSource {
|
enum MessageSource {
|
||||||
User,
|
User,
|
||||||
AI,
|
AI,
|
||||||
|
AIExplanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotMessage {
|
export interface CopilotMessage {
|
||||||
source: MessageSource;
|
source: MessageSource;
|
||||||
message: string;
|
message: string;
|
||||||
|
sqlQuery?: string;
|
||||||
explanation?: string;
|
explanation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx
Normal file
19
src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx
Normal 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)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -2,57 +2,44 @@ import { Stack, Text } from "@fluentui/react";
|
|||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const ExplanationBubble: React.FC = (): JSX.Element => {
|
export const ExplanationButton: React.FC = (): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
showExplanationBubble,
|
showExplanationBubble,
|
||||||
isGeneratingQuery,
|
isGeneratingQuery,
|
||||||
showQueryExplanation,
|
|
||||||
setShowQueryExplanation,
|
|
||||||
chatMessages,
|
chatMessages,
|
||||||
setChatMessages,
|
setChatMessages,
|
||||||
|
generatedQuery,
|
||||||
generatedQueryComments,
|
generatedQueryComments,
|
||||||
isGeneratingExplanation,
|
isGeneratingExplanation,
|
||||||
setIsGeneratingExplanation,
|
setIsGeneratingExplanation,
|
||||||
shouldIncludeInMessages,
|
|
||||||
setShouldIncludeInMessages,
|
setShouldIncludeInMessages,
|
||||||
|
setShowExplanationBubble,
|
||||||
} = useQueryCopilot();
|
} = useQueryCopilot();
|
||||||
|
|
||||||
const showExplanation = () => {
|
const showExplanation = () => {
|
||||||
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
|
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
|
||||||
setIsGeneratingExplanation(true);
|
setIsGeneratingExplanation(true);
|
||||||
setShouldIncludeInMessages(true);
|
setShouldIncludeInMessages(true);
|
||||||
|
setShowExplanationBubble(false);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
||||||
setIsGeneratingExplanation(false);
|
setIsGeneratingExplanation(false);
|
||||||
setShowQueryExplanation(true);
|
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments, sqlQuery: generatedQuery }]);
|
||||||
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
showExplanationBubble &&
|
showExplanationBubble &&
|
||||||
!isGeneratingQuery &&
|
!isGeneratingQuery &&
|
||||||
!isGeneratingExplanation &&
|
!isGeneratingExplanation && (
|
||||||
(showQueryExplanation && shouldIncludeInMessages ? (
|
|
||||||
<Stack
|
|
||||||
horizontalAlign="center"
|
|
||||||
tokens={{ padding: 8, childrenGap: 8 }}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
borderRadius: "8px",
|
|
||||||
margin: "5px 10px",
|
|
||||||
textAlign: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{generatedQueryComments}
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Stack
|
<Stack
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "5px 5px 5px 40px",
|
padding: "5px 5px 5px 50px",
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
width: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@ -69,6 +56,6 @@ export const ExplanationBubble: React.FC = (): JSX.Element => {
|
|||||||
Explain this query to me
|
Explain this query to me
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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`] = `""`;
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ExplanationBubble } from "./ExplanationBubble";
|
|
||||||
|
|
||||||
describe("Explanation Bubble", () => {
|
describe("Explanation Bubble snapshot tests", () => {
|
||||||
const initialStoreState = useQueryCopilot.getState();
|
it("should render", () => {
|
||||||
beforeEach(() => {
|
const mockCopilotMessage: CopilotMessage = {
|
||||||
useQueryCopilot.setState(initialStoreState, true);
|
source: 2,
|
||||||
useQueryCopilot.getState().showExplanationBubble = true;
|
message: "Mock message",
|
||||||
useQueryCopilot.getState().shouldIncludeInMessages = false;
|
};
|
||||||
jest.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
const wrapper = shallow(<ExplanationBubble copilotMessage={mockCopilotMessage} />);
|
||||||
jest.clearAllMocks();
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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
|
<Stack
|
||||||
horizontalAlign="center"
|
horizontalAlign="start"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"backgroundColor": "white",
|
"backgroundColor": "white",
|
||||||
@ -17,7 +17,22 @@ exports[`Explanation Bubble should render explanation bubble with generated comm
|
|||||||
"padding": 8,
|
"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`] = `""`;
|
|
||||||
|
@ -10,7 +10,7 @@ describe("Copy button snapshot tests", () => {
|
|||||||
it("should render and click copy", async () => {
|
it("should render and click copy", async () => {
|
||||||
const testInput = "test input query";
|
const testInput = "test input query";
|
||||||
useQueryCopilot.getState().setGeneratedQuery(testInput);
|
useQueryCopilot.getState().setGeneratedQuery(testInput);
|
||||||
const wrapper = shallow(<CopyButton />);
|
const wrapper = shallow(<CopyButton sqlQuery={""} />);
|
||||||
|
|
||||||
const button = wrapper.find(IconButton).first();
|
const button = wrapper.find(IconButton).first();
|
||||||
button.simulate("click", {});
|
button.simulate("click", {});
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { IconButton } from "@fluentui/react";
|
import { IconButton } from "@fluentui/react";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CopilotCopy from "../../../../../../../../images/CopilotCopy.svg";
|
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 copyGeneratedCode = (): void => {
|
||||||
const queryElement = document.createElement("textarea");
|
const queryElement = document.createElement("textarea");
|
||||||
queryElement.value = useQueryCopilot.getState().generatedQuery;
|
queryElement.value = sqlQuery;
|
||||||
document.body.appendChild(queryElement);
|
document.body.appendChild(queryElement);
|
||||||
queryElement.select();
|
queryElement.select();
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
|
@ -20,7 +20,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe("Feedback buttons snapshot tests", () => {
|
describe("Feedback buttons snapshot tests", () => {
|
||||||
it("should click like and show callout", () => {
|
it("should click like and show callout", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
const dislikeButton = wrapper.find(IconButton).last();
|
const dislikeButton = wrapper.find(IconButton).last();
|
||||||
@ -35,7 +35,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should click like and dismiss callout", () => {
|
it("should click like and dismiss callout", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
const likeButton = wrapper.find(IconButton).first();
|
const likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("click");
|
likeButton.simulate("click");
|
||||||
@ -49,7 +49,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
|
|
||||||
it("should click like and submit feedback", () => {
|
it("should click like and submit feedback", () => {
|
||||||
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
const likeButton = wrapper.find(IconButton).first();
|
const likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("click");
|
likeButton.simulate("click");
|
||||||
@ -61,7 +61,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over like", () => {
|
it("should hover over like", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("mouseover");
|
likeButton.simulate("mouseover");
|
||||||
@ -72,7 +72,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over rest like and leave", () => {
|
it("should hover over rest like and leave", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("mouseover");
|
likeButton.simulate("mouseover");
|
||||||
@ -84,7 +84,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over pressed like and leave", () => {
|
it("should hover over pressed like and leave", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("click");
|
likeButton.simulate("click");
|
||||||
@ -97,7 +97,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over like and click", () => {
|
it("should hover over like and click", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("mouseover");
|
likeButton.simulate("mouseover");
|
||||||
@ -109,7 +109,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should dobule click on like", () => {
|
it("should dobule click on like", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let likeButton = wrapper.find(IconButton).first();
|
let likeButton = wrapper.find(IconButton).first();
|
||||||
likeButton.simulate("click");
|
likeButton.simulate("click");
|
||||||
@ -124,7 +124,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
|
|
||||||
it("should click dislike and show popup", () => {
|
it("should click dislike and show popup", () => {
|
||||||
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
const likeButton = wrapper.find(IconButton).first();
|
const likeButton = wrapper.find(IconButton).first();
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
@ -140,7 +140,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over dislike", () => {
|
it("should hover over dislike", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
dislikeButton.simulate("mouseover");
|
dislikeButton.simulate("mouseover");
|
||||||
@ -151,7 +151,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over rest dislike and leave", () => {
|
it("should hover over rest dislike and leave", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
dislikeButton.simulate("mouseover");
|
dislikeButton.simulate("mouseover");
|
||||||
@ -163,7 +163,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over pressed dislike and leave", () => {
|
it("should hover over pressed dislike and leave", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
dislikeButton.simulate("click");
|
dislikeButton.simulate("click");
|
||||||
@ -178,7 +178,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should hover over dislike and click", () => {
|
it("should hover over dislike and click", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
dislikeButton.simulate("mouseover");
|
dislikeButton.simulate("mouseover");
|
||||||
@ -190,7 +190,7 @@ describe("Feedback buttons snapshot tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should dobule click on dislike", () => {
|
it("should dobule click on dislike", () => {
|
||||||
const wrapper = shallow(<FeedbackButtons />);
|
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
|
||||||
|
|
||||||
let dislikeButton = wrapper.find(IconButton).last();
|
let dislikeButton = wrapper.find(IconButton).last();
|
||||||
dislikeButton.simulate("click");
|
dislikeButton.simulate("click");
|
||||||
|
@ -6,8 +6,8 @@ import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
|
|||||||
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
|
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
|
||||||
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
|
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
|
||||||
|
|
||||||
export const FeedbackButtons: React.FC = (): JSX.Element => {
|
export const FeedbackButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||||
const { generatedQuery, userPrompt } = useQueryCopilot();
|
const { userPrompt } = useQueryCopilot();
|
||||||
|
|
||||||
const [likeQuery, setLikeQuery] = useState<boolean>(false);
|
const [likeQuery, setLikeQuery] = useState<boolean>(false);
|
||||||
const [dislikeQuery, setDislikeQuery] = useState<boolean>(false);
|
const [dislikeQuery, setDislikeQuery] = useState<boolean>(false);
|
||||||
@ -35,7 +35,7 @@ export const FeedbackButtons: React.FC = (): JSX.Element => {
|
|||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCalloutVisible(false);
|
setCalloutVisible(false);
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
useQueryCopilot.getState().openFeedbackModal(sqlQuery, true, userPrompt);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
more feedback?
|
more feedback?
|
||||||
@ -82,7 +82,7 @@ export const FeedbackButtons: React.FC = (): JSX.Element => {
|
|||||||
setLikeQuery(false);
|
setLikeQuery(false);
|
||||||
setDislikeImageLink(LikePressed);
|
setDislikeImageLink(LikePressed);
|
||||||
setLikeImageLink(LikeRest);
|
setLikeImageLink(LikeRest);
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
useQueryCopilot.getState().openFeedbackModal(sqlQuery, false, userPrompt);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseOver={() => setDislikeImageLink(LikeHover)}
|
onMouseOver={() => setDislikeImageLink(LikeHover)}
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import { ActionButton } from "@fluentui/react";
|
|
||||||
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
|
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
describe("Insert button snapshot tests", () => {
|
describe("Insert button snapshot tests", () => {
|
||||||
it("should click and update state", () => {
|
it("should click and update state", () => {
|
||||||
const testQuery = "test query";
|
const wrapper = shallow(<InsertButton sqlQuery={""} />);
|
||||||
useQueryCopilot.getState().setGeneratedQuery(testQuery);
|
|
||||||
const wrapper = shallow(<InsertButton />);
|
|
||||||
|
|
||||||
const button = wrapper.find(ActionButton).first();
|
|
||||||
button.simulate("click");
|
|
||||||
|
|
||||||
expect(useQueryCopilot.getState().query).toEqual(testQuery);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,12 +3,12 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import CopilotInsert from "../../../../../../../../images/CopilotInsert.svg";
|
import CopilotInsert from "../../../../../../../../images/CopilotInsert.svg";
|
||||||
|
|
||||||
export const InsertButton: React.FC = (): JSX.Element => {
|
export const InsertButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
iconProps={{ imageProps: { src: CopilotInsert } }}
|
iconProps={{ imageProps: { src: CopilotInsert } }}
|
||||||
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
|
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
|
||||||
onClick={() => useQueryCopilot.getState().setQuery(useQueryCopilot.getState().generatedQuery)}
|
onClick={() => useQueryCopilot.getState().setQuery(sqlQuery)}
|
||||||
>
|
>
|
||||||
Insert
|
Insert
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
|
|
||||||
describe("Output Bubble Buttons snapshot tests", () => {
|
describe("Output Bubble Buttons snapshot tests", () => {
|
||||||
it("should render", () => {
|
it("should render", () => {
|
||||||
const wrapper = shallow(<OutputBubbleButtons />);
|
const wrapper = shallow(<OutputBubbleButtons sqlQuery={""} />);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -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 { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const OutputBubbleButtons: React.FC = (): JSX.Element => {
|
export const OutputBubbleButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item style={{ paddingTop: "5px" }}>
|
<Stack.Item style={{ paddingTop: "5px" }}>
|
||||||
<InsertButton />
|
<InsertButton sqlQuery={sqlQuery} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<CopyButton />
|
<CopyButton sqlQuery={sqlQuery} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<FeedbackButtons />
|
<FeedbackButtons sqlQuery={sqlQuery} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<MoreButton />
|
<MoreButton />
|
||||||
|
@ -11,13 +11,19 @@ exports[`Output Bubble Buttons snapshot tests should render 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<InsertButton />
|
<InsertButton
|
||||||
|
sqlQuery=""
|
||||||
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<CopyButton />
|
<CopyButton
|
||||||
|
sqlQuery=""
|
||||||
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<FeedbackButtons />
|
<FeedbackButtons
|
||||||
|
sqlQuery=""
|
||||||
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<MoreButton />
|
<MoreButton />
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import { withHooks } from "jest-react-hooks-shallow";
|
import { withHooks } from "jest-react-hooks-shallow";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
describe("Output Bubble snapshot tests", () => {
|
describe("Output Bubble snapshot tests", () => {
|
||||||
it("should render and update height", () => {
|
it("should render and update height", () => {
|
||||||
withHooks(() => {
|
withHooks(() => {
|
||||||
useQueryCopilot.getState().setGeneratedQuery("test query");
|
const wrapper = shallow(
|
||||||
useQueryCopilot.getState().setGeneratedQueryComments("test comments");
|
<OutputBubble
|
||||||
const wrapper = shallow(<OutputBubble />);
|
copilotMessage={{ message: "testMessage", source: 1, explanation: "testExplanation", sqlQuery: "testSQL" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const editor = wrapper.find(EditorReact).first();
|
const editor = wrapper.find(EditorReact).first();
|
||||||
|
|
||||||
|
@ -1,19 +1,32 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
import { Stack, Text } from "@fluentui/react";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
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 { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { userContext } from "UserContext";
|
||||||
import React, { useState } from "react";
|
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 [windowHeight, setWindowHeight] = useState<string>();
|
||||||
|
const textHeightWithPadding = 16;
|
||||||
|
|
||||||
const calculateQueryWindowHeight = (): string => {
|
const calculateQueryWindowHeight = (): string => {
|
||||||
const calculatedHeight = document.getElementById("outputBubble")?.clientHeight * (3 / 5);
|
const outputWidth = document.getElementById("outputBubble")?.clientWidth;
|
||||||
return `${calculatedHeight}px`;
|
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(() => {
|
React.useEffect(() => {
|
||||||
|
if (userContext.features.copilotChatFixedMonacoEditorHeight) {
|
||||||
|
setWindowHeight(`${textHeightWithPadding * 5}px`);
|
||||||
|
} else {
|
||||||
setWindowHeight(calculateQueryWindowHeight());
|
setWindowHeight(calculateQueryWindowHeight());
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,13 +42,11 @@ export const OutputBubble: React.FC = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
tokens={{ padding: 8, childrenGap: 8 }}
|
tokens={{ padding: 8, childrenGap: 8 }}
|
||||||
>
|
>
|
||||||
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>
|
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>{copilotMessage.message}</Stack.Item>
|
||||||
{useQueryCopilot.getState()?.generatedQueryComments}
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
|
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={useQueryCopilot.getState()?.generatedQuery}
|
content={copilotMessage.sqlQuery}
|
||||||
isReadOnly={true}
|
isReadOnly={true}
|
||||||
ariaLabel={"AI Response"}
|
ariaLabel={"AI Response"}
|
||||||
wordWrap="on"
|
wordWrap="on"
|
||||||
@ -48,7 +59,7 @@ export const OutputBubble: React.FC = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={{ alignSelf: "flex-start" }}>
|
<Stack.Item style={{ alignSelf: "flex-start" }}>
|
||||||
<OutputBubbleButtons />
|
<OutputBubbleButtons sqlQuery={copilotMessage.sqlQuery} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
|
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
|
||||||
|
@ -28,7 +28,7 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
test comments
|
testMessage
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem
|
<StackItem
|
||||||
style={
|
style={
|
||||||
@ -40,7 +40,7 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
|
|||||||
>
|
>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
ariaLabel="AI Response"
|
ariaLabel="AI Response"
|
||||||
content="test query"
|
content="testSQL"
|
||||||
isReadOnly={true}
|
isReadOnly={true}
|
||||||
language="sql"
|
language="sql"
|
||||||
lineDecorationsWidth={0}
|
lineDecorationsWidth={0}
|
||||||
@ -68,7 +68,9 @@ exports[`Output Bubble snapshot tests should render and update height 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<OutputBubbleButtons />
|
<OutputBubbleButtons
|
||||||
|
sqlQuery="testSQL"
|
||||||
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
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 { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
||||||
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
||||||
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||||
@ -42,8 +43,10 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WelcomeBubble />
|
<WelcomeBubble />
|
||||||
{chatMessages.map((message, index) =>
|
{chatMessages.map((message, index) => {
|
||||||
message.source === 0 ? (
|
switch (message.source) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
key={index}
|
key={index}
|
||||||
horizontalAlign="center"
|
horizontalAlign="center"
|
||||||
@ -57,26 +60,17 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
|
|||||||
>
|
>
|
||||||
{message.message}
|
{message.message}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
);
|
||||||
// This part should be wired with OutputBubble
|
case 1:
|
||||||
<Stack
|
return <OutputBubble key={index} copilotMessage={message} />;
|
||||||
key={index}
|
case 2:
|
||||||
horizontalAlign="center"
|
return <ExplanationBubble key={index} copilotMessage={message} />;
|
||||||
tokens={{ padding: 8, childrenGap: 8 }}
|
default:
|
||||||
style={{
|
return <></>;
|
||||||
backgroundColor: "white",
|
}
|
||||||
borderRadius: "8px",
|
})}
|
||||||
margin: "5px 10px",
|
|
||||||
textAlign: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message.message}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<OutputBubble />
|
|
||||||
<RetrievingBubble />
|
<RetrievingBubble />
|
||||||
<ExplanationBubble />
|
<ExplanationButton />
|
||||||
|
|
||||||
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
|
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -125,25 +125,26 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
>
|
>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
ariaLabel="Editing Query"
|
ariaLabel="Editing Query"
|
||||||
content=""
|
content="SELECT * FROM c"
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
language="sql"
|
language="sql"
|
||||||
lineNumbers="on"
|
lineNumbers="on"
|
||||||
onContentChanged={[Function]}
|
onContentChanged={[Function]}
|
||||||
onContentSelected={[Function]}
|
onContentSelected={[Function]}
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
<QueryCopilotResults />
|
||||||
error=""
|
|
||||||
executeQueryDocumentsPage={[Function]}
|
|
||||||
isExecuting={false}
|
|
||||||
isMongoDB={false}
|
|
||||||
queryEditorContent=""
|
|
||||||
/>
|
|
||||||
</t>
|
</t>
|
||||||
</Stack>
|
</Stack>
|
||||||
<WelcomeModal
|
<WelcomeModal
|
||||||
visible={true}
|
visible={true}
|
||||||
/>
|
/>
|
||||||
|
<DeletePopup
|
||||||
|
clearFeedback={[Function]}
|
||||||
|
setQuery={[Function]}
|
||||||
|
setShowDeletePopup={[Function]}
|
||||||
|
showDeletePopup={false}
|
||||||
|
showFeedbackBar={[Function]}
|
||||||
|
/>
|
||||||
<CopyPopup
|
<CopyPopup
|
||||||
setShowCopyPopup={[Function]}
|
setShowCopyPopup={[Function]}
|
||||||
showCopyPopup={false}
|
showCopyPopup={false}
|
||||||
|
@ -2,20 +2,26 @@ import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import React from "react";
|
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" }}>
|
<Stack style={{ padding: "16px 20px" }}>
|
||||||
<Text block>
|
<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).
|
(0.0.0.0-255.255.255).
|
||||||
</Text>
|
</Text>
|
||||||
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
|
<Text block>We strongly recommend removing this rule once you finish using the {shellName} shell.</Text>
|
||||||
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
|
<Image style={{ margin: "20px 0" }} src={screenshot} />
|
||||||
<PrimaryButton
|
<PrimaryButton style={{ width: 150 }} onClick={() => sendMessage({ type: messageType })}>
|
||||||
style={{ width: 150 }}
|
|
||||||
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
|
|
||||||
>
|
|
||||||
Add firewall rule
|
Add firewall rule
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DefaultButton,
|
DefaultButton,
|
||||||
Icon,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Image,
|
Image,
|
||||||
IPivotItemProps,
|
|
||||||
Pivot,
|
Pivot,
|
||||||
PivotItem,
|
PivotItem,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
@ -21,18 +19,10 @@ import {
|
|||||||
queryCommand,
|
queryCommand,
|
||||||
queryCommandForDisplay,
|
queryCommandForDisplay,
|
||||||
} from "Explorer/Quickstart/PostgreQuickstartCommands";
|
} from "Explorer/Quickstart/PostgreQuickstartCommands";
|
||||||
|
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||||
import { useTerminal } from "hooks/useTerminal";
|
import { useTerminal } from "hooks/useTerminal";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Youtube from "react-youtube";
|
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 CompleteIcon from "../../../images/QuickstartComplete.svg";
|
||||||
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
||||||
|
|
||||||
@ -53,44 +43,6 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
document.execCommand("copy");
|
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 (
|
return (
|
||||||
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
||||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||||
@ -103,7 +55,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Login"
|
headerText="Login"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
|
||||||
|
}
|
||||||
itemKey={GuideSteps[0]}
|
itemKey={GuideSteps[0]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
@ -125,7 +79,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="New table"
|
headerText="New table"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 1)}
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
|
||||||
|
}
|
||||||
itemKey={GuideSteps[1]}
|
itemKey={GuideSteps[1]}
|
||||||
onClick={() => setCurrentStep(1)}
|
onClick={() => setCurrentStep(1)}
|
||||||
>
|
>
|
||||||
@ -165,7 +121,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Distribute table"
|
headerText="Distribute table"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 2)}
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
|
||||||
|
}
|
||||||
itemKey={GuideSteps[2]}
|
itemKey={GuideSteps[2]}
|
||||||
onClick={() => setCurrentStep(2)}
|
onClick={() => setCurrentStep(2)}
|
||||||
>
|
>
|
||||||
@ -210,7 +168,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Load data"
|
headerText="Load data"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 3)}
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
|
||||||
|
}
|
||||||
itemKey={GuideSteps[3]}
|
itemKey={GuideSteps[3]}
|
||||||
onClick={() => setCurrentStep(3)}
|
onClick={() => setCurrentStep(3)}
|
||||||
>
|
>
|
||||||
@ -250,7 +210,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Query"
|
headerText="Query"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 4)}
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
||||||
|
}
|
||||||
itemKey={GuideSteps[4]}
|
itemKey={GuideSteps[4]}
|
||||||
onClick={() => setCurrentStep(4)}
|
onClick={() => setCurrentStep(4)}
|
||||||
>
|
>
|
||||||
|
50
src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx
Normal file
50
src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
34
src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts
Normal file
34
src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts
Normal 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})`;
|
310
src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx
Normal file
310
src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx
Normal 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 <user> and <password>. 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 ‘Networking’
|
||||||
|
tab of your database settings, or modify your own network'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'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've created your database and collection, it'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's populate your sampleCollection with data. We'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 you’ve 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>
|
||||||
|
);
|
||||||
|
};
|
@ -260,30 +260,33 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
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 (
|
return (
|
||||||
<div className="connectExplorerContainer">
|
<div className="connectExplorerContainer">
|
||||||
<form className="connectExplorerFormContainer">
|
<form className="connectExplorerFormContainer">
|
||||||
<div className="splashScreenContainer">
|
<div className="splashScreenContainer">
|
||||||
<div className="splashScreen">
|
<div className="splashScreen">
|
||||||
<h1
|
<h1 className="title" role="heading" aria-label={title}>
|
||||||
className="title"
|
{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"}
|
|
||||||
<FeaturePanelLauncher />
|
<FeaturePanelLauncher />
|
||||||
</h1>
|
</h1>
|
||||||
<div className="subtitle">
|
<div className="subtitle">{subtitle}</div>
|
||||||
{userContext.apiType === "Postgres"
|
|
||||||
? "Get started with our sample datasets, documentation, and additional tools."
|
|
||||||
: "Globally distributed, multi-model database service for any scale"}
|
|
||||||
</div>
|
|
||||||
{this.getSplashScreenButtons()}
|
{this.getSplashScreenButtons()}
|
||||||
{useCarousel.getState().showCoachMark && (
|
{useCarousel.getState().showCoachMark && (
|
||||||
<Coachmark
|
<Coachmark
|
||||||
@ -313,7 +316,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
</TeachingBubbleContent>
|
</TeachingBubbleContent>
|
||||||
</Coachmark>
|
</Coachmark>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "Postgres" ? (
|
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
|
||||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
||||||
<Stack style={{ width: "33%" }}>
|
<Stack style={{ width: "33%" }}>
|
||||||
<Text
|
<Text
|
||||||
@ -380,7 +383,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
if (
|
if (
|
||||||
userContext.apiType === "SQL" ||
|
userContext.apiType === "SQL" ||
|
||||||
userContext.apiType === "Mongo" ||
|
userContext.apiType === "Mongo" ||
|
||||||
(userContext.apiType === "Postgres" && !userContext.isReplica)
|
(userContext.apiType === "Postgres" && !userContext.isReplica) ||
|
||||||
|
userContext.apiType === "VCoreMongo"
|
||||||
) {
|
) {
|
||||||
const launchQuickstartBtn = {
|
const launchQuickstartBtn = {
|
||||||
id: "quickstartDescription",
|
id: "quickstartDescription",
|
||||||
@ -388,9 +392,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
title: "Launch quick start",
|
title: "Launch quick start",
|
||||||
description: "Launch a quick start tutorial to get started with sample data",
|
description: "Launch a quick start tutorial to get started with sample data",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
userContext.apiType === "Postgres"
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
? useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart)
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||||
: this.container.onNewCollectionClicked({ isQuickstart: true });
|
} else {
|
||||||
|
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||||
|
}
|
||||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -405,16 +411,31 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
heroes.push(newNotebookBtn);
|
heroes.push(newNotebookBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
heroes.push(this.getShellCard());
|
||||||
|
heroes.push(this.getThirdCard());
|
||||||
|
return heroes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getShellCard() {
|
||||||
if (userContext.apiType === "Postgres") {
|
if (userContext.apiType === "Postgres") {
|
||||||
const postgreShellBtn = {
|
return {
|
||||||
iconSrc: PowerShellIcon,
|
iconSrc: PowerShellIcon,
|
||||||
title: "PostgreSQL Shell",
|
title: "PostgreSQL Shell",
|
||||||
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
||||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||||
};
|
};
|
||||||
heroes.push(postgreShellBtn);
|
}
|
||||||
} else {
|
|
||||||
const newContainerBtn = {
|
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 {
|
||||||
iconSrc: ContainersIcon,
|
iconSrc: ContainersIcon,
|
||||||
title: `New ${getCollectionName()}`,
|
title: `New ${getCollectionName()}`,
|
||||||
description: "Create a new container for storage and throughput",
|
description: "Create a new container for storage and throughput",
|
||||||
@ -423,21 +444,32 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
heroes.push(newContainerBtn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectBtn = {
|
private getThirdCard() {
|
||||||
iconSrc: ConnectIcon,
|
let icon = ConnectIcon;
|
||||||
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
|
let title = "Connect";
|
||||||
description:
|
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
|
||||||
userContext.apiType === "Postgres"
|
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||||
? "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);
|
|
||||||
|
|
||||||
return heroes;
|
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) {
|
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||||
@ -587,6 +619,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -724,6 +758,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
cdbLiveTv,
|
cdbLiveTv,
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -749,8 +785,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNextStepItems(): JSX.Element {
|
private postgresNextStepItems: { link: string; title: string; description: string }[] = [
|
||||||
const items: { link: string; title: string; description: string }[] = [
|
|
||||||
{
|
{
|
||||||
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
||||||
title: "Data Modeling",
|
title: "Data Modeling",
|
||||||
@ -768,6 +803,29 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
@ -785,8 +843,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTipsAndLearnMoreItems(): JSX.Element {
|
private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||||
const items: { link: string; title: string; description: string }[] = [
|
|
||||||
{
|
{
|
||||||
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
||||||
title: "Performance Tuning",
|
title: "Performance Tuning",
|
||||||
@ -804,6 +861,27 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
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 { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@ -276,7 +278,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: this.onExecuteQueryClick,
|
onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
@ -365,11 +367,18 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
selectedContent: "",
|
selectedContent: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isCopilotTabActive) {
|
||||||
|
selectedContent.trim().length > 0
|
||||||
|
? useQueryCopilot.getState().setSelectedQuery(selectedContent)
|
||||||
|
: useQueryCopilot.getState().setSelectedQuery("");
|
||||||
|
}
|
||||||
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEditorContent(): string {
|
public setEditorContent(): string {
|
||||||
if (this.state.queryCopilotGeneratedQuery) {
|
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||||
return this.state.queryCopilotGeneratedQuery;
|
return this.state.queryCopilotGeneratedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,14 +425,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
{this.isCopilotTabActive ? (
|
||||||
|
<QueryCopilotResults />
|
||||||
|
) : (
|
||||||
<QueryResultSection
|
<QueryResultSection
|
||||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||||
queryEditorContent={this.state.sqlQueryEditorContent}
|
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||||
error={this.state.error}
|
error={this.state.error}
|
||||||
queryResults={this.state.queryResults}
|
queryResults={this.state.queryResults}
|
||||||
isExecuting={this.state.isExecuting}
|
isExecuting={this.state.isExecuting}
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
|
this._executeQueryDocumentsPage(firstItemIndex)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</SplitterLayout>
|
</SplitterLayout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
import { PoolIdType } from "Common/Constants";
|
import { PoolIdType } from "Common/Constants";
|
||||||
import { configContext } from "ConfigContext";
|
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||||
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { armRequest } from "Utils/arm/request";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
|
|
||||||
interface QuickstartTabProps {
|
interface QuickstartTabProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@ -26,29 +26,12 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
forwardingId: notebookServerInfo.forwardingId,
|
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(() => {
|
useEffect(() => {
|
||||||
checkFirewallRules();
|
checkFirewallRules(
|
||||||
|
"2022-11-08",
|
||||||
|
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||||
|
setIsAllPublicIPAddressEnabled
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -61,7 +44,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
<QuickstartGuide />
|
<QuickstartGuide />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||||
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
{!isAllPublicIPAddressEnabled && (
|
||||||
|
<QuickstartFirewallNotification
|
||||||
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
|
screenshot={FirewallRuleScreenshot}
|
||||||
|
shellName="PostgreSQL"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||||
<NotebookTerminalComponent
|
<NotebookTerminalComponent
|
||||||
notebookServerInfo={getNotebookServerInfo()}
|
notebookServerInfo={getNotebookServerInfo()}
|
||||||
|
44
src/Explorer/Tabs/Shared/CheckFirewallRules.ts
Normal file
44
src/Explorer/Tabs/Shared/CheckFirewallRules.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,8 @@ import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
|||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||||
|
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||||
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
@ -35,7 +37,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
actions={
|
actions={
|
||||||
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
|
<MessageBarButton
|
||||||
|
onClick={() =>
|
||||||
|
sendMessage({
|
||||||
|
type:
|
||||||
|
userContext.apiType === "VCoreMongo"
|
||||||
|
? MessageTypes.OpenVCoreMongoNetworkingBlade
|
||||||
|
: MessageTypes.OpenPostgresNetworkingBlade,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
Change network settings
|
Change network settings
|
||||||
</MessageBarButton>
|
</MessageBarButton>
|
||||||
}
|
}
|
||||||
@ -127,12 +138,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
||||||
className="tabNavText"
|
|
||||||
style={active ? { fontWeight: "bolder", borderBottom: "2px solid rgba(0,120,212,1)" } : {}}
|
|
||||||
>
|
|
||||||
{useObservable(tab?.tabTitle || getReactTabTitle())}
|
|
||||||
</span>
|
|
||||||
{tabKind !== ReactTabKind.Home && (
|
{tabKind !== ReactTabKind.Home && (
|
||||||
<span className="tabIconSection">
|
<span className="tabIconSection">
|
||||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
<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 => {
|
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||||
switch (activeReactTab) {
|
switch (activeReactTab) {
|
||||||
case ReactTabKind.Connect:
|
case ReactTabKind.Connect:
|
||||||
return userContext.apiType === "Postgres" ? <PostgresConnectTab /> : <ConnectTab />;
|
return userContext.apiType === "VCoreMongo" ? (
|
||||||
|
<VcoreMongoConnectTab />
|
||||||
|
) : userContext.apiType === "Postgres" ? (
|
||||||
|
<PostgresConnectTab />
|
||||||
|
) : (
|
||||||
|
<ConnectTab />
|
||||||
|
);
|
||||||
case ReactTabKind.Home:
|
case ReactTabKind.Home:
|
||||||
return <SplashScreen explorer={explorer} />;
|
return <SplashScreen explorer={explorer} />;
|
||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return <QuickstartTab explorer={explorer} />;
|
return userContext.apiType === "VCoreMongo" ? (
|
||||||
|
<VcoreMongoQuickstartTab explorer={explorer} />
|
||||||
|
) : (
|
||||||
|
<QuickstartTab explorer={explorer} />
|
||||||
|
);
|
||||||
case ReactTabKind.QueryCopilot:
|
case ReactTabKind.QueryCopilot:
|
||||||
return <QueryCopilotTab explorer={explorer} />;
|
return <QueryCopilotTab explorer={explorer} />;
|
||||||
default:
|
default:
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
import { configContext } from "ConfigContext";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { armRequest } from "Utils/arm/request";
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@ -18,6 +19,7 @@ export interface TerminalTabOptions extends ViewModels.TabOptions {
|
|||||||
account: DataModels.DatabaseAccount;
|
account: DataModels.DatabaseAccount;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
kind: ViewModels.TerminalKind;
|
kind: ViewModels.TerminalKind;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,12 +32,19 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||||
private getTabId: () => string,
|
private getTabId: () => string,
|
||||||
|
private getUsername: () => string,
|
||||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||||
return <QuickstartFirewallNotification />;
|
return (
|
||||||
|
<QuickstartFirewallNotification
|
||||||
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
|
screenshot={FirewallRuleScreenshot}
|
||||||
|
shellName="PostgreSQL"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.parameters() ? (
|
return this.parameters() ? (
|
||||||
@ -43,6 +52,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
notebookServerInfo={this.getNotebookServerInfo()}
|
notebookServerInfo={this.getNotebookServerInfo()}
|
||||||
databaseAccount={this.getDatabaseAccount()}
|
databaseAccount={this.getDatabaseAccount()}
|
||||||
tabId={this.getTabId()}
|
tabId={this.getTabId()}
|
||||||
|
username={this.getUsername()}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||||
@ -64,6 +74,7 @@ export default class TerminalTab extends TabsBase {
|
|||||||
() => this.getNotebookServerInfo(options),
|
() => this.getNotebookServerInfo(options),
|
||||||
() => userContext?.databaseAccount,
|
() => userContext?.databaseAccount,
|
||||||
() => this.tabId,
|
() => this.tabId,
|
||||||
|
() => this.getUsername(),
|
||||||
this.isAllPublicIPAddressesEnabled
|
this.isAllPublicIPAddressesEnabled
|
||||||
);
|
);
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
@ -79,7 +90,21 @@ export default class TerminalTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
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";
|
endpointSuffix = "postgresql";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ViewModels.TerminalKind.VCoreMongo:
|
||||||
|
endpointSuffix = "mongovcore";
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Terminal kind: ${options.kind} not supported`);
|
throw new Error(`Terminal kind: ${options.kind} not supported`);
|
||||||
}
|
}
|
||||||
@ -127,24 +156,11 @@ export default class TerminalTab extends TabsBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkPostgresFirewallRules(): Promise<void> {
|
private getUsername(): string {
|
||||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
return undefined;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return userContext.vcoreMongoConnectionParams.adminLogin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
src/Explorer/Tabs/VCoreMongoConnectTab.tsx
Normal file
37
src/Explorer/Tabs/VCoreMongoConnectTab.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
80
src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx
Normal file
80
src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -37,6 +37,7 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
|
|||||||
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
import { Platform, configContext } from "./../../ConfigContext";
|
||||||
import ConflictId from "./ConflictId";
|
import ConflictId from "./ConflictId";
|
||||||
import DocumentId from "./DocumentId";
|
import DocumentId from "./DocumentId";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
@ -205,7 +206,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
.map((node) => <Trigger>node);
|
.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.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
|
||||||
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
|
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
|
||||||
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
|
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
|
||||||
|
@ -39,6 +39,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
|||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
import { Platform, configContext } from "./../../ConfigContext";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
@ -69,7 +70,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const { activeTab, refreshActiveTab } = useTabs();
|
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 pseudoDirPath = "PsuedoDir";
|
||||||
|
|
||||||
const buildGalleryCallout = (): JSX.Element => {
|
const buildGalleryCallout = (): JSX.Element => {
|
||||||
|
@ -40,6 +40,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
|||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
import { Platform, configContext } from "./../../ConfigContext";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
@ -249,7 +250,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
* @param container
|
* @param container
|
||||||
*/
|
*/
|
||||||
private static showScriptNodes(container: Explorer): boolean {
|
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 {
|
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
|
||||||
|
108
src/Explorer/Tree2/ResourceTree.tsx
Normal file
108
src/Explorer/Tree2/ResourceTree.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
318
src/Explorer/Tree2/containerTreeNodeUtil.ts
Normal file
318
src/Explorer/Tree2/containerTreeNodeUtil.ts
Normal 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);
|
||||||
|
};
|
84
src/Explorer/Tree2/useDatabaseTreeNodes.ts
Normal file
84
src/Explorer/Tree2/useDatabaseTreeNodes.ts
Normal 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;
|
||||||
|
};
|
25
src/Main.tsx
25
src/Main.tsx
@ -1,5 +1,5 @@
|
|||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
@ -26,6 +26,8 @@ import "../less/TableStyles/CustomizeColumns.less";
|
|||||||
import "../less/TableStyles/EntityEditor.less";
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
|
import * as StyleConstants from "./Common/StyleConstants";
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import "../less/documentDB.less";
|
import "../less/documentDB.less";
|
||||||
import "../less/forms.less";
|
import "../less/forms.less";
|
||||||
import "../less/infobox.less";
|
import "../less/infobox.less";
|
||||||
@ -57,6 +59,7 @@ import "./Libs/jquery";
|
|||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
|
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
@ -67,6 +70,10 @@ const App: React.FunctionComponent = () => {
|
|||||||
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
|
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
if (config?.platform === Platform.Fabric) {
|
||||||
|
loadTheme(appThemeFabric);
|
||||||
|
}
|
||||||
|
StyleConstants.updateStyles();
|
||||||
const explorer = useKnockoutExplorer(config?.platform);
|
const explorer = useKnockoutExplorer(config?.platform);
|
||||||
|
|
||||||
const toggleLeftPaneExpanded = () => {
|
const toggleLeftPaneExpanded = () => {
|
||||||
@ -84,6 +91,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer" aria-hidden="false">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
|
<LoadFabricOverrides />
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
@ -91,7 +99,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
{/* Collections Tree and Tabs - Begin */}
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
<div className="resourceTreeAndTabs">
|
<div className="resourceTreeAndTabs">
|
||||||
{/* Collections Tree - Start */}
|
{/* Collections Tree - Start */}
|
||||||
{userContext.apiType !== "Postgres" && (
|
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||||
<div className="collectionsTreeWithSplitter">
|
<div className="collectionsTreeWithSplitter">
|
||||||
{/* Collections Tree Expanded - Start */}
|
{/* Collections Tree Expanded - Start */}
|
||||||
@ -135,6 +143,19 @@ const App: React.FunctionComponent = () => {
|
|||||||
|
|
||||||
ReactDOM.render(<App />, document.body);
|
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 {
|
function LoadingExplorer(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="splashLoaderContainer">
|
<div className="splashLoaderContainer">
|
||||||
|
7
src/Platform/Fabric/FabricPlatform.tsx
Normal file
7
src/Platform/Fabric/FabricPlatform.tsx
Normal 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 <></>;
|
||||||
|
}
|
208
src/Platform/Fabric/FabricTheme.tsx
Normal file
208
src/Platform/Fabric/FabricTheme.tsx
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
@ -4,7 +4,7 @@
|
|||||||
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react";
|
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent, useEffect, useState } from "react";
|
import { FunctionComponent, useEffect, useState } from "react";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
|
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
|
||||||
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ErrorImage from "../../../../images/error.svg";
|
|
||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||||
|
import ErrorImage from "../../../../images/error.svg";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import { HttpHeaders } from "../../../Common/Constants";
|
import { HttpHeaders } from "../../../Common/Constants";
|
||||||
import { configContext } from "../../../ConfigContext";
|
import { configContext } from "../../../ConfigContext";
|
||||||
@ -16,6 +16,19 @@ interface Props {
|
|||||||
setAuthType: (authType: AuthType) => void;
|
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> = ({
|
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||||
setEncryptedToken,
|
setEncryptedToken,
|
||||||
login,
|
login,
|
||||||
@ -44,16 +57,8 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = new Headers();
|
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
setEncryptedToken(encryptedToken);
|
||||||
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));
|
|
||||||
setAuthType(AuthType.ConnectionString);
|
setAuthType(AuthType.ConnectionString);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -40,6 +40,7 @@ export type Features = {
|
|||||||
readonly copilotVersion?: string;
|
readonly copilotVersion?: string;
|
||||||
readonly disableCopilotPhoenixGateaway: boolean;
|
readonly disableCopilotPhoenixGateaway: boolean;
|
||||||
readonly enableCopilotFullSchema: boolean;
|
readonly enableCopilotFullSchema: boolean;
|
||||||
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@ -108,10 +109,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||||
enablePriorityBasedThrottling: "true" === get("enableprioritybasedthrottling"),
|
enablePriorityBasedThrottling: "true" === get("enableprioritybasedthrottling"),
|
||||||
enableCopilot: "true" === get("enablecopilot"),
|
enableCopilot: "true" === get("enablecopilot", "true"),
|
||||||
copilotVersion: get("copilotversion") ?? "v1.0",
|
copilotVersion: get("copilotversion") ?? "v1.0",
|
||||||
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
||||||
enableCopilotFullSchema: "true" === get("enablecopilotfullschema"),
|
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
||||||
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IButtonStyles, ICommandBarStyles, ISeparatorStyles, IStackTokens } from "@fluentui/react";
|
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 } };
|
export const commandBarItemStyles: IButtonStyles = { root: { paddingLeft: 20 } };
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@ export class JupyterLabAppFactory {
|
|||||||
this.isShellStarted = content?.includes("citus=>");
|
this.isShellStarted = content?.includes("citus=>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isVCoreMongoShellStarted(content: string | undefined) {
|
||||||
|
this.isShellStarted = content?.includes("Enter password");
|
||||||
|
}
|
||||||
|
|
||||||
constructor(closeTab: () => void) {
|
constructor(closeTab: () => void) {
|
||||||
this.onShellExited = closeTab;
|
this.onShellExited = closeTab;
|
||||||
this.isShellStarted = false;
|
this.isShellStarted = false;
|
||||||
@ -43,6 +47,9 @@ export class JupyterLabAppFactory {
|
|||||||
case "Postgres":
|
case "Postgres":
|
||||||
this.checkShellStarted = this.isPostgresShellStarted;
|
this.checkShellStarted = this.isPostgresShellStarted;
|
||||||
break;
|
break;
|
||||||
|
case "VCoreMongo":
|
||||||
|
this.checkShellStarted = this.isVCoreMongoShellStarted;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,4 +11,5 @@ export interface TerminalProps {
|
|||||||
apiType: ApiType;
|
apiType: ApiType;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,24 @@ import { HttpHeaders } from "../Common/Constants";
|
|||||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import "./index.css";
|
|
||||||
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
||||||
import { TerminalProps } from "./TerminalProps";
|
import { TerminalProps } from "./TerminalProps";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
||||||
let body: BodyInit | undefined;
|
let body: BodyInit | undefined;
|
||||||
let headers: HeadersInit | undefined;
|
let headers: HeadersInit | undefined;
|
||||||
if (props.terminalEndpoint) {
|
if (props.terminalEndpoint) {
|
||||||
body = JSON.stringify({
|
let bodyObj: { endpoint: string; username?: string } = {
|
||||||
endpoint: props.terminalEndpoint,
|
endpoint: props.terminalEndpoint,
|
||||||
});
|
};
|
||||||
|
if (props.username) {
|
||||||
|
bodyObj = {
|
||||||
|
...bodyObj,
|
||||||
|
username: props.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
body = JSON.stringify(bodyObj);
|
||||||
headers = {
|
headers = {
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: "application/json",
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useCarousel } from "hooks/useCarousel";
|
|
||||||
import { usePostgres } from "hooks/usePostgres";
|
|
||||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
|
import { usePostgres } from "hooks/usePostgres";
|
||||||
import { AuthType } from "./AuthType";
|
import { AuthType } from "./AuthType";
|
||||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||||
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
||||||
import { extractFeatures, Features } from "./Platform/Hosted/extractFeatures";
|
import { Features, extractFeatures } from "./Platform/Hosted/extractFeatures";
|
||||||
import { CollectionCreation, CollectionCreationDefaults } from "./Shared/Constants";
|
import { CollectionCreation, CollectionCreationDefaults } from "./Shared/Constants";
|
||||||
|
|
||||||
interface ThroughputDefaults {
|
interface ThroughputDefaults {
|
||||||
@ -41,6 +41,11 @@ export interface PostgresConnectionStrParams {
|
|||||||
isFreeTier: boolean;
|
isFreeTier: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VCoreMongoConnectionParams {
|
||||||
|
adminLogin: string;
|
||||||
|
connectionString: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
@ -71,9 +76,10 @@ interface UserContext {
|
|||||||
readonly isReplica?: boolean;
|
readonly isReplica?: boolean;
|
||||||
collectionCreationDefaults: CollectionCreationDefaults;
|
collectionCreationDefaults: CollectionCreationDefaults;
|
||||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||||
|
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
||||||
|
|
||||||
const ONE_WEEK_IN_MS = 604800000;
|
const ONE_WEEK_IN_MS = 604800000;
|
||||||
@ -156,6 +162,9 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
|||||||
if (account.kind === "Postgres") {
|
if (account.kind === "Postgres") {
|
||||||
return "Postgres";
|
return "Postgres";
|
||||||
}
|
}
|
||||||
|
if (account.kind === "VCoreMongo") {
|
||||||
|
return "VCoreMongo";
|
||||||
|
}
|
||||||
return "SQL";
|
return "SQL";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { userContext } from "../UserContext";
|
|||||||
|
|
||||||
export const getCollectionName = (isPlural?: boolean): string => {
|
export const getCollectionName = (isPlural?: boolean): string => {
|
||||||
let collectionName: string;
|
let collectionName: string;
|
||||||
let unknownApiType: never;
|
|
||||||
switch (userContext.apiType) {
|
switch (userContext.apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
collectionName = "Container";
|
collectionName = "Container";
|
||||||
@ -20,8 +19,7 @@ export const getCollectionName = (isPlural?: boolean): string => {
|
|||||||
case "Postgres":
|
case "Postgres":
|
||||||
return "";
|
return "";
|
||||||
default:
|
default:
|
||||||
unknownApiType = userContext.apiType;
|
throw new Error(`Unknown API type: ${userContext.apiType}`);
|
||||||
throw new Error(`Unknown API type: ${unknownApiType}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlural) {
|
if (isPlural) {
|
||||||
@ -72,6 +70,8 @@ export const getApiShortDisplayName = (): string => {
|
|||||||
return "NoSQL API";
|
return "NoSQL API";
|
||||||
case "Tables":
|
case "Tables":
|
||||||
return "Table API";
|
return "Table API";
|
||||||
|
case "VCoreMongo":
|
||||||
|
return "MongoDB (vCore) API";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
const PortalIPs: { [key: string]: string[] } = {
|
const PortalIPs: { [key: string]: string[] } = {
|
||||||
@ -10,27 +11,45 @@ const PortalIPs: { [key: string]: string[] } = {
|
|||||||
usnat: ["7.28.202.68"],
|
usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNetworkSettingsWarningMessage = (): string => {
|
export const getNetworkSettingsWarningMessage = async (
|
||||||
|
setStateFunc: (warningMessage: string) => void
|
||||||
|
): Promise<void> => {
|
||||||
const accountProperties = userContext.databaseAccount?.properties;
|
const accountProperties = userContext.databaseAccount?.properties;
|
||||||
|
const accessMessage =
|
||||||
|
"The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
||||||
|
const publicAccessMessage =
|
||||||
|
"The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||||
|
|
||||||
if (!accountProperties) {
|
if (userContext.apiType === "Postgres") {
|
||||||
return "";
|
checkFirewallRules(
|
||||||
}
|
"2022-11-08",
|
||||||
|
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||||
|
undefined,
|
||||||
|
setStateFunc,
|
||||||
|
accessMessage
|
||||||
|
);
|
||||||
|
} else if (userContext.apiType === "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"),
|
||||||
|
undefined,
|
||||||
|
setStateFunc,
|
||||||
|
accessMessage
|
||||||
|
);
|
||||||
|
} else if (accountProperties) {
|
||||||
// public network access is disabled
|
// public network access is disabled
|
||||||
if (
|
if (
|
||||||
accountProperties.publicNetworkAccess !== "Enabled" &&
|
accountProperties.publicNetworkAccess !== "Enabled" &&
|
||||||
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
|
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
|
||||||
) {
|
) {
|
||||||
return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
setStateFunc(publicAccessMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ipRules = accountProperties.ipRules;
|
const ipRules = accountProperties.ipRules;
|
||||||
// public network access is set to "All networks"
|
// public network access is NOT set to "All networks"
|
||||||
if (ipRules.length === 0) {
|
if (ipRules.length > 0) {
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
|
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
|
||||||
const portalIPs = PortalIPs[userContext.portalEnv];
|
const portalIPs = PortalIPs[userContext.portalEnv];
|
||||||
let numberOfMatches = 0;
|
let numberOfMatches = 0;
|
||||||
@ -41,9 +60,9 @@ export const getNetworkSettingsWarningMessage = (): string => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numberOfMatches !== portalIPs.length) {
|
if (numberOfMatches !== portalIPs.length) {
|
||||||
return "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
setStateFunc(accessMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
|
|
||||||
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
||||||
// Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window.
|
// Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window.
|
||||||
try {
|
try {
|
||||||
@ -5,7 +7,11 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
|
|||||||
if (currentWindow.parent === currentWindow) {
|
if (currentWindow.parent === currentWindow) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (currentWindow.parent === currentWindow.top) {
|
if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) {
|
||||||
|
// in Fabric data explorer is inside an extension iframe, so we have two parent iframes
|
||||||
|
return currentWindow;
|
||||||
|
}
|
||||||
|
if (configContext.platform !== Platform.Fabric && currentWindow.parent === currentWindow.top) {
|
||||||
return currentWindow;
|
return currentWindow;
|
||||||
}
|
}
|
||||||
currentWindow = currentWindow.parent;
|
currentWindow = currentWindow.parent;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
|
import { fetchAccessData } from "hooks/usePortalAccessToken";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
@ -60,6 +62,26 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
} else if (platform === Platform.Portal) {
|
} else if (platform === Platform.Portal) {
|
||||||
const explorer = await configurePortal();
|
const explorer = await configurePortal();
|
||||||
setExplorer(explorer);
|
setExplorer(explorer);
|
||||||
|
} else if (platform === Platform.Fabric) {
|
||||||
|
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
|
||||||
|
const connectionString = sessionStorage.getItem("connectionString");
|
||||||
|
if (!connectionString) {
|
||||||
|
console.error("No connection string found in session storage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||||
|
// TODO Duplicated from useTokenMetadata
|
||||||
|
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
|
||||||
|
|
||||||
|
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||||
|
win.hostedConfig = {
|
||||||
|
authType: AuthType.EncryptedToken,
|
||||||
|
encryptedToken,
|
||||||
|
encryptedTokenMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
const explorer = await configureHosted();
|
||||||
|
setExplorer(explorer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -105,7 +127,10 @@ async function configureHosted(): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.data?.type === MessageTypes.CloseTab) {
|
if (event.data?.type === MessageTypes.CloseTab) {
|
||||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
if (
|
||||||
|
event.data?.data?.tabId === "QuickstartPSQLShell" ||
|
||||||
|
event.data?.data?.tabId === "QuickstartVcoreMongoShell"
|
||||||
|
) {
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||||
} else {
|
} else {
|
||||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||||
@ -294,13 +319,19 @@ async function configurePortal(): Promise<Explorer> {
|
|||||||
updateContextsFromPortalMessage(inputs);
|
updateContextsFromPortalMessage(inputs);
|
||||||
explorer = new Explorer();
|
explorer = new Explorer();
|
||||||
resolve(explorer);
|
resolve(explorer);
|
||||||
|
if (userContext.apiType === "Postgres") {
|
||||||
|
explorer.openNPSSurveyDialog();
|
||||||
|
}
|
||||||
if (openAction) {
|
if (openAction) {
|
||||||
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
||||||
}
|
}
|
||||||
} else if (shouldForwardMessage(message, event.origin)) {
|
} else if (shouldForwardMessage(message, event.origin)) {
|
||||||
sendMessage(message);
|
sendMessage(message);
|
||||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
if (
|
||||||
|
event.data?.data?.tabId === "QuickstartPSQLShell" ||
|
||||||
|
event.data?.data?.tabId === "QuickstartVcoreMongoShell"
|
||||||
|
) {
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||||
} else {
|
} else {
|
||||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||||
@ -372,8 +403,16 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const warningMessage = getNetworkSettingsWarningMessage();
|
if (inputs.isVCoreMongoAccount) {
|
||||||
useTabs.getState().setNetworkSettingsWarning(warningMessage);
|
if (inputs.connectionStringParams) {
|
||||||
|
updateUserContext({
|
||||||
|
apiType: "VCoreMongo",
|
||||||
|
vcoreMongoConnectionParams: inputs.connectionStringParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning);
|
||||||
|
|
||||||
if (inputs.features) {
|
if (inputs.features) {
|
||||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||||
|
@ -37,7 +37,6 @@ export interface QueryCopilotState {
|
|||||||
chatMessages: CopilotMessage[];
|
chatMessages: CopilotMessage[];
|
||||||
shouldIncludeInMessages: boolean;
|
shouldIncludeInMessages: boolean;
|
||||||
showExplanationBubble: boolean;
|
showExplanationBubble: boolean;
|
||||||
showQueryExplanation: boolean;
|
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
containerStatus: ContainerInfo;
|
containerStatus: ContainerInfo;
|
||||||
isAllocatingContainer: boolean;
|
isAllocatingContainer: boolean;
|
||||||
@ -72,7 +71,6 @@ export interface QueryCopilotState {
|
|||||||
setChatMessages: (chatMessages: CopilotMessage[]) => void;
|
setChatMessages: (chatMessages: CopilotMessage[]) => void;
|
||||||
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void;
|
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void;
|
||||||
setShowExplanationBubble: (showExplanationBubble: boolean) => void;
|
setShowExplanationBubble: (showExplanationBubble: boolean) => void;
|
||||||
setShowQueryExplanation: (showQueryExplanation: boolean) => void;
|
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||||
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
|
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
|
||||||
@ -90,7 +88,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
showFeedbackModal: false,
|
showFeedbackModal: false,
|
||||||
hideFeedbackModalForLikedQueries: false,
|
hideFeedbackModalForLikedQueries: false,
|
||||||
correlationId: "",
|
correlationId: "",
|
||||||
query: "",
|
query: "SELECT * FROM c",
|
||||||
selectedQuery: "",
|
selectedQuery: "",
|
||||||
isGeneratingQuery: false,
|
isGeneratingQuery: false,
|
||||||
isGeneratingExplanation: false,
|
isGeneratingExplanation: false,
|
||||||
@ -113,7 +111,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
chatMessages: [],
|
chatMessages: [],
|
||||||
shouldIncludeInMessages: true,
|
shouldIncludeInMessages: true,
|
||||||
showExplanationBubble: false,
|
showExplanationBubble: false,
|
||||||
showQueryExplanation: false,
|
|
||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
@ -158,7 +155,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
|
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
|
||||||
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
|
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
|
||||||
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
|
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
|
||||||
setShowQueryExplanation: (showQueryExplanation: boolean) => set({ showQueryExplanation }),
|
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
set({ notebookServerInfo }),
|
set({ notebookServerInfo }),
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||||
@ -206,7 +202,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
chatMessages: [],
|
chatMessages: [],
|
||||||
shouldIncludeInMessages: true,
|
shouldIncludeInMessages: true,
|
||||||
showExplanationBubble: false,
|
showExplanationBubble: false,
|
||||||
showQueryExplanation: false,
|
|
||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<clear />
|
<clear />
|
||||||
<add name="X-Xss-Protection" value="1; mode=block" />
|
<add name="X-Xss-Protection" value="1; mode=block" />
|
||||||
<add name="X-Content-Type-Options" value="nosniff" />
|
<add name="X-Content-Type-Options" value="nosniff" />
|
||||||
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net" />
|
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com pbi-rdb-edog.analysis-df.windows.net" />
|
||||||
</customHeaders>
|
</customHeaders>
|
||||||
<redirectHeaders>
|
<redirectHeaders>
|
||||||
<clear />
|
<clear />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user