mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 22:02:01 +00:00
Compare commits
1 Commits
archie-dar
...
before_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd905403b |
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -61,8 +61,6 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
@LinkColor: #2d6da4;
|
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
@@ -248,10 +246,6 @@
|
|||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusedBorder() {
|
|
||||||
border: 1px dashed @FocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
Common Toggle Switch
|
Common Toggle Switch
|
||||||
*************************************************************************************************/
|
*************************************************************************************************/
|
||||||
|
|||||||
@@ -1772,9 +1772,9 @@ input::-webkit-calendar-picker-indicator {
|
|||||||
|
|
||||||
.paddingspan4 {
|
.paddingspan4 {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
padding-left: 20px;
|
||||||
color: white;
|
color: white;
|
||||||
padding-left: 25px;
|
font-size: 14px;
|
||||||
padding-right: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.closebtnn {
|
.closebtnn {
|
||||||
@@ -1914,29 +1914,13 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
background-color: var(--colorNeutralBackground1);
|
height: 32px;
|
||||||
color: var(--colorNeutralForeground1);
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: -0.5px;
|
|
||||||
// border-bottom: 1px solid var(--colorNeutralStroke1);
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1949,20 +1933,8 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
.nav.nav-tabs.qslevel > li > a:hover {
|
.nav.nav-tabs.qslevel > li > a:hover {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: var(--colorNeutralBackground1Selected) !important;
|
background-color: transparent !important;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
|
||||||
// border-bottom: 2px solid var(--colorNeutralStroke1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
|
||||||
background-color: var(--colorNeutralBackground1Selected);
|
|
||||||
// border-bottom: 2px solid var(--colorCompoundBrandBackground);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.numbersize {
|
.numbersize {
|
||||||
@@ -2396,9 +2368,7 @@ a:link {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -2654,10 +2624,9 @@ a:link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
@@ -2679,11 +2648,11 @@ a:link {
|
|||||||
.nav-tabs > li.active > .tabNavContentContainer,
|
.nav-tabs > li.active > .tabNavContentContainer,
|
||||||
.nav-tabs > li.active > .tabNavContentContainer:focus,
|
.nav-tabs > li.active > .tabNavContentContainer:focus,
|
||||||
.nav-tabs > li.active > .tabNavContentContainer:hover {
|
.nav-tabs > li.active > .tabNavContentContainer:hover {
|
||||||
color: var(--colorNeutralForeground1);
|
color: #555;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
background-color: var(--colorNeutralBackground1Selected);
|
background-color: @BaseLight;
|
||||||
border-color: var(--colorNeutralStroke1);
|
border-color: @BaseMedium;
|
||||||
// border-bottom-color: var(--colorCompoundBrandBackground);
|
border-bottom-color: @BaseLight;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
height: @ActiveTabHeight;
|
height: @ActiveTabHeight;
|
||||||
@@ -2692,7 +2661,7 @@ a:link {
|
|||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
border-bottom: 2px solid var(--colorCompoundBrandBackground);
|
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
||||||
@@ -2705,7 +2674,7 @@ a:link {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: 2px 2px 0 0;
|
border-radius: 2px 2px 0 0;
|
||||||
padding: @DefaultSpace 0px @SmallSpace 0px;
|
padding: @DefaultSpace 0px @SmallSpace 0px;
|
||||||
color: var(--colorNeutralForeground1);
|
color: @BaseHigh;
|
||||||
width: @TabsWidth;
|
width: @TabsWidth;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -2713,29 +2682,75 @@ a:link {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
background-color: @BaseMediumLow;
|
||||||
border-color: transparent;
|
border-color: @BaseMediumLow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
background-color: @BaseMediumLow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab_Content {
|
.tab_Content {
|
||||||
.flex-display();
|
.flex-display();
|
||||||
width: @TabsWidth;
|
width: @TabsWidth;
|
||||||
border-right: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
border-right: @ButtonBorderWidth solid @BaseMedium;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.flex-display();
|
.flex-display();
|
||||||
width: @ContentWrapper;
|
width: @ContentWrapper;
|
||||||
|
|
||||||
|
.statusIconContainer {
|
||||||
|
width: @StatusIconContainerSize;
|
||||||
|
height: @StatusIconContainerSize;
|
||||||
|
margin-left: @SmallSpace;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
.errorIconContainer {
|
||||||
|
width: @ErrorIconContainer;
|
||||||
|
height: @ErrorIconContainer;
|
||||||
|
margin-top: 1px;
|
||||||
|
|
||||||
|
.errorIcon {
|
||||||
|
width: @ErrorIconWidth;
|
||||||
|
height: @LoadingErrorIconSize;
|
||||||
|
background-image: url(../images/error_no_outline.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 3px;
|
||||||
|
display: block;
|
||||||
|
margin: 1px 0px 0px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIconContainer.actionsEnabled {
|
||||||
|
&:hover {
|
||||||
|
.hover();
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIconContainer[tabindex]:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingIcon {
|
||||||
|
width: @LoadingErrorIconSize;
|
||||||
|
height: @LoadingErrorIconSize;
|
||||||
|
margin: 0px 0px @SmallSpace @SmallSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabNavText {
|
.tabNavText {
|
||||||
margin-left: @SmallSpace;
|
margin-left: @SmallSpace;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
color: var(--colorNeutralForeground1);
|
color: @BaseDark;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2746,36 +2761,21 @@ a:link {
|
|||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
width: 29px;
|
width: 29px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-top: 2px;
|
||||||
|
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
padding: 0px @SmallSpace 0px @SmallSpace;
|
padding: 0px @SmallSpace 0px @SmallSpace;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
.hover();
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
.focus();
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
.active();
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "×";
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3129,12 +3129,3 @@ a:link {
|
|||||||
.sidebarContainer {
|
.sidebarContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-Icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,40 +37,12 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: var(--colorNeutralBackground1);
|
background-color: #ffffff;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
background-color: var(--colorNeutralBackground1);
|
background-color: #ffffff;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
border-bottom: 1px solid var(--colorNeutralStroke1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
|
||||||
border-bottom: 2px solid var(--colorNeutralStroke1);
|
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
|
||||||
border-bottom: 2px solid var(--colorCompoundBrandBackground);
|
|
||||||
background-color: var(--colorNeutralBackground1Selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
|
||||||
border-bottom: 0px none transparent;
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabPanesContainer {
|
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
@@ -95,12 +67,24 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 > .contentWrapper > .tabNavText {
|
||||||
|
border-bottom: 0px none transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.tabNavContentContainer {
|
.tabNavContentContainer {
|
||||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +93,6 @@ a:focus {
|
|||||||
margin: 0px @SmallSpace 0px @SmallSpace;
|
margin: 0px @SmallSpace 0px @SmallSpace;
|
||||||
width: calc(@TabsWidth - (@SmallSpace * 2));
|
width: calc(@TabsWidth - (@SmallSpace * 2));
|
||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
@@ -120,18 +103,17 @@ a:focus {
|
|||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
padding: 0px 0px 0px @SmallSpace;
|
padding: 0px 0px 0px @SmallSpace;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,13 @@
|
|||||||
|
|
||||||
.storageCapacityTitle {
|
.storageCapacityTitle {
|
||||||
padding: @LargeSpace 0px;
|
padding: @LargeSpace 0px;
|
||||||
|
|
||||||
}
|
}
|
||||||
.throughputStorageValue {
|
.throughputStorageValue {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimatedCost,
|
.estimatedCost, .largePartitionKeyEnabled {
|
||||||
.largePartitionKeyEnabled {
|
|
||||||
padding: @SmallSpace 0px @LargeSpace;
|
padding: @SmallSpace 0px @LargeSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,25 +109,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.formTree {
|
.formTree {
|
||||||
border: 1px solid var(--colorNeutralStroke1);
|
border: 1px solid #969696;
|
||||||
color: var(--colorNeutralForeground1);
|
color: #393939;
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
padding: 0px 12px 1px 8px;
|
padding: 0px 12px 1px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formTree:hover {
|
.formTree:hover {
|
||||||
border: 1px solid var(--colorNeutralStroke1Hover);
|
border: 1px solid #969696;
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
background-color: #e6f8fe;
|
||||||
}
|
|
||||||
|
|
||||||
.formTree::placeholder {
|
|
||||||
color: var(--colorNeutralForeground2);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.formTree:active {
|
.formTree:active {
|
||||||
border: 1px solid var(--colorNeutralStroke1Pressed);
|
border: 1px solid #1ebbee;
|
||||||
background-color: var(--colorNeutralBackground1Pressed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scaleForm {
|
.scaleForm {
|
||||||
@@ -146,6 +139,7 @@
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.spUdfTriggerHeader {
|
.spUdfTriggerHeader {
|
||||||
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
|
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
|
||||||
}
|
}
|
||||||
@@ -157,33 +151,33 @@
|
|||||||
|
|
||||||
.unselectedRadio {
|
.unselectedRadio {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-color: #eee !important;
|
border-color: #EEE!important;
|
||||||
color: black!important;
|
color: black!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabledRadio {
|
.disabledRadio {
|
||||||
background-color: #a19f9d;
|
background-color: #A19F9D;
|
||||||
border-color: #eee !important;
|
border-color: #EEE!important;
|
||||||
color: white!important;
|
color: white!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedRadio {
|
.selectedRadio {
|
||||||
background-color: @AccentMediumHigh;
|
background-color: @AccentMediumHigh;
|
||||||
border-color: #eee !important;
|
border-color: #EEE!important;
|
||||||
color: white!important;
|
color: white!important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedRadio:hover {
|
.selectedRadio:hover {
|
||||||
background-color: @AccentMediumHigh;
|
background-color: @AccentMediumHigh;
|
||||||
border-color: #eee !important;
|
border-color: #EEE!important;
|
||||||
color: white!important;
|
color: white!important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedRadio:active {
|
.selectedRadio:active {
|
||||||
background-color: #0072c6;
|
background-color: #0072c6;
|
||||||
border-color: #eee !important;
|
border-color: #EEE!important;
|
||||||
color: white!important;
|
color: white!important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #0072c6;
|
border: 1px solid #0072c6;
|
||||||
@@ -210,18 +204,8 @@
|
|||||||
|
|
||||||
.trigger-field {
|
.trigger-field {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
margin-top: 10px;
|
margin-top: 10px
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.trigger-field input::placeholder {
|
|
||||||
color: var(--colorNeutralForeground3);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger-form {
|
.trigger-form {
|
||||||
background-color: var(--colorNeutralBackground1);
|
padding: 10px 30px 10px 30px;
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
padding: 10px 30px;
|
|
||||||
}
|
}
|
||||||
444
less/tree.less
444
less/tree.less
@@ -1,270 +1,270 @@
|
|||||||
// @import "./Common/Constants";
|
@import "./Common/Constants";
|
||||||
|
|
||||||
// .resourceTree {
|
.resourceTree {
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
// flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
// .main {
|
.main {
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .resourceTreeScroll {
|
.resourceTreeScroll {
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
// display: flex;
|
display: flex;
|
||||||
// overflow-y: auto;
|
overflow-y: auto;
|
||||||
// overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
// padding-right: 10px;
|
padding-right: 10px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .userSelectNone {
|
.userSelectNone {
|
||||||
// -webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
// -moz-user-select: none;
|
-moz-user-select: none;
|
||||||
// -ms-user-select: none;
|
-ms-user-select: none;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .treeHovermargin {
|
.treeHovermargin {
|
||||||
// margin-left: 16px;
|
margin-left: 16px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .highlight {
|
.highlight {
|
||||||
// padding: @SmallSpace 2px;
|
padding: @SmallSpace 2px;
|
||||||
// outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
// &:hover {
|
&:hover {
|
||||||
// .hover();
|
.hover();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:active {
|
&:active {
|
||||||
// .active();
|
.active();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:focus {
|
&:focus {
|
||||||
// .focus();
|
.focus();
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .contextmenushowing {
|
.contextmenushowing {
|
||||||
// background-color: #eee;
|
background-color: #eee;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionstree {
|
.collectionstree {
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// margin-top: @DefaultSpace;
|
margin-top: @DefaultSpace;
|
||||||
|
|
||||||
// .databaseList {
|
.databaseList {
|
||||||
// list-style-type: none;
|
list-style-type: none;
|
||||||
// padding-left: 0px;
|
padding-left: 0px;
|
||||||
|
|
||||||
// .collectionList {
|
.collectionList {
|
||||||
// padding-left: (2 * @MediumSpace);
|
padding-left: (2 * @MediumSpace);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionChildList {
|
.collectionChildList {
|
||||||
// padding-left: @LargeSpace;
|
padding-left: @LargeSpace;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .databaseDocuments {
|
.databaseDocuments {
|
||||||
// padding-left: (5 * @MediumSpace);
|
padding-left: (5 * @MediumSpace);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .pointerCursor {
|
.pointerCursor {
|
||||||
// cursor: pointer;
|
cursor: pointer;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .menuEllipsis {
|
.menuEllipsis {
|
||||||
// padding-right: 6px;
|
padding-right: 6px;
|
||||||
// font-weight: bold;
|
font-weight: bold;
|
||||||
// font-size: 18px;
|
font-size: 18px;
|
||||||
// position: relative;
|
position: relative;
|
||||||
// top: -5px;
|
top: -5px;
|
||||||
// left: 0px;
|
left: 0px;
|
||||||
// float: right;
|
float: right;
|
||||||
// display: none;
|
display: none;
|
||||||
// padding-left: 6px !important;
|
padding-left: 6px !important;
|
||||||
// line-height: @TreeLineHeight;
|
line-height: @TreeLineHeight;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .databaseMenu {
|
.databaseMenu {
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .databaseMenu:hover .menuEllipsis,
|
.databaseMenu:hover .menuEllipsis,
|
||||||
// .databaseMenu:focus .menuEllipsis {
|
.databaseMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .databaseCollChildTextOverflow {
|
.databaseCollChildTextOverflow {
|
||||||
// text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
// white-space: nowrap;
|
white-space: nowrap;
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// flex: 1;
|
flex: 1;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionMenu {
|
.collectionMenu {
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionMenu:hover .menuEllipsis,
|
.collectionMenu:hover .menuEllipsis,
|
||||||
// .collectionMenu:focus .menuEllipsis {
|
.collectionMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .documentsMenu:hover .menuEllipsis,
|
.documentsMenu:hover .menuEllipsis,
|
||||||
// .documentsMenu:focus .menuEllipsis {
|
.documentsMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .treeChildMenu {
|
.treeChildMenu {
|
||||||
// display: flex;
|
display: flex;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .storedProcedureMenu:hover .menuEllipsis,
|
.storedProcedureMenu:hover .menuEllipsis,
|
||||||
// .storedProcedureMenu:focus .menuEllipsis {
|
.storedProcedureMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .childMenu {
|
.childMenu {
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
// white-space: nowrap;
|
white-space: nowrap;
|
||||||
// padding-left: (6 * @MediumSpace);
|
padding-left: (6 * @MediumSpace);
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .storedChildMenu:hover .menuEllipsis,
|
.storedChildMenu:hover .menuEllipsis,
|
||||||
// .storedChildMenu:focus .menuEllipsis {
|
.storedChildMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .contextmenu6 {
|
.contextmenu6 {
|
||||||
// top: -29px;
|
top: -29px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .userDefinedMenu:hover .contextmenu6 {
|
.userDefinedMenu:hover .contextmenu6 {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .userDefinedchildMenu:hover .menuEllipsis,
|
.userDefinedchildMenu:hover .menuEllipsis,
|
||||||
// .userDefinedchildMenu:focus .menuEllipsis {
|
.userDefinedchildMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .triggersMenu:hover .menuEllipsis,
|
.triggersMenu:hover .menuEllipsis,
|
||||||
// .triggersMenu:focus .menuEllipsis {
|
.triggersMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .triggersChildMenu:hover .menuEllipsis,
|
.triggersChildMenu:hover .menuEllipsis,
|
||||||
// .triggersChildMenu:focus .menuEllipsis {
|
.triggersChildMenu:focus .menuEllipsis {
|
||||||
// display: block;
|
display: block;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .databaseId {
|
.databaseId {
|
||||||
// font-size: 14px;
|
font-size: 14px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .storedUdfTriggerMenu {
|
.storedUdfTriggerMenu {
|
||||||
// padding-left: 0px;
|
padding-left: 0px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionstree img {
|
.collectionstree img {
|
||||||
// width: 16px;
|
width: 16px;
|
||||||
// height: 16px;
|
height: 16px;
|
||||||
// vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// img.collectionsTreeCollapseExpand {
|
img.collectionsTreeCollapseExpand {
|
||||||
// width: 10px;
|
width: 10px;
|
||||||
// height: 10px;
|
height: 10px;
|
||||||
// vertical-align: middle;
|
vertical-align: middle;
|
||||||
// margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collapsed::before {
|
.collapsed::before {
|
||||||
// content: "\23F5";
|
content: "\23F5";
|
||||||
// margin-left: 0px;
|
margin-left: 0px;
|
||||||
// font-size: 15px;
|
font-size: 15px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .expanded::before {
|
.expanded::before {
|
||||||
// content: "\23F7";
|
content: "\23F7";
|
||||||
// margin-left: 0px;
|
margin-left: 0px;
|
||||||
// font-size: 15px;
|
font-size: 15px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .collectionMenuChildren {
|
.collectionMenuChildren {
|
||||||
// padding-left: 42px;
|
padding-left: 42px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .main-nav {
|
.main-nav {
|
||||||
// width: 100vh;
|
width: 100vh;
|
||||||
// height: 40px;
|
height: 40px;
|
||||||
// background: white;
|
background: white;
|
||||||
// transform-origin: left top;
|
transform-origin: left top;
|
||||||
// -webkit-transform-origin: left top;
|
-webkit-transform-origin: left top;
|
||||||
// -ms-transform-origin: left top;
|
-ms-transform-origin: left top;
|
||||||
// transform: rotate(-90deg) translateX(-100%);
|
transform: rotate(-90deg) translateX(-100%);
|
||||||
// -webkit-transform: rotate(-90deg) translateX(-100%);
|
-webkit-transform: rotate(-90deg) translateX(-100%);
|
||||||
// -ms-transform: rotate(-90deg) translateX(-100%);
|
-ms-transform: rotate(-90deg) translateX(-100%);
|
||||||
// border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .main-nav-img {
|
.main-nav-img {
|
||||||
// width: 16px;
|
width: 16px;
|
||||||
// height: 16px;
|
height: 16px;
|
||||||
// margin: -32px 0 0 0;
|
margin: -32px 0 0 0;
|
||||||
// transform: rotate(-90deg) translateX(-100%);
|
transform: rotate(-90deg) translateX(-100%);
|
||||||
// -webkit-transform: rotate(-90deg) translateX(-100%);
|
-webkit-transform: rotate(-90deg) translateX(-100%);
|
||||||
// -ms-transform: rotate(-90deg) translateX(-100%);
|
-ms-transform: rotate(-90deg) translateX(-100%);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .main-nav-img.main-nav-sub-img {
|
.main-nav-img.main-nav-sub-img {
|
||||||
// width: 16px;
|
width: 16px;
|
||||||
// height: 16px;
|
height: 16px;
|
||||||
// margin: 0px 0px 0 0;
|
margin: 0px 0px 0 0;
|
||||||
// transform: rotate(180deg) translateX(0%);
|
transform: rotate(180deg) translateX(0%);
|
||||||
// -webkit-transform: rotate(180deg) translateX(0%);
|
-webkit-transform: rotate(180deg) translateX(0%);
|
||||||
// -ms-transform: rotate(180deg) translateX(0%);
|
-ms-transform: rotate(180deg) translateX(0%);
|
||||||
// position: absolute;
|
position: absolute;
|
||||||
// right: -8px;
|
right: -8px;
|
||||||
// top: 16px;
|
top: 16px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// ul.nav {
|
ul.nav {
|
||||||
// margin: 0 auto;
|
margin: 0 auto;
|
||||||
// margin-top: 0px;
|
margin-top: 0px;
|
||||||
// margin-left: 0px;
|
margin-left: 0px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .mini ul.nav li {
|
.mini ul.nav li {
|
||||||
// float: right;
|
float: right;
|
||||||
// line-height: 25px;
|
line-height: 25px;
|
||||||
// height: auto;
|
height: auto;
|
||||||
// margin-top: 3px;
|
margin-top: 3px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .spancolchildstyle {
|
.spancolchildstyle {
|
||||||
// padding: 4px;
|
padding: 4px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .contextmenubutton {
|
.contextmenubutton {
|
||||||
// float: right;
|
float: right;
|
||||||
// display: none;
|
display: none;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .highlight:hover > .contextmenubutton {
|
.highlight:hover > .contextmenubutton {
|
||||||
// display: unset;
|
display: unset;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .highlight:hover > .contextmenubutton::after {
|
.highlight:hover > .contextmenubutton::after {
|
||||||
// content: "\2026";
|
content: "\2026";
|
||||||
// font-size: 12px;
|
font-size: 12px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .showEllipsis {
|
.showEllipsis {
|
||||||
// text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
// white-space: nowrap;
|
white-space: nowrap;
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// }
|
}
|
||||||
|
|||||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -86,7 +86,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
@@ -12662,9 +12662,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/retry": {
|
"node_modules/@types/retry": {
|
||||||
"version": "0.12.2",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
|
||||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/sanitize-html": {
|
"node_modules/@types/sanitize-html": {
|
||||||
@@ -21801,18 +21799,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-network-error": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -30257,20 +30243,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "6.2.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/retry": "0.12.2",
|
"@types/retry": "0.12.0",
|
||||||
"is-network-error": "^1.0.0",
|
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.17"
|
"node": ">=8"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-try": {
|
"node_modules/p-try": {
|
||||||
@@ -36017,13 +35997,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-dev-server/node_modules/@types/retry": {
|
|
||||||
"version": "0.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
|
||||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/webpack-dev-server/node_modules/ajv": {
|
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -36071,20 +36044,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-dev-server/node_modules/p-retry": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
|
||||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/retry": "0.12.0",
|
|
||||||
"retry": "^0.13.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
|
|||||||
37045
preview/package-lock.json
generated
37045
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -530,10 +530,6 @@ export class ariaLabelForLearnMoreLink {
|
|||||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeedbackLabels {
|
|
||||||
public static readonly provideFeedback: string = "Provide feedback";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|
||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -20,7 +19,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
@@ -43,7 +42,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
switch (requestInfo.resourceType) {
|
switch (requestInfo.resourceType) {
|
||||||
case Cosmos.ResourceType.conflicts:
|
case Cosmos.ResourceType.conflicts:
|
||||||
case Cosmos.ResourceType.container:
|
case Cosmos.ResourceType.container:
|
||||||
@@ -55,13 +54,8 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// User resource tokens
|
// User resource tokens
|
||||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
const resourceTokens = (
|
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
checkDatabaseResourceTokensValidity(
|
|
||||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo.resourceTokensTimestamp,
|
|
||||||
);
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -72,9 +66,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||||
// (which is a valid token, but won't work for these operations).
|
// (which is a valid token, but won't work for these operations).
|
||||||
const resourceTokens2 = (
|
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||||
@@ -133,6 +125,10 @@ export async function getTokenFromAuthService(
|
|||||||
resourceType: string,
|
resourceType: string,
|
||||||
resourceId?: string,
|
resourceId?: string,
|
||||||
): Promise<AuthorizationToken> {
|
): Promise<AuthorizationToken> {
|
||||||
|
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||||
|
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||||
@@ -155,6 +151,34 @@ export async function getTokenFromAuthService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTokenFromAuthService_ToBeDeprecated(
|
||||||
|
verb: string,
|
||||||
|
resourceType: string,
|
||||||
|
resourceId?: string,
|
||||||
|
): Promise<AuthorizationToken> {
|
||||||
|
try {
|
||||||
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
|
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-encrypted-auth-token": userContext.accessToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
verb,
|
||||||
|
resourceType,
|
||||||
|
resourceId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
|
||||||
|
const result = JSON.parse(await response.json());
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||||
enum SDKSupportedCapabilities {
|
enum SDKSupportedCapabilities {
|
||||||
None = 0,
|
None = 0,
|
||||||
@@ -179,10 +203,8 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||||
|
|
||||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.ConnectionString ||
|
userContext.authType === AuthType.ConnectionString ||
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -188,8 +196,20 @@ describe("MongoProxyClient", () => {
|
|||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds the correct proxy URL in development", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
describe("deleteDocuments", () => {
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("deleteDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -206,9 +226,9 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -218,9 +238,9 @@ describe("MongoProxyClient", () => {
|
|||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -255,4 +275,33 @@ describe("MongoProxyClient", () => {
|
|||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||||
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
|
});
|
||||||
|
const features = extractFeatures(params);
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a local endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a production endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||||
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
|
import {
|
||||||
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
defaultAllowedMongoProxyEndpoints,
|
||||||
|
validateEndpoint,
|
||||||
|
} from "Utils/EndpointUtils";
|
||||||
|
import queryString from "querystring";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -60,6 +67,10 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||||
|
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||||
|
}
|
||||||
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -78,7 +89,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -116,11 +127,76 @@ export function queryDocuments(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queryDocuments_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
isResourceList: boolean,
|
||||||
|
query: string,
|
||||||
|
continuationToken?: string,
|
||||||
|
): Promise<QueryResponse> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
|
rid: collection.rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||||
|
? collection.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[CosmosSDKConstants.HttpHeaders.IsQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
|
||||||
|
[HttpHeaders.contentType]: "application/query+json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (continuationToken) {
|
||||||
|
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = isResourceList ? "/resourcelist" : "";
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}${path}?${queryString.stringify(params)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ query }),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return {
|
||||||
|
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
|
||||||
|
documents: (await response.json()).Documents as DataModels.DocumentId[],
|
||||||
|
headers: response.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await errorHandling(response, "querying documents", params);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function readDocument(
|
export function readDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||||
|
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -141,7 +217,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -161,12 +237,61 @@ export function readDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentId: DocumentId,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 4).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
|
||||||
|
JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "reading document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createDocument(
|
export function createDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||||
|
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -183,7 +308,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -203,12 +328,54 @@ export function createDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
partitionKeyProperty: string,
|
||||||
|
documentContent: unknown,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
|
rid: collection.rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(documentContent),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "creating document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateDocument(
|
export function updateDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||||
|
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -229,7 +396,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -250,6 +417,139 @@ export function updateDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentId: DocumentId,
|
||||||
|
documentContent: string,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: documentContent,
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "updating document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
||||||
|
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
|
}
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
databaseID: databaseId,
|
||||||
|
collectionID: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
resourceID: rid,
|
||||||
|
resourceType: "docs",
|
||||||
|
subscriptionID: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseAccountName: databaseAccount.name,
|
||||||
|
partitionKey:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentId: DocumentId,
|
||||||
|
): Promise<void> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteDocuments(
|
export function deleteDocuments(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
@@ -275,7 +575,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -299,6 +599,9 @@ export function deleteDocuments(
|
|||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
|
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||||
|
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
|
||||||
@@ -319,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -339,6 +642,70 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
||||||
|
params: DataModels.CreateCollectionParams,
|
||||||
|
): Promise<DataModels.Collection> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
const mongoParams: DataModels.MongoParameters = {
|
||||||
|
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||||
|
db: params.databaseId,
|
||||||
|
coll: params.collectionId,
|
||||||
|
pk: shardKey,
|
||||||
|
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
||||||
|
cd: params.createNewDatabase,
|
||||||
|
st: params.databaseLevelThroughput,
|
||||||
|
is: !!shardKey,
|
||||||
|
rid: "",
|
||||||
|
rtype: "colls",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
isAutoPilot: !!params.autoPilotMaxThroughput,
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(
|
||||||
|
`${endpoint}/createCollection?${queryString.stringify(
|
||||||
|
mongoParams as unknown as queryString.ParsedUrlQueryInput,
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "creating collection", mongoParams);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
|
let endpoint;
|
||||||
|
if (useMongoProxyEndpoint(feature)) {
|
||||||
|
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
|
} else {
|
||||||
|
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||||
|
...defaultAllowedMongoProxyEndpoints,
|
||||||
|
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
];
|
||||||
|
endpoint =
|
||||||
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||||
|
? userContext.features.mongoProxyEndpoint
|
||||||
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEndpoint(endpoint: string): string {
|
export function getEndpoint(endpoint: string): string {
|
||||||
let url = endpoint + "/api/mongo/explorer";
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
@@ -352,6 +719,84 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||||
|
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||||
|
[MongoProxyApi.ResourceList]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.QueryDocuments]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.CreateDocument]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.ReadDocument]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.UpdateDocument]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.DeleteDocument]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.LegacyMongoShell]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.BulkDelete]: [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
export class ThrottlingError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||||
|
|
||||||
export function updateStyles(): void {
|
export function updateStyles(): void {
|
||||||
if (isFabric()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { useDatabases } from "../../Explorer/useDatabases";
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let collection: DataModels.Collection;
|
let collection: DataModels.Collection;
|
||||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.createNewDatabase) {
|
if (params.createNewDatabase) {
|
||||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
@@ -13,7 +12,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
await deleteCollectionWithARM(databaseId, collectionId);
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).delete();
|
await client().database(databaseId).container(collectionId).delete();
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
? parseInt(resource.softAllowedMaximumThroughput)
|
? parseInt(resource.softAllowedMaximumThroughput)
|
||||||
: resource.softAllowedMaximumThroughput;
|
: resource.softAllowedMaximumThroughput;
|
||||||
|
|
||||||
const throughputBuckets = resource?.throughputBuckets;
|
|
||||||
|
|
||||||
if (autoscaleSettings) {
|
if (autoscaleSettings) {
|
||||||
return {
|
return {
|
||||||
id: offerId,
|
id: offerId,
|
||||||
@@ -116,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -17,13 +16,15 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
|
||||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
if (
|
||||||
|
configContext.platform === Platform.Fabric &&
|
||||||
|
userContext.fabricContext &&
|
||||||
|
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||||
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in (
|
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens) {
|
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
@@ -55,8 +56,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readCollectionsWithARM(databaseId);
|
return await readCollectionsWithARM(databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -11,9 +11,8 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
if (isFabricMirroredKey() || isFabricNative()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||||
// For Fabric Native, it is not supported.
|
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -24,8 +23,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readDatabaseOfferWithARM(params.databaseId);
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||||
isFabricMirroredKey() &&
|
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
|
||||||
.resourceTokens
|
|
||||||
) {
|
|
||||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo;
|
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
@@ -52,28 +46,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
}));
|
}));
|
||||||
clearMessage();
|
clearMessage();
|
||||||
return databases;
|
return databases;
|
||||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
|
||||||
const databaseId = userContext.fabricContext.databaseName;
|
|
||||||
databases = [
|
|
||||||
{
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: databaseId,
|
|
||||||
collections: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
clearMessage();
|
|
||||||
return databases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
databases = await readDatabasesWithARM();
|
databases = await readDatabasesWithARM();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Collection } from "../../Contracts/DataModels";
|
import { Collection } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -37,8 +36,7 @@ export async function updateCollection(
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import {
|
||||||
migrateCassandraKeyspaceToAutoscale,
|
migrateCassandraKeyspaceToAutoscale,
|
||||||
@@ -57,7 +56,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.collectionId) {
|
if (params.collectionId) {
|
||||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||||
} else if (userContext.apiType === "Tables") {
|
} else if (userContext.apiType === "Tables") {
|
||||||
@@ -360,13 +359,6 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
body.properties.resource.throughput = params.manualThroughput;
|
body.properties.resource.throughput = params.manualThroughput;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.throughputBuckets) {
|
|
||||||
const throughputBuckets = params.throughputBuckets.filter(
|
|
||||||
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
|
|
||||||
);
|
|
||||||
body.properties.resource.throughputBuckets = throughputBuckets;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
|
allowedMongoBackendEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
@@ -49,8 +50,10 @@ export interface ConfigContext {
|
|||||||
CATALOG_API_KEY: string;
|
CATALOG_API_KEY: string;
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
|
BACKEND_ENDPOINT?: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
@@ -106,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
@@ -148,6 +152,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
@@ -157,6 +170,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||||
|
|||||||
@@ -275,12 +275,6 @@ export interface Offer {
|
|||||||
offerReplacePending: boolean;
|
offerReplacePending: boolean;
|
||||||
instantMaximumThroughput?: number;
|
instantMaximumThroughput?: number;
|
||||||
softAllowedMaximumThroughput?: number;
|
softAllowedMaximumThroughput?: number;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThroughputBucket {
|
|
||||||
id: number;
|
|
||||||
maxThroughputPercentage: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SDKOfferDefinition extends Resource {
|
export interface SDKOfferDefinition extends Resource {
|
||||||
@@ -403,7 +397,6 @@ export interface UpdateOfferParams {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
migrateToAutoPilot?: boolean;
|
migrateToAutoPilot?: boolean;
|
||||||
migrateToManual?: boolean;
|
migrateToManual?: boolean;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
GetAccessToken = "GetAccessToken",
|
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
// This is the version of these messages
|
||||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
export const FABRIC_RPC_VERSION = "2";
|
||||||
|
|
||||||
// Fabric to Data Explorer
|
// Fabric to Data Explorer
|
||||||
|
|
||||||
|
// TODO Deprecated. Remove this section once DE is updated
|
||||||
|
export type FabricMessageV1 =
|
||||||
|
| {
|
||||||
|
type: "newContainer";
|
||||||
|
databaseName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "initialize";
|
||||||
|
message: {
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "authorizationToken";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: AuthorizationToken | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "allResourceTokens";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
export type FabricMessageV2 =
|
export type FabricMessageV2 =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
|
|||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: ResourceTokenInfo | undefined;
|
data: FabricDatabaseConnectionInfo | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -41,81 +79,17 @@ export type FabricMessageV2 =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FabricMessageV3 =
|
export type CosmosDBTokenResponse = {
|
||||||
| {
|
token: string;
|
||||||
type: "newContainer";
|
date: string;
|
||||||
databaseName: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "initialize";
|
|
||||||
version: string;
|
|
||||||
id: string;
|
|
||||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "authorizationToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "allResourceTokens_v2";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: ResourceTokenInfo | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "explorerVisible";
|
|
||||||
message: {
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "accessToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: { accessToken: string };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CosmosDbArtifactType {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
MIRRORED_KEY = "MIRRORED_KEY",
|
|
||||||
MIRRORED_AAD = "MIRRORED_AAD",
|
|
||||||
NATIVE = "NATIVE",
|
|
||||||
}
|
|
||||||
export interface ArtifactConnectionInfo {
|
|
||||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
|
||||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
|
||||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessTokenConnectionInfo {
|
|
||||||
accessToken: string;
|
|
||||||
databaseName: string;
|
|
||||||
accountEndpoint: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
|
||||||
connectionId: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
isReadOnly: boolean;
|
|
||||||
artifactType: T;
|
|
||||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
|
||||||
}
|
|
||||||
export interface CosmosDBConnectionInfoResponse {
|
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: Record<string, string> | undefined;
|
resourceTokens: { [resourceId: string]: string };
|
||||||
accessToken: string | undefined;
|
};
|
||||||
isReadOnly: boolean;
|
|
||||||
credentialType: "Key" | "OAuth2" | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
TriggerDefinition,
|
TriggerDefinition,
|
||||||
UserDefinedFunctionDefinition,
|
UserDefinedFunctionDefinition,
|
||||||
} from "@azure/cosmos";
|
} from "@azure/cosmos";
|
||||||
import type Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
|
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
|
||||||
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
|
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
@@ -406,6 +406,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
csmEndpoint?: string;
|
csmEndpoint?: string;
|
||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
extensionEndpoint?: string;
|
||||||
portalBackendEndpoint?: string;
|
portalBackendEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
mongoProxyEndpoint?: string;
|
||||||
cassandraProxyEndpoint?: string;
|
cassandraProxyEndpoint?: string;
|
||||||
@@ -462,6 +463,3 @@ export interface DropdownOption<T> {
|
|||||||
value: T;
|
value: T;
|
||||||
disable?: boolean;
|
disable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the duplicate Explorer interface and export the type
|
|
||||||
export type { Explorer };
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
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 { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -21,6 +19,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 Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||||
@@ -42,7 +41,7 @@ export interface DatabaseContextMenuButtonParams {
|
|||||||
* New resource tree (in ReactJS)
|
* New resource tree (in ReactJS)
|
||||||
*/
|
*/
|
||||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
@@ -146,7 +145,7 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
import { monacoTheme } from "hooks/useTheme";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||||
// import "./EditorReact.less";
|
// import "./EditorReact.less";
|
||||||
@@ -212,7 +211,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
ariaLabel: this.props.ariaLabel,
|
ariaLabel: this.props.ariaLabel,
|
||||||
fontSize: this.props.fontSize || 12,
|
fontSize: this.props.fontSize || 12,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
theme: monacoTheme,
|
theme: this.props.theme,
|
||||||
wordWrap: this.props.wordWrap || "off",
|
wordWrap: this.props.wordWrap || "off",
|
||||||
lineNumbers: this.props.lineNumbers || "off",
|
lineNumbers: this.props.lineNumbers || "off",
|
||||||
lineNumbersMinChars: this.props.lineNumbersMinChars,
|
lineNumbersMinChars: this.props.lineNumbersMinChars,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2ToolTip {
|
.settingsV2ToolTip {
|
||||||
@@ -25,8 +23,6 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2Editor {
|
.settingsV2Editor {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { IndexingPolicy } from "@azure/cosmos";
|
|
||||||
import { act } from "@testing-library/react";
|
|
||||||
import { AuthType } from "AuthType";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { Features } from "Platform/Hosted/extractFeatures";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
@@ -252,86 +247,4 @@ describe("SettingsComponent", () => {
|
|||||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "SQL",
|
|
||||||
features: { enableThroughputBuckets: true } as Features,
|
|
||||||
authType: AuthType.AAD,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
|
||||||
|
|
||||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
|
||||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
|
||||||
expect(isEnabled).toBe(true);
|
|
||||||
|
|
||||||
wrapper.setState({
|
|
||||||
isThroughputBucketsSaveable: true,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await settingsComponentInstance.onSaveClick();
|
|
||||||
|
|
||||||
expect(updateOffer).toHaveBeenCalledWith({
|
|
||||||
databaseId: collection.databaseId,
|
|
||||||
collectionId: collection.id(),
|
|
||||||
currentOffer: expect.any(Object),
|
|
||||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
|
||||||
manualThroughput: collection.offer().manualThroughput,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("SettingsComponent - indexing policy subscription", () => {
|
|
||||||
const baseProps: SettingsComponentProps = {
|
|
||||||
settingsTab: new CollectionSettingsTabV2({
|
|
||||||
collection: collection,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
node: undefined,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
it("subscribes to the correct container's indexing policy and updates state on change", async () => {
|
|
||||||
const containerId = collection.id();
|
|
||||||
const mockIndexingPolicy: IndexingPolicy = {
|
|
||||||
automatic: false,
|
|
||||||
indexingMode: "lazy",
|
|
||||||
includedPaths: [{ path: "/foo/*" }],
|
|
||||||
excludedPaths: [{ path: "/bar/*" }],
|
|
||||||
compositeIndexes: [],
|
|
||||||
spatialIndexes: [],
|
|
||||||
vectorIndexes: [],
|
|
||||||
fullTextIndexes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
|
||||||
const instance = wrapper.instance() as SettingsComponent;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
useIndexingPolicyStore.setState({
|
|
||||||
indexingPolicies: {
|
|
||||||
[containerId]: mockIndexingPolicy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.state("indexingPolicyContent")).toEqual(mockIndexingPolicy);
|
|
||||||
expect(wrapper.state("indexingPolicyContentBaseline")).toEqual(mockIndexingPolicy);
|
|
||||||
// @ts-expect-error: rawDataModel is intentionally accessed for test validation
|
|
||||||
expect(instance.collection.rawDataModel.indexingPolicy).toEqual(mockIndexingPolicy);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem, Stack, getTheme } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
import {
|
import {
|
||||||
ComputedPropertiesComponent,
|
ComputedPropertiesComponent,
|
||||||
ComputedPropertiesComponentProps,
|
ComputedPropertiesComponentProps,
|
||||||
@@ -7,11 +7,6 @@ import {
|
|||||||
ContainerPolicyComponent,
|
ContainerPolicyComponent,
|
||||||
ContainerPolicyComponentProps,
|
ContainerPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||||
import {
|
|
||||||
ThroughputBucketsComponent,
|
|
||||||
ThroughputBucketsComponentProps,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
|
||||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
@@ -66,6 +61,7 @@ import {
|
|||||||
parseConflictResolutionMode,
|
parseConflictResolutionMode,
|
||||||
parseConflictResolutionProcedure,
|
parseConflictResolutionProcedure,
|
||||||
} from "./SettingsUtils";
|
} from "./SettingsUtils";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
content: JSX.Element;
|
content: JSX.Element;
|
||||||
@@ -90,8 +86,6 @@ export interface SettingsComponentState {
|
|||||||
wasAutopilotOriginallySet: boolean;
|
wasAutopilotOriginallySet: boolean;
|
||||||
isScaleSaveable: boolean;
|
isScaleSaveable: boolean;
|
||||||
isScaleDiscardable: boolean;
|
isScaleDiscardable: boolean;
|
||||||
throughputBuckets: DataModels.ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
|
||||||
throughputError: string;
|
throughputError: string;
|
||||||
|
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
@@ -110,7 +104,6 @@ export interface SettingsComponentState {
|
|||||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||||
isSubSettingsSaveable: boolean;
|
isSubSettingsSaveable: boolean;
|
||||||
isSubSettingsDiscardable: boolean;
|
isSubSettingsDiscardable: boolean;
|
||||||
isThroughputBucketsSaveable: boolean;
|
|
||||||
|
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||||
@@ -165,9 +158,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
private throughputBucketsEnabled: boolean;
|
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
private unsubscribe: () => void;
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -183,10 +175,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
this.throughputBucketsEnabled =
|
|
||||||
userContext.apiType === "SQL" &&
|
|
||||||
userContext.features.enableThroughputBuckets &&
|
|
||||||
userContext.authType === AuthType.AAD;
|
|
||||||
|
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
this.isFixedContainer =
|
this.isFixedContainer =
|
||||||
@@ -205,8 +193,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
throughputBuckets: undefined,
|
|
||||||
throughputBucketsBaseline: undefined,
|
|
||||||
throughputError: undefined,
|
throughputError: undefined,
|
||||||
|
|
||||||
timeToLive: undefined,
|
timeToLive: undefined,
|
||||||
@@ -225,7 +211,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
changeFeedPolicyBaseline: undefined,
|
changeFeedPolicyBaseline: undefined,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
|
|
||||||
vectorEmbeddingPolicy: undefined,
|
vectorEmbeddingPolicy: undefined,
|
||||||
vectorEmbeddingPolicyBaseline: undefined,
|
vectorEmbeddingPolicyBaseline: undefined,
|
||||||
@@ -298,19 +283,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
this.unsubscribe = useIndexingPolicyStore.subscribe(
|
|
||||||
() => {
|
|
||||||
this.refreshCollectionData();
|
|
||||||
},
|
|
||||||
(state) => state.indexingPolicies[this.collection.id()],
|
|
||||||
);
|
|
||||||
this.refreshCollectionData();
|
|
||||||
}
|
|
||||||
componentWillUnmount(): void {
|
|
||||||
if (this.unsubscribe) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(): void {
|
componentDidUpdate(): void {
|
||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
@@ -353,8 +327,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -366,8 +339,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -447,8 +419,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
throughput: this.state.throughputBaseline,
|
throughput: this.state.throughputBaseline,
|
||||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
timeToLive: this.state.timeToLiveBaseline,
|
timeToLive: this.state.timeToLiveBaseline,
|
||||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||||
@@ -471,7 +441,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isContainerPolicyDirty: false,
|
isContainerPolicyDirty: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
@@ -510,10 +479,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||||
|
|
||||||
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
|
||||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
|
||||||
};
|
|
||||||
|
|
||||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||||
|
|
||||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||||
@@ -783,13 +748,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
{ name: "name_of_property", query: "query_to_compute_property" },
|
{ name: "name_of_property", query: "query_to_compute_property" },
|
||||||
] as DataModels.ComputedProperties;
|
] as DataModels.ComputedProperties;
|
||||||
}
|
}
|
||||||
const throughputBuckets = this.offer?.throughputBuckets;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
throughputBaseline: offerThroughput,
|
throughputBaseline: offerThroughput,
|
||||||
throughputBuckets,
|
|
||||||
throughputBucketsBaseline: throughputBuckets,
|
|
||||||
changeFeedPolicy: changeFeedPolicy,
|
changeFeedPolicy: changeFeedPolicy,
|
||||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||||
timeToLive: timeToLive,
|
timeToLive: timeToLive,
|
||||||
@@ -877,10 +839,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({ throughput: newThroughput, throughputError });
|
this.setState({ throughput: newThroughput, throughputError });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
|
||||||
this.setState({ throughputBuckets });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||||
|
|
||||||
@@ -934,31 +892,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
startKey,
|
startKey,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
private refreshCollectionData = async (): Promise<void> => {
|
|
||||||
const containerId = this.collection.id();
|
|
||||||
const latestIndexingPolicy = useIndexingPolicyStore.getState().indexingPolicies[containerId];
|
|
||||||
const rawPolicy = latestIndexingPolicy ?? this.collection.indexingPolicy();
|
|
||||||
|
|
||||||
const latestCollection: DataModels.IndexingPolicy = {
|
|
||||||
automatic: rawPolicy?.automatic ?? true,
|
|
||||||
indexingMode: rawPolicy?.indexingMode ?? "consistent",
|
|
||||||
includedPaths: rawPolicy?.includedPaths ?? [],
|
|
||||||
excludedPaths: rawPolicy?.excludedPaths ?? [],
|
|
||||||
compositeIndexes: rawPolicy?.compositeIndexes ?? [],
|
|
||||||
spatialIndexes: rawPolicy?.spatialIndexes ?? [],
|
|
||||||
vectorIndexes: rawPolicy?.vectorIndexes ?? [],
|
|
||||||
fullTextIndexes: rawPolicy?.fullTextIndexes ?? [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.collection.rawDataModel.indexingPolicy = latestCollection;
|
|
||||||
this.setState({
|
|
||||||
indexingPolicyContent: latestCollection,
|
|
||||||
indexingPolicyContentBaseline: latestCollection,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isContainerPolicyDirty ||
|
this.state.isContainerPolicyDirty ||
|
||||||
@@ -1092,24 +1029,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
|
||||||
? this.collection.offer().autoscaleMaxThroughput
|
|
||||||
: undefined,
|
|
||||||
manualThroughput: this.collection.offer().manualThroughput
|
|
||||||
? this.collection.offer().manualThroughput
|
|
||||||
: undefined,
|
|
||||||
throughputBuckets: this.state.throughputBuckets,
|
|
||||||
});
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.offer = updatedOffer;
|
|
||||||
this.setState({ isThroughputBucketsSaveable: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.collection.databaseId,
|
databaseId: this.collection.databaseId,
|
||||||
@@ -1168,7 +1087,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const theme = getTheme();
|
|
||||||
const scaleComponentProps: ScaleComponentProps = {
|
const scaleComponentProps: ScaleComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
@@ -1186,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
throughputError: this.state.throughputError,
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.isCollectionSettingsTab) {
|
if (!this.isCollectionSettingsTab) {
|
||||||
return (
|
return (
|
||||||
<div className="settingsV2MainContainer">
|
<div className="settingsV2MainContainer">
|
||||||
@@ -1290,13 +1209,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
|
||||||
currentBuckets: this.state.throughputBuckets,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
onBucketsChange: this.onThroughputBucketChange,
|
|
||||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
@@ -1359,113 +1271,33 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
|
||||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
};
|
};
|
||||||
|
|
||||||
const pivotStyles = {
|
const pivotItems = tabs.map((tab) => {
|
||||||
root: {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
selectors: {
|
|
||||||
'& .ms-Pivot-link': {
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
'& .ms-Pivot-link.is-selected::before': {
|
|
||||||
backgroundColor: 'var(--colorCompoundBrandBackground)'
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
selectors: {
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
'&:active': {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
'&[aria-selected="true"]': {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
selectors: {
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
'&:active': {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
itemContainer: {
|
|
||||||
// padding: '20px 24px',
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentStyles = {
|
|
||||||
root: {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
// padding: '20px 24px'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="settingsV2MainContainer" style={{
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
position: 'relative'
|
|
||||||
} as React.CSSProperties}>
|
|
||||||
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
|
||||||
<div>This table shared throughput is configured at the keyspace</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="settingsV2TabsContainer" style={{
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
position: 'relative',
|
|
||||||
padding: '20px 24px'
|
|
||||||
} as React.CSSProperties}>
|
|
||||||
<Pivot {...pivotProps} styles={pivotStyles}>
|
|
||||||
{tabs.map((tab) => {
|
|
||||||
const pivotItemProps: IPivotItemProps = {
|
const pivotItemProps: IPivotItemProps = {
|
||||||
itemKey: SettingsV2TabTypes[tab.tab],
|
itemKey: SettingsV2TabTypes[tab.tab],
|
||||||
style: {
|
style: { marginTop: 20 },
|
||||||
marginTop: 20,
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
headerText: getTabTitle(tab.tab),
|
headerText: getTabTitle(tab.tab),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||||
<Stack styles={contentStyles}>
|
|
||||||
{tab.content}
|
{tab.content}
|
||||||
</Stack>
|
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
);
|
);
|
||||||
})}
|
});
|
||||||
</Pivot>
|
|
||||||
|
return (
|
||||||
|
<div className="settingsV2MainContainer">
|
||||||
|
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
||||||
|
<div>This table shared throughput is configured at the keyspace</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="settingsV2TabsContainer">
|
||||||
|
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export interface PriceBreakdown {
|
|||||||
|
|
||||||
export type editorType = "indexPolicy" | "computedProperties";
|
export type editorType = "indexPolicy" | "computedProperties";
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "var(--colorNeutralForeground1)" } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -166,7 +166,7 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
||||||
root: { marginTop: "5px", backgroundColor: "var(--colorNeutralBackground1)" },
|
root: { marginTop: "5px", backgroundColor: "white" },
|
||||||
text: { fontSize: 14 },
|
text: { fontSize: 14 },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,11 +214,9 @@ export const getEstimatedSpendingElement = (
|
|||||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
|
<Text style={{ fontWeight: 600 }}>Cost estimate*</Text>
|
||||||
{costElement}
|
{costElement}
|
||||||
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
<Text style={{ fontWeight: 600, marginTop: 15 }}>How we calculate this</Text>
|
||||||
How we calculate this
|
|
||||||
</Text>
|
|
||||||
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
||||||
<span>
|
<span>
|
||||||
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
|
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
|
||||||
@@ -232,7 +230,7 @@ export const getEstimatedSpendingElement = (
|
|||||||
{priceBreakdown.pricePerRu}/RU
|
{priceBreakdown.pricePerRu}/RU
|
||||||
</span>
|
</span>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
<Text style={{ marginTop: 15 }}>
|
||||||
<em>*{estimatedCostDisclaimer}</em>
|
<em>*{estimatedCostDisclaimer}</em>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -274,7 +272,7 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
|||||||
|
|
||||||
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Text id="updateThroughputDelayedApplyWarningMessage">
|
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||||
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
||||||
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
||||||
with this value and wait until the scale-up is completed.
|
with this value and wait until the scale-up is completed.
|
||||||
@@ -292,7 +290,7 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
|
|||||||
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
||||||
There are three options you can choose from to proceed:
|
There are three options you can choose from to proceed:
|
||||||
</Text>
|
</Text>
|
||||||
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
|
<ol style={{ fontSize: 14, color: "windowtext", marginTop: "5px" }}>
|
||||||
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
|
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
|
||||||
{instantMaximumThroughput < maximumThroughput && (
|
{instantMaximumThroughput < maximumThroughput && (
|
||||||
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
|
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
|
||||||
@@ -328,7 +326,7 @@ export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Ele
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const saveThroughputWarningMessage: JSX.Element = (
|
export const saveThroughputWarningMessage: JSX.Element = (
|
||||||
<Text>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||||
before saving your changes
|
before saving your changes
|
||||||
</Text>
|
</Text>
|
||||||
@@ -509,25 +507,11 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
|
|||||||
height: 25,
|
height: 25,
|
||||||
width: 300,
|
width: 300,
|
||||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
|
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
|
||||||
backgroundColor: "var(--colorNeutralBackground3)",
|
|
||||||
selectors: {
|
selectors: {
|
||||||
":disabled": {
|
":disabled": {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: StyleConstants.BaseMedium,
|
||||||
borderColor: StyleConstants.BaseMediumHigh,
|
borderColor: StyleConstants.BaseMediumHigh,
|
||||||
},
|
},
|
||||||
"input:disabled": {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground3)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
subComponentStyles: {
|
|
||||||
label: {
|
|
||||||
root: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -537,9 +521,6 @@ export const getChoiceGroupStyles = (
|
|||||||
baseline: isDirtyTypes,
|
baseline: isDirtyTypes,
|
||||||
isHorizontal?: boolean,
|
isHorizontal?: boolean,
|
||||||
): Partial<IChoiceGroupStyles> => ({
|
): Partial<IChoiceGroupStyles> => ({
|
||||||
label: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
flexContainer: [
|
flexContainer: [
|
||||||
{
|
{
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -554,7 +535,6 @@ export const getChoiceGroupStyles = (
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: StyleConstants.DataExplorerFont,
|
fontFamily: StyleConstants.DataExplorerFont,
|
||||||
padding: "2px 5px",
|
padding: "2px 5px",
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
display: isHorizontal ? "inline-flex" : "default",
|
display: isHorizontal ? "inline-flex" : "default",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import * as DataModels from "Contracts/DataModels";
|
|||||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||||
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||||
import { monacoTheme } from "hooks/useTheme";
|
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
export interface ComputedPropertiesComponentProps {
|
export interface ComputedPropertiesComponentProps {
|
||||||
computedPropertiesContent: DataModels.ComputedProperties;
|
computedPropertiesContent: DataModels.ComputedProperties;
|
||||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
||||||
@@ -86,7 +86,6 @@ export class ComputedPropertiesComponent extends React.Component<
|
|||||||
value: value,
|
value: value,
|
||||||
language: "json",
|
language: "json",
|
||||||
ariaLabel: "Computed properties",
|
ariaLabel: "Computed properties",
|
||||||
theme:monacoTheme,
|
|
||||||
});
|
});
|
||||||
if (this.computedPropertiesEditor) {
|
if (this.computedPropertiesEditor) {
|
||||||
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
|
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
|
||||||
import { monacoTheme } from "hooks/useTheme";
|
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
@@ -7,6 +6,7 @@ import { loadMonaco } from "../../../LazyMonaco";
|
|||||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface IndexingPolicyComponentProps {
|
export interface IndexingPolicyComponentProps {
|
||||||
shouldDiscardIndexingPolicy: boolean;
|
shouldDiscardIndexingPolicy: boolean;
|
||||||
resetShouldDiscardIndexingPolicy: () => void;
|
resetShouldDiscardIndexingPolicy: () => void;
|
||||||
@@ -87,18 +87,13 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async createIndexingPolicyEditor(): Promise<void> {
|
private async createIndexingPolicyEditor(): Promise<void> {
|
||||||
if (!this.indexingPolicyDiv.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
if (this.indexingPolicyDiv.current) {
|
|
||||||
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
||||||
value: value,
|
value: value,
|
||||||
language: "json",
|
language: "json",
|
||||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||||
ariaLabel: "Indexing Policy",
|
ariaLabel: "Indexing Policy",
|
||||||
theme: monacoTheme,
|
|
||||||
});
|
});
|
||||||
if (this.indexingPolicyEditor) {
|
if (this.indexingPolicyEditor) {
|
||||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||||
@@ -106,52 +101,6 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
this.props.logIndexingPolicySuccessMessage();
|
this.props.logIndexingPolicySuccessMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// private async createIndexingPolicyEditor(): Promise<void> {
|
|
||||||
// const isDarkMode = true;
|
|
||||||
// const monacoThemeName = "fluent-theme";
|
|
||||||
|
|
||||||
// if (!this.indexingPolicyDiv.current) return;
|
|
||||||
|
|
||||||
// const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
|
||||||
// const monaco = await loadMonaco();
|
|
||||||
|
|
||||||
// // Safely get Fluent UI theme colors
|
|
||||||
// const bodyStyles = getComputedStyle(document.body);
|
|
||||||
// const backgroundColor = bodyStyles.getPropertyValue("--colorNeutralBackground1").trim() || "#1b1a19";
|
|
||||||
// const foregroundColor = bodyStyles.getPropertyValue("--colorNeutralForeground1").trim() || "#ffffff";
|
|
||||||
|
|
||||||
// // Define Monaco theme using Fluent UI colors
|
|
||||||
// monaco.editor.defineTheme(monacoThemeName, {
|
|
||||||
// base: isDarkMode ? "vs-dark" : "vs",
|
|
||||||
// inherit: true,
|
|
||||||
// rules: [],
|
|
||||||
// colors: {
|
|
||||||
// "editor.background": backgroundColor,
|
|
||||||
// "editor.foreground": foregroundColor,
|
|
||||||
// "editorCursor.foreground": "#ffcc00",
|
|
||||||
// "editorLineNumber.foreground": "#aaaaaa",
|
|
||||||
// "editor.selectionBackground": "#666666",
|
|
||||||
// "editor.lineHighlightBackground": "#333333"
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Create the editor with the custom theme
|
|
||||||
// this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
|
||||||
// value,
|
|
||||||
// language: "json",
|
|
||||||
// readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
|
||||||
// ariaLabel: "Indexing Policy",
|
|
||||||
// theme: monacoThemeName
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (this.indexingPolicyEditor) {
|
|
||||||
// const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
|
||||||
// indexingPolicyEditorModel?.onDidChangeContent(this.onEditorContentChange.bind(this));
|
|
||||||
// this.props.logIndexingPolicySuccessMessage();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
private onEditorContentChange = (): void => {
|
private onEditorContentChange = (): void => {
|
||||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||||
import {
|
import {
|
||||||
mongoIndexTransformationRefreshingMessage,
|
mongoIndexTransformationRefreshingMessage,
|
||||||
renderMongoIndexTransformationRefreshMessage,
|
renderMongoIndexTransformationRefreshMessage,
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
|
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||||
import { isIndexTransforming } from "../../SettingsUtils";
|
import { isIndexTransforming } from "../../SettingsUtils";
|
||||||
|
|
||||||
export interface IndexingPolicyRefreshComponentProps {
|
export interface IndexingPolicyRefreshComponentProps {
|
||||||
|
|||||||
@@ -56,15 +56,13 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
const partitionKeyValue = getPartitionKeyValue();
|
const partitionKeyValue = getPartitionKeyValue();
|
||||||
|
|
||||||
const textHeadingStyle = {
|
const textHeadingStyle = {
|
||||||
root: { fontWeight: FontWeights.semibold, fontSize: 16, color: 'var(--colorNeutralForeground1)' },
|
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const textSubHeadingStyle = {
|
const textSubHeadingStyle = {
|
||||||
root: { fontWeight: FontWeights.semibold , color: 'var(--colorNeutralForeground1)' },
|
root: { fontWeight: FontWeights.semibold },
|
||||||
};
|
|
||||||
const textSubHeadingStyle1 = {
|
|
||||||
root: {color: 'var(--colorNeutralForeground1)' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
|
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
|
||||||
if (isCurrentJobInProgress(currentJob)) {
|
if (isCurrentJobInProgress(currentJob)) {
|
||||||
const jobName = currentJob?.properties?.jobName;
|
const jobName = currentJob?.properties?.jobName;
|
||||||
@@ -160,8 +158,8 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
<Text>{partitionKeyValue}</Text>
|
||||||
<Text styles={textSubHeadingStyle1}>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -176,7 +174,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
<Text styles={textSubHeadingStyle1}>
|
<Text>
|
||||||
To change the partition key, a new destination container must be created or an existing destination container
|
To change the partition key, a new destination container must be created or an existing destination container
|
||||||
selected. Data will then be copied to the destination container.
|
selected. Data will then be copied to the destination container.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, TextField, getTheme, mergeStyleSets } from "@fluentui/react";
|
import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
@@ -25,13 +25,6 @@ import {
|
|||||||
} from "../SettingsUtils";
|
} from "../SettingsUtils";
|
||||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||||
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
const classNames = mergeStyleSets({
|
|
||||||
hintText: {
|
|
||||||
color: 'var(--colorNeutralForeground1)', // theme-aware
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export interface SubSettingsComponentProps {
|
export interface SubSettingsComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
@@ -188,19 +181,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
userContext.apiType === "Mongo" ? (
|
userContext.apiType === "Mongo" ? (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
styles={{
|
styles={{ text: { fontSize: 14 } }}
|
||||||
root: {
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: theme.semanticColors.bodyText,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: theme.semanticColors.bodyText,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
To enable time-to-live (TTL) for your collection/documents,
|
To enable time-to-live (TTL) for your collection/documents,
|
||||||
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
|
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
|
||||||
@@ -342,14 +323,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
||||||
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
|
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" &&
|
{userContext.apiType === "SQL" &&
|
||||||
(this.isHierarchicalPartitionedContainer() ? (
|
(this.isHierarchicalPartitionedContainer() ? (
|
||||||
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
|
<Text>Hierarchically partitioned container.</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
|
<Text>Non-hierarchically partitioned container.</Text>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
|
||||||
|
|
||||||
describe("ThroughputBucketsComponent", () => {
|
|
||||||
const mockOnBucketsChange = jest.fn();
|
|
||||||
const mockOnSaveableChange = jest.fn();
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
currentBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
throughputBucketsBaseline: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 40 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
],
|
|
||||||
onBucketsChange: mockOnBucketsChange,
|
|
||||||
onSaveableChange: mockOnSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the correct number of buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders buckets in the correct order even if input is unordered", () => {
|
|
||||||
const unorderedBuckets = [
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
];
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
|
||||||
|
|
||||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
|
||||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
|
||||||
const oversizedBuckets = [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 90 },
|
|
||||||
{ id: 6, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 7, maxThroughputPercentage: 40 },
|
|
||||||
];
|
|
||||||
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
|
||||||
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onBucketsChange when a bucket value changes", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "70" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers onSaveableChange when values change", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates state consistently after multiple changes to different buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
const input2 = screen.getByDisplayValue("60");
|
|
||||||
fireEvent.change(input2, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to baseline when currentBuckets are reset", () => {
|
|
||||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "50" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
|
||||||
render(
|
|
||||||
<ThroughputBucketsComponent
|
|
||||||
{...defaultProps}
|
|
||||||
currentBuckets={[
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
|
||||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
|
||||||
expect(disabledInputs[0]).toBeDisabled();
|
|
||||||
|
|
||||||
const sliders = screen.getAllByRole("slider");
|
|
||||||
expect(sliders.length).toBeGreaterThan(0);
|
|
||||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const toggles = screen.getAllByRole("switch");
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ensures default buckets are used when no buckets are provided", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
|
||||||
import { ThroughputBucket } from "Contracts/DataModels";
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
|
||||||
import { isDirty } from "../../SettingsUtils";
|
|
||||||
|
|
||||||
const MAX_BUCKET_SIZES = 5;
|
|
||||||
|
|
||||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export interface ThroughputBucketsComponentProps {
|
|
||||||
currentBuckets: ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: ThroughputBucket[];
|
|
||||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
|
||||||
onSaveableChange: (isSaveable: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
|
||||||
currentBuckets,
|
|
||||||
throughputBucketsBaseline,
|
|
||||||
onBucketsChange,
|
|
||||||
onSaveableChange,
|
|
||||||
}) => {
|
|
||||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
|
||||||
if (!buckets || buckets.length === 0) {
|
|
||||||
return DEFAULT_BUCKETS;
|
|
||||||
}
|
|
||||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
|
||||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return adjustedDefaultBuckets.map(
|
|
||||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
|
||||||
onSaveableChange(false);
|
|
||||||
}, [currentBuckets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
|
||||||
onSaveableChange(isChanged);
|
|
||||||
}, [throughputBuckets]);
|
|
||||||
|
|
||||||
const handleBucketChange = (id: number, newValue: number) => {
|
|
||||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
|
||||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
|
||||||
);
|
|
||||||
setThroughputBuckets(updatedBuckets);
|
|
||||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
|
||||||
settingsChanged && onBucketsChange(updatedBuckets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (id: number, checked: boolean) => {
|
|
||||||
handleBucketChange(id, checked ? 50 : 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
|
||||||
<Label>Throughput Buckets</Label>
|
|
||||||
<Stack>
|
|
||||||
{throughputBuckets?.map((bucket) => (
|
|
||||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
|
||||||
<Slider
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={bucket.maxThroughputPercentage}
|
|
||||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
|
||||||
showValue={false}
|
|
||||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
|
||||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={bucket.maxThroughputPercentage.toString()}
|
|
||||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
|
||||||
type="number"
|
|
||||||
suffix="%"
|
|
||||||
styles={{
|
|
||||||
fieldGroup: { width: 80 },
|
|
||||||
}}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
onText="Active"
|
|
||||||
offText="Inactive"
|
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
|
||||||
></Toggle>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -235,12 +235,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)' }}>Updated cost per month</Text>
|
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||||
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
|
<Text style={{ width: "50%" }}>
|
||||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
|
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)'}}>
|
<Text style={{ width: "50%" }}>
|
||||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -253,12 +253,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
<Text style={{ fontWeight: 600, color: 'var(--colorNeutralForeground1)' }}>Current cost per month</Text>
|
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||||
<Stack horizontal style={{ marginTop: 5, color: 'var(--colorNeutralForeground1)' }}>
|
<Stack horizontal style={{ marginTop: 5 }}>
|
||||||
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
|
<Text style={{ width: "50%" }}>
|
||||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
|
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
|
<Text style={{ width: "50%" }}>
|
||||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -268,10 +268,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
|
|
||||||
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
|
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
|
||||||
};
|
};
|
||||||
settingsAndScaleStyle = {
|
|
||||||
root: { width: "33%",
|
|
||||||
color: 'var(--colorNeutralForeground1)' },
|
|
||||||
};
|
|
||||||
private getEstimatedManualSpendElement = (
|
private getEstimatedManualSpendElement = (
|
||||||
throughput: number,
|
throughput: number,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
@@ -291,15 +288,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text style={{ fontWeight: 600, color: 'var(--colorNeutralForeground1)' }}>Updated cost per month</Text>
|
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
|
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
|
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
|
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -312,15 +309,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
<Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)'}}>Current cost per month</Text>
|
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||||
<Stack horizontal style={{ marginTop: 5 }}>
|
<Stack horizontal style={{ marginTop: 5 }}>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
|
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
|
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={ this.settingsAndScaleStyle.root }>
|
<Text style={{ width: "33%" }}>
|
||||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
|
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -405,8 +402,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
|
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<Label style={{ color: 'var(--colorNeutralForeground1)'}}>Storage capacity</Label>
|
<Label>Storage capacity</Label>
|
||||||
<Text style={{ color: 'var(--colorNeutralForeground1)'}}>{capacity}</Text>
|
<Text>{capacity}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -611,7 +608,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{this.props.isAutoPilotSelected ? (
|
{this.props.isAutoPilotSelected ? (
|
||||||
<Text style={{ marginTop: "40px" , color: 'var(--colorNeutralForeground1)'}}>
|
<Text style={{ marginTop: "40px" }}>
|
||||||
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
|
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
|
||||||
<b>
|
<b>
|
||||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
||||||
@@ -633,7 +630,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && (
|
{!this.overrideWithProvisionedThroughputSettings() && (
|
||||||
<Text style={{ color: 'var(--colorNeutralForeground1)'}}>
|
<Text>
|
||||||
Estimate your required RU/s with
|
Estimate your required RU/s with
|
||||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DirectionalHint, IIconStyles, Icon, Stack, Text, TooltipHost } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { Stack, Text, IIconStyles, Icon, TooltipHost, DirectionalHint } from "@fluentui/react";
|
||||||
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
|
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
|
||||||
|
|
||||||
export interface ToolTipLabelComponentProps {
|
export interface ToolTipLabelComponentProps {
|
||||||
@@ -14,7 +14,7 @@ export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponent
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
|
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
|
||||||
{this.props.label && <Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)'}}>{this.props.label}</Text>}
|
{this.props.label && <Text style={{ fontWeight: 600 }}>{this.props.label}</Text>}
|
||||||
{this.props.toolTipElement && (
|
{this.props.toolTipElement && (
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
content={this.props.toolTipElement}
|
content={this.props.toolTipElement}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export type isDirtyTypes =
|
|||||||
| DataModels.IndexingPolicy
|
| DataModels.IndexingPolicy
|
||||||
| DataModels.ComputedProperties
|
| DataModels.ComputedProperties
|
||||||
| DataModels.VectorEmbedding[]
|
| DataModels.VectorEmbedding[]
|
||||||
| DataModels.FullTextPolicy
|
| DataModels.FullTextPolicy;
|
||||||
| DataModels.ThroughputBucket[];
|
|
||||||
export const TtlOff = "off";
|
export const TtlOff = "off";
|
||||||
export const TtlOn = "on";
|
export const TtlOn = "on";
|
||||||
export const TtlOnNoDefault = "on-nodefault";
|
export const TtlOnNoDefault = "on-nodefault";
|
||||||
@@ -56,7 +55,6 @@ export enum SettingsV2TabTypes {
|
|||||||
PartitionKeyTab,
|
PartitionKeyTab,
|
||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@@ -169,8 +167,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Computed Properties";
|
return "Computed Properties";
|
||||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||||
return "Container Policies";
|
return "Container Policies";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
|
||||||
return "Throughput Buckets";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,27 +69,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyProperties": [
|
"partitionKeyProperties": [
|
||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
|
||||||
"uniqueKeys": [
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
"/id",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
@@ -169,27 +148,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyProperties": [
|
"partitionKeyProperties": [
|
||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
|
||||||
"uniqueKeys": [
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
"/id",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
@@ -229,25 +187,17 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
indexingPolicyContent={
|
indexingPolicyContent={
|
||||||
{
|
{
|
||||||
"automatic": true,
|
"automatic": true,
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
"excludedPaths": [],
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
"includedPaths": [],
|
||||||
"indexingMode": "consistent",
|
"indexingMode": "consistent",
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
indexingPolicyContentBaseline={
|
indexingPolicyContentBaseline={
|
||||||
{
|
{
|
||||||
"automatic": true,
|
"automatic": true,
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
"excludedPaths": [],
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
"includedPaths": [],
|
||||||
"indexingMode": "consistent",
|
"indexingMode": "consistent",
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isVectorSearchEnabled={false}
|
isVectorSearchEnabled={false}
|
||||||
@@ -317,27 +267,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyProperties": [
|
"partitionKeyProperties": [
|
||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
|
||||||
"uniqueKeys": [
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
"/id",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
@@ -407,121 +336,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardComputedProperties={false}
|
shouldDiscardComputedProperties={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
|
||||||
headerText="Global Secondary Index (Preview)"
|
|
||||||
itemKey="GlobalSecondaryIndexTab"
|
|
||||||
key="GlobalSecondaryIndexTab"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GlobalSecondaryIndexComponent
|
|
||||||
collection={
|
|
||||||
{
|
|
||||||
"analyticalStorageTtl": [Function],
|
|
||||||
"changeFeedPolicy": [Function],
|
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
|
||||||
"container": Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"databaseId": "test",
|
|
||||||
"defaultTtl": [Function],
|
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
|
||||||
"getDatabase": [Function],
|
|
||||||
"id": [Function],
|
|
||||||
"indexingPolicy": [Function],
|
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
|
||||||
"partitionKey": {
|
|
||||||
"kind": "hash",
|
|
||||||
"paths": [],
|
|
||||||
"version": 2,
|
|
||||||
},
|
|
||||||
"partitionKeyProperties": [
|
|
||||||
"partitionKey",
|
|
||||||
],
|
|
||||||
"rawDataModel": {
|
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
|
||||||
"uniqueKeys": [
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
"/id",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"readSettings": [Function],
|
|
||||||
"usageSizeInKB": [Function],
|
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,20 +35,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
let defaultThroughput: number;
|
const defaultThroughput: number =
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||||
) {
|
? AutoPilotUtils.autoPilotThroughput1K
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
: AutoPilotUtils.autoPilotThroughput4K;
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
|
||||||
} else {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { makeStyles } from "@fluentui/react-components";
|
|
||||||
import React from "react";
|
|
||||||
import type { Explorer } from "../Contracts/ViewModels";
|
|
||||||
import { useTheme } from "../hooks/useTheme";
|
|
||||||
|
|
||||||
interface DataExplorerProps {
|
|
||||||
dataExplorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const DataExplorer: React.FC<DataExplorerProps> = ({ dataExplorer }) => {
|
|
||||||
const { isDarkMode } = useTheme();
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`dataExplorerContainer ${styles.root}`}>
|
|
||||||
<div>Data Explorer Content</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React, { Component, ErrorInfo, ReactNode } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasError: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ErrorBoundary extends Component<Props, State> {
|
|
||||||
public state: State = {
|
|
||||||
hasError: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static getDerivedStateFromError(error: Error): State {
|
|
||||||
return { hasError: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "20px", color: "red" }}>
|
|
||||||
<h2>Something went wrong.</h2>
|
|
||||||
<details style={{ whiteSpace: "pre-wrap" }}>{this.state.error && this.state.error.toString()}</details>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
|||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
|||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
@@ -187,10 +187,6 @@ export default class Explorer {
|
|||||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirrored()) {
|
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshExplorer();
|
this.refreshExplorer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +347,8 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,7 +1127,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
@@ -1156,27 +1152,26 @@ export default class Explorer {
|
|||||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshSampleData(): void {
|
public async refreshSampleData(): Promise<void> {
|
||||||
|
try {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const collection: DataModels.Collection = await readSampleCollection();
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||||
if (!databaseId) {
|
if (!databaseId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
readSampleCollection()
|
|
||||||
.then((collection: DataModels.Collection) => {
|
|
||||||
if (!collection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error) => {
|
Logger.logError(getErrorMessage(error), "Explorer");
|
||||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
return;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
.flex-direction(@direction: row);
|
.flex-direction(@direction: row);
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.valueCol {
|
.valueCol {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
@@ -59,10 +63,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customTrashIcon {
|
|
||||||
padding-top: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,10 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div className="labelCol">
|
<div className="labelCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Key"}
|
|
||||||
type="text"
|
type="text"
|
||||||
id="propertyKeyNewVertexPane"
|
id="propertyKeyNewVertexPane"
|
||||||
componentRef={input}
|
componentRef={input}
|
||||||
required
|
aria-required="true"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -154,11 +153,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
|
||||||
<div className="valueCol">
|
<div className="valueCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Value"}
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -170,8 +169,6 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
role="combobox"
|
role="combobox"
|
||||||
label={index === 0 && "Type"}
|
|
||||||
ariaLabel="Type"
|
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
defaultSelectedKey={data.values[0].type}
|
defaultSelectedKey={data.values[0].type}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -184,7 +181,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
span {
|
span {
|
||||||
|
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
|
||||||
margin: 0 10px 0 10px;
|
margin: 0 10px 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
border-bottom: 1px solid var(--colorNeutralStroke1);
|
border-bottom: 1px solid @BaseMedium;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { makeStyles, useFluent } from "@fluentui/react-components";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
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 } 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";
|
||||||
@@ -30,26 +30,18 @@ export interface CommandBarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
contextButtons: [] as CommandButtonComponentProps[],
|
contextButtons: [],
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
commandBarContainer: {
|
|
||||||
borderBottom: "1px solid var(--colorNeutralStroke1)"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const { targetDocument } = useFluent();
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
// const isDarkMode = targetDocument?.body.classList.contains("isDarkMode");
|
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
@@ -57,15 +49,12 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
return (
|
return (
|
||||||
<div className={styles.commandBarContainer} style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<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={CommandBarUtil.convertButton(buttons, "var(--colorNeutralBackground1)")}
|
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: { backgroundColor: backgroundColor },
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
@@ -79,18 +68,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, "var(--colorNeutralBackground1)");
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||||
if (buttons && buttons.length > 0) {
|
if (buttons && buttons.length > 0) {
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, "var(--colorNeutralBackground1)");
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
@@ -104,19 +93,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyle = isFabric()
|
const rootStyle =
|
||||||
|
configContext.platform === Platform.Fabric
|
||||||
? {
|
? {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: "transparent",
|
||||||
padding: "2px 8px 0px 8px",
|
padding: "2px 8px 0px 8px",
|
||||||
color: "var(--colorNeutralForeground1)"
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: backgroundColor,
|
||||||
color: "var(--colorNeutralForeground1)"
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
@@ -124,7 +112,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.commandBarContainer} style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<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)}
|
||||||
|
|||||||
@@ -37,25 +37,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
it("Button should not be visible for Tables API", () => {
|
||||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
updateUserContext({
|
||||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
databaseAccount: {
|
||||||
//
|
properties: {
|
||||||
// it("Button should not be visible for Tables API", () => {
|
capabilities: [{ name: "EnableTable" }],
|
||||||
// updateUserContext({
|
},
|
||||||
// databaseAccount: {
|
} as DatabaseAccount,
|
||||||
// properties: {
|
});
|
||||||
// capabilities: [{ name: "EnableTable" }],
|
|
||||||
// },
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
// } as DatabaseAccount,
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
// });
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
//
|
);
|
||||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
});
|
||||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
|
||||||
// );
|
|
||||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
|
||||||
//});
|
|
||||||
|
|
||||||
it("Button should not be visible for Cassandra API", () => {
|
it("Button should not be visible for Cassandra API", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -62,7 +61,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (userContext.apiType === "SQL") {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
const result: ICommandBarItemProps = {
|
const result: ICommandBarItemProps = {
|
||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth,
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: btn.iconName ? "baseline" : undefined,
|
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||||
filter: getFilter(btn.disabled),
|
filter: getFilter(btn.disabled),
|
||||||
},
|
},
|
||||||
@@ -79,7 +79,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
"data-test": `CommandBar/Button:${label}`,
|
"data-test": `CommandBar/Button:${label}`,
|
||||||
buttonStyles: {
|
buttonStyles: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: backgroundColor,
|
||||||
height: buttonHeightPx,
|
height: buttonHeightPx,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
@@ -87,29 +87,15 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
minWidth: 24,
|
minWidth: 24,
|
||||||
marginLeft: isSplit ? 0 : 5,
|
marginLeft: isSplit ? 0 : 5,
|
||||||
marginRight: isSplit ? 0 : 5,
|
marginRight: isSplit ? 0 : 5,
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
selectors: {
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
"&:active": {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Pressed)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
rootDisabled: {
|
rootDisabled: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: backgroundColor,
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
color: "var(--colorNeutralForegroundDisabled)"
|
|
||||||
},
|
},
|
||||||
splitButtonMenuButton: {
|
splitButtonMenuButton: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: backgroundColor,
|
||||||
selectors: {
|
selectors: {
|
||||||
":hover": {
|
":hover": { backgroundColor: hoverColor },
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)"
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
width: 16,
|
width: 16,
|
||||||
},
|
},
|
||||||
@@ -118,22 +104,13 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.DefaultFontSize
|
? StyleConstants.DefaultFontSize
|
||||||
: StyleConstants.mediumFontSize,
|
: StyleConstants.mediumFontSize,
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
rootHovered: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
rootPressed: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Pressed)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
},
|
||||||
|
rootHovered: { backgroundColor: hoverColor },
|
||||||
|
rootPressed: { backgroundColor: hoverColor },
|
||||||
splitButtonMenuButtonExpanded: {
|
splitButtonMenuButtonExpanded: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1Pressed)",
|
backgroundColor: StyleConstants.AccentExtra,
|
||||||
selectors: {
|
selectors: {
|
||||||
":hover": {
|
":hover": { backgroundColor: hoverColor },
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)"
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
splitButtonDivider: {
|
splitButtonDivider: {
|
||||||
@@ -142,7 +119,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
icon: {
|
icon: {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
},
|
||||||
splitButtonContainer: {
|
splitButtonContainer: {
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
|
|||||||
@@ -37,10 +37,6 @@
|
|||||||
background-color:@NotificationHigh;
|
background-color:@NotificationHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focusedBorder();
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBar {
|
.statusBar {
|
||||||
.dataTypeIcons {
|
.dataTypeIcons {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setElememntRef = (element: HTMLElement): void => {
|
||||||
|
this.consoleHeaderElement = element;
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const numInProgress = this.state.allConsoleData.filter(
|
const numInProgress = this.state.allConsoleData.filter(
|
||||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||||
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
role="button"
|
ref={this.setElememntRef}
|
||||||
aria-label="Console"
|
|
||||||
aria-expanded={this.props.isConsoleExpanded}
|
|
||||||
onClick={() => this.expandCollapseConsole()}
|
onClick={() => this.expandCollapseConsole()}
|
||||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -107,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div className="statusBar">
|
<div className="statusBar">
|
||||||
<span className="dataTypeIcons">
|
<span className="dataTypeIcons">
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={LoadingIcon} alt="In progress items" />
|
<img src={LoadingIcon} alt="in progress items" />
|
||||||
<span className="numInProgress">{numInProgress}</span>
|
<span className="numInProgress">{numInProgress}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={ErrorBlackIcon} alt="Error items" />
|
<img src={ErrorBlackIcon} alt="error items" />
|
||||||
<span className="numErroredItems">{numErroredItems}</span>
|
<span className="numErroredItems">{numErroredItems}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={infoBubbleIcon} alt="Info items" />
|
<img src={infoBubbleIcon} alt="info items" />
|
||||||
<span className="numInfoItems">{numInfoItems}</span>
|
<span className="numInfoItems">{numInfoItems}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -127,10 +129,17 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
<div
|
||||||
|
className="expandCollapseButton"
|
||||||
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||||
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,6 +259,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onConsoleWasExpanded = (): void => {
|
private onConsoleWasExpanded = (): void => {
|
||||||
|
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||||
|
this.consoleHeaderElement.focus();
|
||||||
|
}
|
||||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -24,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -37,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -50,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -74,11 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,13 +176,10 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -194,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -207,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -220,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -246,11 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||||
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
|||||||
private clearReconnectionAttemptMessage? = () => {};
|
private clearReconnectionAttemptMessage? = () => {};
|
||||||
private isResettingWorkspace: boolean;
|
private isResettingWorkspace: boolean;
|
||||||
private phoenixClient: PhoenixClient;
|
private phoenixClient: PhoenixClient;
|
||||||
private retryOptions: Options;
|
private retryOptions: promiseRetry.Options;
|
||||||
private scheduleTimerId: NodeJS.Timeout;
|
private scheduleTimerId: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
@@ -127,7 +128,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
? databaseAccount?.location
|
? databaseAccount?.location
|
||||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
||||||
|
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
||||||
|
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(disallowedLocationsUri, {
|
const response = await fetch(disallowedLocationsUri, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -58,9 +58,9 @@ function openCollectionTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isFabricMirrored() &&
|
configContext.platform === Platform.Fabric &&
|
||||||
!(
|
!(
|
||||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||||
(
|
(
|
||||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { CollectionCreation } from "Shared/Constants";
|
import { CollectionCreation } from "Shared/Constants";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -42,7 +41,6 @@ import {
|
|||||||
isVectorSearchEnabled,
|
isVectorSearchEnabled,
|
||||||
} from "Utils/CapabilityUtils";
|
} from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
import "../Controls/ThroughputInput/ThroughputInput.less";
|
import "../Controls/ThroughputInput/ThroughputInput.less";
|
||||||
@@ -286,7 +284,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
|
||||||
<Stack hidden={userContext.apiType === "Tables"}>
|
<Stack hidden={userContext.apiType === "Tables"}>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
@@ -352,8 +349,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
@@ -431,7 +428,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
<Separator className="panelSeparator" />
|
<Separator className="panelSeparator" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -460,8 +456,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., ${getCollectionName()}1`}
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
@@ -670,7 +666,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
@@ -751,7 +747,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
@@ -941,7 +937,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
@@ -1264,7 +1260,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1290,7 +1286,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
@@ -205,8 +204,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
size={40}
|
size={40}
|
||||||
aria-label={databaseIdLabel}
|
aria-label={databaseIdLabel}
|
||||||
placeholder={databaseIdPlaceHolder}
|
placeholder={databaseIdPlaceHolder}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
data-lpignore={true}
|
data-lpignore={true}
|
||||||
id="database-id"
|
id="database-id"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
size={40}
|
size={40}
|
||||||
styles={
|
styles={
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Action } 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 { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
@@ -203,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Type a new keyspace id"
|
placeholder="Type a new keyspace id"
|
||||||
size={40}
|
size={40}
|
||||||
value={newKeyspaceId}
|
value={newKeyspaceId}
|
||||||
@@ -293,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-table Id Create table"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Enter table Id"
|
placeholder="Enter table Id"
|
||||||
size={20}
|
size={20}
|
||||||
value={tableId}
|
value={tableId}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
|||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
@@ -236,8 +235,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., ${getCollectionName()}1`}
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
|
|||||||
@@ -94,7 +94,6 @@
|
|||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
|
|
||||||
.paneErrorLink {
|
.paneErrorLink {
|
||||||
color: @LinkColor;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
} from "Shared/StorageUtility";
|
} from "Shared/StorageUtility";
|
||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
@@ -184,7 +183,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
const showEnableEntraIdRbac =
|
const showEnableEntraIdRbac =
|
||||||
isDataplaneRbacSupported(userContext.apiType) &&
|
userContext.apiType === "SQL" &&
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
configContext.platform !== Platform.Fabric &&
|
configContext.platform !== Platform.Fabric &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
id="newDatabaseId"
|
id="newDatabaseId"
|
||||||
name="newDatabaseId"
|
name="newDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
id="collectionId"
|
id="collectionId"
|
||||||
name="collectionId"
|
name="collectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="e.g., Container1"
|
placeholder="e.g., Container1"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
|
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
@@ -393,7 +393,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
disabled={isGeneratingQuery}
|
disabled={isGeneratingQuery}
|
||||||
autoComplete="off"
|
autoComplete="list"
|
||||||
|
aria-expanded={showSamplePrompts}
|
||||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
aria-labelledby="copilot-textfield-label"
|
aria-labelledby="copilot-textfield-label"
|
||||||
onRenderSuffix={() => {
|
onRenderSuffix={() => {
|
||||||
@@ -579,7 +580,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text>
|
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||||
<Callout
|
<Callout
|
||||||
role="status"
|
role="status"
|
||||||
@@ -629,9 +630,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
id="likeBtn"
|
id="likeBtn"
|
||||||
style={{ marginLeft: 10 }}
|
style={{ marginLeft: 10 }}
|
||||||
aria-label={FeedbackLabels.provideFeedback}
|
aria-label="Like"
|
||||||
role="button"
|
role="toggle"
|
||||||
title="Like"
|
|
||||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCallout(!likeQuery);
|
setShowCallout(!likeQuery);
|
||||||
@@ -649,9 +649,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{ margin: "0 4px" }}
|
style={{ margin: "0 4px" }}
|
||||||
role="button"
|
role="toggle"
|
||||||
aria-label={FeedbackLabels.provideFeedback}
|
aria-label="Dislike"
|
||||||
title="Dislike"
|
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let toggleStatusValue = "Unpressed";
|
let toggleStatusValue = "Unpressed";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
|
BackendApi,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
HttpStatusCodes,
|
HttpStatusCodes,
|
||||||
@@ -31,6 +32,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
@@ -80,7 +82,9 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||||
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||||
|
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||||
|
: configContext.BACKEND_ENDPOINT;
|
||||||
|
|
||||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
makeStyles,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuButtonProps,
|
MenuButtonProps,
|
||||||
@@ -8,26 +7,26 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
|
SplitButton,
|
||||||
|
makeStyles,
|
||||||
|
mergeClasses,
|
||||||
shorthands,
|
shorthands,
|
||||||
SplitButton
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
import { Tabs } from "Explorer/Tabs/Tabs";
|
import { Tabs } from "Explorer/Tabs/Tabs";
|
||||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||||
import { Allotment, AllotmentHandle } from "allotment";
|
import { Allotment, AllotmentHandle } from "allotment";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTheme } from "hooks/useTheme";
|
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ResourceTree } from "./Tree/ResourceTree";
|
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -35,67 +34,38 @@ const useSidebarStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
sidebarContainer: {
|
sidebarContainer: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
|
||||||
borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
||||||
transition: "all 0.2s ease-in-out",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
position: "relative",
|
|
||||||
},
|
},
|
||||||
expandedContent: {
|
expandedContent: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
|
||||||
gridTemplateRows: `calc(${tokens.layoutRowHeight} * 2) 1fr`,
|
gridTemplateRows: `calc(${tokens.layoutRowHeight} * 2) 1fr`,
|
||||||
},
|
},
|
||||||
floatingControlsContainer: {
|
floatingControlsContainer: {
|
||||||
position: "absolute",
|
position: "relative",
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
width: "auto",
|
width: "100%",
|
||||||
padding: tokens.spacingHorizontalS,
|
|
||||||
},
|
},
|
||||||
floatingControls: {
|
floatingControls: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: tokens.spacingHorizontalXS,
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
},
|
},
|
||||||
floatingControlButton: {
|
floatingControlButton: {
|
||||||
...shorthands.border("none"),
|
...shorthands.border("none"),
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
color: tokens.colorNeutralForeground1,
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: tokens.spacingHorizontalXS,
|
|
||||||
borderRadius: tokens.borderRadiusMedium,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
|
||||||
color: tokens.colorNeutralForeground1,
|
|
||||||
},
|
|
||||||
":active": {
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1Pressed,
|
|
||||||
color: tokens.colorNeutralForeground1,
|
|
||||||
},
|
|
||||||
":disabled": {
|
|
||||||
color: tokens.colorNeutralForegroundDisabled,
|
|
||||||
cursor: "not-allowed",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
globalCommandsContainer: {
|
globalCommandsContainer: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
containerType: "size",
|
containerType: "size", // Use this container for "@container" queries below this.
|
||||||
padding: tokens.spacingHorizontalS,
|
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
|
||||||
},
|
},
|
||||||
loadingProgressBar: {
|
loadingProgressBar: {
|
||||||
|
// Float above the content
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "2px",
|
height: "2px",
|
||||||
@@ -105,7 +75,7 @@ const useSidebarStyles = makeStyles({
|
|||||||
animationDuration: "3s",
|
animationDuration: "3s",
|
||||||
animationName: {
|
animationName: {
|
||||||
"0%": {
|
"0%": {
|
||||||
opacity: ".2",
|
opacity: ".2", // matches indeterminate bar width
|
||||||
},
|
},
|
||||||
"50%": {
|
"50%": {
|
||||||
opacity: "1",
|
opacity: "1",
|
||||||
@@ -127,12 +97,6 @@ const useSidebarStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
treeContainer: {
|
|
||||||
flex: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
|
||||||
color: tokens.colorNeutralForeground1,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GlobalCommandsProps {
|
interface GlobalCommandsProps {
|
||||||
@@ -159,7 +123,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
(isFabric() && userContext.fabricContext?.isReadOnly) ||
|
configContext.platform === Platform.Fabric ||
|
||||||
userContext.apiType === "Postgres" ||
|
userContext.apiType === "Postgres" ||
|
||||||
userContext.apiType === "VCoreMongo"
|
userContext.apiType === "VCoreMongo"
|
||||||
) {
|
) {
|
||||||
@@ -173,15 +137,12 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
id: "new_collection",
|
id: "new_collection",
|
||||||
label: `New ${getCollectionName()}`,
|
label: `New ${getCollectionName()}`,
|
||||||
icon: <Add16Regular />,
|
icon: <Add16Regular />,
|
||||||
onClick: () => {
|
onClick: () => explorer.onNewCollectionClicked(),
|
||||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
|
||||||
explorer.onNewCollectionClicked({ databaseId });
|
|
||||||
},
|
|
||||||
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
|
if (userContext.apiType !== "Tables") {
|
||||||
actions.push({
|
actions.push({
|
||||||
id: "new_database",
|
id: "new_database",
|
||||||
label: `New ${getDatabaseName()}`,
|
label: `New ${getDatabaseName()}`,
|
||||||
@@ -285,7 +246,6 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
const [expandedSize, setExpandedSize] = React.useState(300);
|
const [expandedSize, setExpandedSize] = React.useState(300);
|
||||||
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
|
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
|
||||||
const allotment = useRef<AllotmentHandle>(null);
|
const allotment = useRef<AllotmentHandle>(null);
|
||||||
const { isDarkMode } = useTheme();
|
|
||||||
|
|
||||||
const expand = useCallback(() => {
|
const expand = useCallback(() => {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
@@ -328,7 +288,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
}, [setLoading]);
|
}, [setLoading]);
|
||||||
|
|
||||||
const hasGlobalCommands = !(
|
const hasGlobalCommands = !(
|
||||||
isFabricMirrored() ||
|
configContext.platform === Platform.Fabric ||
|
||||||
userContext.apiType === "Postgres" ||
|
userContext.apiType === "Postgres" ||
|
||||||
userContext.apiType === "VCoreMongo"
|
userContext.apiType === "VCoreMongo"
|
||||||
);
|
);
|
||||||
@@ -340,7 +300,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
{hasSidebar && (
|
{hasSidebar && (
|
||||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||||
<CosmosFluentProvider>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
<div className={styles.sidebarContainer}>
|
<div className={styles.sidebarContainer}>
|
||||||
{loading && (
|
{loading && (
|
||||||
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
||||||
@@ -371,12 +331,13 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.expandedContent} style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}>
|
<div
|
||||||
|
className={styles.expandedContent}
|
||||||
|
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
|
||||||
|
>
|
||||||
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
|
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
|
||||||
<div className={styles.treeContainer}>
|
|
||||||
<ResourceTree explorer={explorer} />
|
<ResourceTree explorer={explorer} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
/**
|
|
||||||
* Accordion top class
|
|
||||||
*/
|
|
||||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import * as React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
|
|
||||||
export interface SplashScreenProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
homeContainer: {
|
|
||||||
width: "100%",
|
|
||||||
alignContent: "center",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: "center",
|
|
||||||
fontSize: "20px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
buttonsContainer: {
|
|
||||||
width: "584px",
|
|
||||||
margin: "auto",
|
|
||||||
display: "grid",
|
|
||||||
padding: "16px",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: "10px",
|
|
||||||
gridAutoRows: "minmax(184px, auto)",
|
|
||||||
},
|
|
||||||
one: {
|
|
||||||
gridColumn: "1 / 3",
|
|
||||||
gridRow: "1 / 3",
|
|
||||||
"& svg": {
|
|
||||||
width: "48px",
|
|
||||||
height: "48px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
two: {
|
|
||||||
gridColumn: "3",
|
|
||||||
gridRow: "1",
|
|
||||||
"& img": {
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
three: {
|
|
||||||
gridColumn: "3",
|
|
||||||
gridRow: "2",
|
|
||||||
"& svg": {
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
cursor: "pointer",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
|
||||||
"border-color": tokens.colorNeutralStroke1Hover,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttonUpperPart: {
|
|
||||||
textAlign: "center",
|
|
||||||
flexGrow: 1,
|
|
||||||
display: "flex",
|
|
||||||
backgroundColor: "#e3f7ef",
|
|
||||||
},
|
|
||||||
buttonLowerPart: {
|
|
||||||
borderTop: "1px solid #e0e0e0",
|
|
||||||
height: "76px",
|
|
||||||
padding: "8px",
|
|
||||||
"> div:nth-child(1)": {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FabricHomeScreenButtonProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: JSX.Element;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className: string }> = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
// TODO Make this a11y copmliant: aria-label for icon
|
|
||||||
return (
|
|
||||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
|
||||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
|
||||||
<div className={styles.buttonLowerPart}>
|
|
||||||
<div>{title}</div>
|
|
||||||
<div>{description}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const getSplashScreenButtons = (): JSX.Element => {
|
|
||||||
const buttons: FabricHomeScreenButtonProps[] = [
|
|
||||||
{
|
|
||||||
title: "New container",
|
|
||||||
description: "Create a destination container to store your data",
|
|
||||||
icon: <DocumentAddRegular />,
|
|
||||||
onClick: () => {
|
|
||||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
|
||||||
props.explorer.onNewCollectionClicked({ databaseId });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Sample data",
|
|
||||||
description: "Automatically load sample data in your database",
|
|
||||||
icon: <img src={CosmosDbBlackIcon} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "App development",
|
|
||||||
description: "Start here to use an SDK to build your apps",
|
|
||||||
icon: <LinkMultipleRegular />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.buttonsContainer}>
|
|
||||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
|
||||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
|
||||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = "Build your database";
|
|
||||||
return (
|
|
||||||
<div className={styles.homeContainer}>
|
|
||||||
<div className={styles.title} role="heading" aria-label={title}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
{getSplashScreenButtons()}
|
|
||||||
<div className={styles.footer}>
|
|
||||||
Need help?{" "}
|
|
||||||
<Link href="https://cosmos.azure.com/docs" target="_blank">
|
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,178 +1,178 @@
|
|||||||
// @import "../../../less/Common/Constants";
|
@import "../../../less/Common/Constants";
|
||||||
|
|
||||||
// .splashScreenContainer {
|
.splashScreenContainer {
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// overflow-y: scroll;
|
overflow-y: auto;
|
||||||
// overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
// .splashScreen {
|
.splashScreen {
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// .flex-direction();
|
.flex-direction();
|
||||||
// text-align: left;
|
text-align: left;
|
||||||
// margin: auto;
|
margin: auto;
|
||||||
// padding-left: 21px;
|
padding-left: 21px;
|
||||||
// padding-right: 16px;
|
padding-right: 16px;
|
||||||
// max-width: 1168px;
|
max-width: 1168px;
|
||||||
|
|
||||||
// > .title {
|
> .title {
|
||||||
// position: relative; // To attach FeaturePanelLauncher as absolute
|
position: relative; // To attach FeaturePanelLauncher as absolute
|
||||||
// color: @BaseHigh;
|
color: @BaseHigh;
|
||||||
// font-size: 48px;
|
font-size: 48px;
|
||||||
// padding-left: 0px;
|
padding-left: 0px;
|
||||||
// margin: 16px auto;
|
margin: 16px auto;
|
||||||
// text-align: center;
|
text-align: center;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// > .subtitle {
|
> .subtitle {
|
||||||
// color: @BaseHigh;
|
color: @BaseHigh;
|
||||||
// font-size: 18px;
|
font-size: 18px;
|
||||||
// padding-left: 0px;
|
padding-left: 0px;
|
||||||
// margin: 0px auto;
|
margin: 0px auto;
|
||||||
// text-align: center;
|
text-align: center;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .mainButtonsContainer {
|
.mainButtonsContainer {
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// text-align: center;
|
text-align: center;
|
||||||
// cursor: pointer;
|
cursor: pointer;
|
||||||
// margin: 40px auto;
|
margin: 40px auto;
|
||||||
// width: 84%;
|
width: 84%;
|
||||||
|
|
||||||
// > .mainButton {
|
> .mainButton {
|
||||||
// min-width: 124px;
|
min-width: 124px;
|
||||||
// max-width: 296px;
|
max-width: 296px;
|
||||||
// padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
// background-color: @BaseLight;
|
background-color: @BaseLight;
|
||||||
// border: 1px solid #949494;
|
border: 1px solid #949494;
|
||||||
// box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
// box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
// border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
// > .legendContainer {
|
> .legendContainer {
|
||||||
// margin-left: 16px;
|
margin-left: 16px;
|
||||||
// text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
// .legend {
|
.legend {
|
||||||
// font-family: @SemiboldFont;
|
font-family: @SemiboldFont;
|
||||||
// font-size: 18px;
|
font-size: 18px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .description {
|
.description {
|
||||||
// font-size: 10px;
|
font-size: 10px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .newDescription {
|
.newDescription {
|
||||||
// font-size: 13px;
|
font-size: 13px;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// > :nth-child(n + 2) {
|
> :nth-child(n + 2) {
|
||||||
// margin-left: 32px;
|
margin-left: 32px;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .moreStuffContainer {
|
.moreStuffContainer {
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
// .moreStuffColumn {
|
.moreStuffColumn {
|
||||||
// flex-grow: 1;
|
flex-grow: 1;
|
||||||
// flex-basis: 0;
|
flex-basis: 0;
|
||||||
// min-width: 124px;
|
min-width: 124px;
|
||||||
// max-width: 296px;
|
max-width: 296px;
|
||||||
|
|
||||||
// > .title {
|
> .title {
|
||||||
// font-size: 18px;
|
font-size: 18px;
|
||||||
// font-family: @SemiboldFont;
|
font-family: @SemiboldFont;
|
||||||
// color: @BaseDark;
|
color: @BaseDark;
|
||||||
// padding: 0px;
|
padding: 0px;
|
||||||
// margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// > ul {
|
> ul {
|
||||||
// list-style: none;
|
list-style: none;
|
||||||
// padding-left: 0px;
|
padding-left: 0px;
|
||||||
// margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
||||||
// li {
|
li {
|
||||||
// padding: @DefaultSpace;
|
padding: @DefaultSpace;
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
// > img {
|
> img {
|
||||||
// margin-right: @DefaultSpace;
|
margin-right: @DefaultSpace;
|
||||||
// width: 24px;
|
width: 24px;
|
||||||
// height: 24px;
|
height: 24px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .oneLineContent {
|
.oneLineContent {
|
||||||
// margin-top: 4px;
|
margin-top: 4px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .description {
|
.description {
|
||||||
// font-size: 10px;
|
font-size: 10px;
|
||||||
// color: @BaseMediumHigh;
|
color: @BaseMediumHigh;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .tipContainer {
|
.tipContainer {
|
||||||
// padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// cursor: pointer;
|
cursor: pointer;
|
||||||
// .flex-display();
|
.flex-display();
|
||||||
// .flex-direction();
|
.flex-direction();
|
||||||
|
|
||||||
// > .title {
|
> .title {
|
||||||
// color: @BaseDark;
|
color: @BaseDark;
|
||||||
// padding: 0px;
|
padding: 0px;
|
||||||
// font-size: 12px;
|
font-size: 12px;
|
||||||
// }
|
}
|
||||||
// > .description {
|
> .description {
|
||||||
// color: @BaseDark;
|
color: @BaseDark;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:not(:hover):not(:focus) {
|
&:not(:hover):not(:focus) {
|
||||||
// background-color: @BaseLow;
|
background-color: @BaseLow;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &.commonTasks {
|
&.commonTasks {
|
||||||
// li {
|
li {
|
||||||
// cursor: pointer;
|
cursor: pointer;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &.tipsContainer {
|
&.tipsContainer {
|
||||||
// li {
|
li {
|
||||||
// margin: 2px 0px;
|
margin: 2px 0px;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .focusable {
|
.focusable {
|
||||||
// &:hover {
|
&:hover {
|
||||||
// .hover();
|
.hover();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:focus {
|
&:focus {
|
||||||
// .focus();
|
.focus();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:active {
|
&:active {
|
||||||
// .active();
|
.active();
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .notebookSplashScreenItem {
|
.notebookSplashScreenItem {
|
||||||
// padding: 12px 0 12px 12px;
|
padding: 12px 0 12px 12px;
|
||||||
|
|
||||||
// .itemText {
|
.itemText {
|
||||||
// margin-left: 12px;
|
margin-left: 12px;
|
||||||
// font-family: @SemiboldFont;
|
font-family: @SemiboldFont;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
TeachingBubbleContent,
|
TeachingBubbleContent,
|
||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { makeStyles, shorthands } from "@fluentui/react-components";
|
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { TerminalKind } from "Contracts/ViewModels";
|
import { TerminalKind } from "Contracts/ViewModels";
|
||||||
@@ -34,7 +33,8 @@ import CollectionIcon from "../../../images/tree-collection.svg";
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
import { useTheme } from "../../hooks/useTheme";
|
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
|
||||||
|
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
@@ -55,177 +55,70 @@ export interface SplashScreenProps {
|
|||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
splashScreenContainer: {
|
private readonly container: Explorer;
|
||||||
display: "flex",
|
private subscriptions: Array<{ dispose: () => void }>;
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
// justifyContent: "center",
|
|
||||||
minHeight: "100vh",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
|
|
||||||
},
|
constructor(props: SplashScreenProps) {
|
||||||
splashScreen: {
|
super(props);
|
||||||
display: "flex",
|
this.container = props.explorer;
|
||||||
// overflow: "scroll",
|
this.subscriptions = [];
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
textAlign: "left",
|
|
||||||
// ...shorthands.padding("40px")
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: "48px",
|
|
||||||
fontWeight: "500",
|
|
||||||
margin: "16px auto",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: "18px",
|
|
||||||
marginBottom: "40px",
|
|
||||||
color: "var(--colorNeutralForeground2)"
|
|
||||||
},
|
|
||||||
cardContainer: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
|
||||||
gap: "16px",
|
|
||||||
width: "66%",
|
|
||||||
margin: "0 auto",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "left",
|
|
||||||
...shorthands.padding("32px", "16px"),
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
boxShadow: "var(--shadow4)",
|
|
||||||
cursor: "pointer",
|
|
||||||
minHeight: "150px",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)"
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
cardContent: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
marginLeft: "16px",
|
|
||||||
textAlign: "left",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
cardTitle: {
|
|
||||||
fontSize: "18px",
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
textAlign: "left",
|
|
||||||
marginBottom: "8px"
|
|
||||||
},
|
|
||||||
cardDescription: {
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "var(--colorNeutralForeground2)",
|
|
||||||
textAlign: "left"
|
|
||||||
},
|
|
||||||
moreStuffContainer: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: "32px",
|
|
||||||
width: "66%",
|
|
||||||
margin: "40px auto",
|
|
||||||
},
|
|
||||||
moreStuffColumn: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
// justifyContent:"space-between"
|
|
||||||
},
|
|
||||||
columnTitle: {
|
|
||||||
fontSize: "20px",
|
|
||||||
fontWeight: "600",
|
|
||||||
marginBottom: "16px",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
marginBottom: "26px",
|
|
||||||
},
|
|
||||||
listItemTitle: {
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "var(--colorBrandForegroundLink)",
|
|
||||||
"&:hover": {
|
|
||||||
color: "var(--colorBrandForegroundLink)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
listItemSubtitle: {
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "var(--colorNeutralForeground2)"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
public componentWillUnmount(): void {
|
||||||
const styles = useStyles();
|
while (this.subscriptions.length) {
|
||||||
const { isDarkMode } = useTheme();
|
this.subscriptions.pop().dispose();
|
||||||
const container = explorer;
|
}
|
||||||
const subscriptions: Array<{ dispose: () => void }> = [];
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
public componentDidMount(): void {
|
||||||
subscriptions.push(
|
this.subscriptions.push(
|
||||||
{
|
{
|
||||||
dispose: useNotebook.subscribe(
|
dispose: useNotebook.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.isNotebookEnabled,
|
(state) => state.isNotebookEnabled,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ dispose: useSelectedNode.subscribe(() => setState({})) },
|
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
||||||
{
|
{
|
||||||
dispose: useCarousel.subscribe(
|
dispose: useCarousel.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.showCoachMark,
|
(state) => state.showCoachMark,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: usePostgres.subscribe(
|
dispose: usePostgres.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.showPostgreTeachingBubble,
|
(state) => state.showPostgreTeachingBubble,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: usePostgres.subscribe(
|
dispose: usePostgres.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.showResetPasswordBubble,
|
(state) => state.showResetPasswordBubble,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: useDatabases.subscribe(
|
dispose: useDatabases.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.sampleDataResourceTokenCollection,
|
(state) => state.sampleDataResourceTokenCollection,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: useQueryCopilot.subscribe(
|
dispose: useQueryCopilot.subscribe(
|
||||||
() => setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.copilotEnabled,
|
(state) => state.copilotEnabled,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
|
||||||
while (subscriptions.length) {
|
|
||||||
subscriptions.pop().dispose();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [state, setState] = React.useState({});
|
private clearMostRecent = (): void => {
|
||||||
|
|
||||||
const clearMostRecent = () => {
|
|
||||||
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
||||||
setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (
|
if (
|
||||||
userContext.apiType === "SQL" &&
|
userContext.apiType === "SQL" &&
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
@@ -239,7 +132,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
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={() => {
|
||||||
container.onNewCollectionClicked({ isQuickstart: true });
|
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -248,7 +141,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
title={`New ${getCollectionName()}`}
|
title={`New ${getCollectionName()}`}
|
||||||
description={"Create a new container for storage and throughput"}
|
description={"Create a new container for storage and throughput"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
container.onNewCollectionClicked();
|
this.container.onNewCollectionClicked();
|
||||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -284,7 +177,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainItems = createMainItems();
|
const mainItems = this.createMainItems();
|
||||||
return (
|
return (
|
||||||
<div className="mainButtonsContainer">
|
<div className="mainButtonsContainer">
|
||||||
{userContext.apiType === "Postgres" &&
|
{userContext.apiType === "Postgres" &&
|
||||||
@@ -321,7 +214,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
className="mainButton focusable"
|
className="mainButton focusable"
|
||||||
key={`${item.title}`}
|
key={`${item.title}`}
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
onKeyPress={(event: React.KeyboardEvent) => onSplashScreenItemKeyPress(event, item.onClick)}
|
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
@@ -374,7 +267,125 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMainItems = (): SplashScreenItem[] => {
|
public render(): JSX.Element {
|
||||||
|
let title: string;
|
||||||
|
let subtitle: string;
|
||||||
|
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "Postgres":
|
||||||
|
title = "Welcome to Azure Cosmos DB for PostgreSQL";
|
||||||
|
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||||
|
break;
|
||||||
|
case "VCoreMongo":
|
||||||
|
title = "Welcome to Azure Cosmos DB for MongoDB (vCore)";
|
||||||
|
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = "Welcome to Azure Cosmos DB";
|
||||||
|
subtitle = "Globally distributed, multi-model database service for any scale";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="connectExplorerContainer">
|
||||||
|
<form className="connectExplorerFormContainer">
|
||||||
|
<div className="splashScreenContainer">
|
||||||
|
<div className="splashScreen">
|
||||||
|
<h1 className="title" role="heading" aria-label={title}>
|
||||||
|
{title}
|
||||||
|
<FeaturePanelLauncher />
|
||||||
|
</h1>
|
||||||
|
<div className="subtitle">{subtitle}</div>
|
||||||
|
{this.getSplashScreenButtons()}
|
||||||
|
{useCarousel.getState().showCoachMark && (
|
||||||
|
<Coachmark
|
||||||
|
target="#quickstartDescription"
|
||||||
|
positioningContainerProps={{ directionalHint: DirectionalHint.rightTopEdge }}
|
||||||
|
persistentBeak
|
||||||
|
>
|
||||||
|
<TeachingBubbleContent
|
||||||
|
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
|
||||||
|
hasCloseButton
|
||||||
|
closeButtonAriaLabel="Close"
|
||||||
|
primaryButtonProps={{
|
||||||
|
text: "Get started",
|
||||||
|
onClick: () => {
|
||||||
|
useCarousel.getState().setShowCoachMark(false);
|
||||||
|
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
secondaryButtonProps={{
|
||||||
|
text: "Cancel",
|
||||||
|
onClick: () => useCarousel.getState().setShowCoachMark(false),
|
||||||
|
}}
|
||||||
|
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
|
||||||
|
>
|
||||||
|
You will be guided to create a sample container with sample data, then we will give you a tour of
|
||||||
|
data explorer. You can also cancel launching this tour and explore yourself
|
||||||
|
</TeachingBubbleContent>
|
||||||
|
</Coachmark>
|
||||||
|
)}
|
||||||
|
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
|
||||||
|
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
||||||
|
<Stack style={{ width: "33%" }}>
|
||||||
|
<Text
|
||||||
|
variant="large"
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next steps
|
||||||
|
</Text>
|
||||||
|
{this.getNextStepItems()}
|
||||||
|
</Stack>
|
||||||
|
<Stack style={{ width: "33%" }}>
|
||||||
|
<Text
|
||||||
|
variant="large"
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tips & learn more
|
||||||
|
</Text>
|
||||||
|
{this.getTipsAndLearnMoreItems()}
|
||||||
|
</Stack>
|
||||||
|
<Stack style={{ width: "33%" }}></Stack>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<div className="moreStuffContainer">
|
||||||
|
<div className="moreStuffColumn commonTasks">
|
||||||
|
<h2 className="title">Recents</h2>
|
||||||
|
{this.getRecentItems()}
|
||||||
|
</div>
|
||||||
|
<div className="moreStuffColumn">
|
||||||
|
<h2 className="title">Top 3 things you need to know</h2>
|
||||||
|
{this.top3Items()}
|
||||||
|
</div>
|
||||||
|
<div className="moreStuffColumn tipsContainer">
|
||||||
|
<h2 className="title">Learning Resources</h2>
|
||||||
|
{this.getLearningResourceItems()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exists to enable unit testing
|
||||||
|
*/
|
||||||
|
public createDataSampleUtil(): DataSamplesUtil {
|
||||||
|
return new DataSamplesUtil(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* public for testing purposes
|
||||||
|
*/
|
||||||
|
public createMainItems(): SplashScreenItem[] {
|
||||||
const heroes: SplashScreenItem[] = [];
|
const heroes: SplashScreenItem[] = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -392,7 +403,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||||
} else {
|
} else {
|
||||||
container.onNewCollectionClicked({ isQuickstart: true });
|
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||||
}
|
}
|
||||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||||
},
|
},
|
||||||
@@ -400,18 +411,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
heroes.push(launchQuickstartBtn);
|
heroes.push(launchQuickstartBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
heroes.push(getShellCard());
|
heroes.push(this.getShellCard());
|
||||||
heroes.push(getThirdCard());
|
heroes.push(this.getThirdCard());
|
||||||
return heroes;
|
return heroes;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getShellCard = (): SplashScreenItem => {
|
private getShellCard() {
|
||||||
if (userContext.apiType === "Postgres") {
|
if (userContext.apiType === "Postgres") {
|
||||||
return {
|
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: () => container.openNotebookTerminal(TerminalKind.Postgres),
|
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +431,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
iconSrc: PowerShellIcon,
|
iconSrc: PowerShellIcon,
|
||||||
title: "Mongo Shell",
|
title: "Mongo Shell",
|
||||||
description: "Create a collection and interact with data using MongoDB's shell interface",
|
description: "Create a collection and interact with data using MongoDB's shell interface",
|
||||||
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
|
onClick: () => this.container.openNotebookTerminal(TerminalKind.VCoreMongo),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,13 +440,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
title: `New ${getCollectionName()}`,
|
title: `New ${getCollectionName()}`,
|
||||||
description: "Create a new container for storage and throughput",
|
description: "Create a new container for storage and throughput",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
container.onNewCollectionClicked();
|
this.container.onNewCollectionClicked();
|
||||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const getThirdCard = (): SplashScreenItem => {
|
private getThirdCard() {
|
||||||
let icon = ConnectIcon;
|
let icon = ConnectIcon;
|
||||||
let title = "Connect";
|
let title = "Connect";
|
||||||
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
|
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
|
||||||
@@ -459,34 +470,34 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
description: description,
|
description: description,
|
||||||
onClick: onClick,
|
onClick: onClick,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const decorateOpenCollectionActivity = (activity: MostRecentActivity.OpenCollectionItem): SplashScreenItem => {
|
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||||
return {
|
return {
|
||||||
iconSrc: CollectionIcon,
|
iconSrc: CollectionIcon,
|
||||||
title: activity.collectionId,
|
title: collectionId,
|
||||||
description: getCollectionName(),
|
description: getCollectionName(),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const collection = useDatabases.getState().findCollection(activity.databaseId, activity.collectionId);
|
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
|
||||||
collection?.openTab();
|
collection?.openTab();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const decorateOpenNotebookActivity = (activity: MostRecentActivity.OpenNotebookItem): SplashScreenItem => {
|
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
|
||||||
return {
|
return {
|
||||||
info: activity.path,
|
info: path,
|
||||||
iconSrc: NotebookIcon,
|
iconSrc: NotebookIcon,
|
||||||
title: activity.name,
|
title: name,
|
||||||
description: "Notebook",
|
description: "Notebook",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
|
const notebookItem = this.container.createNotebookContentItemFile(name, path);
|
||||||
notebookItem && container.openNotebook(notebookItem);
|
notebookItem && this.container.openNotebook(notebookItem);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const createRecentItems = (): SplashScreenItem[] => {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
@@ -494,22 +505,22 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
throw new Error(`Unknown activity: ${unknownActivity}`);
|
throw new Error(`Unknown activity: ${unknownActivity}`);
|
||||||
}
|
}
|
||||||
case MostRecentActivity.Type.OpenNotebook:
|
case MostRecentActivity.Type.OpenNotebook:
|
||||||
return decorateOpenNotebookActivity(activity);
|
return this.decorateOpenNotebookActivity(activity);
|
||||||
|
|
||||||
case MostRecentActivity.Type.OpenCollection:
|
case MostRecentActivity.Type.OpenCollection:
|
||||||
return decorateOpenCollectionActivity(activity);
|
return this.decorateOpenCollectionActivity(activity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const onSplashScreenItemKeyPress = (event: React.KeyboardEvent, callback: () => void) => {
|
private onSplashScreenItemKeyPress(event: React.KeyboardEvent, callback: () => void) {
|
||||||
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
||||||
callback();
|
callback();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const top3Items = (): JSX.Element => {
|
private top3Items(): JSX.Element {
|
||||||
let items: { link: string; title: string; description: string }[];
|
let items: { link: string; title: string; description: string }[];
|
||||||
switch (userContext.apiType) {
|
switch (userContext.apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
@@ -621,54 +632,44 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
href={item.link}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{ marginRight: 5 }}
|
style={{ marginRight: 5 }}
|
||||||
className={styles.listItemTitle}
|
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} alt={item.title} />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text className={styles.listItemSubtitle}>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const getRecentItems = (): JSX.Element => {
|
private getRecentItems(): JSX.Element {
|
||||||
const recentItems = createRecentItems()?.filter((item) => item.description !== "Notebook");
|
const recentItems = this.createRecentItems()?.filter((item) => item.description !== "Notebook");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<ul>
|
<ul>
|
||||||
{recentItems.map((item, index) => (
|
{recentItems.map((item, index) => (
|
||||||
<li key={`${item.title}${item.description}${index}`} className={styles.listItem}>
|
<li key={`${item.title}${item.description}${index}`}>
|
||||||
<Stack style={{ marginBottom: 26 }}>
|
<Stack style={{ marginBottom: 26 }}>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<svg
|
<Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
|
||||||
width="16"
|
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
||||||
height="16"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M4 4c0-1.1.9-2 2-2h3.59c.4 0 .78.16 1.06.44l3.91 3.91c.28.28.44.67.44 1.06V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 9 6.5V3H6Zm4 .2v3.3c0 .28.22.5.5.5h3.3L10 3.2ZM17 9a1 1 0 0 0-1-1v6a3 3 0 0 1-3 3H6a1 1 0 0 0 1 1h6.06A3.94 3.94 0 0 0 17 14.06V9Z" />
|
|
||||||
</svg>
|
|
||||||
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info} className={styles.listItemTitle}>
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text className={styles.listItemSubtitle}>{item.description}</Text>
|
<Text style={{ color: "#605E5C" }}>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{recentItems.length > 0 && <Link onClick={() => clearMostRecent()} className={styles.listItemTitle}>Clear Recents</Link>}
|
{recentItems.length > 0 && <Link onClick={() => this.clearMostRecent()}>Clear Recents</Link>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const getLearningResourceItems = (): JSX.Element => {
|
private getLearningResourceItems(): JSX.Element {
|
||||||
interface item {
|
interface item {
|
||||||
link: string;
|
link: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -784,20 +785,19 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
href={item.link}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{ marginRight: 5 }}
|
style={{ marginRight: 5 }}
|
||||||
className={styles.listItemTitle}
|
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} alt={item.title} />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text className={styles.listItemSubtitle}>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const postgresNextStepItems: { link: string; title: string; description: string }[] = [
|
private postgresNextStepItems: { 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",
|
||||||
@@ -815,7 +815,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
|
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",
|
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import",
|
||||||
title: "Migrate Data",
|
title: "Migrate Data",
|
||||||
@@ -833,27 +833,27 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getNextStepItems = (): JSX.Element => {
|
private getNextStepItems(): JSX.Element {
|
||||||
const items = userContext.apiType === "Postgres" ? postgresNextStepItems : vcoreMongoNextStepItems;
|
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) => (
|
||||||
<Stack key={`nextStep${i}`} style={{ marginBottom: 26 }}>
|
<Stack key={`nextStep${i}`} style={{ marginBottom: 26 }}>
|
||||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||||
<Link href={item.link} target="_blank" style={{ marginRight: 5 }} className={styles.listItemTitle}>
|
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} />
|
<Image src={LinkIcon} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text className={styles.listItemSubtitle}>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
|
private postgresLearnMoreItems: { 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",
|
||||||
@@ -871,7 +871,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
|
private vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||||
{
|
{
|
||||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
|
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
|
||||||
title: "Vector Search",
|
title: "Vector Search",
|
||||||
@@ -889,109 +889,23 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getTipsAndLearnMoreItems = (): JSX.Element => {
|
private getTipsAndLearnMoreItems(): JSX.Element {
|
||||||
const items = userContext.apiType === "Postgres" ? postgresLearnMoreItems : vcoreMongoLearnMoreItems;
|
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) => (
|
||||||
<Stack key={`tips${i}`} style={{ marginBottom: 26 }}>
|
<Stack key={`tips${i}`} style={{ marginBottom: 26 }}>
|
||||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||||
<Link href={item.link} target="_blank" style={{ marginRight: 5 }} className={styles.listItemTitle}>
|
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} />
|
<Image src={LinkIcon} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text className={styles.listItemSubtitle}>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
return (
|
|
||||||
<div className={styles.splashScreenContainer}>
|
|
||||||
<div className={styles.splashScreen}>
|
|
||||||
<h1 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
|
||||||
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
|
||||||
</h1>
|
|
||||||
<div className={styles.subtitle}>
|
|
||||||
Globally distributed, multi-model database service for any scale
|
|
||||||
</div>
|
|
||||||
{getSplashScreenButtons()}
|
|
||||||
{useCarousel.getState().showCoachMark && (
|
|
||||||
<Coachmark
|
|
||||||
target="#quickstartDescription"
|
|
||||||
positioningContainerProps={{ directionalHint: DirectionalHint.rightTopEdge }}
|
|
||||||
persistentBeak
|
|
||||||
>
|
|
||||||
<TeachingBubbleContent
|
|
||||||
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
|
|
||||||
hasCloseButton
|
|
||||||
closeButtonAriaLabel="Close"
|
|
||||||
primaryButtonProps={{
|
|
||||||
text: "Get started",
|
|
||||||
onClick: () => {
|
|
||||||
useCarousel.getState().setShowCoachMark(false);
|
|
||||||
container.onNewCollectionClicked({ isQuickstart: true });
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
secondaryButtonProps={{
|
|
||||||
text: "Cancel",
|
|
||||||
onClick: () => useCarousel.getState().setShowCoachMark(false),
|
|
||||||
}}
|
|
||||||
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
|
|
||||||
>
|
|
||||||
You will be guided to create a sample container with sample data, then we will give you a tour of
|
|
||||||
data explorer. You can also cancel launching this tour and explore yourself
|
|
||||||
</TeachingBubbleContent>
|
|
||||||
</Coachmark>
|
|
||||||
)}
|
|
||||||
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
|
|
||||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
|
||||||
<Stack style={{ width: "33%" }}>
|
|
||||||
<Text
|
|
||||||
variant="large"
|
|
||||||
style={{
|
|
||||||
marginBottom: 16,
|
|
||||||
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Next steps
|
|
||||||
</Text>
|
|
||||||
{getNextStepItems()}
|
|
||||||
</Stack>
|
|
||||||
<Stack style={{ width: "33%" }}>
|
|
||||||
<Text
|
|
||||||
variant="large"
|
|
||||||
style={{
|
|
||||||
marginBottom: 16,
|
|
||||||
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tips & learn more
|
|
||||||
</Text>
|
|
||||||
{getTipsAndLearnMoreItems()}
|
|
||||||
</Stack>
|
|
||||||
<Stack style={{ width: "33%" }}></Stack>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<div className={styles.moreStuffContainer}>
|
|
||||||
<div className={styles.moreStuffColumn}>
|
|
||||||
<h2 className={styles.columnTitle}>Recents</h2>
|
|
||||||
{getRecentItems()}
|
|
||||||
</div>
|
|
||||||
<div className={styles.moreStuffColumn}>
|
|
||||||
<h2 className={styles.columnTitle}>Top 3 things you need to know</h2>
|
|
||||||
{top3Items()}
|
|
||||||
</div>
|
|
||||||
<div className={styles.moreStuffColumn}>
|
|
||||||
<h2 className={styles.columnTitle}>Learning Resources</h2>
|
|
||||||
{getLearningResourceItems()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
import { Stack, Text } from "@fluentui/react";
|
||||||
import { makeStyles } from "@fluentui/react-components";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { KeyCodes } from "../../Common/Constants";
|
import { KeyCodes } from "../../Common/Constants";
|
||||||
|
|
||||||
@@ -10,50 +9,25 @@ interface SplashScreenButtonProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
button: {
|
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
boxShadow: "var(--shadow4)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: "32px 16px",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
width: "100%",
|
|
||||||
minHeight: "150px",
|
|
||||||
cursor: "pointer",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1Hover)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
marginLeft: "16px",
|
|
||||||
textAlign: "left"
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: "18px",
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
marginBottom: "8px"
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "var(--colorNeutralForeground2)"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||||
imgSrc,
|
imgSrc,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
onClick,
|
onClick,
|
||||||
}: SplashScreenButtonProps): JSX.Element => {
|
}: SplashScreenButtonProps): JSX.Element => {
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
horizontal
|
horizontal
|
||||||
className={styles.button}
|
style={{
|
||||||
|
border: "1px solid #949494",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
boxShadow: "0 4px 4px rgba(0, 0, 0, 0.25)",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "32px 16px",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 150,
|
||||||
|
}}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyPress={(event: React.KeyboardEvent) => {
|
onKeyPress={(event: React.KeyboardEvent) => {
|
||||||
if (event.charCode === KeyCodes.Space || event.charCode === KeyCodes.Enter) {
|
if (event.charCode === KeyCodes.Space || event.charCode === KeyCodes.Enter) {
|
||||||
@@ -67,9 +41,9 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
<img src={imgSrc} alt={title} aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<Stack className={styles.content}>
|
<Stack style={{ marginLeft: 16 }}>
|
||||||
<Text className={styles.title}>{title}</Text>
|
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||||
<Text className={styles.description}>{description}</Text>
|
<Text style={{ fontSize: 13 }}>{description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as ko from "knockout";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
@@ -264,6 +264,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
shouldNotify?: boolean,
|
shouldNotify?: boolean,
|
||||||
paginationToken?: string,
|
paginationToken?: string,
|
||||||
): Promise<Entities.IListTableEntitiesResult> {
|
): Promise<Entities.IListTableEntitiesResult> {
|
||||||
|
if (!this.useCassandraProxyEndpoint("postQuery")) {
|
||||||
|
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
|
||||||
|
}
|
||||||
const clearMessage =
|
const clearMessage =
|
||||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||||
try {
|
try {
|
||||||
@@ -306,6 +309,55 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async queryDocuments_ToBeDeprecated(
|
||||||
|
collection: ViewModels.Collection,
|
||||||
|
query: string,
|
||||||
|
shouldNotify?: boolean,
|
||||||
|
paginationToken?: string,
|
||||||
|
): Promise<Entities.IListTableEntitiesResult> {
|
||||||
|
const clearMessage =
|
||||||
|
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||||
|
try {
|
||||||
|
const { authType, databaseAccount } = userContext;
|
||||||
|
const apiEndpoint: string =
|
||||||
|
authType === AuthType.EncryptedToken
|
||||||
|
? Constants.CassandraBackend.guestQueryApi
|
||||||
|
: Constants.CassandraBackend.queryApi;
|
||||||
|
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
accountName: databaseAccount?.name,
|
||||||
|
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||||
|
resourceId: databaseAccount?.id,
|
||||||
|
keyspaceId: collection.databaseId,
|
||||||
|
tableId: collection.id(),
|
||||||
|
query,
|
||||||
|
paginationToken,
|
||||||
|
},
|
||||||
|
beforeSend: this.setAuthorizationHeader as any,
|
||||||
|
cache: false,
|
||||||
|
});
|
||||||
|
shouldNotify &&
|
||||||
|
NotificationConsoleUtils.logConsoleInfo(
|
||||||
|
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
Results: data.result,
|
||||||
|
ContinuationToken: data.paginationToken,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
shouldNotify &&
|
||||||
|
handleError(
|
||||||
|
error,
|
||||||
|
"QueryDocuments_ToBeDeprecated_Cassandra",
|
||||||
|
`Failed to query rows for table ${collection.id()}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async deleteDocuments(
|
public async deleteDocuments(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
entitiesToDelete: Entities.ITableEntity[],
|
entitiesToDelete: Entities.ITableEntity[],
|
||||||
@@ -419,6 +471,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
|
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||||
|
return this.getTableKeys_ToBeDeprecated(collection);
|
||||||
|
}
|
||||||
|
|
||||||
if (!!collection.cassandraKeys) {
|
if (!!collection.cassandraKeys) {
|
||||||
return Q.resolve(collection.cassandraKeys);
|
return Q.resolve(collection.cassandraKeys);
|
||||||
}
|
}
|
||||||
@@ -459,7 +515,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
|
if (!!collection.cassandraKeys) {
|
||||||
|
return Q.resolve(collection.cassandraKeys);
|
||||||
|
}
|
||||||
|
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||||
|
const { authType, databaseAccount } = userContext;
|
||||||
|
const apiEndpoint: string =
|
||||||
|
authType === AuthType.EncryptedToken
|
||||||
|
? Constants.CassandraBackend.guestKeysApi
|
||||||
|
: Constants.CassandraBackend.keysApi;
|
||||||
|
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||||
|
const deferred = Q.defer<CassandraTableKeys>();
|
||||||
|
|
||||||
|
$.ajax(endpoint, {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
accountName: databaseAccount?.name,
|
||||||
|
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||||
|
resourceId: databaseAccount?.id,
|
||||||
|
keyspaceId: collection.databaseId,
|
||||||
|
tableId: collection.id(),
|
||||||
|
},
|
||||||
|
beforeSend: this.setAuthorizationHeader as any,
|
||||||
|
cache: false,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(data: CassandraTableKeys) => {
|
||||||
|
collection.cassandraKeys = data;
|
||||||
|
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||||
|
deferred.resolve(data);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.done(clearInProgressMessage);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||||
|
if (!this.useCassandraProxyEndpoint("getSchema")) {
|
||||||
|
return this.getTableSchema_ToBeDeprecated(collection);
|
||||||
|
}
|
||||||
|
|
||||||
if (!!collection.cassandraSchema) {
|
if (!!collection.cassandraSchema) {
|
||||||
return Q.resolve(collection.cassandraSchema);
|
return Q.resolve(collection.cassandraSchema);
|
||||||
}
|
}
|
||||||
@@ -501,7 +602,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||||
|
if (!!collection.cassandraSchema) {
|
||||||
|
return Q.resolve(collection.cassandraSchema);
|
||||||
|
}
|
||||||
|
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||||
|
const { databaseAccount, authType } = userContext;
|
||||||
|
const apiEndpoint: string =
|
||||||
|
authType === AuthType.EncryptedToken
|
||||||
|
? Constants.CassandraBackend.guestSchemaApi
|
||||||
|
: Constants.CassandraBackend.schemaApi;
|
||||||
|
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||||
|
const deferred = Q.defer<CassandraTableKey[]>();
|
||||||
|
|
||||||
|
$.ajax(endpoint, {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
accountName: databaseAccount?.name,
|
||||||
|
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||||
|
resourceId: databaseAccount?.id,
|
||||||
|
keyspaceId: collection.databaseId,
|
||||||
|
tableId: collection.id(),
|
||||||
|
},
|
||||||
|
beforeSend: this.setAuthorizationHeader as any,
|
||||||
|
cache: false,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(data: any) => {
|
||||||
|
collection.cassandraSchema = data.columns;
|
||||||
|
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||||
|
deferred.resolve(data.columns);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.done(clearInProgressMessage);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||||
|
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
|
||||||
|
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
|
||||||
|
}
|
||||||
|
|
||||||
const deferred = Q.defer();
|
const deferred = Q.defer();
|
||||||
const { authType, databaseAccount } = userContext;
|
const { authType, databaseAccount } = userContext;
|
||||||
const apiEndpoint: string =
|
const apiEndpoint: string =
|
||||||
@@ -531,6 +677,38 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createOrDeleteQuery_ToBeDeprecated(
|
||||||
|
cassandraEndpoint: string,
|
||||||
|
resourceId: string,
|
||||||
|
query: string,
|
||||||
|
): Q.Promise<any> {
|
||||||
|
const deferred = Q.defer();
|
||||||
|
const { authType, databaseAccount } = userContext;
|
||||||
|
const apiEndpoint: string =
|
||||||
|
authType === AuthType.EncryptedToken
|
||||||
|
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||||
|
: Constants.CassandraBackend.createOrDeleteApi;
|
||||||
|
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
accountName: databaseAccount?.name,
|
||||||
|
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||||
|
resourceId: resourceId,
|
||||||
|
query: query,
|
||||||
|
},
|
||||||
|
beforeSend: this.setAuthorizationHeader as any,
|
||||||
|
cache: false,
|
||||||
|
}).then(
|
||||||
|
(data: any) => {
|
||||||
|
deferred.resolve();
|
||||||
|
},
|
||||||
|
(reason) => {
|
||||||
|
deferred.reject(reason);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||||
if (!cassandraEndpoint) {
|
if (!cassandraEndpoint) {
|
||||||
return cassandraEndpoint;
|
return cassandraEndpoint;
|
||||||
@@ -569,4 +747,23 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||||
return collection.cassandraKeys.partitionKeys[0].property;
|
return collection.cassandraKeys.partitionKeys[0].property;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private useCassandraProxyEndpoint(api: string): boolean {
|
||||||
|
const activeCassandraProxyEndpoints: string[] = [
|
||||||
|
CassandraProxyEndpoints.Development,
|
||||||
|
CassandraProxyEndpoints.Mpac,
|
||||||
|
CassandraProxyEndpoints.Prod,
|
||||||
|
CassandraProxyEndpoints.Fairfax,
|
||||||
|
CassandraProxyEndpoints.Mooncake,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
|||||||
import { waitFor } from "@testing-library/react";
|
import { waitFor } from "@testing-library/react";
|
||||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
@@ -342,15 +341,10 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
updateConfigContext({ platform: Platform.Fabric });
|
updateConfigContext({ platform: Platform.Fabric });
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
fabricContext: {
|
fabricContext: {
|
||||||
databaseName: "database",
|
|
||||||
artifactInfo: {
|
|
||||||
connectionId: "test",
|
connectionId: "test",
|
||||||
resourceTokenInfo: undefined,
|
databaseConnectionInfo: undefined,
|
||||||
},
|
|
||||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
fabricClientRpcVersion: "rpcVersion",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||||
import { readDocument } from "Common/dataAccess/readDocument";
|
import { readDocument } from "Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
@@ -42,7 +43,6 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
|||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -55,7 +55,6 @@ import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
|||||||
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
||||||
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||||
@@ -132,14 +131,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
|
||||||
position: "absolute",
|
|
||||||
top: "3px",
|
|
||||||
right: "4px",
|
|
||||||
float: "right",
|
|
||||||
zIndex: 1,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
deleteProgressContent: {
|
deleteProgressContent: {
|
||||||
paddingTop: tokens.spacingVerticalL,
|
paddingTop: tokens.spacingVerticalL,
|
||||||
},
|
},
|
||||||
@@ -353,7 +344,7 @@ export const getTabsButtons = ({
|
|||||||
onRevertExistingDocumentClick,
|
onRevertExistingDocumentClick,
|
||||||
onDeleteExistingDocumentsClick,
|
onDeleteExistingDocumentsClick,
|
||||||
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
||||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
// All the following buttons require write access
|
// All the following buttons require write access
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1158,6 +1149,16 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
} else {
|
} else {
|
||||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (isMongoBulkDeleteDisabled) {
|
||||||
|
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||||
|
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||||
|
deletePromise = MongoProxyClient.deleteDocument(
|
||||||
|
_collection.databaseId,
|
||||||
|
_collection as ViewModels.Collection,
|
||||||
|
toDeleteDocumentIds[0],
|
||||||
|
).then(() => [toDeleteDocumentIds[0]]);
|
||||||
|
// ----------------------------------------------------------------------------------------------------
|
||||||
} else {
|
} else {
|
||||||
deletePromise = MongoProxyClient.deleteDocuments(
|
deletePromise = MongoProxyClient.deleteDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
@@ -1170,6 +1171,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return deletePromise
|
return deletePromise
|
||||||
.then(
|
.then(
|
||||||
@@ -2052,8 +2054,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||||
|
|
||||||
|
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||||
const isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
|
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||||
|
const isBulkDeleteDisabled =
|
||||||
|
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|
||||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||||
@@ -2145,7 +2150,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds={selectedColumnIds}
|
selectedColumnIds={selectedColumnIds}
|
||||||
columnDefinitions={columnDefinitions}
|
columnDefinitions={columnDefinitions}
|
||||||
isRowSelectionDisabled={
|
isRowSelectionDisabled={
|
||||||
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
|
isBulkDeleteDisabled ||
|
||||||
|
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||||
}
|
}
|
||||||
onColumnSelectionChange={onColumnSelectionChange}
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
defaultColumnSelection={getInitialColumnSelection()}
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
@@ -2153,18 +2159,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
|
|
||||||
<div
|
|
||||||
title="Refresh"
|
|
||||||
className={styles.refreshBtn}
|
|
||||||
role="button"
|
|
||||||
onClick={() => refreshDocumentsGrid(false)}
|
|
||||||
aria-label="Refresh"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<img src={RefreshIcon} alt="Refresh" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
aria-label="Select column"
|
aria-label="Select column"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<MoreHorizontalRegular />}
|
icon={<MoreHorizontalRegular />}
|
||||||
style={{ position: "absolute", right: 10, backgroundColor: tokens.colorNeutralBackground1 }}
|
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||||
/>
|
/>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { configContext } from "../../../ConfigContext";
|
import { configContext } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -48,13 +50,15 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
IMongoShellTabComponentStates
|
IMongoShellTabComponentStates
|
||||||
> {
|
> {
|
||||||
private _logTraces: Map<string, number>;
|
private _logTraces: Map<string, number>;
|
||||||
|
private _useMongoProxyEndpoint: boolean;
|
||||||
|
|
||||||
constructor(props: IMongoShellTabComponentProps) {
|
constructor(props: IMongoShellTabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._logTraces = new Map();
|
this._logTraces = new Map();
|
||||||
|
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
url: getMongoShellUrl(),
|
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||||
};
|
};
|
||||||
|
|
||||||
props.onMongoShellTabAccessor({
|
props.onMongoShellTabAccessor({
|
||||||
@@ -109,9 +113,17 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
const resourceId = databaseAccount?.id;
|
const resourceId = databaseAccount?.id;
|
||||||
const accountName = databaseAccount?.name;
|
const accountName = databaseAccount?.name;
|
||||||
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
|
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
|
||||||
|
const mongoEndpoint =
|
||||||
|
documentEndpoint.substr(
|
||||||
|
Constants.MongoDBAccounts.protocol.length + 3,
|
||||||
|
documentEndpoint.length -
|
||||||
|
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length),
|
||||||
|
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||||
const databaseId = this.props.collection.databaseId;
|
const databaseId = this.props.collection.databaseId;
|
||||||
const collectionId = this.props.collection.id();
|
const collectionId = this.props.collection.id();
|
||||||
const apiEndpoint = configContext.MONGO_PROXY_ENDPOINT;
|
const apiEndpoint = this._useMongoProxyEndpoint
|
||||||
|
? configContext.MONGO_PROXY_ENDPOINT
|
||||||
|
: configContext.BACKEND_ENDPOINT;
|
||||||
const encryptedAuthToken: string = userContext.accessToken;
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
@@ -120,7 +132,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
data: {
|
data: {
|
||||||
resourceId: resourceId,
|
resourceId: resourceId,
|
||||||
accountName: accountName,
|
accountName: accountName,
|
||||||
mongoEndpoint: documentEndpoint,
|
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint,
|
||||||
authorization: authorization,
|
authorization: authorization,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Platform, resetConfigContext, updateConfigContext } from "../../../Conf
|
|||||||
import { updateUserContext, userContext } from "../../../UserContext";
|
import { updateUserContext, userContext } from "../../../UserContext";
|
||||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||||
|
|
||||||
|
const mongoBackendEndpoint = "https://localhost:1234";
|
||||||
|
|
||||||
describe("getMongoShellUrl", () => {
|
describe("getMongoShellUrl", () => {
|
||||||
let queryString = "";
|
let queryString = "";
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ describe("getMongoShellUrl", () => {
|
|||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,7 +37,12 @@ describe("getMongoShellUrl", () => {
|
|||||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return /index.html by default", () => {
|
it("should return /indexv2.html by default", () => {
|
||||||
expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
|
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||||
|
const useMongoProxyEndpoint: boolean = true;
|
||||||
|
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
|
||||||
export function getMongoShellUrl(): string {
|
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
|
||||||
const { databaseAccount: account } = userContext;
|
const { databaseAccount: account } = userContext;
|
||||||
const resourceId = account?.id;
|
const resourceId = account?.id;
|
||||||
const accountName = account?.name;
|
const accountName = account?.name;
|
||||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||||
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||||
|
|
||||||
return `/mongoshell/index.html?${queryString}`;
|
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { CircleFilled } from "@fluentui/react-icons";
|
|
||||||
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
|
||||||
import * as React from "react";
|
|
||||||
interface IndexObject {
|
|
||||||
index: string;
|
|
||||||
impact: string;
|
|
||||||
section: "Included" | "Not Included" | "Header";
|
|
||||||
composite?: {
|
|
||||||
path: string;
|
|
||||||
order: "ascending" | "descending";
|
|
||||||
}[];
|
|
||||||
path?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IndexMetricsJson {
|
|
||||||
included?: IIndexMetric[];
|
|
||||||
notIncluded?: IIndexMetric[];
|
|
||||||
}
|
|
||||||
export function parseIndexMetrics(indexMetrics: string | IndexMetricsJson): {
|
|
||||||
included: IIndexMetric[];
|
|
||||||
notIncluded: IIndexMetric[];
|
|
||||||
} {
|
|
||||||
// If already JSON, just extract arrays
|
|
||||||
if (typeof indexMetrics === "object" && indexMetrics !== null) {
|
|
||||||
return {
|
|
||||||
included: Array.isArray(indexMetrics.included) ? indexMetrics.included : [],
|
|
||||||
notIncluded: Array.isArray(indexMetrics.notIncluded) ? indexMetrics.notIncluded : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, parse as string (current SDK)
|
|
||||||
const included: IIndexMetric[] = [];
|
|
||||||
const notIncluded: IIndexMetric[] = [];
|
|
||||||
const lines = (indexMetrics as string)
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
let currentSection = "";
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
if (line.startsWith("Utilized Single Indexes") || line.startsWith("Utilized Composite Indexes")) {
|
|
||||||
currentSection = "included";
|
|
||||||
} else if (line.startsWith("Potential Single Indexes") || line.startsWith("Potential Composite Indexes")) {
|
|
||||||
currentSection = "notIncluded";
|
|
||||||
} else if (line.startsWith("Index Spec:")) {
|
|
||||||
const index = line.replace("Index Spec:", "").trim();
|
|
||||||
const impactLine = lines[i + 1];
|
|
||||||
const impact = impactLine?.includes("Index Impact Score:") ? impactLine.split(":")[1].trim() : "Unknown";
|
|
||||||
|
|
||||||
const isComposite = index.includes(",");
|
|
||||||
|
|
||||||
const sectionMap: Record<string, "Included" | "Not Included"> = {
|
|
||||||
included: "Included",
|
|
||||||
notIncluded: "Not Included",
|
|
||||||
};
|
|
||||||
|
|
||||||
const indexObj: IndexObject = { index, impact, section: sectionMap[currentSection] ?? "Header" };
|
|
||||||
if (isComposite) {
|
|
||||||
indexObj.composite = index.split(",").map((part: string) => {
|
|
||||||
const [path, order] = part.trim().split(/\s+/);
|
|
||||||
return {
|
|
||||||
path: path.trim(),
|
|
||||||
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let path = "/unknown/*";
|
|
||||||
const pathRegex = /\/[^/\s*?]+(?:\/[^/\s*?]+)*(\/\*|\?)/;
|
|
||||||
const match = index.match(pathRegex);
|
|
||||||
if (match) {
|
|
||||||
path = match[0];
|
|
||||||
} else {
|
|
||||||
const simplePathRegex = /\/[^/\s]+/;
|
|
||||||
const simpleMatch = index.match(simplePathRegex);
|
|
||||||
if (simpleMatch) {
|
|
||||||
path = simpleMatch[0] + "/*";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indexObj.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSection === "included") {
|
|
||||||
included.push(indexObj);
|
|
||||||
} else if (currentSection === "notIncluded") {
|
|
||||||
notIncluded.push(indexObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { included, notIncluded };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderImpactDots = (impact: string): JSX.Element => {
|
|
||||||
const style = useIndexAdvisorStyles();
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (impact === "High") {
|
|
||||||
count = 3;
|
|
||||||
} else if (impact === "Medium") {
|
|
||||||
count = 2;
|
|
||||||
} else if (impact === "Low") {
|
|
||||||
count = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.indexAdvisorImpactDots}>
|
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
|
||||||
<CircleFilled key={i} className={style.indexAdvisorImpactDot} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link, tokens } from "@fluentui/react-components";
|
import { Link } from "@fluentui/react-components";
|
||||||
import QueryError from "Common/QueryError";
|
import QueryError from "Common/QueryError";
|
||||||
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
||||||
import { MessageBanner } from "Explorer/Controls/MessageBanner";
|
import { MessageBanner } from "Explorer/Controls/MessageBanner";
|
||||||
@@ -29,7 +29,7 @@ const ExecuteQueryCallToAction: React.FC = () => {
|
|||||||
<p>
|
<p>
|
||||||
<img src={RunQuery} aria-hidden="true" />
|
<img src={RunQuery} aria-hidden="true" />
|
||||||
</p>
|
</p>
|
||||||
<p style={{ color: tokens.colorNeutralForeground1 }}>Execute a query to see the results</p>
|
<p>Execute a query to see the results</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { TabsState, useTabs } from "hooks/useTabs";
|
import { TabsState, useTabs } from "hooks/useTabs";
|
||||||
import { monacoTheme } from "hooks/useTheme";
|
|
||||||
import React, { Fragment, createRef } from "react";
|
import React, { Fragment, createRef } from "react";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import create from "zustand";
|
|
||||||
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||||
@@ -56,20 +54,6 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
|||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import "./QueryTabComponent.less";
|
import "./QueryTabComponent.less";
|
||||||
|
|
||||||
export interface QueryMetadataStore {
|
|
||||||
userQuery: string;
|
|
||||||
databaseId: string;
|
|
||||||
containerId: string;
|
|
||||||
setMetadata: (query1: string, db: string, container: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
|
|
||||||
userQuery: "",
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
enum ToggleState {
|
enum ToggleState {
|
||||||
Result,
|
Result,
|
||||||
QueryMetrics,
|
QueryMetrics,
|
||||||
@@ -274,10 +258,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onExecuteQueryClick = async (): Promise<void> => {
|
public onExecuteQueryClick = async (): Promise<void> => {
|
||||||
const query1 = this.state.sqlQueryEditorContent;
|
|
||||||
const db = this.props.collection.databaseId;
|
|
||||||
const container = this.props.collection.id();
|
|
||||||
useQueryMetadataStore.getState().setMetadata(query1, db, container);
|
|
||||||
this._iterator = undefined;
|
this._iterator = undefined;
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -395,7 +375,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
ruCapPerOperation: ruThreshold,
|
ruCapPerOperation: ruThreshold,
|
||||||
} as QueryOperationOptions;
|
} as QueryOperationOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryDocuments = async (firstItemIndex: number) =>
|
const queryDocuments = async (firstItemIndex: number) =>
|
||||||
await queryDocumentsPage(
|
await queryDocumentsPage(
|
||||||
this.props.collection && this.props.collection.id(),
|
this.props.collection && this.props.collection.id(),
|
||||||
@@ -776,7 +755,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
wordWrap={"on"}
|
wordWrap={"on"}
|
||||||
ariaLabel={"Editing Query"}
|
ariaLabel={"Editing Query"}
|
||||||
lineNumbers={"on"}
|
lineNumbers={"on"}
|
||||||
theme={monacoTheme}
|
|
||||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||||
onContentSelected={(selectedContent: string, selection: monaco.Selection) =>
|
onContentSelected={(selectedContent: string, selection: monaco.Selection) =>
|
||||||
this.onSelectedContent(selectedContent, selection)
|
this.onSelectedContent(selectedContent, selection)
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
||||||
import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const mockReplace = jest.fn();
|
|
||||||
const mockFetchAll = jest.fn();
|
|
||||||
const mockRead = jest.fn();
|
|
||||||
const mockLogConsoleProgress = jest.fn();
|
|
||||||
const mockHandleError = jest.fn();
|
|
||||||
|
|
||||||
const indexMetricsString = `
|
|
||||||
Utilized Single Indexes
|
|
||||||
Index Spec: /foo/?
|
|
||||||
Index Impact Score: High
|
|
||||||
Potential Single Indexes
|
|
||||||
Index Spec: /bar/?
|
|
||||||
Index Impact Score: Medium
|
|
||||||
Utilized Composite Indexes
|
|
||||||
Index Spec: /baz/? DESC, /qux/? ASC
|
|
||||||
Index Impact Score: Low
|
|
||||||
`;
|
|
||||||
mockRead.mockResolvedValue({
|
|
||||||
resource: {
|
|
||||||
indexingPolicy: {
|
|
||||||
automatic: true,
|
|
||||||
indexingMode: "consistent",
|
|
||||||
includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
|
|
||||||
excludedPaths: [],
|
|
||||||
},
|
|
||||||
partitionKey: "pk",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mockReplace.mockResolvedValue({
|
|
||||||
resource: {
|
|
||||||
indexingPolicy: {
|
|
||||||
automatic: true,
|
|
||||||
indexingMode: "consistent",
|
|
||||||
includedPaths: [{ path: "/*" }],
|
|
||||||
excludedPaths: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock("./QueryTabComponent", () => ({
|
|
||||||
useQueryMetadataStore: () => ({
|
|
||||||
userQuery: "SELECT * FROM c",
|
|
||||||
databaseId: "db1",
|
|
||||||
containerId: "col1",
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock("Common/CosmosClient", () => ({
|
|
||||||
client: () => ({
|
|
||||||
database: () => ({
|
|
||||||
container: () => ({
|
|
||||||
items: {
|
|
||||||
query: () => ({
|
|
||||||
fetchAll: mockFetchAll.mockResolvedValueOnce({ indexMetrics: indexMetricsString }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
read: mockRead,
|
|
||||||
replace: mockReplace,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock("./StylesAdvisor", () => ({
|
|
||||||
useIndexAdvisorStyles: () => ({}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
|
|
||||||
logConsoleProgress: (...args: unknown[]) => {
|
|
||||||
mockLogConsoleProgress(...args);
|
|
||||||
return () => {};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../../Common/ErrorHandlingUtils", () => {
|
|
||||||
return {
|
|
||||||
handleError: (...args: unknown[]) => mockHandleError(...args),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logs progress message when fetching index metrics", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(mockLogConsoleProgress).toHaveBeenCalledWith(expect.stringContaining("IndexMetrics")));
|
|
||||||
});
|
|
||||||
test("renders both Included and Not Included sections after loading", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
|
|
||||||
expect(screen.getByText("Not Included in Current Policy")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("/foo/?")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("/bar/?")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
test("shows update button only when an index is selected", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
|
|
||||||
const checkboxes = screen.getAllByRole("checkbox");
|
|
||||||
expect(checkboxes.length).toBeGreaterThan(1);
|
|
||||||
fireEvent.click(checkboxes[1]);
|
|
||||||
expect(screen.getByText(/Update Indexing Policy/)).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(checkboxes[1]);
|
|
||||||
expect(screen.queryByText(/Update Indexing Policy/)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
test("calls replace when update policy is confirmed", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
|
|
||||||
const checkboxes = screen.getAllByRole("checkbox");
|
|
||||||
fireEvent.click(checkboxes[1]);
|
|
||||||
const updateButton = screen.getByText(/Update Indexing Policy/);
|
|
||||||
fireEvent.click(updateButton);
|
|
||||||
await waitFor(() => expect(mockReplace).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls replace when update button is clicked", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
|
|
||||||
const checkboxes = screen.getAllByRole("checkbox");
|
|
||||||
fireEvent.click(checkboxes[1]); // Select /bar/?
|
|
||||||
fireEvent.click(screen.getByText(/Update Indexing Policy/));
|
|
||||||
await waitFor(() => expect(mockReplace).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("fetches indexing policy via read", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockRead).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selects all indexes when select-all is clicked", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
|
|
||||||
const checkboxes = screen.getAllByRole("checkbox");
|
|
||||||
|
|
||||||
fireEvent.click(checkboxes[0]);
|
|
||||||
checkboxes.forEach((cb) => {
|
|
||||||
expect(cb).toBeChecked();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows spinner while loading and hides after fetchIndexMetrics resolves", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
|
||||||
await waitFor(() => expect(screen.queryByRole("progressbar")).not.toBeInTheDocument());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls fetchAll with correct query and options", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
test("renders IndexAdvisorTab when clicked from ResultsView", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
|
|
||||||
expect(screen.getByText("/foo/?")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
test("renders index metrics from SDK response", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/foo/?")).toBeInTheDocument());
|
|
||||||
expect(screen.getByText("/bar/?")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("/baz/? DESC, /qux/? ASC")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls handleError if fetchIndexMetrics throws", async () => {
|
|
||||||
mockFetchAll.mockRejectedValueOnce(new Error("fail"));
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(mockHandleError).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls handleError if fetchIndexMetrics throws2nd", async () => {
|
|
||||||
mockFetchAll.mockRejectedValueOnce(new Error("fail"));
|
|
||||||
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(mockHandleError).toHaveBeenCalled());
|
|
||||||
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IndexingPolicyStore stores updated policy on componentDidMount", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(mockRead).toHaveBeenCalled());
|
|
||||||
|
|
||||||
const readResult = await mockRead.mock.results[0].value;
|
|
||||||
const policy = readResult.resource.indexingPolicy;
|
|
||||||
|
|
||||||
expect(policy).toBeDefined();
|
|
||||||
expect(policy.automatic).toBe(true);
|
|
||||||
expect(policy.indexingMode).toBe("consistent");
|
|
||||||
expect(policy.includedPaths).toEqual(expect.arrayContaining([{ path: "/*" }, { path: "/foo/?" }]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("refreshCollectionData updates observable and re-renders", async () => {
|
|
||||||
render(<IndexAdvisorTab />);
|
|
||||||
await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
|
|
||||||
|
|
||||||
const checkboxes = screen.getAllByRole("checkbox");
|
|
||||||
fireEvent.click(checkboxes[1]); // Select /bar/?
|
|
||||||
fireEvent.click(screen.getByText(/Update Indexing Policy/));
|
|
||||||
|
|
||||||
await waitFor(() => expect(mockReplace).toHaveBeenCalled());
|
|
||||||
expect(screen.getByText("/bar/?")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
|
|
||||||
import { FontIcon } from "@fluentui/react";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DataGridBody,
|
DataGridBody,
|
||||||
DataGridCell,
|
DataGridCell,
|
||||||
@@ -11,44 +8,28 @@ import {
|
|||||||
DataGridRow,
|
DataGridRow,
|
||||||
SelectTabData,
|
SelectTabData,
|
||||||
SelectTabEvent,
|
SelectTabEvent,
|
||||||
Spinner,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableColumnDefinition,
|
TableColumnDefinition,
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
createTableColumn,
|
createTableColumn,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {
|
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
|
||||||
ArrowDownloadRegular,
|
|
||||||
ChevronDown20Regular,
|
|
||||||
ChevronRight20Regular,
|
|
||||||
CopyRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import copy from "clipboard-copy";
|
|
||||||
import { HttpHeaders } from "Common/Constants";
|
import { HttpHeaders } from "Common/Constants";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { QueryMetrics } from "Contracts/DataModels";
|
import { QueryMetrics } from "Contracts/DataModels";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { parseIndexMetrics, renderImpactDots } from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
|
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import { IDocument, useQueryMetadataStore } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import copy from "clipboard-copy";
|
||||||
import create from "zustand";
|
import React, { useCallback, useState } from "react";
|
||||||
import { client } from "../../../Common/CosmosClient";
|
|
||||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { ResultsViewProps } from "./QueryResultSection";
|
import { ResultsViewProps } from "./QueryResultSection";
|
||||||
import { useIndexAdvisorStyles } from "./StylesAdvisor";
|
|
||||||
enum ResultsTabs {
|
enum ResultsTabs {
|
||||||
Results = "results",
|
Results = "results",
|
||||||
QueryStats = "queryStats",
|
QueryStats = "queryStats",
|
||||||
IndexAdvisor = "indexadv",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
|
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const queryResultsString = queryResults
|
const queryResultsString = queryResults
|
||||||
@@ -374,286 +355,6 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IIndexMetric {
|
|
||||||
index: string;
|
|
||||||
impact: string;
|
|
||||||
section: "Included" | "Not Included" | "Header";
|
|
||||||
path?: string;
|
|
||||||
composite?: { path: string; order: string }[];
|
|
||||||
}
|
|
||||||
export const IndexAdvisorTab: React.FC = () => {
|
|
||||||
const style = useIndexAdvisorStyles();
|
|
||||||
const { userQuery, databaseId, containerId } = useQueryMetadataStore();
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [indexMetrics, setIndexMetrics] = useState<string | null>(null);
|
|
||||||
const [showIncluded, setShowIncluded] = useState(true);
|
|
||||||
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
|
||||||
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [updateMessageShown, setUpdateMessageShown] = useState(false);
|
|
||||||
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchIndexMetrics = async () => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
|
|
||||||
try {
|
|
||||||
const querySpec = {
|
|
||||||
query: userQuery,
|
|
||||||
};
|
|
||||||
const sdkResponse = await client()
|
|
||||||
.database(databaseId)
|
|
||||||
.container(containerId)
|
|
||||||
.items.query(querySpec, {
|
|
||||||
populateIndexMetrics: true,
|
|
||||||
})
|
|
||||||
.fetchAll();
|
|
||||||
setIndexMetrics(sdkResponse.indexMetrics);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (userQuery && databaseId && containerId) {
|
|
||||||
fetchIndexMetrics();
|
|
||||||
}
|
|
||||||
}, [userQuery, databaseId, containerId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!indexMetrics) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { included, notIncluded } = parseIndexMetrics(indexMetrics);
|
|
||||||
setIncludedIndexes(included);
|
|
||||||
setNotIncludedIndexes(notIncluded);
|
|
||||||
if (justUpdatedPolicy) {
|
|
||||||
setJustUpdatedPolicy(false);
|
|
||||||
} else {
|
|
||||||
setUpdateMessageShown(false);
|
|
||||||
}
|
|
||||||
}, [indexMetrics]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const allSelected =
|
|
||||||
notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
|
|
||||||
setSelectAll(allSelected);
|
|
||||||
}, [selectedIndexes, notIncluded]);
|
|
||||||
|
|
||||||
const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedIndexes((prev) => [...prev, indexObj]);
|
|
||||||
} else {
|
|
||||||
setSelectedIndexes((prev) => prev.filter((item) => item.index !== indexObj.index));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = (checked: boolean) => {
|
|
||||||
setSelectAll(checked);
|
|
||||||
setSelectedIndexes(checked ? notIncluded : []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePolicy = async () => {
|
|
||||||
setIsUpdating(true);
|
|
||||||
try {
|
|
||||||
const containerRef = client().database(databaseId).container(containerId);
|
|
||||||
const { resource: containerDef } = await containerRef.read();
|
|
||||||
|
|
||||||
const newIncludedPaths = selectedIndexes
|
|
||||||
.filter((index) => !index.composite)
|
|
||||||
.map((index) => {
|
|
||||||
return {
|
|
||||||
path: index.path,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const newCompositeIndexes: CompositePath[][] = selectedIndexes
|
|
||||||
.filter((index) => Array.isArray(index.composite))
|
|
||||||
.map(
|
|
||||||
(index) =>
|
|
||||||
(index.composite as { path: string; order: string }[]).map((comp) => ({
|
|
||||||
path: comp.path,
|
|
||||||
order: comp.order === "descending" ? "descending" : "ascending",
|
|
||||||
})) as CompositePath[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedPolicy: IndexingPolicy = {
|
|
||||||
...containerDef.indexingPolicy,
|
|
||||||
includedPaths: [...(containerDef.indexingPolicy?.includedPaths || []), ...newIncludedPaths],
|
|
||||||
compositeIndexes: [...(containerDef.indexingPolicy?.compositeIndexes || []), ...newCompositeIndexes],
|
|
||||||
automatic: containerDef.indexingPolicy?.automatic ?? true,
|
|
||||||
indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent",
|
|
||||||
excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [],
|
|
||||||
};
|
|
||||||
await containerRef.replace({
|
|
||||||
id: containerId,
|
|
||||||
partitionKey: containerDef.partitionKey,
|
|
||||||
indexingPolicy: updatedPolicy,
|
|
||||||
});
|
|
||||||
useIndexingPolicyStore.getState().setIndexingPolicyFor(containerId, updatedPolicy);
|
|
||||||
const selectedIndexSet = new Set(selectedIndexes.map((s) => s.index));
|
|
||||||
const updatedNotIncluded: typeof notIncluded = [];
|
|
||||||
const newlyIncluded: typeof included = [];
|
|
||||||
for (const item of notIncluded) {
|
|
||||||
if (selectedIndexSet.has(item.index)) {
|
|
||||||
newlyIncluded.push(item);
|
|
||||||
} else {
|
|
||||||
updatedNotIncluded.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newIncluded = [...included, ...newlyIncluded];
|
|
||||||
const newNotIncluded = updatedNotIncluded;
|
|
||||||
setIncludedIndexes(newIncluded);
|
|
||||||
setNotIncludedIndexes(newNotIncluded);
|
|
||||||
setSelectedIndexes([]);
|
|
||||||
setSelectAll(false);
|
|
||||||
setUpdateMessageShown(true);
|
|
||||||
setJustUpdatedPolicy(true);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to update indexing policy:", err);
|
|
||||||
} finally {
|
|
||||||
setIsUpdating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRow = (item: IIndexMetric, index: number) => {
|
|
||||||
const isHeader = item.section === "Header";
|
|
||||||
const isNotIncluded = item.section === "Not Included";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell colSpan={2}>
|
|
||||||
<div className={style.indexAdvisorGrid}>
|
|
||||||
{isNotIncluded ? (
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedIndexes.some((selected) => selected.index === item.index)}
|
|
||||||
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
|
|
||||||
/>
|
|
||||||
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
|
|
||||||
<Checkbox checked={selectAll} onChange={(_, data) => handleSelectAll(data.checked === true)} />
|
|
||||||
) : (
|
|
||||||
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
|
||||||
)}
|
|
||||||
{isHeader ? (
|
|
||||||
<span
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.index === "Included in Current Policy") {
|
|
||||||
setShowIncluded(!showIncluded);
|
|
||||||
} else if (item.index === "Not Included in Current Policy") {
|
|
||||||
setShowNotIncluded(!showNotIncluded);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.index === "Included in Current Policy" ? (
|
|
||||||
showIncluded ? (
|
|
||||||
<ChevronDown20Regular />
|
|
||||||
) : (
|
|
||||||
<ChevronRight20Regular />
|
|
||||||
)
|
|
||||||
) : showNotIncluded ? (
|
|
||||||
<ChevronDown20Regular />
|
|
||||||
) : (
|
|
||||||
<ChevronRight20Regular />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className={style.indexAdvisorChevronSpacer}></div>
|
|
||||||
)}
|
|
||||||
<div className={isHeader ? style.indexAdvisorRowBold : style.indexAdvisorRowNormal}>{item.index}</div>
|
|
||||||
<div className={isHeader ? style.indexAdvisorRowImpactHeader : style.indexAdvisorRowImpact}>
|
|
||||||
{!isHeader && item.impact}
|
|
||||||
</div>
|
|
||||||
<div>{!isHeader && renderImpactDots(item.impact)}</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const indexMetricItems = React.useMemo(() => {
|
|
||||||
const items: IIndexMetric[] = [];
|
|
||||||
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
|
|
||||||
if (showNotIncluded) {
|
|
||||||
notIncluded.forEach((item) => items.push({ ...item, section: "Not Included" }));
|
|
||||||
}
|
|
||||||
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
|
|
||||||
if (showIncluded) {
|
|
||||||
included.forEach((item) => items.push({ ...item, section: "Included" }));
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}, [included, notIncluded, showIncluded, showNotIncluded]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Spinner
|
|
||||||
size="small"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--spinner-size": "16px",
|
|
||||||
"--spinner-thickness": "2px",
|
|
||||||
"--spinner-color": "#0078D4",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={style.indexAdvisorMessage}>
|
|
||||||
{updateMessageShown ? (
|
|
||||||
<>
|
|
||||||
<span className={style.indexAdvisorSuccessIcon}>
|
|
||||||
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Your indexing policy has been updated with the new included paths. You may review the changes in Scale &
|
|
||||||
Settings.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Here is an analysis on the indexes utilized for executing the query. Based on the analysis, Cosmos DB recommends adding the selected indexes to your indexing policy to optimize the performance of this particular query."
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={style.indexAdvisorTitle}>Indexes analysis</div>
|
|
||||||
<Table className={style.indexAdvisorTable}>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2}>
|
|
||||||
<div className={style.indexAdvisorGrid}>
|
|
||||||
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
|
||||||
<div className={style.indexAdvisorChevronSpacer}></div>
|
|
||||||
<div>Index</div>
|
|
||||||
<div>
|
|
||||||
<span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>{indexMetricItems.map(renderRow)}</TableBody>
|
|
||||||
</Table>
|
|
||||||
{selectedIndexes.length > 0 && (
|
|
||||||
<div className={style.indexAdvisorButtonBar}>
|
|
||||||
{isUpdating ? (
|
|
||||||
<div className={style.indexAdvisorButtonSpinner}>
|
|
||||||
<Spinner size="tiny" />{" "}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button onClick={handleUpdatePolicy} className={style.indexAdvisorButton}>
|
|
||||||
Update Indexing Policy with selected index(es)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
|
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
|
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
|
||||||
@@ -661,6 +362,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
|
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
|
||||||
setActiveTab(data.value as ResultsTabs);
|
setActiveTab(data.value as ResultsTabs);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
|
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
|
||||||
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
|
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
|
||||||
@@ -678,13 +380,6 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
>
|
>
|
||||||
Query Stats
|
Query Stats
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
|
|
||||||
id={ResultsTabs.IndexAdvisor}
|
|
||||||
value={ResultsTabs.IndexAdvisor}
|
|
||||||
>
|
|
||||||
Index Advisor
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
</TabList>
|
||||||
<div className={styles.queryResultsTabContentContainer}>
|
<div className={styles.queryResultsTabContentContainer}>
|
||||||
{activeTab === ResultsTabs.Results && (
|
{activeTab === ResultsTabs.Results && (
|
||||||
@@ -695,23 +390,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
||||||
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export interface IndexingPolicyStore {
|
|
||||||
indexingPolicies: { [containerId: string]: IndexingPolicy };
|
|
||||||
setIndexingPolicyFor: (containerId: string, indexingPolicy: IndexingPolicy) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useIndexingPolicyStore = create<IndexingPolicyStore>((set) => ({
|
|
||||||
indexingPolicies: {},
|
|
||||||
setIndexingPolicyFor: (containerId, indexingPolicy) =>
|
|
||||||
set((state) => ({
|
|
||||||
indexingPolicies: {
|
|
||||||
...state.indexingPolicies,
|
|
||||||
[containerId]: { ...indexingPolicy },
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { makeStyles } from "@fluentui/react-components";
|
|
||||||
export type IndexAdvisorStyles = ReturnType<typeof useIndexAdvisorStyles>;
|
|
||||||
export const useIndexAdvisorStyles = makeStyles({
|
|
||||||
indexAdvisorMessage: {
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
},
|
|
||||||
indexAdvisorSuccessIcon: {
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "#107C10",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
indexAdvisorTitle: {
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "1.3rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorTable: {
|
|
||||||
display: "block",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "7rem",
|
|
||||||
},
|
|
||||||
indexAdvisorGrid: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "30px 30px 1fr 50px 120px",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorCheckboxSpacer: {
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
},
|
|
||||||
indexAdvisorChevronSpacer: {
|
|
||||||
width: "24px",
|
|
||||||
},
|
|
||||||
indexAdvisorRowBold: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorRowNormal: {
|
|
||||||
fontWeight: "normal",
|
|
||||||
},
|
|
||||||
indexAdvisorRowImpactHeader: {
|
|
||||||
fontSize: 0,
|
|
||||||
},
|
|
||||||
indexAdvisorRowImpact: {
|
|
||||||
fontWeight: "normal",
|
|
||||||
},
|
|
||||||
indexAdvisorImpactDot: {
|
|
||||||
color: "#0078D4",
|
|
||||||
fontSize: "12px",
|
|
||||||
display: "inline-flex",
|
|
||||||
},
|
|
||||||
indexAdvisorImpactDots: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "4px",
|
|
||||||
},
|
|
||||||
indexAdvisorButtonBar: {
|
|
||||||
padding: "1rem",
|
|
||||||
marginTop: "-7rem",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
},
|
|
||||||
indexAdvisorButtonSpinner: {
|
|
||||||
marginTop: "1rem",
|
|
||||||
minWidth: "320px",
|
|
||||||
minHeight: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "left",
|
|
||||||
justifyContent: "left",
|
|
||||||
marginLeft: "10rem",
|
|
||||||
},
|
|
||||||
indexAdvisorButton: {
|
|
||||||
backgroundColor: "#0078D4",
|
|
||||||
color: "white",
|
|
||||||
padding: "8px 16px",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
marginTop: "1rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
transition: "background 0.2s",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "#005a9e",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import create from "zustand";
|
|
||||||
|
|
||||||
interface QueryMetadataStore {
|
|
||||||
userQuery: string;
|
|
||||||
databaseId: string;
|
|
||||||
containerId: string;
|
|
||||||
setMetadata: (query1: string, db: string, container: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
|
|
||||||
userQuery: "",
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
|
|
||||||
}));
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -58,7 +57,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.id = editable.observable<string>();
|
this.id = editable.observable<string>();
|
||||||
this.id.validations([IsValidCosmosDbResourceId]);
|
this.id.validations([ScriptTabBase._isValidId]);
|
||||||
|
|
||||||
this.editorContent = editable.observable<string>();
|
this.editorContent = editable.observable<string>();
|
||||||
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
||||||
@@ -263,6 +262,29 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
|||||||
this.updateNavbarWithTabsButtons();
|
this.updateNavbarWithTabsButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static _isValidId(id: string): boolean {
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStartCharacters = /^[/?#\\]/;
|
||||||
|
if (invalidStartCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||||
|
if (invalidMiddleCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||||
|
if (invalidEndCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static _isNotEmpty(value: string): boolean {
|
private static _isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
@@ -456,12 +455,11 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
const isValidId: boolean = event.currentTarget.reportValidity();
|
|
||||||
if (this.state.saveButton.visible) {
|
if (this.state.saveButton.visible) {
|
||||||
this.setState({
|
this.setState({
|
||||||
id: event.target.value,
|
id: event.target.value,
|
||||||
saveButton: {
|
saveButton: {
|
||||||
enabled: isValidId,
|
enabled: true,
|
||||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||||
},
|
},
|
||||||
discardButton: {
|
discardButton: {
|
||||||
@@ -530,8 +528,8 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
className="formTree"
|
className="formTree"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
aria-label="Stored procedure id"
|
aria-label="Stored procedure id"
|
||||||
placeholder="Enter the new stored procedure id"
|
placeholder="Enter the new stored procedure id"
|
||||||
size={40}
|
size={40}
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
|
||||||
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome";
|
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
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 { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
|
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||||
|
import errorIcon from "../../../images/close-black.svg";
|
||||||
import errorQuery from "../../../images/error_no_outline.svg";
|
import errorQuery from "../../../images/error_no_outline.svg";
|
||||||
import { useObservable } from "../../hooks/useObservable";
|
import { useObservable } from "../../hooks/useObservable";
|
||||||
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
||||||
@@ -29,6 +34,10 @@ interface TabsProps {
|
|||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||||
|
const [
|
||||||
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
|
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||||
|
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,16 +48,28 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
}, [setKeyboardHandlers]);
|
}, [setKeyboardHandlers]);
|
||||||
|
|
||||||
// Add useEffect to handle context buttons
|
const defaultMessageBarStyles: IMessageBarStyles = {
|
||||||
useEffect(() => {
|
root: {
|
||||||
if (activeReactTab !== undefined) {
|
height: `${LayoutConstants.rowHeight}px`,
|
||||||
// React tabs have no context buttons
|
overflow: "hidden",
|
||||||
useCommandBar.getState().setContextButtons([]);
|
flexDirection: "row",
|
||||||
}
|
},
|
||||||
}, [activeReactTab]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.warning}
|
||||||
|
styles={defaultMessageBarStyles}
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||||
|
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
<div className="nav-tabs-margin">
|
<div className="nav-tabs-margin">
|
||||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||||
{openedReactTabs.map((tab) => (
|
{openedReactTabs.map((tab) => (
|
||||||
@@ -125,17 +146,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
|
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
|
||||||
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
|
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
|
||||||
{isTabExecuting(tab, tabKind) && (
|
{isTabExecuting(tab, tabKind) && (
|
||||||
<Spinner
|
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||||
size={SpinnerSize.small}
|
|
||||||
styles={{
|
|
||||||
circle: {
|
|
||||||
borderTopColor: "var(--colorNeutralForeground1)",
|
|
||||||
borderLeftColor: "var(--colorNeutralForeground1)",
|
|
||||||
borderBottomColor: "var(--colorNeutralForeground1)",
|
|
||||||
borderRightColor: "var(--colorNeutralBackground1)"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{isQueryErrorThrown(tab, tabKind) && (
|
{isQueryErrorThrown(tab, tabKind) && (
|
||||||
<img
|
<img
|
||||||
@@ -186,11 +197,14 @@ const CloseButton = ({
|
|||||||
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||||
|
// tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
|
||||||
}}
|
}}
|
||||||
tabIndex={active ? 0 : undefined}
|
tabIndex={active ? 0 : undefined}
|
||||||
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
||||||
>
|
>
|
||||||
<span className="tabIcon close-Icon" />
|
<span className="tabIcon close-Icon">
|
||||||
|
<img src={errorIcon} title="Close" alt="Close" aria-label="hidden" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -273,6 +287,10 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||||
|
// React tabs have no context buttons.
|
||||||
|
useCommandBar.getState().setContextButtons([]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
switch (activeReactTab) {
|
switch (activeReactTab) {
|
||||||
case ReactTabKind.Connect:
|
case ReactTabKind.Connect:
|
||||||
return userContext.apiType === "VCoreMongo" ? (
|
return userContext.apiType === "VCoreMongo" ? (
|
||||||
@@ -283,11 +301,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
<ConnectTab />
|
<ConnectTab />
|
||||||
);
|
);
|
||||||
case ReactTabKind.Home:
|
case ReactTabKind.Home:
|
||||||
if (isFabricNative()) {
|
|
||||||
return <FabricHomeScreen explorer={explorer} />;
|
|
||||||
} else {
|
|
||||||
return <SplashScreen explorer={explorer} />;
|
return <SplashScreen explorer={explorer} />;
|
||||||
}
|
|
||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return userContext.apiType === "VCoreMongo" ? (
|
return userContext.apiType === "VCoreMongo" ? (
|
||||||
<VcoreMongoQuickstartTab explorer={explorer} />
|
<VcoreMongoQuickstartTab explorer={explorer} />
|
||||||
@@ -297,6 +311,60 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
case ReactTabKind.QueryCopilot:
|
case ReactTabKind.QueryCopilot:
|
||||||
return <QueryCopilotTab explorer={explorer} />;
|
return <QueryCopilotTab explorer={explorer} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
|
if (
|
||||||
|
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||||
|
(userContext.apiType === "Cassandra" &&
|
||||||
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||||
|
ipRules?.length
|
||||||
|
) {
|
||||||
|
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||||
|
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||||
|
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||||
|
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||||
|
);
|
||||||
|
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||||
|
configContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||||
|
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||||
|
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||||
|
|
||||||
|
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||||
|
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !ipRulesIncludeMongoProxy;
|
||||||
|
} else if (userContext.apiType === "Cassandra") {
|
||||||
|
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||||
|
CassandraProxyEndpoints.Mpac,
|
||||||
|
CassandraProxyEndpoints.Prod,
|
||||||
|
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||||
|
|
||||||
|
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||||
|
? [
|
||||||
|
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||||
|
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||||
|
]
|
||||||
|
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||||
|
|
||||||
|
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||||
|
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !ipRulesIncludeCassandraProxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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 FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
import VcoreFirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.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";
|
||||||
@@ -43,11 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={
|
screenshot={FirewallRuleScreenshot}
|
||||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
|
||||||
? VcoreFirewallRuleScreenshot
|
|
||||||
: FirewallRuleScreenshot
|
|
||||||
}
|
|
||||||
shellName={this.getShellNameForDisplay(this.kind)}
|
shellName={this.getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { IDropdownOption, IDropdownStyles, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
import { Dropdown } from "@fluentui/react/lib/Dropdown";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
@@ -18,24 +16,12 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
|||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import TriggerTab from "./TriggerTab";
|
import TriggerTab from "./TriggerTab";
|
||||||
|
|
||||||
const triggerTypeOptions: IDropdownOption[] = [
|
const triggerTypeOptions: IDropdownOption[] = [
|
||||||
{ key: "Pre", text: "Pre" },
|
{ key: "Pre", text: "Pre" },
|
||||||
{ key: "Post", text: "Post" },
|
{ key: "Post", text: "Post" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
|
||||||
label: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
dropdown: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerOperationOptions: IDropdownOption[] = [
|
const triggerOperationOptions: IDropdownOption[] = [
|
||||||
{ key: "All", text: "All" },
|
{ key: "All", text: "All" },
|
||||||
{ key: "Create", text: "Create" },
|
{ key: "Create", text: "Create" },
|
||||||
@@ -206,6 +192,29 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isValidId(id: string): boolean {
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStartCharacters = /^[/?#\\]/;
|
||||||
|
if (invalidStartCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||||
|
if (invalidMiddleCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||||
|
if (invalidEndCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private isNotEmpty(value: string): boolean {
|
private isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
@@ -277,13 +286,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string,
|
newValue?: string,
|
||||||
): void => {
|
): void => {
|
||||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||||
let isValidId: boolean = true;
|
|
||||||
if (inputElement) {
|
|
||||||
isValidId = inputElement.reportValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
|
||||||
this.setState({ triggerId: newValue });
|
this.setState({ triggerId: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,30 +313,12 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
|
||||||
placeholder="Enter the new trigger id"
|
placeholder="Enter the new trigger id"
|
||||||
size={40}
|
size={40}
|
||||||
value={triggerId}
|
value={triggerId}
|
||||||
readOnly={!isIdEditable}
|
readOnly={!isIdEditable}
|
||||||
onChange={this.handleTriggerIdChange}
|
onChange={this.handleTriggerIdChange}
|
||||||
styles={{
|
|
||||||
root: { width: "40%", marginTop: "10px" },
|
|
||||||
fieldGroup: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
subComponentStyles: {
|
|
||||||
label: {
|
|
||||||
root: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder="Trigger Type"
|
placeholder="Trigger Type"
|
||||||
@@ -342,7 +327,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
selectedKey={triggerType}
|
selectedKey={triggerType}
|
||||||
className="trigger-field"
|
className="trigger-field"
|
||||||
onChange={(event, selectedKey) => this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")}
|
onChange={(event, selectedKey) => this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")}
|
||||||
styles={dropdownStyles}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder="Trigger Operation"
|
placeholder="Trigger Operation"
|
||||||
@@ -353,7 +337,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
onChange={(event, selectedKey) =>
|
onChange={(event, selectedKey) =>
|
||||||
this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation")
|
this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation")
|
||||||
}
|
}
|
||||||
styles={dropdownStyles}
|
|
||||||
/>
|
/>
|
||||||
<Label className="trigger-field">Trigger Body</Label>
|
<Label className="trigger-field">Trigger Body</Label>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { isDarkMode } from "hooks/useTheme";
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
@@ -18,6 +15,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
|||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
||||||
|
|
||||||
interface IUserDefinedFunctionTabContentState {
|
interface IUserDefinedFunctionTabContentState {
|
||||||
udfId: string;
|
udfId: string;
|
||||||
udfBody: string;
|
udfBody: string;
|
||||||
@@ -66,13 +64,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string,
|
newValue?: string,
|
||||||
): void => {
|
): void => {
|
||||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||||
let isValidId: boolean = true;
|
|
||||||
if (inputElement) {
|
|
||||||
isValidId = inputElement.reportValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
|
||||||
this.setState({ udfId: newValue });
|
this.setState({ udfId: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,6 +238,29 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isValidId(id: string): boolean {
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStartCharacters = /^[/?#\\]/;
|
||||||
|
if (invalidStartCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||||
|
if (invalidMiddleCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||||
|
if (invalidEndCharacters.test(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private isNotEmpty(value: string): boolean {
|
private isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
@@ -259,10 +274,8 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const { udfId, udfBody, isUdfIdEditable } = this.state;
|
const { udfId, udfBody, isUdfIdEditable } = this.state;
|
||||||
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
||||||
<FluentProvider theme={currentTheme}>
|
|
||||||
<TextField
|
<TextField
|
||||||
className="trigger-field"
|
className="trigger-field"
|
||||||
label="User Defined Function Id"
|
label="User Defined Function Id"
|
||||||
@@ -271,34 +284,12 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
required
|
required
|
||||||
readOnly={!isUdfIdEditable}
|
readOnly={!isUdfIdEditable}
|
||||||
type="text"
|
type="text"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
|
||||||
placeholder="Enter the new user defined function id"
|
placeholder="Enter the new user defined function id"
|
||||||
size={40}
|
size={40}
|
||||||
value={udfId}
|
value={udfId}
|
||||||
onChange={this.handleUdfIdChange}
|
onChange={this.handleUdfIdChange}
|
||||||
styles={{
|
/>
|
||||||
root: {
|
|
||||||
width: "40%",
|
|
||||||
marginTop: "10px",
|
|
||||||
},
|
|
||||||
fieldGroup: {
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
color: "var(--colorNeutralForeground2)",
|
|
||||||
},
|
|
||||||
subComponentStyles: {
|
|
||||||
label: {
|
|
||||||
root: {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>{" "}
|
|
||||||
</FluentProvider>
|
|
||||||
<Label className="trigger-field">User Defined Function Body</Label>
|
<Label className="trigger-field">User Defined Function Body</Label>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"javascript"}
|
language={"javascript"}
|
||||||
|
|||||||
@@ -4,19 +4,16 @@ import {
|
|||||||
FluentProvider,
|
FluentProvider,
|
||||||
FluentProviderSlots,
|
FluentProviderSlots,
|
||||||
Theme,
|
Theme,
|
||||||
createDarkTheme,
|
|
||||||
createLightTheme,
|
createLightTheme,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
mergeClasses,
|
mergeClasses,
|
||||||
shorthands,
|
shorthands,
|
||||||
themeToTokensObject,
|
themeToTokensObject,
|
||||||
webDarkTheme,
|
webLightTheme,
|
||||||
webLightTheme
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
||||||
import { useTheme } from "../../hooks/useTheme";
|
|
||||||
|
|
||||||
export const LayoutConstants = {
|
export const LayoutConstants = {
|
||||||
rowHeight: 32,
|
rowHeight: 32,
|
||||||
@@ -50,7 +47,6 @@ export const CosmosFluentProvider: React.FC<CosmosFluentProviderProps> = ({ chil
|
|||||||
// As we convert components to Fluent UI 9, if we end up with nested FluentProviders, the inner FluentProvider will be a no-op.
|
// As we convert components to Fluent UI 9, if we end up with nested FluentProviders, the inner FluentProvider will be a no-op.
|
||||||
const { isInFluentProvider } = React.useContext(FluentProviderContext);
|
const { isInFluentProvider } = React.useContext(FluentProviderContext);
|
||||||
const styles = useDefaultRootStyles();
|
const styles = useDefaultRootStyles();
|
||||||
const { isDarkMode } = useTheme();
|
|
||||||
|
|
||||||
if (isInFluentProvider) {
|
if (isInFluentProvider) {
|
||||||
// We're already in a fluent context, don't create another.
|
// We're already in a fluent context, don't create another.
|
||||||
@@ -65,7 +61,7 @@ export const CosmosFluentProvider: React.FC<CosmosFluentProviderProps> = ({ chil
|
|||||||
return (
|
return (
|
||||||
<FluentProviderContext.Provider value={{ isInFluentProvider: true }}>
|
<FluentProviderContext.Provider value={{ isInFluentProvider: true }}>
|
||||||
<FluentProvider
|
<FluentProvider
|
||||||
theme={getPlatformTheme(configContext.platform, isDarkMode)}
|
theme={getPlatformTheme(configContext.platform)}
|
||||||
className={mergeClasses(styles.fluentProvider, className)}
|
className={mergeClasses(styles.fluentProvider, className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -118,16 +114,7 @@ const cosmosTheme = {
|
|||||||
sidebarInitialWidth: "300px",
|
sidebarInitialWidth: "300px",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the current theme tokens based on the root theme
|
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosTheme, ...sizeMappings[LayoutSize.Compact] });
|
||||||
export const getThemeTokens = (isDarkMode: boolean) =>
|
|
||||||
themeToTokensObject({
|
|
||||||
...(isDarkMode ? webDarkTheme : webLightTheme),
|
|
||||||
...cosmosTheme,
|
|
||||||
...sizeMappings[LayoutSize.Compact]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize with light theme, will be updated by the provider
|
|
||||||
export const tokens = getThemeTokens(false);
|
|
||||||
|
|
||||||
export const cosmosShorthands = {
|
export const cosmosShorthands = {
|
||||||
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
@@ -137,12 +124,11 @@ export const cosmosShorthands = {
|
|||||||
borderLeft: () => shorthands.borderLeft("1px", "solid", tokens.colorNeutralStroke2),
|
borderLeft: () => shorthands.borderLeft("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPlatformTheme(platform: Platform, isDarkMode: boolean = false): CosmosTheme {
|
export function getPlatformTheme(platform: Platform): CosmosTheme {
|
||||||
const createTheme = isDarkMode ? createDarkTheme : createLightTheme;
|
|
||||||
const baseTheme =
|
const baseTheme =
|
||||||
platform === Platform.Fabric
|
platform === Platform.Fabric
|
||||||
? createTheme(appThemeFabricTealBrandRamp)
|
? createLightTheme(appThemeFabricTealBrandRamp)
|
||||||
: createTheme(appThemePortalBrandRamp);
|
: createLightTheme(appThemePortalBrandRamp);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseTheme,
|
...baseTheme,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -35,6 +34,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";
|
||||||
@@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showScriptsMenus: boolean =
|
const showScriptsMenus: boolean =
|
||||||
!isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
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);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||||
import { Home16Regular } from "@fluentui/react-icons";
|
import { Home16Regular } from "@fluentui/react-icons";
|
||||||
import { AuthType } from "AuthType";
|
import { AuthType } from "AuthType";
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
} from "Explorer/Tree/treeNodeUtil";
|
} from "Explorer/Tree/treeNodeUtil";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -76,7 +76,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
: [];
|
: [];
|
||||||
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
||||||
|
|
||||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
const headerNodes: TreeNode[] =
|
||||||
|
configContext.platform === Platform.Fabric
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
|
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
@@ -753,12 +753,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New SQL Query",
|
"label": "New SQL Query",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -780,12 +774,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New SQL Query",
|
"label": "New SQL Query",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -834,12 +822,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New SQL Query",
|
"label": "New SQL Query",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -888,12 +870,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New SQL Query",
|
"label": "New SQL Query",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -939,145 +915,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": undefined,
|
|
||||||
"className": "collectionNode",
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "standardCollection",
|
|
||||||
"onClick": [Function],
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": undefined,
|
|
||||||
"className": "collectionNode",
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "conflictsCollection",
|
|
||||||
"onClick": [Function],
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "databaseNode",
|
|
||||||
"contextMenu": undefined,
|
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "standardDb",
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": undefined,
|
|
||||||
"className": "collectionNode",
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "sampleItemsCollection",
|
|
||||||
"onClick": [Function],
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "databaseNode",
|
|
||||||
"contextMenu": undefined,
|
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "sharedDatabase",
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": undefined,
|
|
||||||
"className": "collectionNode",
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "schemaCollection",
|
|
||||||
"onClick": [Function],
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "loadMoreNode",
|
|
||||||
"label": "load more",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "databaseNode",
|
|
||||||
"contextMenu": undefined,
|
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "giganticDatabase",
|
|
||||||
"onCollapsed": [Function],
|
|
||||||
"onContextMenuOpen": [Function],
|
|
||||||
"onExpanded": [Function],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
|
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -1135,7 +972,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockSproc4",
|
"label": "mockSproc3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1153,7 +990,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockUdf4",
|
"label": "mockUdf3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1171,7 +1008,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockTrigger4",
|
"label": "mockTrigger3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user