mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-01 23:32:02 +00:00
Compare commits
27 Commits
archie-dar
...
release/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7224dd26c1 | ||
|
|
4c73a1cc47 | ||
|
|
610da6a9a5 | ||
|
|
7812ca4914 | ||
|
|
bc4f18ba79 | ||
|
|
fd2551423d | ||
|
|
508abcd21c | ||
|
|
7774589d60 | ||
|
|
e23ba5ec8c | ||
|
|
75719b3cf0 | ||
|
|
f3f8fd241a | ||
|
|
2dc2e59162 | ||
|
|
6b811b5e76 | ||
|
|
69cf523274 | ||
|
|
2e45d8a2a4 | ||
|
|
8624bf0423 | ||
|
|
db1600d81b | ||
|
|
176bb47cb5 | ||
|
|
b6d17284b5 | ||
|
|
7b7a2817b6 | ||
|
|
f0e32491d7 | ||
|
|
c33c497fd9 | ||
|
|
cec621443d | ||
|
|
b8017763b7 | ||
|
|
59619a856e | ||
|
|
b1f016a796 | ||
|
|
ae3912cbf2 |
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
200
less/forms.less
200
less/forms.less
@@ -1,227 +1,211 @@
|
|||||||
@import "./Common/Constants";
|
@import "./Common/Constants";
|
||||||
|
|
||||||
.dirty {
|
.dirty {
|
||||||
border: 1px solid #9b4f96;
|
border: 1px solid #9b4f96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dirty:focus {
|
.dirty:focus {
|
||||||
outline: 1px solid #9b4f96;
|
outline: 1px solid #9b4f96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabForm {
|
.tabForm {
|
||||||
padding: 12px 20px 20px 20px;
|
padding: 12px 20px 20px 20px;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.storedTabForm {
|
.storedTabForm {
|
||||||
padding-top: @LargeSpace;
|
padding-top: @LargeSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scaleSettingScrollable {
|
.scaleSettingScrollable {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: 100%;
|
height:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disableFocusDefaults[tabindex] {
|
.disableFocusDefaults[tabindex] {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.indexingPolicyEditor {
|
.indexingPolicyEditor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(~"100vh - 400px");
|
height: calc(~"100vh - 400px");
|
||||||
}
|
}
|
||||||
|
|
||||||
.scaleDivison {
|
.scaleDivison {
|
||||||
padding: @MediumSpace 0px @DefaultSpace 0px;
|
padding: @MediumSpace 0px @DefaultSpace 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scaleSettingTitle {
|
.scaleSettingTitle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoScaleThroughputTitle {
|
.autoScaleThroughputTitle {
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoScaleDescription {
|
.autoScaleDescription {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssExpandCollapseIcon {
|
.ssExpandCollapseIcon {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssCollapseIcon {
|
.ssCollapseIcon {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssTextAllignment {
|
.ssTextAllignment {
|
||||||
padding-left: 19px;
|
padding-left: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.throughputStorageBlock {
|
.throughputStorageBlock {
|
||||||
border-top: 1px solid #bbb;
|
border-top: 1px solid #bbb;
|
||||||
border-bottom: 1px solid #bbb;
|
border-bottom: 1px solid #bbb;
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
width: 315px;
|
width: 315px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.storagePadding {
|
.storagePadding {
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
padding-bottom: 14px;
|
padding-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dirtyTextbox {
|
.dirtyTextbox {
|
||||||
width: 176px;
|
width: 176px;
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formTitleFirst {
|
.formTitleFirst {
|
||||||
padding: @DefaultSpace (2 * @MediumSpace);
|
padding: @DefaultSpace (2 * @MediumSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
.formTitleTextbox {
|
.formTitleTextbox {
|
||||||
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
|
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
color: @BaseDark;
|
color: @BaseDark;
|
||||||
border: 1px solid #969696;
|
border: 1px solid #969696;
|
||||||
min-width: @ScaleFormWidth;
|
min-width: @ScaleFormWidth;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e6f8fe;
|
background-color: #e6f8fe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formTitle {
|
.formTitle {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.spUdfTriggerHeader {
|
.spUdfTriggerHeader {
|
||||||
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
|
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
.storedUdfTriggerEditor {
|
.storedUdfTriggerEditor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedRadio.dirty {
|
.selectedRadio.dirty {
|
||||||
background-color: #9b4f96;
|
background-color: #9b4f96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formReadOnly {
|
.formReadOnly {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
border: 1px solid #969696;
|
border: 1px solid #969696;
|
||||||
min-width: 184px;
|
min-width: 184px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration:disabled {
|
.migration:disabled {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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",
|
||||||
|
|||||||
37051
preview/package-lock.json
generated
37051
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -530,6 +530,9 @@ 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 MaterializedViewsLabels {
|
||||||
|
public static readonly NewMaterializedView: string = "New Materialized View";
|
||||||
|
}
|
||||||
export class FeedbackLabels {
|
export class FeedbackLabels {
|
||||||
public static readonly provideFeedback: string = "Provide feedback";
|
public static readonly provideFeedback: string = "Provide feedback";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ export function getWorkloadType(): WorkloadType {
|
|||||||
}
|
}
|
||||||
return workloadType;
|
return workloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMaterializedViewsEnabled(): boolean {
|
||||||
|
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
||||||
|
}
|
||||||
|
|||||||
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
|
import {
|
||||||
|
CreateUpdateOptions,
|
||||||
|
SqlContainerResource,
|
||||||
|
SqlDatabaseCreateUpdateParameters,
|
||||||
|
} from "Utils/arm/generatedClients/cosmos/types";
|
||||||
|
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
|
export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
|
||||||
|
const clearMessage = logConsoleProgress(
|
||||||
|
`Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
|
||||||
|
const resource: SqlContainerResource = {
|
||||||
|
id: params.materializedViewId,
|
||||||
|
};
|
||||||
|
if (params.materializedViewDefinition) {
|
||||||
|
resource.materializedViewDefinition = params.materializedViewDefinition;
|
||||||
|
}
|
||||||
|
if (params.analyticalStorageTtl) {
|
||||||
|
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||||
|
}
|
||||||
|
if (params.indexingPolicy) {
|
||||||
|
resource.indexingPolicy = params.indexingPolicy;
|
||||||
|
}
|
||||||
|
if (params.partitionKey) {
|
||||||
|
resource.partitionKey = params.partitionKey;
|
||||||
|
}
|
||||||
|
if (params.uniqueKeyPolicy) {
|
||||||
|
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||||
|
}
|
||||||
|
if (params.vectorEmbeddingPolicy) {
|
||||||
|
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||||
|
}
|
||||||
|
if (params.fullTextPolicy) {
|
||||||
|
resource.fullTextPolicy = params.fullTextPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createResponse = await createUpdateSqlContainer(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
params.materializedViewId,
|
||||||
|
rpPayload,
|
||||||
|
);
|
||||||
|
logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`);
|
||||||
|
|
||||||
|
return createResponse && (createResponse.properties.resource as Collection);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
// TO DO: Remove when we get RP API Spec with materializedViews
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
return rpResponse?.value?.map((collection: any) => {
|
||||||
|
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
|
||||||
|
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
|
||||||
|
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
|
||||||
|
return collectionDataModel;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
writeLocations?: DatabaseAccountResponseLocation[];
|
writeLocations?: DatabaseAccountResponseLocation[];
|
||||||
enableFreeTier?: boolean;
|
enableFreeTier?: boolean;
|
||||||
enableAnalyticalStorage?: boolean;
|
enableAnalyticalStorage?: boolean;
|
||||||
|
enableMaterializedViews?: boolean;
|
||||||
isVirtualNetworkFilterEnabled?: boolean;
|
isVirtualNetworkFilterEnabled?: boolean;
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
@@ -164,6 +165,8 @@ export interface Collection extends Resource {
|
|||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
computedProperties?: ComputedProperties;
|
computedProperties?: ComputedProperties;
|
||||||
|
materializedViews?: MaterializedView[];
|
||||||
|
materializedViewDefinition?: MaterializedViewDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionsWithPagination {
|
export interface CollectionsWithPagination {
|
||||||
@@ -223,6 +226,17 @@ export interface ComputedProperty {
|
|||||||
|
|
||||||
export type ComputedProperties = ComputedProperty[];
|
export type ComputedProperties = ComputedProperty[];
|
||||||
|
|
||||||
|
export interface MaterializedView {
|
||||||
|
id: string;
|
||||||
|
_rid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterializedViewDefinition {
|
||||||
|
definition: string;
|
||||||
|
sourceCollectionId: string;
|
||||||
|
sourceCollectionRid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PartitionKey {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
kind: "Hash" | "Range" | "MultiHash";
|
kind: "Hash" | "Range" | "MultiHash";
|
||||||
@@ -345,9 +359,7 @@ export interface CreateDatabaseParams {
|
|||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParams {
|
export interface CreateCollectionParamsBase {
|
||||||
createNewDatabase: boolean;
|
|
||||||
collectionId: string;
|
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
@@ -361,6 +373,16 @@ export interface CreateCollectionParams {
|
|||||||
fullTextPolicy?: FullTextPolicy;
|
fullTextPolicy?: FullTextPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||||
|
createNewDatabase: boolean;
|
||||||
|
collectionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||||
|
materializedViewId: string;
|
||||||
|
materializedViewDefinition: MaterializedViewDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
export interface VectorEmbeddingPolicy {
|
||||||
vectorEmbeddings: VectorEmbedding[];
|
vectorEmbeddings: VectorEmbedding[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -143,6 +143,8 @@ export interface Collection extends CollectionBase {
|
|||||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
documentIds: ko.ObservableArray<DocumentId>;
|
documentIds: ko.ObservableArray<DocumentId>;
|
||||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
|
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
||||||
|
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
||||||
|
|
||||||
cassandraKeys: CassandraTableKeys;
|
cassandraKeys: CassandraTableKeys;
|
||||||
cassandraSchema: CassandraTableKey[];
|
cassandraSchema: CassandraTableKey[];
|
||||||
@@ -462,6 +464,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,5 +1,11 @@
|
|||||||
|
import { MaterializedViewsLabels } from "Common/Constants";
|
||||||
|
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
|
import {
|
||||||
|
AddMaterializedViewPanel,
|
||||||
|
AddMaterializedViewPanelProps,
|
||||||
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -164,6 +170,24 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
|
||||||
|
items.push({
|
||||||
|
label: MaterializedViewsLabels.NewMaterializedView,
|
||||||
|
onClick: () => {
|
||||||
|
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
||||||
|
explorer: container,
|
||||||
|
sourceContainer: selectedCollection,
|
||||||
|
};
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
|
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +1,5 @@
|
|||||||
import { IndexingPolicy } from "@azure/cosmos";
|
|
||||||
import { act } from "@testing-library/react";
|
|
||||||
import { AuthType } from "AuthType";
|
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 { Features } from "Platform/Hosted/extractFeatures";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -291,47 +288,3 @@ describe("SettingsComponent", () => {
|
|||||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
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,
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
ThroughputBucketsComponent,
|
ThroughputBucketsComponent,
|
||||||
ThroughputBucketsComponentProps,
|
ThroughputBucketsComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
} 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";
|
||||||
@@ -46,6 +45,10 @@ import {
|
|||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
|
import {
|
||||||
|
MaterializedViewComponent,
|
||||||
|
MaterializedViewComponentProps,
|
||||||
|
} from "./SettingsSubComponents/MaterializedViewComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps,
|
MongoIndexingPolicyComponentProps,
|
||||||
@@ -66,6 +69,7 @@ import {
|
|||||||
parseConflictResolutionMode,
|
parseConflictResolutionMode,
|
||||||
parseConflictResolutionProcedure,
|
parseConflictResolutionProcedure,
|
||||||
} from "./SettingsUtils";
|
} from "./SettingsUtils";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
content: JSX.Element;
|
content: JSX.Element;
|
||||||
@@ -162,12 +166,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowComputedPropertiesEditor: boolean;
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
|
private isMaterializedView: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
private throughputBucketsEnabled: boolean;
|
private throughputBucketsEnabled: boolean;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
private unsubscribe: () => void;
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -179,6 +184,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
|
this.isMaterializedView =
|
||||||
|
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
|
||||||
@@ -298,19 +305,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());
|
||||||
@@ -783,6 +779,7 @@ 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;
|
const throughputBuckets = this.offer?.throughputBuckets;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -934,31 +931,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 ||
|
||||||
@@ -1168,7 +1144,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 +1161,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">
|
||||||
@@ -1303,6 +1279,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||||
|
collection: this.collection,
|
||||||
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1366,106 +1347,40 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isMaterializedView) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.MaterializedViewTab,
|
||||||
|
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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: {
|
const pivotItemProps: IPivotItemProps = {
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
itemKey: SettingsV2TabTypes[tab.tab],
|
||||||
color: 'var(--colorNeutralForeground1)',
|
style: { marginTop: 20 },
|
||||||
selectors: {
|
headerText: getTabTitle(tab.tab),
|
||||||
'& .ms-Pivot-link': {
|
};
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
return (
|
||||||
'& .ms-Pivot-link.is-selected::before': {
|
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||||
backgroundColor: 'var(--colorCompoundBrandBackground)'
|
{tab.content}
|
||||||
},
|
</PivotItem>
|
||||||
|
);
|
||||||
}
|
});
|
||||||
},
|
|
||||||
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 (
|
return (
|
||||||
<div className="settingsV2MainContainer" style={{
|
<div className="settingsV2MainContainer">
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
position: 'relative'
|
|
||||||
} as React.CSSProperties}>
|
|
||||||
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
||||||
<div>This table shared throughput is configured at the keyspace</div>
|
<div>This table shared throughput is configured at the keyspace</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="settingsV2TabsContainer" style={{
|
<div className="settingsV2TabsContainer">
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||||
color: 'var(--colorNeutralForeground1)',
|
|
||||||
position: 'relative',
|
|
||||||
padding: '20px 24px'
|
|
||||||
} as React.CSSProperties}>
|
|
||||||
<Pivot {...pivotProps} styles={pivotStyles}>
|
|
||||||
{tabs.map((tab) => {
|
|
||||||
const pivotItemProps: IPivotItemProps = {
|
|
||||||
itemKey: SettingsV2TabTypes[tab.tab],
|
|
||||||
style: {
|
|
||||||
marginTop: 20,
|
|
||||||
backgroundColor: 'var(--colorNeutralBackground1)',
|
|
||||||
color: 'var(--colorNeutralForeground1)'
|
|
||||||
},
|
|
||||||
headerText: getTabTitle(tab.tab),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
|
||||||
<Stack styles={contentStyles}>
|
|
||||||
{tab.content}
|
|
||||||
</Stack>
|
|
||||||
</PivotItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</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,71 +87,20 @@ 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) {
|
||||||
});
|
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||||
if (this.indexingPolicyEditor) {
|
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
this.props.logIndexingPolicySuccessMessage();
|
||||||
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection, container } from "../TestUtils";
|
||||||
|
import { MaterializedViewComponent } from "./MaterializedViewComponent";
|
||||||
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
|
describe("MaterializedViewComponent", () => {
|
||||||
|
let testCollection: typeof collection;
|
||||||
|
let testExplorer: typeof container;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = { ...collection };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only the source component when materializedViewDefinition is missing", () => {
|
||||||
|
testCollection.materializedViews([
|
||||||
|
{ id: "view1", _rid: "rid1" },
|
||||||
|
{ id: "view2", _rid: "rid2" },
|
||||||
|
]);
|
||||||
|
testCollection.materializedViewDefinition(null);
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only the target component when materializedViews is missing", () => {
|
||||||
|
testCollection.materializedViews(null);
|
||||||
|
testCollection.materializedViewDefinition({
|
||||||
|
definition: "SELECT * FROM c WHERE c.id = 1",
|
||||||
|
sourceCollectionId: "source1",
|
||||||
|
sourceCollectionRid: "rid123",
|
||||||
|
});
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders neither component when both are missing", () => {
|
||||||
|
testCollection.materializedViews(null);
|
||||||
|
testCollection.materializedViewDefinition(null);
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
|
export interface MaterializedViewComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
|
||||||
|
const isTargetContainer = !!collection?.materializedViewDefinition();
|
||||||
|
const isSourceContainer = !!collection?.materializedViews();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
||||||
|
<Text>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||||
|
</Link>{" "}
|
||||||
|
about how to define materialized views and how to use them.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
|
||||||
|
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection, container } from "../TestUtils";
|
||||||
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
|
||||||
|
describe("MaterializedViewSourceComponent", () => {
|
||||||
|
let testCollection: typeof collection;
|
||||||
|
let testExplorer: typeof container;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = { ...collection };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the PrimaryButton", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates when new materialized views are provided", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
|
||||||
|
// Simulating an update by modifying the observable directly
|
||||||
|
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
|
||||||
|
|
||||||
|
wrapper.setProps({ collection: testCollection });
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { MaterializedViewsLabels } from "Common/Constants";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||||
|
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export interface MaterializedViewSourceComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
|
||||||
|
collection,
|
||||||
|
explorer,
|
||||||
|
}) => {
|
||||||
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
||||||
|
|
||||||
|
const materializedViews = collection?.materializedViews() ?? [];
|
||||||
|
|
||||||
|
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] with collection id.
|
||||||
|
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
|
||||||
|
let definition = "";
|
||||||
|
let partitionKey: string[] = [];
|
||||||
|
|
||||||
|
useDatabases.getState().databases.find((database) => {
|
||||||
|
const collection = database.collections().find((collection) => collection.id() === viewId);
|
||||||
|
if (collection) {
|
||||||
|
const materializedViewDefinition = collection.materializedViewDefinition();
|
||||||
|
materializedViewDefinition && (definition = materializedViewDefinition.definition);
|
||||||
|
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { definition, partitionKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
//JSON value for the editor using the fetched id and definitions.
|
||||||
|
const jsonValue = JSON.stringify(
|
||||||
|
materializedViews.map((view) => {
|
||||||
|
const { definition, partitionKey } = getViewDetails(view.id);
|
||||||
|
return {
|
||||||
|
name: view.id,
|
||||||
|
partitionKey: partitionKey.join(", "),
|
||||||
|
definition,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize Monaco editor with the computed JSON value.
|
||||||
|
useEffect(() => {
|
||||||
|
let disposed = false;
|
||||||
|
const initMonaco = async () => {
|
||||||
|
const monacoInstance = await loadMonaco();
|
||||||
|
if (disposed || !editorContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
||||||
|
value: jsonValue,
|
||||||
|
language: "json",
|
||||||
|
ariaLabel: "Materialized Views JSON",
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
initMonaco();
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
editorRef.current?.dispose();
|
||||||
|
};
|
||||||
|
}, [jsonValue]);
|
||||||
|
|
||||||
|
// Update the editor when the jsonValue changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.setValue(jsonValue);
|
||||||
|
}
|
||||||
|
}, [jsonValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
ref={editorContainerRef}
|
||||||
|
style={{
|
||||||
|
height: 250,
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
text="Add view"
|
||||||
|
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||||
|
onClick={() =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
|
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Text } from "@fluentui/react";
|
||||||
|
import { Collection } from "Contracts/ViewModels";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection } from "../TestUtils";
|
||||||
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
|
describe("MaterializedViewTargetComponent", () => {
|
||||||
|
let testCollection: Collection;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = {
|
||||||
|
...collection,
|
||||||
|
materializedViewDefinition: collection.materializedViewDefinition,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays the source container ID", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays the materialized view definition", () => {
|
||||||
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export interface MaterializedViewTargetComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
||||||
|
const materializedViewDefinition = collection?.materializedViewDefinition();
|
||||||
|
|
||||||
|
const textHeadingStyle = {
|
||||||
|
root: { fontWeight: "600", fontSize: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const valueBoxStyle = {
|
||||||
|
root: {
|
||||||
|
backgroundColor: "#f3f3f3",
|
||||||
|
padding: "5px 10px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{materializedViewDefinition?.definition}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
@@ -210,7 +191,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
</MessageBar>
|
</MessageBar>
|
||||||
) : (
|
) : (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
id="timeToLive"
|
id="timeToLive"
|
||||||
label="Time to Live"
|
label="Time to Live"
|
||||||
selectedKey={this.props.timeToLive}
|
selectedKey={this.props.timeToLive}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,36 +288,36 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const costElement = (): JSX.Element => {
|
const costElement = (): JSX.Element => {
|
||||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||||
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}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export enum SettingsV2TabTypes {
|
|||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
ThroughputBucketsTab,
|
||||||
|
MaterializedViewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@@ -171,6 +172,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Container Policies";
|
return "Container Policies";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||||
return "Throughput Buckets";
|
return "Throughput Buckets";
|
||||||
|
case SettingsV2TabTypes.MaterializedViewTab:
|
||||||
|
return "Materialized Views (Preview)";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ export const collection = {
|
|||||||
]),
|
]),
|
||||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||||
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
||||||
|
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
||||||
|
{ id: "view1", _rid: "rid1" },
|
||||||
|
{ id: "view2", _rid: "rid2" },
|
||||||
|
]),
|
||||||
|
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
||||||
|
definition: "SELECT * FROM c WHERE c.id = 1",
|
||||||
|
sourceCollectionId: "source1",
|
||||||
|
sourceCollectionRid: "rid123",
|
||||||
|
}),
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
|
"materializedViewDefinition": [Function],
|
||||||
|
"materializedViews": [Function],
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -69,27 +71,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],
|
||||||
@@ -160,6 +141,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
|
"materializedViewDefinition": [Function],
|
||||||
|
"materializedViews": [Function],
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -169,27 +152,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 +191,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}
|
||||||
@@ -308,6 +262,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
|
"materializedViewDefinition": [Function],
|
||||||
|
"materializedViews": [Function],
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -317,27 +273,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],
|
||||||
@@ -408,16 +343,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Global Secondary Index (Preview)"
|
headerText="Materialized Views (Preview)"
|
||||||
itemKey="GlobalSecondaryIndexTab"
|
itemKey="MaterializedViewTab"
|
||||||
key="GlobalSecondaryIndexTab"
|
key="MaterializedViewTab"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"marginTop": 20,
|
"marginTop": 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GlobalSecondaryIndexComponent
|
<MaterializedViewComponent
|
||||||
collection={
|
collection={
|
||||||
{
|
{
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
@@ -467,28 +402,8 @@ 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": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
|
|||||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
|
|||||||
@@ -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,7 +4,6 @@
|
|||||||
* 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 { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -12,6 +11,7 @@ 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 { 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);
|
||||||
@@ -107,16 +96,14 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const rootStyle = isFabric()
|
const rootStyle = isFabric()
|
||||||
? {
|
? {
|
||||||
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 +111,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)}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
ICommandBarItemProps,
|
ICommandBarItemProps,
|
||||||
IComponentAsProps,
|
IComponentAsProps,
|
||||||
IconType,
|
IconType,
|
||||||
IDropdownOption,
|
IDropdownOption,
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -21,11 +21,25 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
|||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import {
|
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
FullTextPoliciesComponent,
|
|
||||||
getFullTextLanguageOptions,
|
|
||||||
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
|
import {
|
||||||
|
AllPropertiesIndexed,
|
||||||
|
AnalyticalStorageContent,
|
||||||
|
ContainerVectorPolicyTooltipContent,
|
||||||
|
FullTextPolicyDefault,
|
||||||
|
getPartitionKey,
|
||||||
|
getPartitionKeyName,
|
||||||
|
getPartitionKeyPlaceHolder,
|
||||||
|
getPartitionKeyTooltipText,
|
||||||
|
isFreeTierAccount,
|
||||||
|
isSynapseLinkEnabled,
|
||||||
|
parseUniqueKeys,
|
||||||
|
scrollToSection,
|
||||||
|
SharedDatabaseDefault,
|
||||||
|
shouldShowAnalyticalStoreOptions,
|
||||||
|
UniqueKeysHeader,
|
||||||
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
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 { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -42,16 +56,15 @@ 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";
|
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||||
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
|
import Explorer from "../../Explorer";
|
||||||
import Explorer from "../Explorer";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||||
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||||
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -59,40 +72,6 @@ export interface AddCollectionPanelProps {
|
|||||||
isQuickstart?: boolean;
|
isQuickstart?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [],
|
|
||||||
excludedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "Number",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "String",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
excludedPaths: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
vectorEmbeddings: [],
|
vectorEmbeddings: [],
|
||||||
};
|
};
|
||||||
@@ -145,7 +124,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: this.getPartitionKey(),
|
partitionKey: getPartitionKey(props.isQuickstart),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
@@ -161,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
fullTextPolicy: FullTextPolicyDefault,
|
||||||
fullTextIndexes: [],
|
fullTextIndexes: [],
|
||||||
fullTextPolicyValidated: true,
|
fullTextPolicyValidated: true,
|
||||||
};
|
};
|
||||||
@@ -175,7 +154,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||||
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||||
this.scrollToSection("panelContainer");
|
scrollToSection("panelContainer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +171,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
{!this.state.errorMessage && isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||||
messageType="info"
|
messageType="info"
|
||||||
@@ -352,8 +331,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"
|
||||||
@@ -400,10 +379,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={this.isFreeTierAccount()}
|
isFreeTier={isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
@@ -460,8 +439,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"
|
||||||
@@ -580,17 +559,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{this.getPartitionKeyName()}
|
{getPartitionKeyName()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={this.getPartitionKeyTooltipText()}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ariaLabel={this.getPartitionKeyTooltipText()}
|
ariaLabel={getPartitionKeyTooltipText()}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -604,8 +580,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
placeholder={this.getPartitionKeyPlaceHolder()}
|
placeholder={getPartitionKeyPlaceHolder()}
|
||||||
aria-label={this.getPartitionKeyName()}
|
aria-label={getPartitionKeyName()}
|
||||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||||
value={this.state.partitionKey}
|
value={this.state.partitionKey}
|
||||||
@@ -643,8 +619,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
tabIndex={index > 0 ? 1 : 0}
|
tabIndex={index > 0 ? 1 : 0}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
placeholder={getPartitionKeyPlaceHolder(index)}
|
||||||
aria-label={this.getPartitionKeyName()}
|
aria-label={getPartitionKeyName()}
|
||||||
pattern={".*"}
|
pattern={".*"}
|
||||||
title={""}
|
title={""}
|
||||||
value={subPartitionKey}
|
value={subPartitionKey}
|
||||||
@@ -735,10 +711,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={this.isFreeTierAccount()}
|
isFreeTier={isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
@@ -753,27 +729,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
{UniqueKeysHeader()}
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Unique keys
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={
|
|
||||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={
|
|
||||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
||||||
@@ -821,10 +777,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{this.getAnalyticalStorageContent()}
|
{AnalyticalStorageContent()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
@@ -832,7 +788,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={this.state.enableAnalyticalStore}
|
checked={this.state.enableAnalyticalStore}
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
disabled={!isSynapseLinkEnabled()}
|
||||||
aria-label="Enable analytical store"
|
aria-label="Enable analytical store"
|
||||||
aria-checked={this.state.enableAnalyticalStore}
|
aria-checked={this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -847,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.enableAnalyticalStore}
|
checked={!this.state.enableAnalyticalStore}
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
disabled={!isSynapseLinkEnabled()}
|
||||||
aria-label="Disable analytical store"
|
aria-label="Disable analytical store"
|
||||||
aria-checked={!this.state.enableAnalyticalStore}
|
aria-checked={!this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -861,7 +817,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!this.isSynapseLinkEnabled() && (
|
{!isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
@@ -891,9 +847,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
@@ -919,7 +875,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Full Text Search Policy"
|
title="Container Full Text Search Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
//TODO: uncomment when learn more text becomes available
|
//TODO: uncomment when learn more text becomes available
|
||||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||||
@@ -947,7 +903,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
this.scrollToSection("collapsibleAdvancedSectionContent");
|
scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
@@ -1057,31 +1013,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPartitionKeyName(isLowerCase?: boolean): string {
|
|
||||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
|
|
||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartitionKeyPlaceHolder(index?: number): string {
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
return "e.g., categoryId";
|
|
||||||
case "Gremlin":
|
|
||||||
return "e.g., /address";
|
|
||||||
case "SQL":
|
|
||||||
return `${
|
|
||||||
index === undefined
|
|
||||||
? "Required - first partition key e.g., /TenantId"
|
|
||||||
: index === 0
|
|
||||||
? "second partition key e.g., /UserId"
|
|
||||||
: "third partition key e.g., /SessionId"
|
|
||||||
}`;
|
|
||||||
default:
|
|
||||||
return "e.g., /address/zipCode";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
if (event.target.checked && !this.state.createNewDatabase) {
|
if (event.target.checked && !this.state.createNewDatabase) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -1169,48 +1100,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return !!selectedDatabase?.offer();
|
return !!selectedDatabase?.offer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFreeTierAccount(): boolean {
|
|
||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFreeTierIndexingText(): string {
|
private getFreeTierIndexingText(): string {
|
||||||
return this.state.enableIndexing
|
return this.state.enableIndexing
|
||||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPartitionKeyTooltipText(): string {
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
|
||||||
}
|
|
||||||
|
|
||||||
let tooltipText = `The ${this.getPartitionKeyName(
|
|
||||||
true,
|
|
||||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
|
||||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltipText;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartitionKey(): string {
|
|
||||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault) {
|
|
||||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault2) {
|
|
||||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
|
||||||
}
|
|
||||||
if (this.props.isQuickstart) {
|
|
||||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartitionKeySubtext(): string {
|
private getPartitionKeySubtext(): string {
|
||||||
if (
|
if (
|
||||||
userContext.features.partitionKeyDefault &&
|
userContext.features.partitionKeyDefault &&
|
||||||
@@ -1222,34 +1117,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
|
||||||
impacting the performance of transactional workloads.{" "}
|
|
||||||
<Link
|
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
|
||||||
target="_blank"
|
|
||||||
href="https://aka.ms/analytical-store-overview"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
|
||||||
queries.{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: uncomment when learn more text becomes available
|
//TODO: uncomment when learn more text becomes available
|
||||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||||
// return (
|
// return (
|
||||||
@@ -1280,7 +1147,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||||
if (!this.isFreeTierAccount()) {
|
if (!isFreeTierAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1289,39 +1156,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
: this.isSelectedDatabaseSharedThroughput();
|
: this.isSelectedDatabaseSharedThroughput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
|
||||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "SQL":
|
|
||||||
case "Mongo":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSynapseLinkEnabled(): boolean {
|
|
||||||
if (!userContext.databaseAccount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { properties } = userContext.databaseAccount;
|
|
||||||
if (!properties) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.enableAnalyticalStorage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties.capabilities?.some(
|
|
||||||
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldShowVectorSearchParameters() {
|
private shouldShowVectorSearchParameters() {
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
@@ -1402,11 +1236,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTtl(): number {
|
private getAnalyticalStorageTtl(): number {
|
||||||
if (!this.isSynapseLinkEnabled()) {
|
if (!isSynapseLinkEnabled()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.shouldShowAnalyticalStoreOptions()) {
|
if (!shouldShowAnalyticalStoreOptions()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1420,10 +1254,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollToSection(id: string): void {
|
|
||||||
document.getElementById(id)?.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSampleDBName(): string {
|
private getSampleDBName(): string {
|
||||||
const existingSampleDBs = useDatabases
|
const existingSampleDBs = useDatabases
|
||||||
.getState()
|
.getState()
|
||||||
@@ -1458,7 +1288,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
partitionKeyString = "/'$pk'";
|
partitionKeyString = "/'$pk'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
|
import React from "react";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
|
export function getPartitionKeyTooltipText(): string {
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltipText = `The ${getPartitionKeyName(
|
||||||
|
true,
|
||||||
|
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPartitionKeyName(isLowerCase?: boolean): string {
|
||||||
|
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
|
|
||||||
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPartitionKeyPlaceHolder(index?: number): string {
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "e.g., categoryId";
|
||||||
|
case "Gremlin":
|
||||||
|
return "e.g., /address";
|
||||||
|
case "SQL":
|
||||||
|
return `${
|
||||||
|
index === undefined
|
||||||
|
? "Required - first partition key e.g., /TenantId"
|
||||||
|
: index === 0
|
||||||
|
? "second partition key e.g., /UserId"
|
||||||
|
: "third partition key e.g., /SessionId"
|
||||||
|
}`;
|
||||||
|
default:
|
||||||
|
return "e.g., /address/zipCode";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPartitionKey(isQuickstart?: boolean): string {
|
||||||
|
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault) {
|
||||||
|
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault2) {
|
||||||
|
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||||
|
}
|
||||||
|
if (isQuickstart) {
|
||||||
|
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFreeTierAccount(): boolean {
|
||||||
|
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UniqueKeysHeader(): JSX.Element {
|
||||||
|
const tooltipContent =
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Unique keys
|
||||||
|
</Text>
|
||||||
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||||
|
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "SQL":
|
||||||
|
case "Mongo":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnalyticalStorageContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
||||||
|
the performance of transactional workloads.{" "}
|
||||||
|
<Link
|
||||||
|
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||||
|
target="_blank"
|
||||||
|
href="https://aka.ms/analytical-store-overview"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSynapseLinkEnabled(): boolean {
|
||||||
|
if (!userContext.databaseAccount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { properties } = userContext.databaseAccount;
|
||||||
|
if (!properties) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.enableAnalyticalStorage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties.capabilities?.some(
|
||||||
|
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollToSection(id: string): void {
|
||||||
|
document.getElementById(id)?.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||||
|
queries.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
|
||||||
|
if (uniqueKeys?.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
|
||||||
|
uniqueKeys.forEach((uniqueKey: string) => {
|
||||||
|
if (uniqueKey) {
|
||||||
|
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
|
||||||
|
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
|
||||||
|
if (trimmedPaths?.length > 0) {
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
trimmedPaths.map((path) => {
|
||||||
|
const transformedPath = path.split(".").join("/");
|
||||||
|
if (transformedPath[0] !== "/") {
|
||||||
|
return "/" + transformedPath;
|
||||||
|
}
|
||||||
|
return transformedPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueKeyPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [],
|
||||||
|
excludedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
|
||||||
|
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
||||||
|
fullTextPaths: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "Number",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "String",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [],
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
@@ -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={
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
|
export interface AddMVAdvancedComponentProps {
|
||||||
|
useHashV1: boolean;
|
||||||
|
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}
|
||||||
|
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
|
||||||
|
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
||||||
|
|
||||||
|
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
||||||
|
setUseHashV1(isChecked);
|
||||||
|
setSubPartitionKeys([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Advanced"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
|
||||||
|
scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
|
<Checkbox
|
||||||
|
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||||
|
checked={useHashV1}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||||
|
}}
|
||||||
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
|
||||||
|
useHashV1CheckboxOnChange(isChecked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
|
||||||
|
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
|
||||||
|
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
AnalyticalStorageContent,
|
||||||
|
isSynapseLinkEnabled,
|
||||||
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
|
|
||||||
|
export interface AddMVAnalyticalStoreComponentProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
enableAnalyticalStore: boolean;
|
||||||
|
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
|
||||||
|
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
||||||
|
|
||||||
|
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
||||||
|
if (checked && !enableAnalyticalStore) {
|
||||||
|
setEnableAnalyticalStore(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
|
||||||
|
if (checked && enableAnalyticalStore) {
|
||||||
|
setEnableAnalyticalStore(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{AnalyticalStorageContent()}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<div role="radiogroup">
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={enableAnalyticalStore}
|
||||||
|
disabled={!isSynapseLinkEnabled()}
|
||||||
|
aria-label="Enable analytical store"
|
||||||
|
aria-checked={enableAnalyticalStore}
|
||||||
|
name="analyticalStore"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="enableAnalyticalStoreBtn"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">On</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={!enableAnalyticalStore}
|
||||||
|
disabled={!isSynapseLinkEnabled()}
|
||||||
|
aria-label="Disable analytical store"
|
||||||
|
aria-checked={!enableAnalyticalStore}
|
||||||
|
name="analyticalStore"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="disableAnalyticalStoreBtn"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Off</span>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{!isSynapseLinkEnabled() && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Text variant="small">
|
||||||
|
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
|
||||||
|
Enable Synapse Link for this Cosmos DB account.{" "}
|
||||||
|
<Link
|
||||||
|
href="https://aka.ms/cosmosdb-synapselink"
|
||||||
|
target="_blank"
|
||||||
|
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||||
|
className="capacitycalculator-link"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<DefaultButton
|
||||||
|
text="Enable"
|
||||||
|
onClick={() => explorer.openEnableSynapseLinkDialog()}
|
||||||
|
style={{ height: 27, width: 80 }}
|
||||||
|
styles={{ label: { fontSize: 12 } }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
|
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface AddMVFullTextSearchComponentProps {
|
||||||
|
fullTextPolicy: FullTextPolicy;
|
||||||
|
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
||||||
|
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
||||||
|
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
|
||||||
|
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Container Full Text Search Policy"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
|
<FullTextPoliciesComponent
|
||||||
|
fullTextPolicy={fullTextPolicy}
|
||||||
|
onFullTextPathChange={(
|
||||||
|
fullTextPolicy: FullTextPolicy,
|
||||||
|
fullTextIndexes: FullTextIndex[],
|
||||||
|
fullTextPolicyValidated: boolean,
|
||||||
|
) => {
|
||||||
|
setFullTextPolicy(fullTextPolicy);
|
||||||
|
setFullTextIndexes(fullTextIndexes);
|
||||||
|
setFullTextPolicyValidated(fullTextPolicyValidated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import {
|
||||||
|
getPartitionKeyName,
|
||||||
|
getPartitionKeyPlaceHolder,
|
||||||
|
getPartitionKeyTooltipText,
|
||||||
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface AddMVPartitionKeyComponentProps {
|
||||||
|
partitionKey?: string;
|
||||||
|
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
subPartitionKeys: string[];
|
||||||
|
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
useHashV1: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
|
||||||
|
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
||||||
|
|
||||||
|
const partitionKeyValueOnChange = (value: string): void => {
|
||||||
|
if (!partitionKey && !value.startsWith("/")) {
|
||||||
|
setPartitionKey("/" + value);
|
||||||
|
} else {
|
||||||
|
setPartitionKey(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
|
||||||
|
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
|
||||||
|
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
|
||||||
|
updatedSubPartitionKeys[index] = "/" + value.trim();
|
||||||
|
} else {
|
||||||
|
updatedSubPartitionKeys[index] = value.trim();
|
||||||
|
}
|
||||||
|
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Partition key
|
||||||
|
</Text>
|
||||||
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addmaterializedView-partitionKeyValue"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
placeholder={getPartitionKeyPlaceHolder()}
|
||||||
|
aria-label={getPartitionKeyName()}
|
||||||
|
pattern=".*"
|
||||||
|
value={partitionKey}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
partitionKeyValueOnChange(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
border: "solid",
|
||||||
|
borderWidth: "0px 0px 1px 1px",
|
||||||
|
marginRight: "5px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addMaterializedView-partitionKeyValue"
|
||||||
|
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
|
||||||
|
className="panelTextField"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
|
||||||
|
aria-label={getPartitionKeyName()}
|
||||||
|
pattern={".*"}
|
||||||
|
title={""}
|
||||||
|
value={subPartitionKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27 }}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedSubPartitionKeys = subPartitionKeys.filter(
|
||||||
|
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
|
||||||
|
);
|
||||||
|
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<DefaultButton
|
||||||
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
|
hidden={useHashV1}
|
||||||
|
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||||
|
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||||
|
>
|
||||||
|
Add hierarchical partition key
|
||||||
|
</DefaultButton>
|
||||||
|
{subPartitionKeys.length > 0 && (
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
|
||||||
|
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
|
||||||
|
JavaScript V3 SDK.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Checkbox, Stack } from "@fluentui/react";
|
||||||
|
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
|
||||||
|
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import React from "react";
|
||||||
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
|
|
||||||
|
export interface AddMVThroughputComponentProps {
|
||||||
|
enableDedicatedThroughput: boolean;
|
||||||
|
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
isSelectedSourceContainerSharedThroughput: () => boolean;
|
||||||
|
showCollectionThroughputInput: () => boolean;
|
||||||
|
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
|
||||||
|
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
|
||||||
|
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
|
||||||
|
const {
|
||||||
|
enableDedicatedThroughput,
|
||||||
|
setEnabledDedicatedThroughput,
|
||||||
|
isSelectedSourceContainerSharedThroughput,
|
||||||
|
showCollectionThroughputInput,
|
||||||
|
materializedViewThroughputOnChange,
|
||||||
|
isMaterializedViewAutoscaleOnChange,
|
||||||
|
setIsThroughputCapExceeded,
|
||||||
|
isCostAknowledgedOnChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<Checkbox
|
||||||
|
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||||
|
checked={enableDedicatedThroughput}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center" },
|
||||||
|
}}
|
||||||
|
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{showCollectionThroughputInput() && (
|
||||||
|
<ThroughputInput
|
||||||
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
||||||
|
isDatabase={false}
|
||||||
|
isSharded={false}
|
||||||
|
isFreeTier={isFreeTierAccount()}
|
||||||
|
isQuickstart={false}
|
||||||
|
setThroughputValue={(throughput: number) => {
|
||||||
|
materializedViewThroughputOnChange(throughput);
|
||||||
|
}}
|
||||||
|
setIsAutoscale={(isAutoscale: boolean) => {
|
||||||
|
isMaterializedViewAutoscaleOnChange(isAutoscale);
|
||||||
|
}}
|
||||||
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
||||||
|
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
||||||
|
}}
|
||||||
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||||
|
isCostAknowledgedOnChange(isAcknowledged);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { ActionButton, IconButton, Stack } from "@fluentui/react";
|
||||||
|
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
|
export interface AddMVUniqueKeysComponentProps {
|
||||||
|
uniqueKeys: string[];
|
||||||
|
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
|
||||||
|
const { uniqueKeys, setUniqueKeys } = props;
|
||||||
|
|
||||||
|
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
||||||
|
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
|
||||||
|
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return uniqueKey;
|
||||||
|
});
|
||||||
|
setUniqueKeys(updatedUniqueKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
|
||||||
|
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
|
||||||
|
setUniqueKeys(updatedUniqueKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUniqueKeyOnClick = (): void => {
|
||||||
|
setUniqueKeys([...uniqueKeys, ""]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{UniqueKeysHeader()}
|
||||||
|
|
||||||
|
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={
|
||||||
|
userContext.apiType === "Mongo"
|
||||||
|
? "Comma separated paths e.g. firstName,address.zipCode"
|
||||||
|
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||||
|
}
|
||||||
|
className="panelTextField"
|
||||||
|
autoFocus
|
||||||
|
value={uniqueKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27 }}
|
||||||
|
onClick={() => {
|
||||||
|
deleteUniqueKeyOnClick(uniqueKeyIndex);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
iconProps={{ iconName: "Add" }}
|
||||||
|
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
|
||||||
|
onClick={() => {
|
||||||
|
addUniqueKeyOnClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add unique key
|
||||||
|
</ActionButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
|
import {
|
||||||
|
ContainerVectorPolicyTooltipContent,
|
||||||
|
scrollToSection,
|
||||||
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface AddMVVectorSearchComponentProps {
|
||||||
|
vectorEmbeddingPolicy: VectorEmbedding[];
|
||||||
|
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
||||||
|
vectorIndexingPolicy: VectorIndex[];
|
||||||
|
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
||||||
|
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
|
||||||
|
const {
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
setVectorEmbeddingPolicy,
|
||||||
|
vectorIndexingPolicy,
|
||||||
|
setVectorIndexingPolicy,
|
||||||
|
setVectorPolicyValidated,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Container Vector Policy"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
|
}}
|
||||||
|
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||||
|
>
|
||||||
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
|
<VectorEmbeddingPoliciesComponent
|
||||||
|
vectorEmbeddings={vectorEmbeddingPolicy}
|
||||||
|
vectorIndexes={vectorIndexingPolicy}
|
||||||
|
onVectorEmbeddingChange={(
|
||||||
|
vectorEmbeddingPolicy: VectorEmbedding[],
|
||||||
|
vectorIndexingPolicy: VectorIndex[],
|
||||||
|
vectorPolicyValidated: boolean,
|
||||||
|
) => {
|
||||||
|
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
|
||||||
|
setVectorIndexingPolicy(vectorIndexingPolicy);
|
||||||
|
setVectorPolicyValidated(vectorPolicyValidated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { shallow, ShallowWrapper } from "enzyme";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
AddMaterializedViewPanel,
|
||||||
|
AddMaterializedViewPanelProps,
|
||||||
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
const props: AddMaterializedViewPanelProps = {
|
||||||
|
explorer: new Explorer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AddMaterializedViewPanel", () => {
|
||||||
|
it("render default panel", () => {
|
||||||
|
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||||
|
<AddMaterializedViewPanel {...props} />,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render form", () => {
|
||||||
|
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||||
|
<AddMaterializedViewPanel {...props} />,
|
||||||
|
);
|
||||||
|
const form = wrapper.find("form").first();
|
||||||
|
expect(form).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
import {
|
||||||
|
DirectionalHint,
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuItemType,
|
||||||
|
Icon,
|
||||||
|
IDropdownOption,
|
||||||
|
Link,
|
||||||
|
Separator,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
|
||||||
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { Collection, Database } from "Contracts/ViewModels";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
AllPropertiesIndexed,
|
||||||
|
FullTextPolicyDefault,
|
||||||
|
getPartitionKey,
|
||||||
|
isSynapseLinkEnabled,
|
||||||
|
parseUniqueKeys,
|
||||||
|
scrollToSection,
|
||||||
|
shouldShowAnalyticalStoreOptions,
|
||||||
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
|
import {
|
||||||
|
chooseSourceContainerStyle,
|
||||||
|
chooseSourceContainerStyles,
|
||||||
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
|
||||||
|
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
|
||||||
|
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
|
||||||
|
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
|
||||||
|
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
|
||||||
|
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
|
||||||
|
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
|
||||||
|
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
|
||||||
|
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
||||||
|
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
||||||
|
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { CollectionCreation } from "Shared/Constants";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
|
|
||||||
|
export interface AddMaterializedViewPanelProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
sourceContainer?: Collection;
|
||||||
|
}
|
||||||
|
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
|
||||||
|
const { explorer, sourceContainer } = props;
|
||||||
|
|
||||||
|
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
||||||
|
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
||||||
|
const [materializedViewId, setMaterializedViewId] = useState<string>();
|
||||||
|
const [definition, setDefinition] = useState<string>();
|
||||||
|
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
||||||
|
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
||||||
|
const [useHashV1, setUseHashV1] = useState<boolean>();
|
||||||
|
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
|
||||||
|
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
|
||||||
|
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
|
||||||
|
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
|
||||||
|
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
|
||||||
|
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
|
||||||
|
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
|
||||||
|
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
|
||||||
|
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
|
||||||
|
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sourceContainerOptions: IDropdownOption[] = [];
|
||||||
|
useDatabases.getState().databases.forEach((database: Database) => {
|
||||||
|
sourceContainerOptions.push({
|
||||||
|
key: database.rid,
|
||||||
|
text: database.id(),
|
||||||
|
itemType: DropdownMenuItemType.Header,
|
||||||
|
});
|
||||||
|
|
||||||
|
database.collections().forEach((collection: Collection) => {
|
||||||
|
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
|
||||||
|
sourceContainerOptions.push({
|
||||||
|
key: collection.rid,
|
||||||
|
text: collection.id(),
|
||||||
|
disabled: isMaterializedView,
|
||||||
|
...(isMaterializedView && {
|
||||||
|
title: "This is a materialized view.",
|
||||||
|
}),
|
||||||
|
data: collection,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setSourceContainerOptions(sourceContainerOptions);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToSection("panelContainer");
|
||||||
|
}, [errorMessage]);
|
||||||
|
|
||||||
|
let materializedViewThroughput: number;
|
||||||
|
let isMaterializedViewAutoscale: boolean;
|
||||||
|
let isCostAcknowledged: boolean;
|
||||||
|
|
||||||
|
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
|
||||||
|
materializedViewThroughput = materializedViewThroughputValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
|
||||||
|
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
||||||
|
isCostAcknowledged = isCostAcknowledgedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedSourceContainerSharedThroughput = (): boolean => {
|
||||||
|
if (!selectedSourceContainer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!selectedSourceContainer.getDatabase().offer();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCollectionThroughputInput = (): boolean => {
|
||||||
|
if (isServerlessAccount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableDedicatedThroughput) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showVectorSearchParameters = (): boolean => {
|
||||||
|
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFullTextSearchParameters = (): boolean => {
|
||||||
|
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnalyticalStorageTtl = (): number => {
|
||||||
|
if (!isSynapseLinkEnabled()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldShowAnalyticalStoreOptions()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableAnalyticalStore) {
|
||||||
|
// TODO: always default to 90 days once the backend hotfix is deployed
|
||||||
|
return userContext.features.ttl90Days
|
||||||
|
? Constants.AnalyticalStorageTtl.Days90
|
||||||
|
: Constants.AnalyticalStorageTtl.Infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInputs = (): boolean => {
|
||||||
|
if (!selectedSourceContainer) {
|
||||||
|
setErrorMessage("Please select a source container");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||||
|
const errorMessage = isMaterializedViewAutoscale
|
||||||
|
? "Please acknowledge the estimated monthly spend."
|
||||||
|
: "Please acknowledge the estimated daily spend.";
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
|
||||||
|
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showVectorSearchParameters()) {
|
||||||
|
if (!vectorPolicyValidated) {
|
||||||
|
setErrorMessage("Please fix errors in container vector policy");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullTextPolicyValidated) {
|
||||||
|
setErrorMessage("Please fix errors in container full text search policy");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||||
|
event?.preventDefault();
|
||||||
|
|
||||||
|
if (!validateInputs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const materializedViewIdTrimmed: string = materializedViewId.trim();
|
||||||
|
|
||||||
|
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
|
||||||
|
sourceCollectionId: sourceContainer.id(),
|
||||||
|
definition: definition,
|
||||||
|
};
|
||||||
|
|
||||||
|
const partitionKeyTrimmed: string = partitionKey.trim();
|
||||||
|
|
||||||
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
|
||||||
|
const partitionKeyVersion = useHashV1 ? undefined : 2;
|
||||||
|
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
|
||||||
|
? {
|
||||||
|
paths: [
|
||||||
|
partitionKeyTrimmed,
|
||||||
|
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
|
||||||
|
],
|
||||||
|
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||||
|
version: partitionKeyVersion,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
|
||||||
|
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
|
||||||
|
|
||||||
|
if (showVectorSearchParameters()) {
|
||||||
|
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
|
||||||
|
vectorEmbeddingPolicyFinal = {
|
||||||
|
vectorEmbeddings: vectorEmbeddingPolicy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showFullTextSearchParameters()) {
|
||||||
|
indexingPolicy.fullTextIndexes = fullTextIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const telemetryData: TelemetryProcessor.TelemetryData = {
|
||||||
|
database: {
|
||||||
|
id: selectedSourceContainer.databaseId,
|
||||||
|
shared: isSelectedSourceContainerSharedThroughput(),
|
||||||
|
},
|
||||||
|
collection: {
|
||||||
|
id: materializedViewIdTrimmed,
|
||||||
|
throughput: materializedViewThroughput,
|
||||||
|
isAutoscale: isMaterializedViewAutoscale,
|
||||||
|
partitionKeyPaths,
|
||||||
|
uniqueKeyPolicy,
|
||||||
|
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
||||||
|
},
|
||||||
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
|
};
|
||||||
|
|
||||||
|
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
||||||
|
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
|
||||||
|
|
||||||
|
let offerThroughput: number;
|
||||||
|
let autoPilotMaxThroughput: number;
|
||||||
|
|
||||||
|
if (!databaseLevelThroughput) {
|
||||||
|
if (isMaterializedViewAutoscale) {
|
||||||
|
autoPilotMaxThroughput = materializedViewThroughput;
|
||||||
|
} else {
|
||||||
|
offerThroughput = materializedViewThroughput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
|
||||||
|
materializedViewId: materializedViewIdTrimmed,
|
||||||
|
materializedViewDefinition: materializedViewDefinition,
|
||||||
|
databaseId: selectedSourceContainer.databaseId,
|
||||||
|
databaseLevelThroughput: databaseLevelThroughput,
|
||||||
|
offerThroughput: offerThroughput,
|
||||||
|
autoPilotMaxThroughput: autoPilotMaxThroughput,
|
||||||
|
analyticalStorageTtl: getAnalyticalStorageTtl(),
|
||||||
|
indexingPolicy: indexingPolicy,
|
||||||
|
partitionKey: partitionKeyPaths,
|
||||||
|
uniqueKeyPolicy: uniqueKeyPolicy,
|
||||||
|
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
|
||||||
|
fullTextPolicy: fullTextPolicy,
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createMaterializedView(createMaterializedViewParams);
|
||||||
|
await explorer.refreshAllDatabases();
|
||||||
|
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
|
||||||
|
useSidePanel.getState().closeSidePanel();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage: string = getErrorMessage(error);
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
setShowErrorDetails(true);
|
||||||
|
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
||||||
|
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
|
||||||
|
} finally {
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
|
||||||
|
{errorMessage && (
|
||||||
|
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||||
|
)}
|
||||||
|
<div className="panelMainContent">
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Source container id
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Dropdown
|
||||||
|
placeholder="Choose source container"
|
||||||
|
options={sourceContainerOptions}
|
||||||
|
defaultSelectedKey={sourceContainer?.rid}
|
||||||
|
styles={chooseSourceContainerStyles()}
|
||||||
|
style={chooseSourceContainerStyle()}
|
||||||
|
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
||||||
|
/>
|
||||||
|
<Separator className="panelSeparator" />
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
View container id
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
id="materializedViewId"
|
||||||
|
type="text"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
placeholder={`e.g., viewByEmailId`}
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
value={materializedViewId}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Materialized View Definition
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={
|
||||||
|
<Link
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||||
|
target="blank"
|
||||||
|
>
|
||||||
|
Learn more about defining materialized views.
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
id="materializedViewDefinition"
|
||||||
|
type="text"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={"SELECT c.email, c.accountId FROM c"}
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
value={definition || ""}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
||||||
|
/>
|
||||||
|
<AddMVPartitionKeyComponent
|
||||||
|
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
||||||
|
/>
|
||||||
|
<AddMVThroughputComponent
|
||||||
|
{...{
|
||||||
|
enableDedicatedThroughput,
|
||||||
|
setEnabledDedicatedThroughput,
|
||||||
|
isSelectedSourceContainerSharedThroughput,
|
||||||
|
showCollectionThroughputInput,
|
||||||
|
materializedViewThroughputOnChange,
|
||||||
|
isMaterializedViewAutoscaleOnChange,
|
||||||
|
setIsThroughputCapExceeded,
|
||||||
|
isCostAknowledgedOnChange,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
||||||
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
|
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
||||||
|
)}
|
||||||
|
{showVectorSearchParameters() && (
|
||||||
|
<AddMVVectorSearchComponent
|
||||||
|
{...{
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
setVectorEmbeddingPolicy,
|
||||||
|
vectorIndexingPolicy,
|
||||||
|
setVectorIndexingPolicy,
|
||||||
|
vectorPolicyValidated,
|
||||||
|
setVectorPolicyValidated,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showFullTextSearchParameters() && (
|
||||||
|
<AddMVFullTextSearchComponent
|
||||||
|
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||||
|
{isExecuting && <PanelLoadingScreen />}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
|
||||||
|
import { CSSProperties } from "react";
|
||||||
|
|
||||||
|
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
|
||||||
|
return {
|
||||||
|
title: { height: 27, lineHeight: 27 },
|
||||||
|
dropdownItem: { fontSize: 12 },
|
||||||
|
dropdownItemDisabled: { fontSize: 12 },
|
||||||
|
dropdownItemSelected: { fontSize: 12 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chooseSourceContainerStyle(): CSSProperties {
|
||||||
|
return { width: 300, fontSize: 12 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AddMaterializedViewPanel render default panel 1`] = `
|
||||||
|
<form
|
||||||
|
className="panelFormWrapper"
|
||||||
|
id="panelMaterializedView"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="panelMainContent"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mandatoryStar"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<Text
|
||||||
|
className="panelTextBold"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Source container id
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Dropdown
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Choose source container"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontSize": 12,
|
||||||
|
"width": 300,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"dropdownItem": {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"dropdownItemDisabled": {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"dropdownItemSelected": {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"height": 27,
|
||||||
|
"lineHeight": 27,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Separator
|
||||||
|
className="panelSeparator"
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mandatoryStar"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<Text
|
||||||
|
className="panelTextBold"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
View container id
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
aria-required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
className="panelTextField"
|
||||||
|
id="materializedViewId"
|
||||||
|
onChange={[Function]}
|
||||||
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
|
placeholder="e.g., viewByEmailId"
|
||||||
|
required={true}
|
||||||
|
size={40}
|
||||||
|
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mandatoryStar"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<Text
|
||||||
|
className="panelTextBold"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Materialized View Definition
|
||||||
|
</Text>
|
||||||
|
<StyledTooltipHostBase
|
||||||
|
content={
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||||
|
target="blank"
|
||||||
|
>
|
||||||
|
Learn more about defining materialized views.
|
||||||
|
</StyledLinkBase>
|
||||||
|
}
|
||||||
|
directionalHint={4}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="panelInfoIcon"
|
||||||
|
iconName="Info"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</StyledTooltipHostBase>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
aria-required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
className="panelTextField"
|
||||||
|
id="materializedViewDefinition"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="SELECT c.email, c.accountId FROM c"
|
||||||
|
required={true}
|
||||||
|
size={40}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<AddMVPartitionKeyComponent
|
||||||
|
partitionKey=""
|
||||||
|
setPartitionKey={[Function]}
|
||||||
|
setSubPartitionKeys={[Function]}
|
||||||
|
subPartitionKeys={[]}
|
||||||
|
/>
|
||||||
|
<AddMVThroughputComponent
|
||||||
|
isCostAknowledgedOnChange={[Function]}
|
||||||
|
isMaterializedViewAutoscaleOnChange={[Function]}
|
||||||
|
isSelectedSourceContainerSharedThroughput={[Function]}
|
||||||
|
materializedViewThroughputOnChange={[Function]}
|
||||||
|
setEnabledDedicatedThroughput={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
|
showCollectionThroughputInput={[Function]}
|
||||||
|
/>
|
||||||
|
<AddMVUniqueKeysComponent
|
||||||
|
setUniqueKeys={[Function]}
|
||||||
|
uniqueKeys={[]}
|
||||||
|
/>
|
||||||
|
<AddMVAnalyticalStoreComponent
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEnableAnalyticalStore={[Function]}
|
||||||
|
/>
|
||||||
|
<AddMVAdvancedComponent
|
||||||
|
setSubPartitionKeys={[Function]}
|
||||||
|
setUseHashV1={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<PanelFooterComponent
|
||||||
|
buttonLabel="OK"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
|
|||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
|
|||||||
@@ -8,15 +8,23 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
|
mergeClasses,
|
||||||
shorthands,
|
shorthands,
|
||||||
SplitButton
|
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 { MaterializedViewsLabels } from "Common/Constants";
|
||||||
|
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } 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 {
|
||||||
|
AddMaterializedViewPanel,
|
||||||
|
AddMaterializedViewPanelProps,
|
||||||
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
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 { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -24,10 +32,8 @@ 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 +41,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 +82,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 +104,6 @@ const useSidebarStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
treeContainer: {
|
|
||||||
flex: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
|
||||||
color: tokens.colorNeutralForeground1,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GlobalCommandsProps {
|
interface GlobalCommandsProps {
|
||||||
@@ -197,6 +168,25 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMaterializedViewsEnabled()) {
|
||||||
|
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
||||||
|
explorer,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
id: "new_materialized_view",
|
||||||
|
label: MaterializedViewsLabels.NewMaterializedView,
|
||||||
|
icon: <Add16Regular />,
|
||||||
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
|
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}, [explorer]);
|
}, [explorer]);
|
||||||
|
|
||||||
@@ -285,7 +275,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) {
|
||||||
@@ -340,7 +329,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,11 +360,12 @@ 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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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 () => {
|
private clearMostRecent = (): void => {
|
||||||
while (subscriptions.length) {
|
|
||||||
subscriptions.pop().dispose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [state, setState] = React.useState({});
|
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
@@ -2153,18 +2144,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,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 () => {
|
||||||
@@ -776,7 +756,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,4 +1,3 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
|
||||||
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";
|
||||||
@@ -16,6 +15,8 @@ import { userContext } from "UserContext";
|
|||||||
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";
|
||||||
@@ -39,14 +40,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
}, [setKeyboardHandlers]);
|
}, [setKeyboardHandlers]);
|
||||||
|
|
||||||
// Add useEffect to handle context buttons
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeReactTab !== undefined) {
|
|
||||||
// React tabs have no context buttons
|
|
||||||
useCommandBar.getState().setContextButtons([]);
|
|
||||||
}
|
|
||||||
}, [activeReactTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
<div className="nav-tabs-margin">
|
<div className="nav-tabs-margin">
|
||||||
@@ -125,17 +118,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 +169,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 +259,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" ? (
|
||||||
@@ -297,6 +287,6 @@ 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]}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,46 +274,22 @@ 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"
|
id="entityTimeId"
|
||||||
id="entityTimeId"
|
autoFocus
|
||||||
autoFocus
|
required
|
||||||
required
|
readOnly={!isUdfIdEditable}
|
||||||
readOnly={!isUdfIdEditable}
|
type="text"
|
||||||
type="text"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
placeholder="Enter the new user defined function id"
|
||||||
title={ValidCosmosDbIdDescription}
|
size={40}
|
||||||
placeholder="Enter the new user defined function id"
|
value={udfId}
|
||||||
size={40}
|
onChange={this.handleUdfIdChange}
|
||||||
value={udfId}
|
/>
|
||||||
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,
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
public usageSizeInKB: ko.Observable<number>;
|
public usageSizeInKB: ko.Observable<number>;
|
||||||
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
|
public materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
||||||
|
public materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
||||||
|
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
@@ -124,6 +126,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.requestSchema = data.requestSchema;
|
this.requestSchema = data.requestSchema;
|
||||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||||
this.computedProperties = ko.observable(data.computedProperties);
|
this.computedProperties = ko.observable(data.computedProperties);
|
||||||
|
this.materializedViews = ko.observable(data.materializedViews);
|
||||||
|
this.materializedViewDefinition = ko.observable(data.materializedViewDefinition);
|
||||||
|
|
||||||
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
||||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
|||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { AddCollectionPanel } from "../Panes/AddCollectionPanel";
|
import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||||
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|||||||
@@ -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 { Collection } from "Contracts/ViewModels";
|
||||||
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 {
|
||||||
@@ -60,7 +61,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
|
|
||||||
const databaseTreeNodes = useMemo(() => {
|
const databaseTreeNodes = useMemo(() => {
|
||||||
return userContext.authType === AuthType.ResourceToken
|
return userContext.authType === AuthType.ResourceToken
|
||||||
? createResourceTokenTreeNodes(resourceTokenCollection)
|
? createResourceTokenTreeNodes(resourceTokenCollection as Collection)
|
||||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
||||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -760,7 +760,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -787,7 +787,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -841,7 +841,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -895,7 +895,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -953,7 +953,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -974,7 +974,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1010,7 +1010,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1046,7 +1046,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1208,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1311,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1445,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1625,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1799,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1897,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2031,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2211,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2266,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ jest.mock("Explorer/Tree/Trigger", () => {
|
|||||||
jest.mock("Common/DatabaseAccountUtility", () => {
|
jest.mock("Common/DatabaseAccountUtility", () => {
|
||||||
return {
|
return {
|
||||||
isPublicInternetAccessAllowed: () => true,
|
isPublicInternetAccessAllowed: () => true,
|
||||||
|
isMaterializedViewsEnabled: () => false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,6 +135,15 @@ const baseCollection = {
|
|||||||
kind: "hash",
|
kind: "hash",
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
|
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
||||||
|
{ id: "view1", _rid: "rid1" },
|
||||||
|
{ id: "view2", _rid: "rid2" },
|
||||||
|
]),
|
||||||
|
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
||||||
|
definition: "SELECT * FROM c WHERE c.id = 1",
|
||||||
|
sourceCollectionId: "source1",
|
||||||
|
sourceCollectionRid: "rid123",
|
||||||
|
}),
|
||||||
storedProcedures: ko.observableArray([]),
|
storedProcedures: ko.observableArray([]),
|
||||||
userDefinedFunctions: ko.observableArray([]),
|
userDefinedFunctions: ko.observableArray([]),
|
||||||
triggers: ko.observableArray([]),
|
triggers: ko.observableArray([]),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
@@ -29,6 +29,7 @@ export const shouldShowScriptNodes = (): boolean => {
|
|||||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||||
|
const MaterializedViewCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
||||||
|
|
||||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
const updatedSampleTree: TreeNode = {
|
const updatedSampleTree: TreeNode = {
|
||||||
@@ -80,7 +81,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
return [updatedSampleTree];
|
return [updatedSampleTree];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -110,7 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@@ -228,7 +229,7 @@ export const buildCollectionNode = (
|
|||||||
children: children,
|
children: children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
iconSrc: TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
|
|||||||
156
src/Main.tsx
156
src/Main.tsx
@@ -2,16 +2,14 @@
|
|||||||
import "./ReactDevTools";
|
import "./ReactDevTools";
|
||||||
|
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme, useTheme } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
import { FluentProvider, makeStyles, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
|
||||||
import { Platform } from "ConfigContext";
|
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
import "allotment/dist/style.css";
|
import "allotment/dist/style.css";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import "../externals/jquery-ui.min.css";
|
import "../externals/jquery-ui.min.css";
|
||||||
import "../externals/jquery-ui.min.js";
|
import "../externals/jquery-ui.min.js";
|
||||||
@@ -21,7 +19,7 @@ import "../externals/jquery.dataTables.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.css";
|
import "../externals/jquery.typeahead.min.css";
|
||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { SidePanel } from "Explorer/Panes/PanelContainerComponent";
|
import { Platform } from "ConfigContext";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { SidebarContainer } from "Explorer/Sidebar";
|
import { SidebarContainer } from "Explorer/Sidebar";
|
||||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
@@ -48,7 +46,6 @@ import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
|||||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -57,32 +54,21 @@ import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
|||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import "./Explorer/Panes/PanelComponent.less";
|
import "./Explorer/Panes/PanelComponent.less";
|
||||||
|
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import "./Libs/jquery";
|
import "./Libs/jquery";
|
||||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import { isDarkMode } from "./hooks/useTheme";
|
|
||||||
// Initialize icons before React is loaded
|
|
||||||
initializeIcons(undefined, { disableWarnings: true });
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
initializeIcons();
|
||||||
root: {
|
|
||||||
height: "100vh",
|
|
||||||
width: "100vw",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App: React.FunctionComponent = () => {
|
||||||
const config = useConfig();
|
|
||||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||||
const styles = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
|
const config = useConfig();
|
||||||
if (config?.platform === Platform.Fabric) {
|
if (config?.platform === Platform.Fabric) {
|
||||||
loadTheme(appThemeFabric);
|
loadTheme(appThemeFabric);
|
||||||
import("../less/documentDBFabric.less");
|
import("../less/documentDBFabric.less");
|
||||||
@@ -95,111 +81,51 @@ const App = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="Main" className={styles.root}>
|
<KeyboardShortcutRoot>
|
||||||
<KeyboardShortcutRoot>
|
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
|
||||||
<div
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
className="flexContainer"
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
style={{
|
{/* Main Command Bar - Start */}
|
||||||
flex: 1,
|
<CommandBar container={explorer} />
|
||||||
display: "flex",
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
flexDirection: "column",
|
<SidebarContainer explorer={explorer} />
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
{/* Collections Tree and Tabs - End */}
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}}
|
|
||||||
aria-hidden="false"
|
|
||||||
data-test="DataExplorerRoot"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
id="divExplorer"
|
className="dataExplorerErrorConsoleContainer"
|
||||||
className="flexContainer hideOverflows"
|
role="contentinfo"
|
||||||
style={{
|
aria-label="Notification console"
|
||||||
flex: 1,
|
id="explorerNotificationConsole"
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<NotificationConsole />
|
||||||
{/* Main Command Bar - Start */}
|
|
||||||
<CommandBar container={explorer} />
|
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
|
||||||
<SidebarContainer explorer={explorer} />
|
|
||||||
{/* Collections Tree and Tabs - End */}
|
|
||||||
<div
|
|
||||||
className="dataExplorerErrorConsoleContainer"
|
|
||||||
role="contentinfo"
|
|
||||||
aria-label="Notification console"
|
|
||||||
id="explorerNotificationConsole"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NotificationConsole />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<SidePanel />
|
|
||||||
<Dialog />
|
|
||||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
|
||||||
{<SQLQuickstartTutorial />}
|
|
||||||
{<MongoQuickstartTutorial />}
|
|
||||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
|
||||||
</div>
|
</div>
|
||||||
</KeyboardShortcutRoot>
|
<SidePanel />
|
||||||
</div>
|
<Dialog />
|
||||||
);
|
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||||
};
|
{<SQLQuickstartTutorial />}
|
||||||
|
{<MongoQuickstartTutorial />}
|
||||||
const Root: React.FC = () => {
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
// Force dark theme
|
</div>
|
||||||
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
|
</KeyboardShortcutRoot>
|
||||||
|
|
||||||
// Apply theme to body for Fluent UI v8 components
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDarkMode) {
|
|
||||||
document.body.classList.add("isDarkMode");
|
|
||||||
document.body.style.backgroundColor = "var(--colorNeutralBackground1)";
|
|
||||||
document.body.style.color = "var(--colorNeutralForeground1)";
|
|
||||||
loadTheme(appThemeFabric);
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove("isDarkMode");
|
|
||||||
document.body.style.backgroundColor = "";
|
|
||||||
document.body.style.color = "";
|
|
||||||
loadTheme(appThemeFabric);
|
|
||||||
}
|
|
||||||
}, [isDarkMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorBoundary>
|
|
||||||
<FluentProvider theme={currentTheme}>
|
|
||||||
<App />
|
|
||||||
</FluentProvider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mainElement = document.getElementById("Main");
|
const mainElement = document.getElementById("Main");
|
||||||
if (mainElement) {
|
ReactDOM.render(<App />, mainElement);
|
||||||
ReactDOM.render(<Root />, mainElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoadingExplorer(): JSX.Element {
|
function LoadingExplorer(): JSX.Element {
|
||||||
const styles = useStyles();
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className="splashLoaderContainer">
|
||||||
<div className="splashLoaderContainer">
|
<div className="splashLoaderContentContainer">
|
||||||
<div className="splashLoaderContentContainer">
|
<p className="connectExplorerContent">
|
||||||
<p className="connectExplorerContent">
|
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
</p>
|
||||||
</p>
|
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
Welcome to Azure Cosmos DB
|
||||||
Welcome to Azure Cosmos DB
|
</p>
|
||||||
</p>
|
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
Connecting...
|
||||||
Connecting...
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
@@ -35,26 +35,21 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|||||||
export class PhoenixClient {
|
export class PhoenixClient {
|
||||||
private armResourceId: string;
|
private armResourceId: string;
|
||||||
private containerHealthHandler: NodeJS.Timeout;
|
private containerHealthHandler: NodeJS.Timeout;
|
||||||
private retryOptions: Options = {
|
private retryOptions: promiseRetry.Options = {
|
||||||
retries: Notebook.retryAttempts,
|
retries: Notebook.retryAttempts,
|
||||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||||
minTimeout: Notebook.retryAttemptDelayMs,
|
minTimeout: Notebook.retryAttemptDelayMs,
|
||||||
};
|
};
|
||||||
private abortController: AbortController;
|
|
||||||
private abortSignal: AbortSignal;
|
|
||||||
|
|
||||||
constructor(armResourceId: string) {
|
constructor(armResourceId: string) {
|
||||||
this.armResourceId = armResourceId;
|
this.armResourceId = armResourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||||
this.initializeCancelEventListener();
|
|
||||||
|
|
||||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||||
retries: 4,
|
retries: 4,
|
||||||
maxTimeout: 20000,
|
maxTimeout: 20000,
|
||||||
minTimeout: 20000,
|
minTimeout: 20000,
|
||||||
signal: this.abortSignal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,17 +270,6 @@ export class PhoenixClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeCancelEventListener(): void {
|
|
||||||
this.abortController = new AbortController();
|
|
||||||
this.abortSignal = this.abortController.signal;
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
|
|
||||||
this.abortController.abort(new AbortError("Request canceled"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||||
const errInfo = jsonData;
|
const errInfo = jsonData;
|
||||||
switch (errInfo?.type) {
|
switch (errInfo?.type) {
|
||||||
|
|||||||
@@ -11,24 +11,13 @@ import { updateUserContext } from "../UserContext";
|
|||||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||||
import "./SelfServe.less";
|
import "./SelfServe.less";
|
||||||
import { SelfServeComponent } from "./SelfServeComponent";
|
import { SelfServeComponent } from "./SelfServeComponent";
|
||||||
import { SelfServeBaseClass, SelfServeDescriptor } from "./SelfServeTypes";
|
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
const loadTranslationFile = async (
|
const loadTranslationFile = async (className: string): Promise<void> => {
|
||||||
className: string | SelfServeBaseClass,
|
|
||||||
selfServeType?: SelfServeType,
|
|
||||||
): Promise<void> => {
|
|
||||||
const language = i18n.languages[0];
|
const language = i18n.languages[0];
|
||||||
let namespace: string; // className is used as a key to retrieve the localized strings
|
const fileName = `${className}.json`;
|
||||||
let fileName: string;
|
|
||||||
if (className instanceof SelfServeBaseClass) {
|
|
||||||
fileName = `${selfServeType}.json`;
|
|
||||||
namespace = className.constructor.name;
|
|
||||||
} else {
|
|
||||||
fileName = `${className}.json`;
|
|
||||||
namespace = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let translations: any;
|
let translations: any;
|
||||||
@@ -39,16 +28,12 @@ const loadTranslationFile = async (
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
||||||
}
|
}
|
||||||
|
i18n.addResourceBundle(language, className, translations.default, true);
|
||||||
i18n.addResourceBundle(language, namespace, translations.default, true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadTranslations = async (
|
const loadTranslations = async (className: string): Promise<void> => {
|
||||||
className: string | SelfServeBaseClass,
|
|
||||||
selfServeType: SelfServeType,
|
|
||||||
): Promise<void> => {
|
|
||||||
await loadTranslationFile("Common");
|
await loadTranslationFile("Common");
|
||||||
await loadTranslationFile(className, selfServeType);
|
await loadTranslationFile(className);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||||
@@ -56,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
case SelfServeType.example: {
|
case SelfServeType.example: {
|
||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
const selfServeExample = new SelfServeExample.default();
|
const selfServeExample = new SelfServeExample.default();
|
||||||
await loadTranslations(selfServeExample, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return selfServeExample.toSelfServeDescriptor();
|
return selfServeExample.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.sqlx: {
|
case SelfServeType.sqlx: {
|
||||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||||
const sqlX = new SqlX.default();
|
const sqlX = new SqlX.default();
|
||||||
await loadTranslations(sqlX, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return sqlX.toSelfServeDescriptor();
|
return sqlX.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.graphapicompute: {
|
case SelfServeType.graphapicompute: {
|
||||||
@@ -70,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||||
);
|
);
|
||||||
const graphAPICompute = new GraphAPICompute.default();
|
const graphAPICompute = new GraphAPICompute.default();
|
||||||
await loadTranslations(graphAPICompute, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return graphAPICompute.toSelfServeDescriptor();
|
return graphAPICompute.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.materializedviewsbuilder: {
|
case SelfServeType.materializedviewsbuilder: {
|
||||||
@@ -78,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||||
);
|
);
|
||||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||||
await loadTranslations(materializedViewsBuilder, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { WithTranslation } from "react-i18next";
|
import { WithTranslation } from "react-i18next";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
@@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
private static readonly defaultRetryIntervalInMs = 30000;
|
private static readonly defaultRetryIntervalInMs = 30000;
|
||||||
private smartUiGeneratorClassName: string;
|
private smartUiGeneratorClassName: string;
|
||||||
private retryIntervalInMs: number;
|
private retryIntervalInMs: number;
|
||||||
private retryOptions: Options;
|
private retryOptions: promiseRetry.Options;
|
||||||
private translationFunction: TFunction;
|
private translationFunction: TFunction;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|||||||
@@ -197,11 +197,6 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
|||||||
const priceMap = new Map<string, Map<string, number>>();
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
let billingCurrency;
|
let billingCurrency;
|
||||||
for (const region of map.keys()) {
|
for (const region of map.keys()) {
|
||||||
// if no offering id is found for that region, skipping calling price API
|
|
||||||
const subMap = map.get(region);
|
|
||||||
if (!subMap || subMap.size === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const regionPriceMap = new Map<string, number>();
|
const regionPriceMap = new Map<string, number>();
|
||||||
const regionShortName = await getRegionShortName(region);
|
const regionShortName = await getRegionShortName(region);
|
||||||
const requestBody: OfferingIdRequest = {
|
const requestBody: OfferingIdRequest = {
|
||||||
@@ -242,7 +237,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
|
return { priceMap: undefined, billingCurrency: undefined };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -291,6 +286,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||||
return new Map<string, Map<string, string>>();
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -227,13 +227,11 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
|||||||
let costPerHour = 0;
|
let costPerHour = 0;
|
||||||
let costBreakdown = "";
|
let costBreakdown = "";
|
||||||
for (const regionItem of regions) {
|
for (const regionItem of regions) {
|
||||||
const incrementalCost = priceMap?.get(regionItem.locationName)?.get(skuName.replace("Cosmos.", ""));
|
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||||
if (incrementalCost === undefined) {
|
if (incrementalCost === undefined) {
|
||||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||||
} else if (incrementalCost === 0) {
|
} else if (incrementalCost === 0) {
|
||||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||||
} else if (currencyCode === undefined) {
|
|
||||||
throw new Error(`Currency code not found in price map.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let regionalInstanceCount = instanceCount;
|
let regionalInstanceCount = instanceCount;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export enum Action {
|
export enum Action {
|
||||||
CollapseTreeNode,
|
CollapseTreeNode,
|
||||||
CreateCollection,
|
CreateCollection,
|
||||||
|
CreateMaterializedView,
|
||||||
CreateDocument,
|
CreateDocument,
|
||||||
CreateStoredProcedure,
|
CreateStoredProcedure,
|
||||||
CreateTrigger,
|
CreateTrigger,
|
||||||
@@ -119,6 +120,7 @@ export enum Action {
|
|||||||
NotebooksGalleryPublishedCount,
|
NotebooksGalleryPublishedCount,
|
||||||
SelfServe,
|
SelfServe,
|
||||||
ExpandAddCollectionPaneAdvancedSection,
|
ExpandAddCollectionPaneAdvancedSection,
|
||||||
|
ExpandAddMaterializedViewPaneAdvancedSection,
|
||||||
SchemaAnalyzerClickAnalyze,
|
SchemaAnalyzerClickAnalyze,
|
||||||
SelfServeComponent,
|
SelfServeComponent,
|
||||||
LaunchQuickstart,
|
LaunchQuickstart,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
|
|||||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||||
this.restartShell = true;
|
this.restartShell = true;
|
||||||
}
|
}
|
||||||
return content?.includes("cosmosshelluser@");
|
return content?.includes("cosmosuser@");
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMongoShellStarted(content: string | undefined) {
|
private isMongoShellStarted(content: string | undefined) {
|
||||||
@@ -68,6 +68,7 @@ export class JupyterLabAppFactory {
|
|||||||
const session = await manager.startNew();
|
const session = await manager.startNew();
|
||||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||||
const content = message.content && message.content[0]?.toString();
|
const content = message.content && message.content[0]?.toString();
|
||||||
|
|
||||||
if (this.checkShellStarted && message.type == "stdout") {
|
if (this.checkShellStarted && message.type == "stdout") {
|
||||||
//Close the terminal tab once the shell closed messages are received
|
//Close the terminal tab once the shell closed messages are received
|
||||||
if (!this.isShellStarted) {
|
if (!this.isShellStarted) {
|
||||||
@@ -113,13 +114,6 @@ export class JupyterLabAppFactory {
|
|||||||
panel.dispose();
|
panel.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close terminal when Ctrl key is pressed
|
|
||||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
this.onShellExited(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ describe("AuthorizationUtils", () => {
|
|||||||
it("should throw an error if token is malformed", () => {
|
it("should throw an error if token is malformed", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
AuthorizationUtils.decryptJWTToken(
|
AuthorizationUtils.decryptJWTToken(
|
||||||
// This is an invalid JWT token used for testing
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
||||||
),
|
),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
@@ -48,7 +47,6 @@ describe("AuthorizationUtils", () => {
|
|||||||
it("should return decrypted token payload", () => {
|
it("should return decrypted token payload", () => {
|
||||||
expect(
|
expect(
|
||||||
AuthorizationUtils.decryptJWTToken(
|
AuthorizationUtils.decryptJWTToken(
|
||||||
// This is an expired JWT token used for testing
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
||||||
),
|
),
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
["validId", true],
|
|
||||||
["forward/slash", false],
|
|
||||||
["back\\slash", false],
|
|
||||||
["question?mark", false],
|
|
||||||
["hash#mark", false],
|
|
||||||
["?invalidstart", false],
|
|
||||||
["invalidEnd/", false],
|
|
||||||
["space-at-end ", false],
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("IsValidCosmosDbResourceId", () => {
|
|
||||||
test.each(testCases)("IsValidCosmosDbResourceId(%p). Expected: %p", (id: string, expected: boolean) => {
|
|
||||||
expect(IsValidCosmosDbResourceId(id)).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// Common methods and constants for validation
|
|
||||||
//
|
|
||||||
|
|
||||||
//
|
|
||||||
// Validation of id for Cosmos DB resources:
|
|
||||||
// - Database
|
|
||||||
// - Container
|
|
||||||
// - Stored Procedure
|
|
||||||
// - User Defined Function (UDF)
|
|
||||||
// - Trigger
|
|
||||||
//
|
|
||||||
// Use these with <input> elements
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
export const ValidCosmosDbIdInputPattern: RegExp = /[^\/?#\\]*[^\/?# \\]/;
|
|
||||||
export const ValidCosmosDbIdDescription: string = "May not end with space nor contain characters '\\' '/' '#' '?'";
|
|
||||||
|
|
||||||
// For a standalone function regex, we need to wrap the previous reg expression,
|
|
||||||
// to test against the entire value. This is done implicitly by input elements.
|
|
||||||
const ValidCosmosDbIdRegex: RegExp = new RegExp(`^(?:${ValidCosmosDbIdInputPattern.source})$`);
|
|
||||||
|
|
||||||
export function IsValidCosmosDbResourceId(id: string): boolean {
|
|
||||||
return id && ValidCosmosDbIdRegex.test(id);
|
|
||||||
}
|
|
||||||
@@ -66,7 +66,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
activateNewTab: (tab: TabsBase): void => {
|
activateNewTab: (tab: TabsBase): void => {
|
||||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined as ReactTabKind | undefined }));
|
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
||||||
tab.triggerPersistState = get().persistTabsState;
|
tab.triggerPersistState = get().persistTabsState;
|
||||||
tab.onActivate();
|
tab.onActivate();
|
||||||
get().persistTabsState();
|
get().persistTabsState();
|
||||||
@@ -115,7 +115,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
set({ activeTab: undefined, activeReactTab: undefined });
|
set({ activeTab: undefined, activeReactTab: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTab && tab.tabId === activeTab.tabId && tabIndex !== -1) {
|
if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
|
||||||
const tabToTheRight = updatedTabs[tabIndex];
|
const tabToTheRight = updatedTabs[tabIndex];
|
||||||
const lastOpenTab = updatedTabs[updatedTabs.length - 1];
|
const lastOpenTab = updatedTabs[updatedTabs.length - 1];
|
||||||
const newActiveTab = tabToTheRight ?? lastOpenTab;
|
const newActiveTab = tabToTheRight ?? lastOpenTab;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user