Compare commits

..

8 Commits

Author SHA1 Message Date
Archie Agarwal
3b2a49e72e removed the comments 2025-07-18 16:53:59 +05:30
Archie Agarwal
896a50288c Darktheme to stored procedures 2025-07-18 10:57:48 +05:30
Archie Agarwal
47f991cce1 feat: Add Index Advisor feature 2025-07-11 11:12:46 +05:30
Archie Agarwal
a576533e4c Dark theme applied to monaco editor 2025-07-10 18:34:37 +05:30
Sakshi Gupta
14568b032e settings theme changes 2025-04-30 15:23:32 +05:30
Sakshi Gupta
8dd2571444 updated tabs , sidebar , splas screen 2025-04-23 18:52:28 +05:30
Sakshi Gupta
b5976fb034 Updated theme on sidebar 2025-04-18 14:27:19 +05:30
Sakshi Gupta
f2d6bbf54e First dark theme commit for command bar 2025-04-17 19:26:25 +05:30
110 changed files with 3173 additions and 4825 deletions

View File

@@ -164,8 +164,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@v4
- name: "Az CLI login"

View File

@@ -1772,9 +1772,9 @@ input::-webkit-calendar-picker-indicator {
.paddingspan4 {
padding-top: 20px;
padding-left: 20px;
color: white;
font-size: 14px;
padding-left: 25px;
padding-right: 25px;
}
.closebtnn {
@@ -1914,7 +1914,8 @@ input::-webkit-calendar-picker-indicator::after {
}
.nav-tabs-margin {
background-color: #f2f2f2;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
.nav-tabs {
display: flex;
@@ -1922,11 +1923,19 @@ input::-webkit-calendar-picker-indicator::after {
align-items: flex-end;
height: 100%;
margin-bottom: -0.5px;
// border-bottom: 1px solid var(--colorNeutralStroke1);
li {
// Override the bootstrap defaults here to align with our layout constants.
margin-bottom: 0px;
height: 32px;
&:hover {
background-color: var(--colorNeutralBackground1Hover);
}
&:active {
background-color: var(--colorNeutralBackground1Pressed);
}
}
}
}
@@ -1940,8 +1949,20 @@ input::-webkit-calendar-picker-indicator::after {
.nav.nav-tabs.qslevel > li > a:hover {
border: none;
border-radius: 0;
background-color: transparent !important;
background-color: var(--colorNeutralBackground1Selected) !important;
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 {
@@ -2375,7 +2396,9 @@ a:link {
height: 100%;
display: flex;
flex-direction: column;
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide
min-width: 0;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.tabs {
@@ -2631,9 +2654,10 @@ a:link {
}
.tabPanesContainer {
display: flex;
flex-grow: 1;
overflow: hidden;
overflow-y: scroll;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.tabs-container {
@@ -2655,11 +2679,11 @@ a:link {
.nav-tabs > li.active > .tabNavContentContainer,
.nav-tabs > li.active > .tabNavContentContainer:focus,
.nav-tabs > li.active > .tabNavContentContainer:hover {
color: #555;
color: var(--colorNeutralForeground1);
cursor: default;
background-color: @BaseLight;
border-color: @BaseMedium;
border-bottom-color: @BaseLight;
background-color: var(--colorNeutralBackground1Selected);
border-color: var(--colorNeutralStroke1);
// border-bottom-color: var(--colorCompoundBrandBackground);
border-style: solid;
border-width: 1px;
height: @ActiveTabHeight;
@@ -2668,7 +2692,7 @@ a:link {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1);
border-bottom: 2px solid var(--colorCompoundBrandBackground);
}
.nav-tabs > li.active:focus > .tabNavContentContainer {
@@ -2681,7 +2705,7 @@ a:link {
justify-content: space-between;
border-radius: 2px 2px 0 0;
padding: @DefaultSpace 0px @SmallSpace 0px;
color: @BaseHigh;
color: var(--colorNeutralForeground1);
width: @TabsWidth;
text-align: center;
position: relative;
@@ -2689,75 +2713,29 @@ a:link {
&:hover {
text-decoration: none;
background-color: @BaseMediumLow;
border-color: @BaseMediumLow;
background-color: var(--colorNeutralBackground1Hover);
border-color: transparent;
}
&:active {
background-color: @BaseMediumLow;
background-color: var(--colorNeutralBackground1Pressed);
}
.tab_Content {
.flex-display();
width: @TabsWidth;
border-right: @ButtonBorderWidth solid @BaseMedium;
border-right: @ButtonBorderWidth solid var(--colorNeutralStroke1);
white-space: nowrap;
color: var(--colorNeutralForeground1);
.contentWrapper {
.flex-display();
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 {
margin-left: @SmallSpace;
margin-right: 2px;
color: @BaseDark;
color: var(--colorNeutralForeground1);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -2768,21 +2746,36 @@ a:link {
.tabIconSection {
width: 29px;
position: relative;
padding-top: 2px;
.cancelButton {
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);
color: var(--colorNeutralForeground1);
}
&:focus {
.focus();
background-color: var(--colorNeutralBackground1Pressed);
color: var(--colorNeutralForeground1);
}
&:active {
.active();
background-color: var(--colorNeutralBackground1Pressed);
color: var(--colorNeutralForeground1);
}
&::before {
content: "×";
font-size: 16px;
line-height: 1;
}
}
}
@@ -3136,3 +3129,12 @@ a:link {
.sidebarContainer {
height: 100%;
}
.close-Icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
}

View File

@@ -37,12 +37,40 @@ a:focus {
}
.tabsManagerContainer {
background-color: #ffffff;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.nav-tabs-margin {
padding-top: 5px;
background-color: #ffffff;
background-color: var(--colorNeutralBackground1);
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 {
@@ -67,24 +95,12 @@ 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 {
padding: @SmallSpace 0px @SmallSpace 0px;
color: var(--colorNeutralForeground1);
&:hover {
background-color: transparent;
background-color: var(--colorNeutralBackground1Hover);
border-color: transparent;
}
@@ -93,6 +109,7 @@ a:focus {
margin: 0px @SmallSpace 0px @SmallSpace;
width: calc(@TabsWidth - (@SmallSpace * 2));
padding-bottom: @SmallSpace;
color: var(--colorNeutralForeground1);
.contentWrapper {
.statusIconContainer {
@@ -103,17 +120,18 @@ a:focus {
.tabIconSection {
.cancelButton {
padding: 0px 0px 0px @SmallSpace;
color: var(--colorNeutralForeground1);
&:hover {
background-color: transparent;
background-color: var(--colorNeutralBackground1Hover);
}
&:focus {
background-color: transparent;
background-color: var(--colorNeutralBackground1Pressed);
}
&:active {
background-color: transparent;
background-color: var(--colorNeutralBackground1Pressed);
}
}
}

View File

@@ -1,211 +1,227 @@
@import "./Common/Constants";
.dirty {
border: 1px solid #9b4f96;
border: 1px solid #9b4f96;
}
.dirty:focus {
outline: 1px solid #9b4f96;
outline: 1px solid #9b4f96;
}
.tabForm {
padding: 12px 20px 20px 20px;
margin-left: 3px;
padding: 12px 20px 20px 20px;
margin-left: 3px;
}
.storedTabForm {
padding-top: @LargeSpace;
padding-top: @LargeSpace;
}
.scaleSettingScrollable {
overflow-y: auto;
overflow-x: hidden;
height:100%;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
}
.disableFocusDefaults[tabindex] {
&:focus {
outline: none;
}
&:focus {
outline: none;
}
&:active {
outline: none;
}
&:active {
outline: none;
}
}
.indexingPolicyEditor {
width: 100%;
height: calc(~"100vh - 400px");
width: 100%;
height: calc(~"100vh - 400px");
}
.scaleDivison {
padding: @MediumSpace 0px @DefaultSpace 0px;
padding: @MediumSpace 0px @DefaultSpace 0px;
}
.scaleSettingTitle {
font-size: 14px;
cursor: pointer;
font-size: 14px;
cursor: pointer;
}
.autoScaleThroughputTitle {
margin-bottom: @SmallSpace;
margin-bottom: @SmallSpace;
}
.autoScaleDescription {
margin-top: 6px;
margin-bottom: @SmallSpace;
margin-top: 6px;
margin-bottom: @SmallSpace;
}
.ssExpandCollapseIcon {
width: 10px;
height: 10px;
width: 10px;
height: 10px;
}
.ssCollapseIcon {
margin-bottom: 5px;
margin-bottom: 5px;
}
.ssTextAllignment {
padding-left: 19px;
padding-left: 19px;
}
.throughputStorageBlock {
border-top: 1px solid #bbb;
border-bottom: 1px solid #bbb;
background-color: #ccc;
padding-left: 10px;
width: 315px;
border-top: 1px solid #bbb;
border-bottom: 1px solid #bbb;
background-color: #ccc;
padding-left: 10px;
width: 315px;
}
.storageCapacityTitle {
padding: @LargeSpace 0px;
padding: @LargeSpace 0px;
}
.throughputStorageValue {
font-size: 12px;
font-size: 12px;
}
.estimatedCost, .largePartitionKeyEnabled {
padding: @SmallSpace 0px @LargeSpace;
.estimatedCost,
.largePartitionKeyEnabled {
padding: @SmallSpace 0px @LargeSpace;
}
.storagePadding {
padding-top: 6px;
padding-bottom: 14px;
padding-top: 6px;
padding-bottom: 14px;
}
.dirtyTextbox {
width: 176px;
margin-top: 7px;
padding-left: 5px;
width: 176px;
margin-top: 7px;
padding-left: 5px;
}
.formTitleFirst {
padding: @DefaultSpace (2 * @MediumSpace);
padding: @DefaultSpace (2 * @MediumSpace);
}
.formTitleTextbox {
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
}
.formTree {
border: 1px solid #969696;
color: #393939;
padding: 0px 12px 1px 8px;
border: 1px solid var(--colorNeutralStroke1);
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground1);
padding: 0px 12px 1px 8px;
}
.formTree:hover {
border: 1px solid #969696;
background-color: #e6f8fe;
border: 1px solid var(--colorNeutralStroke1Hover);
background-color: var(--colorNeutralBackground1Hover);
}
.formTree::placeholder {
color: var(--colorNeutralForeground2);
opacity: 1;
}
.formTree:active {
border: 1px solid #1ebbee;
border: 1px solid var(--colorNeutralStroke1Pressed);
background-color: var(--colorNeutralBackground1Pressed);
}
.scaleForm {
padding-left: 8px;
color: @BaseDark;
border: 1px solid #969696;
min-width: @ScaleFormWidth;
padding-left: 8px;
color: @BaseDark;
border: 1px solid #969696;
min-width: @ScaleFormWidth;
&:hover {
background-color: #e6f8fe;
}
&:hover {
background-color: #e6f8fe;
}
}
.formTitle {
margin-top: 16px;
margin-bottom: 4px;
margin-top: 16px;
margin-bottom: 4px;
}
.spUdfTriggerHeader {
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
}
.storedUdfTriggerEditor {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.unselectedRadio {
background-color: white;
border-color: #EEE!important;
color: black!important;
background-color: white;
border-color: #eee !important;
color: black !important;
}
.disabledRadio {
background-color: #A19F9D;
border-color: #EEE!important;
color: white!important;
background-color: #a19f9d;
border-color: #eee !important;
color: white !important;
}
.selectedRadio {
background-color: @AccentMediumHigh;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
background-color: @AccentMediumHigh;
border-color: #eee !important;
color: white !important;
cursor: pointer;
}
.selectedRadio:hover {
background-color: @AccentMediumHigh;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
background-color: @AccentMediumHigh;
border-color: #eee !important;
color: white !important;
cursor: pointer;
}
.selectedRadio:active {
background-color: #0072c6;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
border: 1px solid #0072c6;
background-color: #0072c6;
border-color: #eee !important;
color: white !important;
cursor: pointer;
border: 1px solid #0072c6;
}
.selectedRadio.dirty {
background-color: #9b4f96;
background-color: #9b4f96;
}
.tabs {
margin: 0;
margin: 0;
}
.formReadOnly {
background-color: #ddd;
border: 1px solid #969696;
min-width: 184px;
padding-left: 8px;
background-color: #ddd;
border: 1px solid #969696;
min-width: 184px;
padding-left: 8px;
}
.migration:disabled {
background-color: #ccc;
background-color: #ccc;
}
.trigger-field {
width: 40%;
margin-top: 10px
width: 40%;
margin-top: 10px;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.trigger-field input::placeholder {
color: var(--colorNeutralForeground3);
opacity: 1;
}
.trigger-form {
padding: 10px 30px 10px 30px;
}
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
padding: 10px 30px;
}

View File

@@ -1,270 +1,270 @@
@import "./Common/Constants";
// @import "./Common/Constants";
.resourceTree {
height: 100%;
flex: 0 0 auto;
.main {
height: 100%;
}
}
// .resourceTree {
// height: 100%;
// flex: 0 0 auto;
// .main {
// height: 100%;
// }
// }
.resourceTreeScroll {
height: 100%;
display: flex;
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
}
// .resourceTreeScroll {
// height: 100%;
// display: flex;
// overflow-y: auto;
// overflow-x: hidden;
// padding-right: 10px;
// }
.userSelectNone {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
// .userSelectNone {
// -webkit-user-select: none;
// -moz-user-select: none;
// -ms-user-select: none;
// }
.treeHovermargin {
margin-left: 16px;
}
// .treeHovermargin {
// margin-left: 16px;
// }
.highlight {
padding: @SmallSpace 2px;
outline: 0;
// .highlight {
// padding: @SmallSpace 2px;
// outline: 0;
&:hover {
.hover();
}
// &:hover {
// .hover();
// }
&:active {
.active();
}
// &:active {
// .active();
// }
&:focus {
.focus();
}
}
// &:focus {
// .focus();
// }
// }
.contextmenushowing {
background-color: #eee;
}
// .contextmenushowing {
// background-color: #eee;
// }
.collectionstree {
width: 100%;
margin-top: @DefaultSpace;
// .collectionstree {
// width: 100%;
// margin-top: @DefaultSpace;
.databaseList {
list-style-type: none;
padding-left: 0px;
// .databaseList {
// list-style-type: none;
// padding-left: 0px;
.collectionList {
padding-left: (2 * @MediumSpace);
}
// .collectionList {
// padding-left: (2 * @MediumSpace);
// }
.collectionChildList {
padding-left: @LargeSpace;
}
// .collectionChildList {
// padding-left: @LargeSpace;
// }
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
}
}
// .databaseDocuments {
// padding-left: (5 * @MediumSpace);
// }
// }
// }
.pointerCursor {
cursor: pointer;
}
// .pointerCursor {
// cursor: pointer;
// }
.menuEllipsis {
padding-right: 6px;
font-weight: bold;
font-size: 18px;
position: relative;
top: -5px;
left: 0px;
float: right;
display: none;
padding-left: 6px !important;
line-height: @TreeLineHeight;
}
// .menuEllipsis {
// padding-right: 6px;
// font-weight: bold;
// font-size: 18px;
// position: relative;
// top: -5px;
// left: 0px;
// float: right;
// display: none;
// padding-left: 6px !important;
// line-height: @TreeLineHeight;
// }
.databaseMenu {
.flex-display();
}
// .databaseMenu {
// .flex-display();
// }
.databaseMenu:hover .menuEllipsis,
.databaseMenu:focus .menuEllipsis {
display: block;
}
// .databaseMenu:hover .menuEllipsis,
// .databaseMenu:focus .menuEllipsis {
// display: block;
// }
.databaseCollChildTextOverflow {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1;
}
// .databaseCollChildTextOverflow {
// text-overflow: ellipsis;
// white-space: nowrap;
// overflow: hidden;
// flex: 1;
// }
.collectionMenu {
.flex-display();
}
// .collectionMenu {
// .flex-display();
// }
.collectionMenu:hover .menuEllipsis,
.collectionMenu:focus .menuEllipsis {
display: block;
}
// .collectionMenu:hover .menuEllipsis,
// .collectionMenu:focus .menuEllipsis {
// display: block;
// }
.documentsMenu:hover .menuEllipsis,
.documentsMenu:focus .menuEllipsis {
display: block;
}
// .documentsMenu:hover .menuEllipsis,
// .documentsMenu:focus .menuEllipsis {
// display: block;
// }
.treeChildMenu {
display: flex;
}
// .treeChildMenu {
// display: flex;
// }
.storedProcedureMenu:hover .menuEllipsis,
.storedProcedureMenu:focus .menuEllipsis {
display: block;
}
// .storedProcedureMenu:hover .menuEllipsis,
// .storedProcedureMenu:focus .menuEllipsis {
// display: block;
// }
.childMenu {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: (6 * @MediumSpace);
width: 100%;
}
// .childMenu {
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// padding-left: (6 * @MediumSpace);
// width: 100%;
// }
.storedChildMenu:hover .menuEllipsis,
.storedChildMenu:focus .menuEllipsis {
display: block;
}
// .storedChildMenu:hover .menuEllipsis,
// .storedChildMenu:focus .menuEllipsis {
// display: block;
// }
.contextmenu6 {
top: -29px;
}
// .contextmenu6 {
// top: -29px;
// }
.userDefinedMenu:hover .contextmenu6 {
display: block;
}
// .userDefinedMenu:hover .contextmenu6 {
// display: block;
// }
.userDefinedchildMenu:hover .menuEllipsis,
.userDefinedchildMenu:focus .menuEllipsis {
display: block;
}
// .userDefinedchildMenu:hover .menuEllipsis,
// .userDefinedchildMenu:focus .menuEllipsis {
// display: block;
// }
.triggersMenu:hover .menuEllipsis,
.triggersMenu:focus .menuEllipsis {
display: block;
}
// .triggersMenu:hover .menuEllipsis,
// .triggersMenu:focus .menuEllipsis {
// display: block;
// }
.triggersChildMenu:hover .menuEllipsis,
.triggersChildMenu:focus .menuEllipsis {
display: block;
}
// .triggersChildMenu:hover .menuEllipsis,
// .triggersChildMenu:focus .menuEllipsis {
// display: block;
// }
.databaseId {
font-size: 14px;
}
// .databaseId {
// font-size: 14px;
// }
.storedUdfTriggerMenu {
padding-left: 0px;
}
// .storedUdfTriggerMenu {
// padding-left: 0px;
// }
.collectionstree img {
width: 16px;
height: 16px;
vertical-align: text-top;
}
// .collectionstree img {
// width: 16px;
// height: 16px;
// vertical-align: text-top;
// }
img.collectionsTreeCollapseExpand {
width: 10px;
height: 10px;
vertical-align: middle;
margin-bottom: 5px;
}
// img.collectionsTreeCollapseExpand {
// width: 10px;
// height: 10px;
// vertical-align: middle;
// margin-bottom: 5px;
// }
.collapsed::before {
content: "\23F5";
margin-left: 0px;
font-size: 15px;
}
// .collapsed::before {
// content: "\23F5";
// margin-left: 0px;
// font-size: 15px;
// }
.expanded::before {
content: "\23F7";
margin-left: 0px;
font-size: 15px;
}
// .expanded::before {
// content: "\23F7";
// margin-left: 0px;
// font-size: 15px;
// }
.collectionMenuChildren {
padding-left: 42px;
}
// .collectionMenuChildren {
// padding-left: 42px;
// }
.main-nav {
width: 100vh;
height: 40px;
background: white;
transform-origin: left top;
-webkit-transform-origin: left top;
-ms-transform-origin: left top;
transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%);
border-bottom: 1px solid #ccc;
}
// .main-nav {
// width: 100vh;
// height: 40px;
// background: white;
// transform-origin: left top;
// -webkit-transform-origin: left top;
// -ms-transform-origin: left top;
// transform: rotate(-90deg) translateX(-100%);
// -webkit-transform: rotate(-90deg) translateX(-100%);
// -ms-transform: rotate(-90deg) translateX(-100%);
// border-bottom: 1px solid #ccc;
// }
.main-nav-img {
width: 16px;
height: 16px;
margin: -32px 0 0 0;
transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%);
}
// .main-nav-img {
// width: 16px;
// height: 16px;
// margin: -32px 0 0 0;
// transform: rotate(-90deg) translateX(-100%);
// -webkit-transform: rotate(-90deg) translateX(-100%);
// -ms-transform: rotate(-90deg) translateX(-100%);
// }
.main-nav-img.main-nav-sub-img {
width: 16px;
height: 16px;
margin: 0px 0px 0 0;
transform: rotate(180deg) translateX(0%);
-webkit-transform: rotate(180deg) translateX(0%);
-ms-transform: rotate(180deg) translateX(0%);
position: absolute;
right: -8px;
top: 16px;
}
// .main-nav-img.main-nav-sub-img {
// width: 16px;
// height: 16px;
// margin: 0px 0px 0 0;
// transform: rotate(180deg) translateX(0%);
// -webkit-transform: rotate(180deg) translateX(0%);
// -ms-transform: rotate(180deg) translateX(0%);
// position: absolute;
// right: -8px;
// top: 16px;
// }
ul.nav {
margin: 0 auto;
margin-top: 0px;
margin-left: 0px;
}
// ul.nav {
// margin: 0 auto;
// margin-top: 0px;
// margin-left: 0px;
// }
.mini ul.nav li {
float: right;
line-height: 25px;
height: auto;
margin-top: 3px;
}
// .mini ul.nav li {
// float: right;
// line-height: 25px;
// height: auto;
// margin-top: 3px;
// }
.spancolchildstyle {
padding: 4px;
}
// .spancolchildstyle {
// padding: 4px;
// }
.contextmenubutton {
float: right;
display: none;
}
// .contextmenubutton {
// float: right;
// display: none;
// }
.highlight:hover > .contextmenubutton {
display: unset;
}
// .highlight:hover > .contextmenubutton {
// display: unset;
// }
.highlight:hover > .contextmenubutton::after {
content: "\2026";
font-size: 12px;
}
// .highlight:hover > .contextmenubutton::after {
// content: "\2026";
// font-size: 12px;
// }
.showEllipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
// .showEllipsis {
// text-overflow: ellipsis;
// white-space: nowrap;
// overflow: hidden;
// }

View File

@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
@@ -28,12 +29,7 @@ export default defineConfig({
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",

View File

@@ -530,9 +530,6 @@ export class ariaLabelForLearnMoreLink {
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
}
export class GlobalSecondaryIndexLabels {
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
}
export class FeedbackLabels {
public static readonly provideFeedback: string = "Provide feedback";
}

View File

@@ -125,11 +125,7 @@ export const endpoint = () => {
const location = _global.parent ? _global.parent.location : _global.location;
return configContext.EMULATOR_ENDPOINT || location.origin;
}
return (
userContext.selectedRegionalEndpoint ||
userContext.endpoint ||
userContext?.databaseAccount?.properties?.documentEndpoint
);
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
};
export async function getTokenFromAuthService(
@@ -207,7 +203,6 @@ export function client(): Cosmos.CosmosClient {
userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders,
connectionPolicy: {
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
retryOptions: {
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),

View File

@@ -1,6 +1,5 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() {
@@ -27,9 +26,3 @@ export function getWorkloadType(): WorkloadType {
}
return workloadType;
}
export function isGlobalSecondaryIndexEnabled(): boolean {
return (
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
);
}

View File

@@ -1,8 +1,5 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
interface QueryResponse {
// [Todo] remove any
@@ -24,9 +21,7 @@ export function nextPage(
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -1,74 +0,0 @@
import { constructRpOptions } from "Common/dataAccess/createCollection";
import { handleError } from "Common/ErrorHandlingUtils";
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } 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 createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
const clearMessage = logConsoleProgress(
`Creating a new global secondary index ${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 global secondary index ${params.materializedViewId}`);
return createResponse && (createResponse.properties.resource as Collection);
} catch (error) {
handleError(
error,
"CreateGlobalSecondaryIndex",
`Error while creating global secondary index ${params.materializedViewId}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@@ -14,11 +13,6 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
if (isFabric()) {
// Not exposing offers in Fabric
return undefined;
}
try {
if (
userContext.authType === AuthType.AAD &&

View File

@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`);
}
// 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;
});
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
}

View File

@@ -32,7 +32,6 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean;
enableMaterializedViews?: boolean;
isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
@@ -165,8 +164,6 @@ export interface Collection extends Resource {
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
materializedViews?: MaterializedView[];
materializedViewDefinition?: MaterializedViewDefinition;
}
export interface CollectionsWithPagination {
@@ -226,17 +223,6 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[];
export interface MaterializedView {
id: string;
_rid: string;
}
export interface MaterializedViewDefinition {
definition: string;
sourceCollectionId: string;
sourceCollectionRid?: string;
}
export interface PartitionKey {
paths: string[];
kind: "Hash" | "Range" | "MultiHash";
@@ -359,7 +345,9 @@ export interface CreateDatabaseParams {
offerThroughput?: number;
}
export interface CreateCollectionParamsBase {
export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
databaseId: string;
databaseLevelThroughput: boolean;
offerThroughput?: number;
@@ -373,16 +361,6 @@ export interface CreateCollectionParamsBase {
fullTextPolicy?: FullTextPolicy;
}
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
materializedViewId: string;
materializedViewDefinition: MaterializedViewDefinition;
}
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}

View File

@@ -81,13 +81,6 @@ export type FabricMessageV3 =
error: string | undefined;
data: { accessToken: string };
};
}
| {
type: "refreshResourceTree";
message: {
id: string;
error: string | undefined;
};
};
export enum CosmosDbArtifactType {

View File

@@ -1,12 +1,11 @@
import {
JSONObject,
QueryMetrics,
Resource,
StoredProcedureDefinition,
TriggerDefinition,
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import Explorer from "../Explorer/Explorer";
import type Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId";
@@ -144,8 +143,6 @@ export interface Collection extends CollectionBase {
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>;
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[];
@@ -207,12 +204,6 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}>;
}
/**
@@ -471,3 +462,6 @@ export interface DropdownOption<T> {
value: T;
disable?: boolean;
}
// Remove the duplicate Explorer interface and export the type
export type { Explorer };

View File

@@ -1,11 +1,5 @@
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -170,24 +164,6 @@ export const createCollectionContextMenuButton = (
});
}
if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
items.push({
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
onClick: () => {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer: container,
sourceContainer: selectedCollection,
};
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
);
},
});
}
return items;
};

View File

@@ -214,10 +214,8 @@ export const Dialog: FC = () => {
{contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} />
{secondaryButtonProps && (
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
)}
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter>
</FluentDialog>
) : (

View File

@@ -1,4 +1,5 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import { monacoTheme } from "hooks/useTheme";
import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less";
@@ -211,7 +212,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
ariaLabel: this.props.ariaLabel,
fontSize: this.props.fontSize || 12,
automaticLayout: true,
theme: this.props.theme,
theme: monacoTheme,
wordWrap: this.props.wordWrap || "off",
lineNumbers: this.props.lineNumbers || "off",
lineNumbersMinChars: this.props.lineNumbersMinChars,

View File

@@ -4,6 +4,8 @@
height: 100%;
overflow-y: auto;
width: 100%;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.settingsV2ToolTip {
@@ -23,6 +25,8 @@
overflow-y: auto;
width: 100%;
font-family: @DataExplorerFont;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.settingsV2Editor {

View File

@@ -1,5 +1,8 @@
import { IndexingPolicy } from "@azure/cosmos";
import { act } from "@testing-library/react";
import { AuthType } from "AuthType";
import { shallow } from "enzyme";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import ko from "knockout";
import { Features } from "Platform/Hosted/extractFeatures";
import React from "react";
@@ -288,3 +291,47 @@ describe("SettingsComponent", () => {
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);
});
});

View File

@@ -1,4 +1,4 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
import { IPivotItemProps, IPivotProps, Pivot, PivotItem, Stack, getTheme } from "@fluentui/react";
import {
ComputedPropertiesComponent,
ComputedPropertiesComponentProps,
@@ -11,8 +11,8 @@ import {
ThroughputBucketsComponent,
ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
@@ -45,10 +45,6 @@ import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent";
import {
GlobalSecondaryIndexComponent,
GlobalSecondaryIndexComponentProps,
} from "./SettingsSubComponents/GlobalSecondaryIndexComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import {
MongoIndexingPolicyComponent,
@@ -70,7 +66,6 @@ import {
parseConflictResolutionMode,
parseConflictResolutionProcedure,
} from "./SettingsUtils";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
content: JSX.Element;
@@ -167,13 +162,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private isGlobalSecondaryIndex: boolean;
private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number;
private throughputBucketsEnabled: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource;
private unsubscribe: () => void;
constructor(props: SettingsComponentProps) {
super(props);
@@ -185,8 +179,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isGlobalSecondaryIndex =
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
@@ -306,8 +298,19 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.props.settingsTab.isActive()) {
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 {
if (this.props.settingsTab.isActive()) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
@@ -780,7 +783,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
{ name: "name_of_property", query: "query_to_compute_property" },
] as DataModels.ComputedProperties;
}
const throughputBuckets = this.offer?.throughputBuckets;
return {
@@ -932,10 +934,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
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> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if (
this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
@@ -1145,6 +1168,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public render(): JSX.Element {
const theme = getTheme();
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
database: this.database,
@@ -1162,7 +1186,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onScaleDiscardableChange: this.onScaleDiscardableChange,
throughputError: this.state.throughputError,
};
if (!this.isCollectionSettingsTab) {
return (
<div className="settingsV2MainContainer">
@@ -1278,12 +1301,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
isReadOnly: isFabricNative(),
};
const globalSecondaryIndexComponentProps: GlobalSecondaryIndexComponentProps = {
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
};
const tabs: SettingsV2TabInfo[] = [];
@@ -1349,40 +1366,106 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
if (this.isGlobalSecondaryIndex) {
tabs.push({
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
});
}
const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
};
const pivotItems = tabs.map((tab) => {
const pivotItemProps: IPivotItemProps = {
itemKey: SettingsV2TabTypes[tab.tab],
style: { marginTop: 20 },
headerText: getTabTitle(tab.tab),
};
return (
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
{tab.content}
</PivotItem>
);
});
const pivotStyles = {
root: {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
selectors: {
'& .ms-Pivot-link': {
color: 'var(--colorNeutralForeground1)'
},
'& .ms-Pivot-link.is-selected::before': {
backgroundColor: 'var(--colorCompoundBrandBackground)'
},
}
},
link: {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
selectors: {
'&:hover': {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)'
},
'&:active': {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)'
},
'&[aria-selected="true"]': {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
selectors: {
'&:hover': {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)'
},
'&:active': {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)'
}
}
}
}
},
itemContainer: {
// padding: '20px 24px',
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)'
}
};
const contentStyles = {
root: {
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
// padding: '20px 24px'
}
};
return (
<div className="settingsV2MainContainer">
<div className="settingsV2MainContainer" style={{
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
position: 'relative'
} as React.CSSProperties}>
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div>
)}
<div className="settingsV2TabsContainer">
<Pivot {...pivotProps}>{pivotItems}</Pivot>
<div className="settingsV2TabsContainer" style={{
backgroundColor: 'var(--colorNeutralBackground1)',
color: 'var(--colorNeutralForeground1)',
position: 'relative',
padding: '20px 24px'
} as React.CSSProperties}>
<Pivot {...pivotProps} styles={pivotStyles}>
{tabs.map((tab) => {
const pivotItemProps: IPivotItemProps = {
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>
);

View File

@@ -63,7 +63,7 @@ export interface PriceBreakdown {
export type editorType = "indexPolicy" | "computedProperties";
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "var(--colorNeutralForeground1)" } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: {
@@ -166,7 +166,7 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
};
export const messageBarStyles: Partial<IMessageBarStyles> = {
root: { marginTop: "5px", backgroundColor: "white" },
root: { marginTop: "5px", backgroundColor: "var(--colorNeutralBackground1)" },
text: { fontSize: 14 },
};
@@ -214,9 +214,11 @@ export const getEstimatedSpendingElement = (
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return (
<Stack>
<Text style={{ fontWeight: 600 }}>Cost estimate*</Text>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
{costElement}
<Text style={{ fontWeight: 600, marginTop: 15 }}>How we calculate this</Text>
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
How we calculate this
</Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span>
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
@@ -230,7 +232,7 @@ export const getEstimatedSpendingElement = (
{priceBreakdown.pricePerRu}/RU
</span>
</Stack>
<Text style={{ marginTop: 15 }}>
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
<em>*{estimatedCostDisclaimer}</em>
</Text>
</Stack>
@@ -272,7 +274,7 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
<Text id="updateThroughputDelayedApplyWarningMessage">
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
with this value and wait until the scale-up is completed.
@@ -290,7 +292,7 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
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:
</Text>
<ol style={{ fontSize: 14, color: "windowtext", marginTop: "5px" }}>
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
{instantMaximumThroughput < maximumThroughput && (
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
@@ -326,7 +328,7 @@ export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Ele
};
export const saveThroughputWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
<Text>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
@@ -507,11 +509,25 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
height: 25,
width: 300,
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
backgroundColor: "var(--colorNeutralBackground3)",
selectors: {
":disabled": {
backgroundColor: StyleConstants.BaseMedium,
backgroundColor: "var(--colorNeutralBackground1)",
borderColor: StyleConstants.BaseMediumHigh,
},
"input:disabled": {
backgroundColor: "var(--colorNeutralBackground3)",
},
},
field: {
color: "var(--colorNeutralForeground1)",
},
},
subComponentStyles: {
label: {
root: {
color: "var(--colorNeutralForeground1)",
},
},
},
});
@@ -521,6 +537,9 @@ export const getChoiceGroupStyles = (
baseline: isDirtyTypes,
isHorizontal?: boolean,
): Partial<IChoiceGroupStyles> => ({
label: {
color: "var(--colorNeutralForeground1)",
},
flexContainer: [
{
selectors: {
@@ -535,6 +554,7 @@ export const getChoiceGroupStyles = (
fontSize: 14,
fontFamily: StyleConstants.DataExplorerFont,
padding: "2px 5px",
color: "var(--colorNeutralForeground1)",
},
},
display: isHorizontal ? "inline-flex" : "default",

View File

@@ -3,9 +3,9 @@ import * as DataModels from "Contracts/DataModels";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco";
import { monacoTheme } from "hooks/useTheme";
import * as monaco from "monaco-editor";
import * as React from "react";
export interface ComputedPropertiesComponentProps {
computedPropertiesContent: DataModels.ComputedProperties;
computedPropertiesContentBaseline: DataModels.ComputedProperties;
@@ -86,6 +86,7 @@ export class ComputedPropertiesComponent extends React.Component<
value: value,
language: "json",
ariaLabel: "Computed properties",
theme:monacoTheme,
});
if (this.computedPropertiesEditor) {
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();

View File

@@ -1,46 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexComponent", () => {
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(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).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(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true);
});
it("renders neither component when both are missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
});
});

View File

@@ -1,41 +0,0 @@
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
export interface GlobalSecondaryIndexComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexComponentProps> = ({
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 }}>
{isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes 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 global secondary indexes and how to use them.
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
{isTargetContainer && <GlobalSecondaryIndexTargetComponent collection={collection} />}
</Stack>
);
};

View File

@@ -1,42 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
describe("GlobalSecondaryIndexSourceComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders without crashing", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.exists()).toBe(true);
});
it("renders the PrimaryButton", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
it("updates when new global secondary indexes are provided", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent 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);
});
});

View File

@@ -1,114 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { MaterializedView } from "Contracts/DataModels";
import Explorer from "Explorer/Explorer";
import { loadMonaco } from "Explorer/LazyMonaco";
import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
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 GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexSourceComponentProps> = ({
collection,
explorer,
}) => {
const editorContainerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? [];
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] 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 globalSecondaryIndexDefinition = collection.materializedViewDefinition();
globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.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(
globalSecondaryIndexes.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: "Global Secondary Index 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 index"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel explorer={explorer} sourceContainer={collection} />,
)
}
/>
</div>
);
};

View File

@@ -1,32 +0,0 @@
import { Text } from "@fluentui/react";
import { Collection } from "Contracts/ViewModels";
import { shallow } from "enzyme";
import React from "react";
import { collection } from "../TestUtils";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexTargetComponent", () => {
let testCollection: Collection;
beforeEach(() => {
testCollection = {
...collection,
materializedViewDefinition: collection.materializedViewDefinition,
};
});
it("renders without crashing", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.exists()).toBe(true);
});
it("displays the source container ID", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
});
it("displays the global secondary index definition", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
});
});

View File

@@ -1,45 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection;
}
export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexTargetComponentProps> = ({
collection,
}) => {
const globalSecondaryIndexDefinition = 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}>Global Secondary Index Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>
</Stack>
</Stack>
);
};

View File

@@ -1,4 +1,5 @@
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import { monacoTheme } from "hooks/useTheme";
import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
@@ -6,7 +7,6 @@ import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface IndexingPolicyComponentProps {
shouldDiscardIndexingPolicy: boolean;
resetShouldDiscardIndexingPolicy: () => void;
@@ -87,20 +87,71 @@ export class IndexingPolicyComponent extends React.Component<
};
private async createIndexingPolicyEditor(): Promise<void> {
if (!this.indexingPolicyDiv.current) {
return;
}
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
const monaco = await loadMonaco();
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value,
language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: "Indexing Policy",
});
if (this.indexingPolicyEditor) {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logIndexingPolicySuccessMessage();
if (this.indexingPolicyDiv.current) {
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value,
language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: "Indexing Policy",
theme: monacoTheme,
});
if (this.indexingPolicyEditor) {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
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 => {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();

View File

@@ -1,10 +1,10 @@
import * as React from "react";
import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import {
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage,
} from "../../SettingsRenderUtils";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { isIndexTransforming } from "../../SettingsUtils";
export interface IndexingPolicyRefreshComponentProps {

View File

@@ -29,26 +29,16 @@ export interface PartitionKeyComponentProps {
database: ViewModels.Database;
collection: ViewModels.Collection;
explorer: Explorer;
isReadOnly?: boolean; // true: cannot change partition key
}
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
database,
collection,
explorer,
isReadOnly,
}) => {
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
const { dataTransferJobs } = useDataTransferJobs();
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
React.useEffect(() => {
if (isReadOnly) {
return;
}
const loadDataTransferJobs = refreshDataTransferOperations;
loadDataTransferJobs();
}, [isReadOnly]);
}, []);
React.useEffect(() => {
const currentJob = findPortalDataTransferJob();
@@ -66,13 +56,15 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = {
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
root: { fontWeight: FontWeights.semibold, fontSize: 16, color: 'var(--colorNeutralForeground1)' },
};
const textSubHeadingStyle = {
root: { fontWeight: FontWeights.semibold },
root: { fontWeight: FontWeights.semibold , color: 'var(--colorNeutralForeground1)' },
};
const textSubHeadingStyle1 = {
root: {color: 'var(--colorNeutralForeground1)' },
};
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
if (isCurrentJobInProgress(currentJob)) {
const jobName = currentJob?.properties?.jobName;
@@ -168,66 +160,61 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
<Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text>{partitionKeyValue}</Text>
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
</Stack>
</Stack>
</Stack>
{!isReadOnly && (
<>
<MessageBar messageBarType={MessageBarType.warning}>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
the source container for the entire duration of the partition key change process.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
>
Learn more
</Link>
</MessageBar>
<Text>
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.
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
styles={{ root: { width: "fit-content" } }}
text="Change"
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
)}
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
styles={{
root: {
alignItems: "center",
},
}}
>
<ProgressIndicator
label={portalDataTransferJob?.properties?.jobName}
description={getProgressDescription()}
percentComplete={getPercentageComplete()}
styles={{
root: {
width: "85%",
},
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>
)}
</>
<MessageBar messageBarType={MessageBarType.warning}>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
source container for the entire duration of the partition key change process.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
>
Learn more
</Link>
</MessageBar>
<Text styles={textSubHeadingStyle1}>
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.
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
styles={{ root: { width: "fit-content" } }}
text="Change"
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
)}
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
styles={{
root: {
alignItems: "center",
},
}}
>
<ProgressIndicator
label={portalDataTransferJob?.properties?.jobName}
description={getProgressDescription()}
percentComplete={getPercentageComplete()}
styles={{
root: {
width: "85%",
},
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>
)}
</Stack>
);

View File

@@ -1,4 +1,4 @@
import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, TextField } from "@fluentui/react";
import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, TextField, getTheme, mergeStyleSets } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
@@ -25,6 +25,13 @@ import {
} from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
const theme = getTheme();
const classNames = mergeStyleSets({
hintText: {
color: 'var(--colorNeutralForeground1)', // theme-aware
},
});
export interface SubSettingsComponentProps {
collection: ViewModels.Collection;
timeToLive: TtlType;
@@ -181,7 +188,19 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
userContext.apiType === "Mongo" ? (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={{ text: { fontSize: 14 } }}
styles={{
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,
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
@@ -191,7 +210,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
</MessageBar>
) : (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
<ChoiceGroup
id="timeToLive"
label="Time to Live"
selectedKey={this.props.timeToLive}
@@ -323,14 +342,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
)}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
)}
{userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? (
<Text>Hierarchically partitioned container.</Text>
) : (
<Text>Non-hierarchically partitioned container.</Text>
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
) : (
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
))}
</Stack>
);

View File

@@ -235,12 +235,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
);
return (
<div>
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
<Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)' }}>Updated cost per month</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={{ width: "50%" }}>
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
</Text>
<Text style={{ width: "50%" }}>
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)'}}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
</Text>
</Stack>
@@ -253,12 +253,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={{ width: "50%" }}>
<Text style={{ fontWeight: 600, color: 'var(--colorNeutralForeground1)' }}>Current cost per month</Text>
<Stack horizontal style={{ marginTop: 5, color: 'var(--colorNeutralForeground1)' }}>
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
</Text>
<Text style={{ width: "50%" }}>
<Text style={{ width: "50%" , color: 'var(--colorNeutralForeground1)' }}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
</Text>
</Stack>
@@ -268,7 +268,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
};
settingsAndScaleStyle = {
root: { width: "33%",
color: 'var(--colorNeutralForeground1)' },
};
private getEstimatedManualSpendElement = (
throughput: number,
serverId: string,
@@ -288,36 +291,36 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
);
return (
<div>
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
<Text style={{ fontWeight: 600, color: 'var(--colorNeutralForeground1)' }}>Updated cost per month</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
</Text>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
</Text>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
</Text>
</Stack>
</div>
);
};
const costElement = (): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
return (
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
<Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)'}}>Current cost per month</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
</Text>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
</Text>
<Text style={{ width: "33%" }}>
<Text style={ this.settingsAndScaleStyle.root }>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
</Text>
</Stack>
@@ -402,8 +405,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
<Text>{capacity}</Text>
<Label style={{ color: 'var(--colorNeutralForeground1)'}}>Storage capacity</Label>
<Text style={{ color: 'var(--colorNeutralForeground1)'}}>{capacity}</Text>
</Stack>
);
};
@@ -608,7 +611,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
</Stack>
)}
{this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px" }}>
<Text style={{ marginTop: "40px" , color: 'var(--colorNeutralForeground1)'}}>
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
@@ -630,7 +633,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
</>
)}
{!this.overrideWithProvisionedThroughputSettings() && (
<Text>
<Text style={{ color: 'var(--colorNeutralForeground1)'}}>
Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />

View File

@@ -1,5 +1,5 @@
import { DirectionalHint, IIconStyles, Icon, Stack, Text, TooltipHost } from "@fluentui/react";
import * as React from "react";
import { Stack, Text, IIconStyles, Icon, TooltipHost, DirectionalHint } from "@fluentui/react";
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
export interface ToolTipLabelComponentProps {
@@ -14,7 +14,7 @@ export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponent
return (
<>
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
{this.props.label && <Text style={{ fontWeight: 600 }}>{this.props.label}</Text>}
{this.props.label && <Text style={{ fontWeight: 600 , color: 'var(--colorNeutralForeground1)'}}>{this.props.label}</Text>}
{this.props.toolTipElement && (
<TooltipHost
content={this.props.toolTipElement}

View File

@@ -57,7 +57,6 @@ export enum SettingsV2TabTypes {
ComputedPropertiesTab,
ContainerVectorPolicyTab,
ThroughputBucketsTab,
GlobalSecondaryIndexTab,
}
export enum ContainerPolicyTabTypes {
@@ -172,8 +171,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets";
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return "Global Secondary Index (Preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}

View File

@@ -48,15 +48,6 @@ export const collection = {
]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
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: () => {
return;
},

View File

@@ -60,8 +60,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -71,6 +69,27 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
@@ -141,8 +160,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -152,6 +169,27 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
@@ -191,17 +229,25 @@ exports[`SettingsComponent renders 1`] = `
indexingPolicyContent={
{
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
}
}
indexingPolicyContentBaseline={
{
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
}
}
isVectorSearchEnabled={false}
@@ -262,8 +308,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -273,6 +317,27 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
@@ -306,7 +371,6 @@ exports[`SettingsComponent renders 1`] = `
},
}
}
isReadOnly={false}
/>
</PivotItem>
<PivotItem
@@ -403,8 +467,28 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}

View File

@@ -0,0 +1,29 @@
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>
);
};

View File

@@ -6,7 +6,6 @@ import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
// TODO: this does not seem to be used. Remove?
export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container";
constructor(private container: Explorer) {}

View File

@@ -0,0 +1,37 @@
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;
}
}

View File

@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";

View File

@@ -4,11 +4,10 @@
padding: @SmallSpace 0px @SmallSpace 0px;
.flex-display();
span {
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
margin: 0 10px 0 10px;
}
}
.commandBarContainer {
border-bottom: 1px solid @BaseMedium;
border-bottom: 1px solid var(--colorNeutralStroke1);
}

View File

@@ -4,6 +4,7 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { makeStyles, useFluent } from "@fluentui/react-components";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
@@ -11,7 +12,6 @@ import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -30,18 +30,26 @@ export interface CommandBarStore {
}
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [],
contextButtons: [] as CommandButtonComponentProps[],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
}));
const useStyles = makeStyles({
commandBarContainer: {
borderBottom: "1px solid var(--colorNeutralStroke1)"
}
});
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const { targetDocument } = useFluent();
// const isDarkMode = targetDocument?.body.classList.contains("isDarkMode");
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const styles = useStyles();
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -49,12 +57,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
? CommandBarComponentButtonFactory.createPostgreButtons(container)
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<div className={styles.commandBarContainer} style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
items={CommandBarUtil.convertButton(buttons, "var(--colorNeutralBackground1)")}
styles={{
root: { backgroundColor: backgroundColor },
root: {
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)"
}
}}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
@@ -68,18 +79,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, "var(--colorNeutralBackground1)");
if (buttons && buttons.length > 0) {
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
}
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, "var(--colorNeutralBackground1)");
if (uiFabricTabsButtons.length > 0) {
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
}
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
const connectionInfo = useNotebook((state) => state.connectionInfo);
@@ -96,14 +107,16 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const rootStyle = isFabric()
? {
root: {
backgroundColor: "transparent",
backgroundColor: "var(--colorNeutralBackground1)",
padding: "2px 8px 0px 8px",
},
color: "var(--colorNeutralForeground1)"
}
}
: {
root: {
backgroundColor: backgroundColor,
},
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)"
}
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
@@ -111,7 +124,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
setKeyboardHandlers(keyboardHandlers);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<div className={styles.commandBarContainer} style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}

View File

@@ -1,10 +1,10 @@
import {
Dropdown,
ICommandBarItemProps,
IComponentAsProps,
IconType,
IDropdownOption,
IDropdownStyles,
Dropdown,
ICommandBarItemProps,
IComponentAsProps,
IconType,
IDropdownOption,
IDropdownStyles,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
@@ -53,7 +53,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
const result: ICommandBarItemProps = {
iconProps: {
style: {
width: StyleConstants.CommandBarIconWidth, // 16
width: StyleConstants.CommandBarIconWidth,
alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
},
@@ -79,7 +79,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
"data-test": `CommandBar/Button:${label}`,
buttonStyles: {
root: {
backgroundColor: backgroundColor,
backgroundColor: "var(--colorNeutralBackground1)",
height: buttonHeightPx,
paddingRight: 0,
paddingLeft: 0,
@@ -87,15 +87,29 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
minWidth: 24,
marginLeft: 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: {
backgroundColor: backgroundColor,
backgroundColor: "var(--colorNeutralBackground1)",
pointerEvents: "auto",
color: "var(--colorNeutralForegroundDisabled)"
},
splitButtonMenuButton: {
backgroundColor: backgroundColor,
backgroundColor: "var(--colorNeutralBackground1)",
selectors: {
":hover": { backgroundColor: hoverColor },
":hover": {
backgroundColor: "var(--colorNeutralBackground1Hover)"
},
},
width: 16,
},
@@ -104,13 +118,22 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
configContext.platform == Platform.Fabric
? StyleConstants.DefaultFontSize
: 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: {
backgroundColor: StyleConstants.AccentExtra,
backgroundColor: "var(--colorNeutralBackground1Pressed)",
selectors: {
":hover": { backgroundColor: hoverColor },
":hover": {
backgroundColor: "var(--colorNeutralBackground1Hover)"
},
},
},
splitButtonDivider: {
@@ -119,6 +142,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
icon: {
paddingLeft: 0,
paddingRight: 0,
color: "var(--colorNeutralForeground1)"
},
splitButtonContainer: {
marginLeft: 5,

View File

@@ -1,6 +1,6 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import Explorer from "../Explorer";
import { AddCollectionPanel } from "./AddCollectionPanel";
const props = {

View File

@@ -21,25 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
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";
FullTextPoliciesComponent,
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
@@ -57,14 +43,15 @@ import {
} from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import "../Controls/ThroughputInput/ThroughputInput.less";
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface AddCollectionPanelProps {
explorer: Explorer;
@@ -72,6 +59,40 @@ export interface AddCollectionPanelProps {
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 = {
vectorEmbeddings: [],
};
@@ -124,7 +145,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
enableIndexing: true,
isSharded: userContext.apiType !== "Tables",
partitionKey: getPartitionKey(props.isQuickstart),
partitionKey: this.getPartitionKey(),
subPartitionKeys: [],
enableDedicatedThroughput: false,
createMongoWildCardIndex:
@@ -140,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [],
vectorPolicyValidated: true,
fullTextPolicy: FullTextPolicyDefault,
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
fullTextIndexes: [],
fullTextPolicyValidated: true,
};
@@ -154,7 +175,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
scrollToSection("panelContainer");
this.scrollToSection("panelContainer");
}
}
@@ -171,7 +192,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
)}
{!this.state.errorMessage && isFreeTierAccount() && (
{!this.state.errorMessage && this.isFreeTierAccount() && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info"
@@ -379,10 +400,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
@@ -559,14 +580,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{getPartitionKeyName()}
{this.getPartitionKeyName()}
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={getPartitionKeyTooltipText()}
ariaLabel={this.getPartitionKeyTooltipText()}
/>
</TooltipHost>
</Stack>
@@ -580,8 +604,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
required
size={40}
className="panelTextField"
placeholder={getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()}
placeholder={this.getPartitionKeyPlaceHolder()}
aria-label={this.getPartitionKeyName()}
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
value={this.state.partitionKey}
@@ -619,8 +643,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={index > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(index)}
aria-label={getPartitionKeyName()}
placeholder={this.getPartitionKeyPlaceHolder(index)}
aria-label={this.getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
@@ -711,10 +735,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -729,7 +753,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack>
{UniqueKeysHeader()}
<Stack horizontal>
<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 => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
@@ -777,10 +821,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{shouldShowAnalyticalStoreOptions() && (
{this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
{this.getAnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
@@ -788,7 +832,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
@@ -803,7 +847,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
@@ -817,7 +861,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
</Stack>
{!isSynapseLinkEnabled() && (
{!this.isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "}
@@ -847,9 +891,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={ContainerVectorPolicyTooltipContent()}
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
@@ -875,7 +919,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
this.scrollToSection("collapsibleFullTextPolicySectionContent");
}}
//TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
@@ -903,7 +947,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
scrollToSection("collapsibleAdvancedSectionContent");
this.scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
@@ -1013,6 +1057,31 @@ 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 {
if (event.target.checked && !this.state.createNewDatabase) {
this.setState({
@@ -1100,12 +1169,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return !!selectedDatabase?.offer();
}
private isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? "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.";
}
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. Its 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 {
if (
userContext.features.partitionKeyDefault &&
@@ -1117,6 +1222,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
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
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
// return (
@@ -1147,7 +1280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!isFreeTierAccount()) {
if (!this.isFreeTierAccount()) {
return false;
}
@@ -1156,6 +1289,39 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: 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() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
@@ -1236,11 +1402,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getAnalyticalStorageTtl(): number {
if (!isSynapseLinkEnabled()) {
if (!this.isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
if (!this.shouldShowAnalyticalStoreOptions()) {
return undefined;
}
@@ -1254,6 +1420,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled;
}
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
private getSampleDBName(): string {
const existingSampleDBs = useDatabases
.getState()
@@ -1288,7 +1458,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKeyString = "/'$pk'";
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString
? {

View File

@@ -1,217 +0,0 @@
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. Its 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: [],
};

View File

@@ -1,28 +0,0 @@
import { shallow, ShallowWrapper } from "enzyme";
import Explorer from "Explorer/Explorer";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import React, { Component } from "react";
const props: AddGlobalSecondaryIndexPanelProps = {
explorer: new Explorer(),
};
describe("AddGlobalSecondaryIndexPanel", () => {
it("render default panel", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
expect(wrapper).toMatchSnapshot();
});
it("should render form", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
const form = wrapper.find("form").first();
expect(form).toBeDefined();
});
});

View File

@@ -1,431 +0,0 @@
import {
DirectionalHint,
Dropdown,
DropdownMenuItemType,
Icon,
IDropdownOption,
Link,
Separator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createGlobalSecondaryIndex } 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/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles";
import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent";
import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent";
import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent";
import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent";
import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent";
import { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent";
import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent";
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";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
export interface AddGlobalSecondaryIndexPanelProps {
explorer: Explorer;
sourceContainer?: Collection;
}
export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => {
const { explorer, sourceContainer } = props;
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = 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 isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition();
sourceContainerOptions.push({
key: collection.rid,
text: collection.id(),
disabled: isGlobalSecondaryIndex,
...(isGlobalSecondaryIndex && {
title: "This is a global secondary index.",
}),
data: collection,
});
});
});
setSourceContainerOptions(sourceContainerOptions);
}, []);
useEffect(() => {
scrollToSection("panelContainer");
}, [errorMessage]);
let globalSecondaryIndexThroughput: number;
let isGlobalSecondaryIndexAutoscale: boolean;
let isCostAcknowledged: boolean;
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
};
const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => {
isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue;
};
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 (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage = isGlobalSecondaryIndexAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setErrorMessage(errorMessage);
return false;
}
if (globalSecondaryIndexThroughput > 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 globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim();
const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = {
sourceCollectionId: selectedSourceContainer.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: globalSecondaryIdTrimmed,
throughput: globalSecondaryIndexThroughput,
isAutoscale: isGlobalSecondaryIndexAutoscale,
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 (isGlobalSecondaryIndexAutoscale) {
autoPilotMaxThroughput = globalSecondaryIndexThroughput;
} else {
offerThroughput = globalSecondaryIndexThroughput;
}
}
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
materializedViewId: globalSecondaryIdTrimmed,
materializedViewDefinition: globalSecondaryIndexDefinition,
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 createGlobalSecondaryIndex(createGlobalSecondaryIndexParams);
await explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, 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.CreateGlobalSecondaryIndex, failureTelemetryData, startKey);
} finally {
setIsExecuting(false);
}
};
return (
<form className="panelFormWrapper" id="panelGlobalSecondaryIndex" onSubmit={submit}>
{errorMessage && (
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
)}
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Source container id
</Text>
</Stack>
<Dropdown
placeholder="Choose source container"
options={sourceContainerOptions}
defaultSelectedKey={selectedSourceContainer?.rid}
styles={chooseSourceContainerStyles()}
style={chooseSourceContainerStyle()}
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
/>
<Separator className="panelSeparator" />
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index container id
</Text>
</Stack>
<input
id="globalSecondaryIndexId"
type="text"
aria-required
required
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., indexbyEmailId`}
size={40}
className="panelTextField"
value={globalSecondaryIndexId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setGlobalSecondaryIndexId(event.target.value)}
/>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index 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 global secondary indexes.
</Link>
}
>
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
id="globalSecondaryIndexDefinition"
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)}
/>
<PartitionKeyComponent
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
/>
<ThroughputComponent
{...{
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
}}
/>
<UniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
{shouldShowAnalyticalStoreOptions() && (
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
{showVectorSearchParameters() && (
<VectorSearchComponent
{...{
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
vectorPolicyValidated,
setVectorPolicyValidated,
}}
/>
)}
{showFullTextSearchParameters() && (
<FullTextSearchComponent
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
/>
)}
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -1,15 +0,0 @@
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 };
}

View File

@@ -1,54 +0,0 @@
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 AdvancedComponentProps {
useHashV1: boolean;
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const AdvancedComponent = (props: AdvancedComponentProps): 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.ExpandAddGlobalSecondaryIndexPaneAdvancedSection);
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>
);
};

View File

@@ -1,99 +0,0 @@
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 AnalyticalStoreComponentProps {
explorer: Explorer;
enableAnalyticalStore: boolean;
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): 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>
);
};

View File

@@ -1,45 +0,0 @@
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 FullTextSearchComponentProps {
fullTextPolicy: FullTextPolicy;
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const FullTextSearchComponent = (props: FullTextSearchComponentProps): 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>
);
};

View File

@@ -1,132 +0,0 @@
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 PartitionKeyComponentProps {
partitionKey?: string;
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
subPartitionKeys: string[];
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
useHashV1: boolean;
}
export const PartitionKeyComponent = (props: PartitionKeyComponentProps): 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">*&nbsp;</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="addGlobalSecondaryIndex-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="addGlobalSecondaryIndex-partitionKeyValue"
key={`addGlobalSecondaryIndex-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>
);
};

View File

@@ -1,71 +0,0 @@
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 ThroughputComponentProps {
enableDedicatedThroughput: boolean;
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
isSelectedSourceContainerSharedThroughput: () => boolean;
showCollectionThroughputInput: () => boolean;
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void;
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
}
export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => {
const {
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
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) => {
globalSecondaryIndexThroughputOnChange(throughput);
}}
setIsAutoscale={(isAutoscale: boolean) => {
isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale);
}}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
setIsThroughputCapExceeded(isThroughputCapExceeded);
}}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
isCostAknowledgedOnChange(isAcknowledged);
}}
/>
)}
</Stack>
);
};

View File

@@ -1,78 +0,0 @@
import { ActionButton, IconButton, Stack } from "@fluentui/react";
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { userContext } from "UserContext";
export interface UniqueKeysComponentProps {
uniqueKeys: string[];
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const UniqueKeysComponent = (props: UniqueKeysComponentProps): 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>
);
};

View File

@@ -1,58 +0,0 @@
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 VectorSearchComponentProps {
vectorEmbeddingPolicy: VectorEmbedding[];
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
vectorIndexingPolicy: VectorIndex[];
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const VectorSearchComponent = (props: VectorSearchComponentProps): 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>
);
};

View File

@@ -1,190 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
<form
className="panelFormWrapper"
id="panelGlobalSecondaryIndex"
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"
>
Global secondary index container id
</Text>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexId"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="e.g., indexbyEmailId"
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"
>
Global secondary index 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 global secondary indexes.
</StyledLinkBase>
}
directionalHint={4}
>
<Icon
className="panelInfoIcon"
iconName="Info"
role="button"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexDefinition"
onChange={[Function]}
placeholder="SELECT c.email, c.accountId FROM c"
required={true}
size={40}
type="text"
value=""
/>
<PartitionKeyComponent
partitionKey=""
setPartitionKey={[Function]}
setSubPartitionKeys={[Function]}
subPartitionKeys={[]}
/>
<ThroughputComponent
globalSecondaryIndexThroughputOnChange={[Function]}
isCostAknowledgedOnChange={[Function]}
isGlobalSecondaryIndexAutoscaleOnChange={[Function]}
isSelectedSourceContainerSharedThroughput={[Function]}
setEnabledDedicatedThroughput={[Function]}
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<UniqueKeysComponent
setUniqueKeys={[Function]}
uniqueKeys={[]}
/>
<AnalyticalStoreComponent
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]}
/>
<AdvancedComponent
setSubPartitionKeys={[Function]}
setUseHashV1={[Function]}
/>
</Stack>
</div>
<PanelFooterComponent
buttonLabel="OK"
/>
</form>
`;

View File

@@ -6,9 +6,7 @@ import {
Checkbox,
ChoiceGroup,
DefaultButton,
Dropdown,
IChoiceGroupOption,
IDropdownOption,
ISpinButtonStyles,
IToggleStyles,
Position,
@@ -23,15 +21,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases";
import { isFabric } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
deleteAllStates,
deleteState,
hasState,
loadState,
saveState,
} from "Shared/AppStatePersistenceUtility";
import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
import {
DefaultRUThreshold,
LocalStorageUtility,
@@ -47,7 +37,6 @@ import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
@@ -154,17 +143,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
: "false",
);
const [selectedRegionalEndpoint, setSelectedRegionalEndpoint] = useState<string>(
hasState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
})
? (loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string)
: undefined,
);
const [retryAttempts, setRetryAttempts] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.RetryAttempts)
? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts)
@@ -211,44 +189,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
configContext.platform !== Platform.Fabric &&
!isEmulator;
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
const uniqueAccountRegions = new Set<string>();
const regionOptions: IDropdownOption[] = [];
regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: `Global (Default)`,
data: {
isGlobal: true,
writeEnabled: true,
},
});
userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read/Write)`,
data: {
isGlobal: false,
writeEnabled: true,
},
});
}
});
userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read)`,
data: {
isGlobal: false,
writeEnabled: false,
},
});
}
});
const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
@@ -334,46 +274,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}
}
const storedRegionalEndpoint = loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string;
const selectedRegionIsGlobal =
selectedRegionalEndpoint === userContext?.databaseAccount?.properties?.documentEndpoint;
if (selectedRegionIsGlobal && storedRegionalEndpoint) {
deleteState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
});
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
} else if (
selectedRegionalEndpoint &&
!selectedRegionIsGlobal &&
selectedRegionalEndpoint !== storedRegionalEndpoint
) {
saveState(
{
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
},
selectedRegionalEndpoint,
);
const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find(
(loc) => loc.documentEndpoint === selectedRegionalEndpoint,
);
updateUserContext({
selectedRegionalEndpoint: selectedRegionalEndpoint,
writeEnabledInSelectedRegion: !!validWriteEndpoint,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint });
}
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
@@ -523,10 +423,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setDefaultQueryResultsView(option.key as SplitterDirection);
};
const handleOnSelectedRegionOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IDropdownOption): void => {
setSelectedRegionalEndpoint(option.key as string);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) {
@@ -687,39 +583,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>Region Selection</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Changes region the Cosmos Client uses to access account.
</div>
<div>
<span className={styles.subHeader}>Select Region</span>
<InfoTooltip className={styles.headerIcon}>
Changes the account endpoint used to perform client operations.
</InfoTooltip>
</div>
<Dropdown
placeholder={
selectedRegionalEndpoint
? regionOptions.find((option) => option.key === selectedRegionalEndpoint)?.text
: regionOptions[0]?.text
}
onChange={handleOnSelectedRegionOptionChange}
options={regionOptions}
styles={{ root: { marginBottom: "10px" } }}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && !isEmulator && (
<>
<AccordionItem value="4">
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>Query Timeout</div>
</AccordionHeader>
@@ -760,7 +626,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
<AccordionItem value="5">
<AccordionItem value="4">
<AccordionHeader>
<div className={styles.header}>RU Limit</div>
</AccordionHeader>
@@ -794,7 +660,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
<AccordionItem value="6">
<AccordionItem value="5">
<AccordionHeader>
<div className={styles.header}>Default Query Results View</div>
</AccordionHeader>
@@ -815,9 +681,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
</>
)}
{showRetrySettings && (
<AccordionItem value="7">
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
@@ -890,7 +755,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{!isEmulator && (
<AccordionItem value="8">
<AccordionItem value="7">
<AccordionHeader>
<div className={styles.header}>Enable container pagination</div>
</AccordionHeader>
@@ -914,7 +779,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowCrossPartitionOption && (
<AccordionItem value="9">
<AccordionItem value="8">
<AccordionHeader>
<div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader>
@@ -939,7 +804,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowParallelismOption && (
<AccordionItem value="10">
<AccordionItem value="9">
<AccordionHeader>
<div className={styles.header}>Max degree of parallelism</div>
</AccordionHeader>
@@ -972,7 +837,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowPriorityLevelOption && (
<AccordionItem value="11">
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>Priority Level</div>
</AccordionHeader>
@@ -995,7 +860,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowGraphAutoVizOption && (
<AccordionItem value="12">
<AccordionItem value="11">
<AccordionHeader>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div>
</AccordionHeader>
@@ -1016,7 +881,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="13">
<AccordionItem value="12">
<AccordionHeader>
<div className={styles.header}>Enable sample database</div>
</AccordionHeader>
@@ -1051,15 +916,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
"Clear History",
undefined,
"Are you sure you want to proceed?",
() => {
deleteAllStates();
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
},
() => deleteAllStates(),
"Cancel",
undefined,
<>
@@ -1070,7 +927,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<li>Reset your customized tab layout, including the splitter positions</li>
<li>Erase your table column preferences, including any custom columns</li>
<li>Clear your filter history</li>
<li>Reset region selection to global</li>
</ul>
</>,
);

View File

@@ -107,7 +107,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="4"
value="3"
>
<AccordionHeader>
<div
@@ -148,7 +148,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="5"
value="4"
>
<AccordionHeader>
<div
@@ -219,7 +219,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="6"
value="5"
>
<AccordionHeader>
<div
@@ -281,7 +281,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="7"
value="6"
>
<AccordionHeader>
<div
@@ -423,7 +423,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="8"
value="7"
>
<AccordionHeader>
<div
@@ -459,7 +459,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="9"
value="8"
>
<AccordionHeader>
<div
@@ -495,7 +495,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="10"
value="9"
>
<AccordionHeader>
<div
@@ -575,7 +575,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="customAccordion ___1uf6361_0000000 fz7g6wx"
>
<AccordionItem
value="7"
value="6"
>
<AccordionHeader>
<div
@@ -717,7 +717,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="8"
value="7"
>
<AccordionHeader>
<div
@@ -753,7 +753,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="12"
value="11"
>
<AccordionHeader>
<div

View File

@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
import Explorer from "Explorer/Explorer";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel";

View File

@@ -8,23 +8,15 @@ import {
MenuList,
MenuPopover,
MenuTrigger,
mergeClasses,
shorthands,
SplitButton,
SplitButton
} from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { Tabs } from "Explorer/Tabs/Tabs";
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { ResourceTree } from "Explorer/Tree/ResourceTree";
import { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
@@ -32,8 +24,10 @@ import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { Allotment, AllotmentHandle } from "allotment";
import { useSidePanel } from "hooks/useSidePanel";
import { useTheme } from "hooks/useTheme";
import { debounce } from "lodash";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { ResourceTree } from "./Tree/ResourceTree";
const useSidebarStyles = makeStyles({
sidebar: {
@@ -41,38 +35,67 @@ const useSidebarStyles = makeStyles({
},
sidebarContainer: {
height: "100%",
width: "100%",
borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
transition: "all 0.2s ease-in-out",
display: "flex",
flexDirection: "column",
backgroundColor: tokens.colorNeutralBackground1,
position: "relative",
},
expandedContent: {
display: "grid",
height: "100%",
width: "100%",
gridTemplateRows: `calc(${tokens.layoutRowHeight} * 2) 1fr`,
},
floatingControlsContainer: {
position: "relative",
position: "absolute",
top: 0,
right: 0,
zIndex: 1000,
width: "100%",
width: "auto",
padding: tokens.spacingHorizontalS,
},
floatingControls: {
display: "flex",
flexDirection: "row",
position: "absolute",
right: 0,
gap: tokens.spacingHorizontalXS,
},
floatingControlButton: {
...shorthands.border("none"),
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: {
display: "grid",
alignItems: "center",
justifyItems: "center",
width: "100%",
containerType: "size", // Use this container for "@container" queries below this.
containerType: "size",
padding: tokens.spacingHorizontalS,
...cosmosShorthands.borderBottom(),
backgroundColor: tokens.colorNeutralBackground1,
},
loadingProgressBar: {
// Float above the content
position: "absolute",
width: "100%",
height: "2px",
@@ -82,7 +105,7 @@ const useSidebarStyles = makeStyles({
animationDuration: "3s",
animationName: {
"0%": {
opacity: ".2", // matches indeterminate bar width
opacity: ".2",
},
"50%": {
opacity: "1",
@@ -104,6 +127,12 @@ const useSidebarStyles = makeStyles({
display: "flex",
},
},
treeContainer: {
flex: 1,
overflow: "auto",
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,
},
});
interface GlobalCommandsProps {
@@ -168,25 +197,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
});
}
if (isGlobalSecondaryIndexEnabled()) {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer,
};
actions.push({
id: "new_materialized_view",
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
icon: <Add16Regular />,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
),
});
}
return actions;
}, [explorer]);
@@ -275,6 +285,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
const [expandedSize, setExpandedSize] = React.useState(300);
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
const allotment = useRef<AllotmentHandle>(null);
const { isDarkMode } = useTheme();
const expand = useCallback(() => {
if (!expanded) {
@@ -329,7 +340,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
{hasSidebar && (
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
<Allotment.Pane minSize={24} preferredSize={250}>
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
<CosmosFluentProvider>
<div className={styles.sidebarContainer}>
{loading && (
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
@@ -340,18 +351,16 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
<>
<div className={styles.floatingControlsContainer}>
<div className={styles.floatingControls}>
{!isFabricNative() && (
<button
type="button"
data-test="Sidebar/RefreshButton"
className={styles.floatingControlButton}
disabled={loading}
title="Refresh"
onClick={onRefreshClick}
>
<ArrowSync12Regular />
</button>
)}
<button
type="button"
data-test="Sidebar/RefreshButton"
className={styles.floatingControlButton}
disabled={loading}
title="Refresh"
onClick={onRefreshClick}
>
<ArrowSync12Regular />
</button>
<button
type="button"
className={styles.floatingControlButton}
@@ -362,12 +371,11 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
</button>
</div>
</div>
<div
className={styles.expandedContent}
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
>
<div className={styles.expandedContent} style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}>
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
<ResourceTree explorer={explorer} />
<div className={styles.treeContainer}>
<ResourceTree explorer={explorer} />
</div>
</div>
</>
) : (

View File

@@ -3,8 +3,6 @@
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import * as React from "react";
import { userContext } from "UserContext";
@@ -110,10 +108,12 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
onClick,
}) => {
const styles = useStyles();
// TODO Make this a11y copmliant: aria-label for icon
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div className={styles.buttonLowerPart}>
<div>{title}</div>
<div>{description}</div>
</div>
@@ -123,8 +123,6 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
const styles = useStyles();
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
const getSplashScreenButtons = (): JSX.Element => {
const buttons: FabricHomeScreenButtonProps[] = [
{
@@ -140,13 +138,11 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
title: "App development",
description: "Start here to use an SDK to build your apps",
icon: <LinkMultipleRegular />,
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
},
];
@@ -161,25 +157,17 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
const title = "Build your database";
return (
<>
<CosmosFluentProvider className={styles.homeContainer}>
<SampleDataImportDialog
open={openSampleDataImportDialog}
setOpen={setOpenSampleDataImportDialog}
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
Need help?{" "}
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
</CosmosFluentProvider>
</>
<div className={styles.homeContainer}>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
Need help?{" "}
<Link href="https://cosmos.azure.com/docs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
</div>
);
};

View File

@@ -1,158 +0,0 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
makeStyles,
Spinner,
tokens,
} from "@fluentui/react-components";
import Explorer from "Explorer/Explorer";
import { checkContainerExists, createContainer, importData } from "Explorer/SplashScreen/SampleUtil";
import React, { useEffect, useState } from "react";
import * as ViewModels from "../../Contracts/ViewModels";
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
const useStyles = makeStyles({
dialogContent: {
alignItems: "center",
marginBottom: tokens.spacingVerticalL,
},
});
/**
* This dialog:
* - creates a container
* - imports data into the container
* @param props
* @returns
*/
export const SampleDataImportDialog: React.FC<{
open: boolean;
setOpen: (open: boolean) => void;
explorer: Explorer;
databaseName: string;
}> = (props) => {
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const containerName = SAMPLE_DATA_CONTAINER_NAME;
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
const styles = useStyles();
useEffect(() => {
// Reset state when dialog opens
if (props.open) {
setStatus("idle");
setErrorMessage(undefined);
}
}, [props.open]);
const handleStartImport = async (): Promise<void> => {
setStatus("creating");
const databaseName = props.databaseName;
if (checkContainerExists(databaseName, containerName)) {
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
setStatus("error");
setErrorMessage(msg);
return;
}
let collection;
try {
collection = await createContainer(databaseName, containerName, props.explorer);
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
return;
}
try {
setStatus("importing");
await importData(collection);
setCollection(collection);
setStatus("completed");
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
}
};
const handleActionOnClick = () => {
switch (status) {
case "idle":
handleStartImport();
break;
case "error":
props.setOpen(false);
break;
case "creating":
case "importing":
props.setOpen(false);
break;
case "completed":
props.setOpen(false);
collection.openTab();
break;
}
};
const renderContent = () => {
switch (status) {
case "idle":
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
case "creating":
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
case "importing":
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
case "completed":
return `Successfully created "${containerName}" with sample data.`;
case "error":
return (
<div style={{ color: "red" }}>
<div>Error: {errorMessage}</div>
</div>
);
}
};
const getButtonLabel = () => {
switch (status) {
case "idle":
return "Start";
case "creating":
case "importing":
return "Close";
case "completed":
return "Close";
case "error":
return "Close";
}
};
return (
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Sample Data</DialogTitle>
<DialogContent>
<div className={styles.dialogContent}>{renderContent()}</div>
</DialogContent>
<DialogActions>
<Button
appearance="primary"
onClick={handleActionOnClick}
disabled={status === "creating" || status === "importing"}
>
{getButtonLabel()}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};

View File

@@ -1,56 +0,0 @@
import { createCollection } from "Common/dataAccess/createCollection";
import Explorer from "Explorer/Explorer";
import { useDatabases } from "Explorer/useDatabases";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
/**
* Public for unit tests
* @param databaseName
* @param containerName
* @param containerDatabases
*/
const hasContainer = (
databaseName: string,
containerName: string,
containerDatabases: ViewModels.Database[],
): boolean => {
const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName);
return (
filteredDatabases.length > 0 &&
filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0
);
};
export const checkContainerExists = (databaseName: string, containerName: string) =>
hasContainer(databaseName, containerName, useDatabases.getState().databases);
export const createContainer = async (
databaseName: string,
containerName: string,
explorer: Explorer,
): Promise<ViewModels.Collection> => {
const createRequest: DataModels.CreateCollectionParams = {
createNewDatabase: false,
collectionId: containerName,
databaseId: databaseName,
databaseLevelThroughput: false,
};
await createCollection(createRequest);
await explorer.refreshAllDatabases();
const database = useDatabases.getState().findDatabaseWithId(databaseName);
if (!database) {
return undefined;
}
await database.loadCollections();
const newCollection = database.findCollectionWithId(containerName);
return newCollection;
};
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
// TODO: keep same chunk as ContainerSampleGenerator
const dataFileContent = await import(
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
);
await collection.bulkInsertDocuments(dataFileContent.data);
};

View File

@@ -1,178 +1,178 @@
@import "../../../less/Common/Constants";
// @import "../../../less/Common/Constants";
.splashScreenContainer {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
// .splashScreenContainer {
// width: 100%;
// overflow-y: scroll;
// overflow-x: hidden;
.splashScreen {
.flex-display();
.flex-direction();
text-align: left;
margin: auto;
padding-left: 21px;
padding-right: 16px;
max-width: 1168px;
// .splashScreen {
// .flex-display();
// .flex-direction();
// text-align: left;
// margin: auto;
// padding-left: 21px;
// padding-right: 16px;
// max-width: 1168px;
> .title {
position: relative; // To attach FeaturePanelLauncher as absolute
color: @BaseHigh;
font-size: 48px;
padding-left: 0px;
margin: 16px auto;
text-align: center;
}
// > .title {
// position: relative; // To attach FeaturePanelLauncher as absolute
// color: @BaseHigh;
// font-size: 48px;
// padding-left: 0px;
// margin: 16px auto;
// text-align: center;
// }
> .subtitle {
color: @BaseHigh;
font-size: 18px;
padding-left: 0px;
margin: 0px auto;
text-align: center;
}
// > .subtitle {
// color: @BaseHigh;
// font-size: 18px;
// padding-left: 0px;
// margin: 0px auto;
// text-align: center;
// }
.mainButtonsContainer {
.flex-display();
text-align: center;
cursor: pointer;
margin: 40px auto;
width: 84%;
// .mainButtonsContainer {
// .flex-display();
// text-align: center;
// cursor: pointer;
// margin: 40px auto;
// width: 84%;
> .mainButton {
min-width: 124px;
max-width: 296px;
padding: 32px 16px;
background-color: @BaseLight;
border: 1px solid #949494;
box-sizing: border-box;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 4px;
// > .mainButton {
// min-width: 124px;
// max-width: 296px;
// padding: 32px 16px;
// background-color: @BaseLight;
// border: 1px solid #949494;
// box-sizing: border-box;
// box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
// border-radius: 4px;
> .legendContainer {
margin-left: 16px;
text-align: left;
// > .legendContainer {
// margin-left: 16px;
// text-align: left;
.legend {
font-family: @SemiboldFont;
font-size: 18px;
}
// .legend {
// font-family: @SemiboldFont;
// font-size: 18px;
// }
.description {
font-size: 10px;
}
// .description {
// font-size: 10px;
// }
.newDescription {
font-size: 13px;
}
}
}
// .newDescription {
// font-size: 13px;
// }
// }
// }
> :nth-child(n + 2) {
margin-left: 32px;
}
}
// > :nth-child(n + 2) {
// margin-left: 32px;
// }
// }
.moreStuffContainer {
.flex-display();
justify-content: space-between;
// .moreStuffContainer {
// .flex-display();
// justify-content: space-between;
.moreStuffColumn {
flex-grow: 1;
flex-basis: 0;
min-width: 124px;
max-width: 296px;
// .moreStuffColumn {
// flex-grow: 1;
// flex-basis: 0;
// min-width: 124px;
// max-width: 296px;
> .title {
font-size: 18px;
font-family: @SemiboldFont;
color: @BaseDark;
padding: 0px;
margin-bottom: 16px;
}
// > .title {
// font-size: 18px;
// font-family: @SemiboldFont;
// color: @BaseDark;
// padding: 0px;
// margin-bottom: 16px;
// }
> ul {
list-style: none;
padding-left: 0px;
margin-bottom: 0px;
// > ul {
// list-style: none;
// padding-left: 0px;
// margin-bottom: 0px;
li {
padding: @DefaultSpace;
.flex-display();
align-items: flex-start;
// li {
// padding: @DefaultSpace;
// .flex-display();
// align-items: flex-start;
> img {
margin-right: @DefaultSpace;
width: 24px;
height: 24px;
}
// > img {
// margin-right: @DefaultSpace;
// width: 24px;
// height: 24px;
// }
.oneLineContent {
margin-top: 4px;
}
// .oneLineContent {
// margin-top: 4px;
// }
.description {
font-size: 10px;
color: @BaseMediumHigh;
}
}
}
// .description {
// font-size: 10px;
// color: @BaseMediumHigh;
// }
// }
// }
.tipContainer {
padding: 8px 16px;
width: 100%;
cursor: pointer;
.flex-display();
.flex-direction();
// .tipContainer {
// padding: 8px 16px;
// width: 100%;
// cursor: pointer;
// .flex-display();
// .flex-direction();
> .title {
color: @BaseDark;
padding: 0px;
font-size: 12px;
}
> .description {
color: @BaseDark;
}
// > .title {
// color: @BaseDark;
// padding: 0px;
// font-size: 12px;
// }
// > .description {
// color: @BaseDark;
// }
&:not(:hover):not(:focus) {
background-color: @BaseLow;
}
}
// &:not(:hover):not(:focus) {
// background-color: @BaseLow;
// }
// }
&.commonTasks {
li {
cursor: pointer;
}
}
// &.commonTasks {
// li {
// cursor: pointer;
// }
// }
&.tipsContainer {
li {
margin: 2px 0px;
}
}
}
}
// &.tipsContainer {
// li {
// margin: 2px 0px;
// }
// }
// }
// }
.focusable {
&:hover {
.hover();
}
// .focusable {
// &:hover {
// .hover();
// }
&:focus {
.focus();
}
// &:focus {
// .focus();
// }
&:active {
.active();
}
}
// &:active {
// .active();
// }
// }
.notebookSplashScreenItem {
padding: 12px 0 12px 12px;
// .notebookSplashScreenItem {
// padding: 12px 0 12px 12px;
.itemText {
margin-left: 12px;
font-family: @SemiboldFont;
}
}
}
}
// .itemText {
// margin-left: 12px;
// font-family: @SemiboldFont;
// }
// }
// }
// }

View File

@@ -11,6 +11,7 @@ import {
TeachingBubbleContent,
Text,
} from "@fluentui/react";
import { makeStyles, shorthands } from "@fluentui/react-components";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { TerminalKind } from "Contracts/ViewModels";
@@ -33,8 +34,7 @@ import CollectionIcon from "../../../images/tree-collection.svg";
import * as Constants from "../../Common/Constants";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import { useTheme } from "../../hooks/useTheme";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
@@ -55,70 +55,177 @@ export interface SplashScreenProps {
explorer: Explorer;
}
export class SplashScreen extends React.Component<SplashScreenProps> {
private readonly container: Explorer;
private subscriptions: Array<{ dispose: () => void }>;
const useStyles = makeStyles({
splashScreenContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
// justifyContent: "center",
minHeight: "100vh",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)",
constructor(props: SplashScreenProps) {
super(props);
this.container = props.explorer;
this.subscriptions = [];
}
public componentWillUnmount(): void {
while (this.subscriptions.length) {
this.subscriptions.pop().dispose();
},
splashScreen: {
display: "flex",
// overflow: "scroll",
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)"
}
});
public componentDidMount(): void {
this.subscriptions.push(
export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const styles = useStyles();
const { isDarkMode } = useTheme();
const container = explorer;
const subscriptions: Array<{ dispose: () => void }> = [];
React.useEffect(() => {
subscriptions.push(
{
dispose: useNotebook.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.isNotebookEnabled,
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
{ dispose: useSelectedNode.subscribe(() => setState({})) },
{
dispose: useCarousel.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.showCoachMark,
),
},
{
dispose: usePostgres.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.showPostgreTeachingBubble,
),
},
{
dispose: usePostgres.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.showResetPasswordBubble,
),
},
{
dispose: useDatabases.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.sampleDataResourceTokenCollection,
),
},
{
dispose: useQueryCopilot.subscribe(
() => this.setState({}),
() => setState({}),
(state) => state.copilotEnabled,
),
},
);
}
private clearMostRecent = (): void => {
return () => {
while (subscriptions.length) {
subscriptions.pop().dispose();
}
};
}, []);
const [state, setState] = React.useState({});
const clearMostRecent = () => {
MostRecentActivity.clear(userContext.databaseAccount?.name);
this.setState({});
setState({});
};
private getSplashScreenButtons = (): JSX.Element => {
const getSplashScreenButtons = (): JSX.Element => {
if (
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
@@ -132,7 +239,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title={"Launch quick start"}
description={"Launch a quick start tutorial to get started with sample data"}
onClick={() => {
this.container.onNewCollectionClicked({ isQuickstart: true });
container.onNewCollectionClicked({ isQuickstart: true });
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
}}
/>
@@ -141,7 +248,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title={`New ${getCollectionName()}`}
description={"Create a new container for storage and throughput"}
onClick={() => {
this.container.onNewCollectionClicked();
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
}}
/>
@@ -177,7 +284,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
const mainItems = this.createMainItems();
const mainItems = createMainItems();
return (
<div className="mainButtonsContainer">
{userContext.apiType === "Postgres" &&
@@ -214,7 +321,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
className="mainButton focusable"
key={`${item.title}`}
onClick={item.onClick}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
onKeyPress={(event: React.KeyboardEvent) => onSplashScreenItemKeyPress(event, item.onClick)}
tabIndex={0}
role="button"
>
@@ -267,125 +374,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
};
public render(): JSX.Element {
let title: string;
let subtitle: string;
switch (userContext.apiType) {
case "Postgres":
title = "Welcome to Azure Cosmos DB for PostgreSQL";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
break;
case "VCoreMongo":
title = "Welcome to Azure Cosmos DB for MongoDB (vCore)";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
break;
default:
title = "Welcome to Azure Cosmos DB";
subtitle = "Globally distributed, multi-model database service for any scale";
}
return (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<h1 className="title" role="heading" aria-label={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 createMainItems = (): SplashScreenItem[] => {
const heroes: SplashScreenItem[] = [];
if (
@@ -403,7 +392,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
} else {
this.container.onNewCollectionClicked({ isQuickstart: true });
container.onNewCollectionClicked({ isQuickstart: true });
}
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
},
@@ -411,18 +400,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
heroes.push(launchQuickstartBtn);
}
heroes.push(this.getShellCard());
heroes.push(this.getThirdCard());
heroes.push(getShellCard());
heroes.push(getThirdCard());
return heroes;
}
};
private getShellCard() {
const getShellCard = (): SplashScreenItem => {
if (userContext.apiType === "Postgres") {
return {
iconSrc: PowerShellIcon,
title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQLs shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
description: "Create table and interact with data using PostgreSQL's shell interface",
onClick: () => container.openNotebookTerminal(TerminalKind.Postgres),
};
}
@@ -431,7 +420,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: PowerShellIcon,
title: "Mongo Shell",
description: "Create a collection and interact with data using MongoDB's shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.VCoreMongo),
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
};
}
@@ -440,13 +429,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => {
this.container.onNewCollectionClicked();
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
},
};
}
};
private getThirdCard() {
const getThirdCard = (): SplashScreenItem => {
let icon = ConnectIcon;
let title = "Connect";
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
@@ -470,34 +459,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
description: description,
onClick: onClick,
};
}
};
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
const decorateOpenCollectionActivity = (activity: MostRecentActivity.OpenCollectionItem): SplashScreenItem => {
return {
iconSrc: CollectionIcon,
title: collectionId,
title: activity.collectionId,
description: getCollectionName(),
onClick: () => {
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
const collection = useDatabases.getState().findCollection(activity.databaseId, activity.collectionId);
collection?.openTab();
},
};
}
};
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
const decorateOpenNotebookActivity = (activity: MostRecentActivity.OpenNotebookItem): SplashScreenItem => {
return {
info: path,
info: activity.path,
iconSrc: NotebookIcon,
title: name,
title: activity.name,
description: "Notebook",
onClick: () => {
const notebookItem = this.container.createNotebookContentItemFile(name, path);
notebookItem && this.container.openNotebook(notebookItem);
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
notebookItem && container.openNotebook(notebookItem);
},
};
}
};
private createRecentItems(): SplashScreenItem[] {
const createRecentItems = (): SplashScreenItem[] => {
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
switch (activity.type) {
default: {
@@ -505,22 +494,22 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
throw new Error(`Unknown activity: ${unknownActivity}`);
}
case MostRecentActivity.Type.OpenNotebook:
return this.decorateOpenNotebookActivity(activity);
return decorateOpenNotebookActivity(activity);
case MostRecentActivity.Type.OpenCollection:
return this.decorateOpenCollectionActivity(activity);
return decorateOpenCollectionActivity(activity);
}
});
}
};
private onSplashScreenItemKeyPress(event: React.KeyboardEvent, callback: () => void) {
const onSplashScreenItemKeyPress = (event: React.KeyboardEvent, callback: () => void) => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
callback();
event.stopPropagation();
}
}
};
private top3Items(): JSX.Element {
const top3Items = (): JSX.Element => {
let items: { link: string; title: string; description: string }[];
switch (userContext.apiType) {
case "SQL":
@@ -632,44 +621,54 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
href={item.link}
target="_blank"
style={{ marginRight: 5 }}
className={styles.listItemTitle}
>
{item.title}
</Link>
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
<Text className={styles.listItemSubtitle}>{item.description}</Text>
</Stack>
))}
</Stack>
);
}
};
private getRecentItems(): JSX.Element {
const recentItems = this.createRecentItems()?.filter((item) => item.description !== "Notebook");
const getRecentItems = (): JSX.Element => {
const recentItems = createRecentItems()?.filter((item) => item.description !== "Notebook");
return (
<Stack>
<ul>
{recentItems.map((item, index) => (
<li key={`${item.title}${item.description}${index}`}>
<li key={`${item.title}${item.description}${index}`} className={styles.listItem}>
<Stack style={{ marginBottom: 26 }}>
<Stack horizontal>
<Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
<svg
width="16"
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}
</Link>
</Stack>
<Text style={{ color: "#605E5C" }}>{item.description}</Text>
<Text className={styles.listItemSubtitle}>{item.description}</Text>
</Stack>
</li>
))}
</ul>
{recentItems.length > 0 && <Link onClick={() => this.clearMostRecent()}>Clear Recents</Link>}
{recentItems.length > 0 && <Link onClick={() => clearMostRecent()} className={styles.listItemTitle}>Clear Recents</Link>}
</Stack>
);
}
};
private getLearningResourceItems(): JSX.Element {
const getLearningResourceItems = (): JSX.Element => {
interface item {
link: string;
title: string;
@@ -785,19 +784,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
href={item.link}
target="_blank"
style={{ marginRight: 5 }}
className={styles.listItemTitle}
>
{item.title}
</Link>
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
<Text className={styles.listItemSubtitle}>{item.description}</Text>
</Stack>
))}
</Stack>
);
}
};
private postgresNextStepItems: { link: string; title: string; description: string }[] = [
const postgresNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
title: "Data Modeling",
@@ -815,7 +815,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
private vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
const vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import",
title: "Migrate Data",
@@ -833,27 +833,27 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
private getNextStepItems(): JSX.Element {
const items = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems;
const getNextStepItems = (): JSX.Element => {
const items = userContext.apiType === "Postgres" ? postgresNextStepItems : vcoreMongoNextStepItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
{items.map((item, i) => (
<Stack key={`nextStep${i}`} style={{ marginBottom: 26 }}>
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
<Link href={item.link} target="_blank" style={{ marginRight: 5 }} className={styles.listItemTitle}>
{item.title}
</Link>
<Image src={LinkIcon} />
</Stack>
<Text>{item.description}</Text>
<Text className={styles.listItemSubtitle}>{item.description}</Text>
</Stack>
))}
</Stack>
);
}
};
private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
const postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
title: "Performance Tuning",
@@ -871,7 +871,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
private vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
const vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
title: "Vector Search",
@@ -889,23 +889,109 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
private getTipsAndLearnMoreItems(): JSX.Element {
const items = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems;
const getTipsAndLearnMoreItems = (): JSX.Element => {
const items = userContext.apiType === "Postgres" ? postgresLearnMoreItems : vcoreMongoLearnMoreItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
{items.map((item, i) => (
<Stack key={`tips${i}`} style={{ marginBottom: 26 }}>
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
<Link href={item.link} target="_blank" style={{ marginRight: 5 }} className={styles.listItemTitle}>
{item.title}
</Link>
<Image src={LinkIcon} />
</Stack>
<Text>{item.description}</Text>
<Text className={styles.listItemSubtitle}>{item.description}</Text>
</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>
);
};

View File

@@ -1,4 +1,5 @@
import { Stack, Text } from "@fluentui/react";
import { makeStyles } from "@fluentui/react-components";
import React from "react";
import { KeyCodes } from "../../Common/Constants";
@@ -9,25 +10,50 @@ interface SplashScreenButtonProps {
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> = ({
imgSrc,
title,
description,
onClick,
}: SplashScreenButtonProps): JSX.Element => {
const styles = useStyles();
return (
<Stack
horizontal
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,
}}
className={styles.button}
onClick={onClick}
onKeyPress={(event: React.KeyboardEvent) => {
if (event.charCode === KeyCodes.Space || event.charCode === KeyCodes.Enter) {
@@ -41,9 +67,9 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
<div>
<img src={imgSrc} alt={title} aria-hidden="true" />
</div>
<Stack style={{ marginLeft: 16 }}>
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
<Text style={{ fontSize: 13 }}>{description}</Text>
<Stack className={styles.content}>
<Text className={styles.title}>{title}</Text>
<Text className={styles.description}>{description}</Text>
</Stack>
</Stack>
);

View File

@@ -16,20 +16,10 @@ export const ConnectTab: React.FC = (): JSX.Element => {
const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState<string>("");
const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState<string>("");
const uri: string = userContext.databaseAccount.properties?.documentEndpoint;
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey};`;
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey};`;
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey};`;
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey};`;
const maskedValue: string =
"*********************************************************************************************************************************";
const [showPrimaryMasterKey, setShowPrimaryMasterKey] = useState<boolean>(false);
const [showSecondaryMasterKey, setShowSecondaryMasterKey] = useState<boolean>(false);
const [showPrimaryReadonlyMasterKey, setShowPrimaryReadonlyMasterKey] = useState<boolean>(false);
const [showSecondaryReadonlyMasterKey, setShowSecondaryReadonlyMasterKey] = useState<boolean>(false);
const [showPrimaryConnectionStr, setShowPrimaryConnectionStr] = useState<boolean>(false);
const [showSecondaryConnectionStr, setShowSecondaryConnectionStr] = useState<boolean>(false);
const [showPrimaryReadonlyConnectionStr, setShowPrimaryReadonlyConnectionStr] = useState<boolean>(false);
const [showSecondaryReadonlyConnectionStr, setShowSecondaryReadonlyConnectionStr] = useState<boolean>(false);
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey}`;
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey}`;
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey}`;
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey}`;
useEffect(() => {
fetchKeys();
@@ -72,97 +62,55 @@ export const ConnectTab: React.FC = (): JSX.Element => {
root: { width: "100%" },
field: { backgroundColor: "rgb(230, 230, 230)" },
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
suffix: {
backgroundColor: "rgb(230, 230, 230)",
margin: 0,
padding: 0,
},
};
const renderCopyButton = (selector: string) => (
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked(selector)}
styles={{
root: {
height: "100%",
backgroundColor: "rgb(230, 230, 230)",
border: "none",
},
rootHovered: {
backgroundColor: "rgb(220, 220, 220)",
},
rootPressed: {
backgroundColor: "rgb(210, 210, 210)",
},
}}
/>
);
return (
<div style={{ width: "100%", padding: 16 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 16, margin: 10 }}>
<TextField
label="URI"
id="uriTextfield"
readOnly
value={uri}
styles={textfieldStyles}
onRenderSuffix={() => renderCopyButton("#uriTextfield")}
/>
<div style={{ width: 32 }}></div>
</Stack>
<Pivot>
{userContext.hasWriteAccess && (
<PivotItem headerText="Read-write Keys">
<Stack style={{ margin: 10, overflow: "auto", maxHeight: "calc(100vh - 300px)" }}>
<Stack style={{ margin: 10 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="URI" id="uriTextfield" readOnly value={uri} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY KEY"
id="primaryKeyTextfield"
readOnly
value={showPrimaryMasterKey ? primaryMasterKey : maskedValue}
value={primaryMasterKey}
styles={textfieldStyles}
{...(showPrimaryMasterKey && {
onRenderSuffix: () => renderCopyButton("#primaryKeyTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showPrimaryMasterKey ? "Hide3" : "View" }}
onClick={() => setShowPrimaryMasterKey(!showPrimaryMasterKey)}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#primaryKeyTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY KEY"
id="secondaryKeyTextfield"
readOnly
value={showSecondaryMasterKey ? secondaryMasterKey : maskedValue}
value={secondaryMasterKey}
styles={textfieldStyles}
{...(showSecondaryMasterKey && {
onRenderSuffix: () => renderCopyButton("#secondaryKeyTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showSecondaryMasterKey ? "Hide3" : "View" }}
onClick={() => setShowSecondaryMasterKey(!showSecondaryMasterKey)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY CONNECTION STRING"
id="primaryConStrTextfield"
readOnly
value={showPrimaryConnectionStr ? primaryConnectionStr : maskedValue}
value={primaryConnectionStr}
styles={textfieldStyles}
{...(showPrimaryConnectionStr && {
onRenderSuffix: () => renderCopyButton("#primaryConStrTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showPrimaryConnectionStr ? "Hide3" : "View" }}
onClick={() => setShowPrimaryConnectionStr(!showPrimaryConnectionStr)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryConStrTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
@@ -170,36 +118,34 @@ export const ConnectTab: React.FC = (): JSX.Element => {
label="SECONDARY CONNECTION STRING"
id="secondaryConStrTextfield"
readOnly
value={showSecondaryConnectionStr ? secondaryConnectionStr : maskedValue}
value={secondaryConnectionStr}
styles={textfieldStyles}
{...(showSecondaryConnectionStr && {
onRenderSuffix: () => renderCopyButton("#secondaryConStrTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showSecondaryConnectionStr ? "Hide3" : "View" }}
onClick={() => setShowSecondaryConnectionStr(!showSecondaryConnectionStr)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryConStrTextfield")}
/>
</Stack>
</Stack>
</PivotItem>
)}
<PivotItem headerText="Read-only Keys">
<Stack style={{ margin: 10, overflow: "auto", maxHeight: "calc(100vh - 300px)" }}>
<Stack style={{ margin: 10 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="URI" id="uriReadOnlyTextfield" readOnly value={uri} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriReadOnlyTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY READ-ONLY KEY"
id="primaryReadonlyKeyTextfield"
readOnly
value={showPrimaryReadonlyMasterKey ? primaryReadonlyMasterKey : maskedValue}
value={primaryReadonlyMasterKey}
styles={textfieldStyles}
{...(showPrimaryReadonlyMasterKey && {
onRenderSuffix: () => renderCopyButton("#primaryReadonlyKeyTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showPrimaryReadonlyMasterKey ? "Hide3" : "View" }}
onClick={() => setShowPrimaryReadonlyMasterKey(!showPrimaryReadonlyMasterKey)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryReadonlyKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
@@ -207,15 +153,12 @@ export const ConnectTab: React.FC = (): JSX.Element => {
label="SECONDARY READ-ONLY KEY"
id="secondaryReadonlyKeyTextfield"
readOnly
value={showSecondaryReadonlyMasterKey ? secondaryReadonlyMasterKey : maskedValue}
value={secondaryReadonlyMasterKey}
styles={textfieldStyles}
{...(showSecondaryReadonlyMasterKey && {
onRenderSuffix: () => renderCopyButton("#secondaryReadonlyKeyTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showSecondaryReadonlyMasterKey ? "Hide3" : "View" }}
onClick={() => setShowSecondaryReadonlyMasterKey(!showSecondaryReadonlyMasterKey)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryReadonlyKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
@@ -223,31 +166,25 @@ export const ConnectTab: React.FC = (): JSX.Element => {
label="PRIMARY READ-ONLY CONNECTION STRING"
id="primaryReadonlyConStrTextfield"
readOnly
value={showPrimaryReadonlyConnectionStr ? primaryReadonlyConnectionStr : maskedValue}
value={primaryReadonlyConnectionStr}
styles={textfieldStyles}
{...(showPrimaryReadonlyConnectionStr && {
onRenderSuffix: () => renderCopyButton("#primaryReadonlyConStrTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showPrimaryReadonlyConnectionStr ? "Hide3" : "View" }}
onClick={() => setShowPrimaryReadonlyConnectionStr(!showPrimaryReadonlyConnectionStr)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryReadonlyConStrTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY READ-ONLY CONNECTION STRING"
id="secondaryReadonlyConStrTextfield"
value={showSecondaryReadonlyConnectionStr ? secondaryReadonlyConnectionStr : maskedValue}
value={secondaryReadonlyConnectionStr}
readOnly
styles={textfieldStyles}
{...(showSecondaryReadonlyConnectionStr && {
onRenderSuffix: () => renderCopyButton("#secondaryReadonlyConStrTextfield"),
})}
/>
<IconButton
iconProps={{ iconName: showSecondaryReadonlyConnectionStr ? "Hide3" : "View" }}
onClick={() => setShowSecondaryReadonlyConnectionStr(!showSecondaryReadonlyConnectionStr)}
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryReadonlyConStrTextfield")}
/>
</Stack>
</Stack>

View File

@@ -49,7 +49,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { Allotment } from "allotment";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { format } from "react-string-format";
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
@@ -306,7 +305,6 @@ export type ButtonsDependencies = {
selectedRows: Set<TableRowId>;
editorState: ViewModels.DocumentExplorerState;
isPreferredApiMongoDB: boolean;
clientWriteEnabled: boolean;
onNewDocumentClick: UiKeyboardEvent;
onSaveNewDocumentClick: UiKeyboardEvent;
onRevertNewDocumentClick: UiKeyboardEvent;
@@ -330,7 +328,6 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps =>
hasPopup: true,
disabled:
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
!useClientWriteEnabled.getState().clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
};
@@ -349,7 +346,6 @@ export const getTabsButtons = ({
selectedRows,
editorState,
isPreferredApiMongoDB,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
@@ -375,7 +371,6 @@ export const getTabsButtons = ({
hasPopup: false,
disabled:
!getNewDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: NEW_DOCUMENT_BUTTON_ID,
});
@@ -393,7 +388,6 @@ export const getTabsButtons = ({
hasPopup: false,
disabled:
!getSaveNewDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: SAVE_BUTTON_ID,
});
@@ -428,7 +422,6 @@ export const getTabsButtons = ({
hasPopup: false,
disabled:
!getSaveExistingDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: UPDATE_BUTTON_ID,
});
@@ -461,7 +454,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || !clientWriteEnabled,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: DELETE_BUTTON_ID,
});
}
@@ -635,7 +628,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
// State
const clientWriteEnabled = useClientWriteEnabled((state) => state.clientWriteEnabled);
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
leftPaneWidthPercent: 35,
@@ -773,14 +765,16 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
[_collection, _partitionKey],
);
const partitionKeyPropertyHeaders: string[] = useMemo(
() => (partitionKey?.systemKey ? [] : _collection?.partitionKeyPropertyHeaders || partitionKey?.paths),
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey],
() => _collection?.partitionKeyPropertyHeaders || partitionKey?.paths,
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths],
);
let partitionKeyProperties = useMemo(
() =>
partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
),
[partitionKeyPropertyHeaders],
);
let partitionKeyProperties = useMemo(() => {
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
);
}, [partitionKeyPropertyHeaders]);
const getInitialColumnSelection = () => {
const defaultColumnsIds = ["id"];
@@ -871,7 +865,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedRows,
editorState,
isPreferredApiMongoDB,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
@@ -1044,7 +1037,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
const selectedDocumentId = documentIds[clickedRowIndex as number];
const originalPartitionKeyValue = selectedDocumentId.partitionKeyValue;
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
onExecutionErrorChange(false);
@@ -1080,10 +1072,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
setColumnDefinitionsFromDocument(documentContent);
},
(error) => {
// in case of any kind of failures of accidently changing partition key, restore the original
// so that when user navigates away from current document and comes back,
// it doesnt fail to load due to using the invalid partition keys
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
@@ -1291,7 +1279,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedRows,
editorState,
isPreferredApiMongoDB,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
@@ -1304,7 +1291,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedRows,
editorState,
isPreferredApiMongoDB,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
@@ -1723,8 +1709,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false);
const _hasShardKeySpecified = (document: unknown): boolean => {
const partitionKeyDefinition: PartitionKeyDefinition = _getPartitionKeyDefinition() as PartitionKeyDefinition;
return partitionKeyDefinition.systemKey || Boolean(extractPartitionKeyValues(document, partitionKeyDefinition));
return Boolean(extractPartitionKeyValues(document, _getPartitionKeyDefinition() as PartitionKeyDefinition));
};
const _getPartitionKeyDefinition = (): DataModels.PartitionKey => {
@@ -1748,7 +1733,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return partitionKey;
};
partitionKeyProperties = partitionKeyProperties.map((partitionKeyProperty, i) => {
partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => {
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
}
@@ -2098,8 +2083,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return (
<CosmosFluentProvider className={styles.container}>
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-test={"DocumentsTab/Filter"} className={styles.filterRow}>
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div className={styles.filterRow}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList
dropdownOptions={getFilterChoices()}
@@ -2141,11 +2126,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}}
>
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div
data-test={"DocumentsTab/DocumentsPane"}
style={{ height: "100%", width: "100%", overflow: "hidden" }}
ref={tableContainerRef}
>
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
<div className={styles.tableContainer}>
<div
style={
@@ -2199,7 +2180,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
</div>
</Allotment.Pane>
<Allotment.Pane minSize={30}>
<div data-test={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}>
<div style={{ height: "100%", width: "100%" }}>
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
<EditorReact
language={"json"}

View File

@@ -6,7 +6,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
>
<div
className="tab-pane active"
data-test="DocumentsTab"
role="tabpanel"
style={
{
@@ -16,7 +15,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
>
<div
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
data-test="DocumentsTab/Filter"
>
<span>
SELECT * FROM c
@@ -67,7 +65,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
preferredSize="35%"
>
<div
data-test="DocumentsTab/DocumentsPane"
style={
{
"height": "100%",
@@ -129,7 +126,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
minSize={30}
>
<div
data-test="DocumentsTab/ResultsPane"
style={
{
"height": "100%",

View File

@@ -0,0 +1,114 @@
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>
);
};

View File

@@ -1,4 +1,4 @@
import { Link } from "@fluentui/react-components";
import { Link, tokens } from "@fluentui/react-components";
import QueryError from "Common/QueryError";
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
import { MessageBanner } from "Explorer/Controls/MessageBanner";
@@ -29,7 +29,7 @@ const ExecuteQueryCallToAction: React.FC = () => {
<p>
<img src={RunQuery} aria-hidden="true" />
</p>
<p>Execute a query to see the results</p>
<p style={{ color: tokens.colorNeutralForeground1 }}>Execute a query to see the results</p>
</div>
</div>
);

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import { AuthType } from "AuthType";
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext";
@@ -22,12 +21,13 @@ import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { Allotment } from "allotment";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs";
import { monacoTheme } from "hooks/useTheme";
import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import create from "zustand";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
@@ -56,6 +56,20 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";
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 {
Result,
QueryMetrics,
@@ -260,6 +274,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
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;
setTimeout(async () => {
@@ -486,9 +504,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!this.saveQueryButton.enabled ||
(!useClientWriteEnabled.getState().clientWriteEnabled && userContext.authType === AuthType.AAD),
disabled: !this.saveQueryButton.enabled,
});
}
@@ -700,7 +716,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
private unsubscribeCopilotSidebar: () => void;
private unsubscribeClientWriteEnabled: () => void;
componentDidMount(): void {
useTabs.subscribe((state: TabsState) => {
@@ -717,17 +732,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
useCommandBar.getState().setContextButtons(this.getTabsButtons());
document.addEventListener("keydown", this.handleCopilotKeyDown);
this.unsubscribeClientWriteEnabled = useClientWriteEnabled.subscribe(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
});
}
componentWillUnmount(): void {
document.removeEventListener("keydown", this.handleCopilotKeyDown);
if (this.unsubscribeClientWriteEnabled) {
this.unsubscribeClientWriteEnabled();
}
}
private getEditorAndQueryResult(): JSX.Element {
@@ -768,6 +776,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
wordWrap={"on"}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
theme={monacoTheme}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
onContentSelected={(selectedContent: string, selection: monaco.Selection) =>
this.onSelectedContent(selectedContent, selection)

View File

@@ -0,0 +1,202 @@
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();
});

View File

@@ -1,5 +1,8 @@
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
import { FontIcon } from "@fluentui/react";
import {
Button,
Checkbox,
DataGrid,
DataGridBody,
DataGridCell,
@@ -8,28 +11,44 @@ import {
DataGridRow,
SelectTabData,
SelectTabEvent,
Spinner,
Tab,
TabList,
Table,
TableBody,
TableCell,
TableColumnDefinition,
TableHeader,
TableRow,
createTableColumn,
} from "@fluentui/react-components";
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
import {
ArrowDownloadRegular,
ChevronDown20Regular,
ChevronRight20Regular,
CopyRegular
} from "@fluentui/react-icons";
import copy from "clipboard-copy";
import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { parseIndexMetrics, renderImpactDots } from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
import { IDocument, useQueryMetadataStore } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import React, { useCallback, useEffect, useState } from "react";
import { userContext } from "UserContext";
import copy from "clipboard-copy";
import React, { useCallback, useState } from "react";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import create from "zustand";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { ResultsViewProps } from "./QueryResultSection";
import { useIndexAdvisorStyles } from "./StylesAdvisor";
enum ResultsTabs {
Results = "results",
QueryStats = "queryStats",
IndexAdvisor = "indexadv",
}
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles();
const queryResultsString = queryResults
@@ -355,6 +374,286 @@ 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 }) => {
const styles = useQueryTabStyles();
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
@@ -362,7 +661,6 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
setActiveTab(data.value as ResultsTabs);
}, []);
return (
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
@@ -380,6 +678,13 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
>
Query Stats
</Tab>
<Tab
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
id={ResultsTabs.IndexAdvisor}
value={ResultsTabs.IndexAdvisor}
>
Index Advisor
</Tab>
</TabList>
<div className={styles.queryResultsTabContentContainer}>
{activeTab === ResultsTabs.Results && (
@@ -390,7 +695,23 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
/>
)}
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
</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 },
},
})),
}));

View File

@@ -0,0 +1,95 @@
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",
},
},
});

View File

@@ -0,0 +1,15 @@
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 }),
}));

View File

@@ -1,3 +1,4 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -15,8 +16,6 @@ import { userContext } from "UserContext";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout";
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 { useObservable } from "../../hooks/useObservable";
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
@@ -40,6 +39,14 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
});
}, [setKeyboardHandlers]);
// Add useEffect to handle context buttons
useEffect(() => {
if (activeReactTab !== undefined) {
// React tabs have no context buttons
useCommandBar.getState().setContextButtons([]);
}
}, [activeReactTab]);
return (
<div className="tabsManagerContainer">
<div className="nav-tabs-margin">
@@ -118,7 +125,17 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
{isTabExecuting(tab, tabKind) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
<Spinner
size={SpinnerSize.small}
styles={{
circle: {
borderTopColor: "var(--colorNeutralForeground1)",
borderLeftColor: "var(--colorNeutralForeground1)",
borderBottomColor: "var(--colorNeutralForeground1)",
borderRightColor: "var(--colorNeutralBackground1)"
}
}}
/>
)}
{isQueryErrorThrown(tab, tabKind) && (
<img
@@ -169,14 +186,11 @@ const CloseButton = ({
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
event.stopPropagation();
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
// tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
}}
tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
>
<span className="tabIcon close-Icon">
<img src={errorIcon} title="Close" alt="Close" aria-label="hidden" />
</span>
<span className="tabIcon close-Icon" />
</span>
);
@@ -259,10 +273,6 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
};
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// React tabs have no context buttons.
useCommandBar.getState().setContextButtons([]);
// eslint-disable-next-line no-console
switch (activeReactTab) {
case ReactTabKind.Connect:
return userContext.apiType === "VCoreMongo" ? (
@@ -287,6 +297,6 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
case ReactTabKind.QueryCopilot:
return <QueryCopilotTab explorer={explorer} />;
default:
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
}
};

View File

@@ -1,5 +1,6 @@
import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
import { IDropdownOption, IDropdownStyles, Label, TextField } from "@fluentui/react";
import { Dropdown } from "@fluentui/react/lib/Dropdown";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { Component } from "react";
@@ -17,12 +18,24 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import TriggerTab from "./TriggerTab";
const triggerTypeOptions: IDropdownOption[] = [
{ key: "Pre", text: "Pre" },
{ 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[] = [
{ key: "All", text: "All" },
{ key: "Create", text: "Create" },
@@ -304,6 +317,23 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
value={triggerId}
readOnly={!isIdEditable}
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
placeholder="Trigger Type"
@@ -312,6 +342,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
selectedKey={triggerType}
className="trigger-field"
onChange={(event, selectedKey) => this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")}
styles={dropdownStyles}
/>
<Dropdown
placeholder="Trigger Operation"
@@ -322,6 +353,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
onChange={(event, selectedKey) =>
this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation")
}
styles={dropdownStyles}
/>
<Label className="trigger-field">Trigger Body</Label>
<EditorReact

View File

@@ -1,7 +1,9 @@
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
import { Label, TextField } from "@fluentui/react";
import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { isDarkMode } from "hooks/useTheme";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -16,7 +18,6 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
interface IUserDefinedFunctionTabContentState {
udfId: string;
udfBody: string;
@@ -258,23 +259,46 @@ export default class UserDefinedFunctionTabContent extends Component<
render(): JSX.Element {
const { udfId, udfBody, isUdfIdEditable } = this.state;
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
return (
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
<TextField
className="trigger-field"
label="User Defined Function Id"
id="entityTimeId"
autoFocus
required
readOnly={!isUdfIdEditable}
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Enter the new user defined function id"
size={40}
value={udfId}
onChange={this.handleUdfIdChange}
/>
<FluentProvider theme={currentTheme}>
<TextField
className="trigger-field"
label="User Defined Function Id"
id="entityTimeId"
autoFocus
required
readOnly={!isUdfIdEditable}
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Enter the new user defined function id"
size={40}
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>
<EditorReact
language={"javascript"}

View File

@@ -4,16 +4,19 @@ import {
FluentProvider,
FluentProviderSlots,
Theme,
createDarkTheme,
createLightTheme,
makeStyles,
mergeClasses,
shorthands,
themeToTokensObject,
webLightTheme,
webDarkTheme,
webLightTheme
} from "@fluentui/react-components";
import { Platform, configContext } from "ConfigContext";
import React from "react";
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
import { useTheme } from "../../hooks/useTheme";
export const LayoutConstants = {
rowHeight: 32,
@@ -47,6 +50,7 @@ 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.
const { isInFluentProvider } = React.useContext(FluentProviderContext);
const styles = useDefaultRootStyles();
const { isDarkMode } = useTheme();
if (isInFluentProvider) {
// We're already in a fluent context, don't create another.
@@ -61,7 +65,7 @@ export const CosmosFluentProvider: React.FC<CosmosFluentProviderProps> = ({ chil
return (
<FluentProviderContext.Provider value={{ isInFluentProvider: true }}>
<FluentProvider
theme={getPlatformTheme(configContext.platform)}
theme={getPlatformTheme(configContext.platform, isDarkMode)}
className={mergeClasses(styles.fluentProvider, className)}
{...props}
>
@@ -114,7 +118,16 @@ const cosmosTheme = {
sidebarInitialWidth: "300px",
};
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosTheme, ...sizeMappings[LayoutSize.Compact] });
// Get the current theme tokens based on the root theme
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 = {
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
@@ -124,11 +137,12 @@ export const cosmosShorthands = {
borderLeft: () => shorthands.borderLeft("1px", "solid", tokens.colorNeutralStroke2),
};
export function getPlatformTheme(platform: Platform): CosmosTheme {
export function getPlatformTheme(platform: Platform, isDarkMode: boolean = false): CosmosTheme {
const createTheme = isDarkMode ? createDarkTheme : createLightTheme;
const baseTheme =
platform === Platform.Fabric
? createLightTheme(appThemeFabricTealBrandRamp)
: createLightTheme(appThemePortalBrandRamp);
? createTheme(appThemeFabricTealBrandRamp)
: createTheme(appThemePortalBrandRamp);
return {
...baseTheme,

View File

@@ -1,10 +1,4 @@
import {
JSONObject,
Resource,
StoredProcedureDefinition,
TriggerDefinition,
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
@@ -64,8 +58,6 @@ export default class Collection implements ViewModels.Collection {
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>;
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 conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
@@ -132,8 +124,6 @@ export default class Collection implements ViewModels.Collection {
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
this.computedProperties = ko.observable(data.computedProperties);
this.materializedViews = ko.observable(data.materializedViews);
this.materializedViewDefinition = ko.observable(data.materializedViewDefinition);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
@@ -1050,22 +1040,9 @@ export default class Collection implements ViewModels.Collection {
}
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
try {
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Start, {
nbFiles: files.length,
});
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Success, {
nbFiles: files.length,
});
return { data };
} catch (error) {
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Failed, {
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
}
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
return { data };
}
private uploadFile(file: File): Promise<UploadDetailsRecord> {
@@ -1092,56 +1069,6 @@ export default class Collection implements ViewModels.Collection {
});
}
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}> {
const stats = {
numSucceeded: 0,
numFailed: 0,
numThrottled: 0,
errors: [] as string[],
};
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
const chunkedContent = Array.from({ length: Math.ceil(documents.length / chunkSize) }, (_, index) =>
documents.slice(index * chunkSize, index * chunkSize + chunkSize),
);
for (const chunk of chunkedContent) {
let retryAttempts = 0;
let chunkComplete = false;
let documentsToAttempt = chunk;
while (retryAttempts < 10 && !chunkComplete) {
const responses = await bulkCreateDocument(this, documentsToAttempt);
const attemptedDocuments = [...documentsToAttempt];
documentsToAttempt = [];
responses.forEach((response, index) => {
if (response.statusCode === 201) {
stats.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else {
stats.numFailed++;
stats.errors.push(JSON.stringify(response.resourceBody));
}
});
if (documentsToAttempt.length === 0) {
chunkComplete = true;
break;
}
logConsoleInfo(
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`,
);
retryAttempts++;
await sleep(retryAttempts);
}
}
return stats;
}
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
const record: UploadDetailsRecord = {
fileName: fileName,
@@ -1154,11 +1081,38 @@ export default class Collection implements ViewModels.Collection {
try {
const parsedContent = JSON.parse(documentContent);
if (Array.isArray(parsedContent)) {
const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent);
record.numSucceeded = numSucceeded;
record.numFailed = numFailed;
record.numThrottled = numThrottled;
record.errors = errors;
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize),
);
for (const chunk of chunkedContent) {
let retryAttempts = 0;
let chunkComplete = false;
let documentsToAttempt = chunk;
while (retryAttempts < 10 && !chunkComplete) {
const responses = await bulkCreateDocument(this, documentsToAttempt);
const attemptedDocuments = [...documentsToAttempt];
documentsToAttempt = [];
responses.forEach((response, index) => {
if (response.statusCode === 201) {
record.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else {
record.numFailed++;
}
});
if (documentsToAttempt.length === 0) {
chunkComplete = true;
break;
}
logConsoleInfo(
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`,
);
retryAttempts++;
await sleep(retryAttempts);
}
}
} else {
await createDocument(this, parsedContent);
record.numSucceeded++;

View File

@@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer";
import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel";
import { AddCollectionPanel } from "../Panes/AddCollectionPanel";
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";

View File

@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -740,38 +740,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
]
`;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only (native) 1`] = `
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
[
{
"children": [
{
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
],
"id": "",
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
{
"id": "",
"isSelected": [Function],
"label": "Settings",
"onClick": [Function],
},
],
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
@@ -786,7 +760,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -798,38 +772,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"onExpanded": [Function],
},
{
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
],
"id": "",
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
{
"id": "",
"isSelected": [Function],
"label": "Settings",
"onClick": [Function],
},
{
"isSelected": [Function],
"label": "Conflicts",
"onClick": [Function],
},
],
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
@@ -844,7 +787,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -863,6 +806,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New Container",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Database",
"onClick": [Function],
"styleClass": "deleteDatabaseMenuItem",
},
],
"iconSrc": <DatabaseRegular
fontSize={16}
@@ -877,33 +826,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
{
"children": [
{
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
],
"id": "sampleItems",
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
{
"id": "sampleSettings",
"isSelected": [Function],
"label": "Settings",
"onClick": [Function],
},
],
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
@@ -918,7 +841,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -937,6 +860,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New Container",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Database",
"onClick": [Function],
"styleClass": "deleteDatabaseMenuItem",
},
],
"iconSrc": <DatabaseRegular
fontSize={16}
@@ -951,88 +880,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
{
"children": [
{
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
],
"id": "",
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
{
"id": "",
"isSelected": [Function],
"label": "Settings",
"onClick": [Function],
},
{
"children": [
{
"children": [
{
"children": [
{
"label": "string",
},
{
"label": "HasNulls: false",
},
],
"label": "street",
},
{
"children": [
{
"label": "string",
},
{
"label": "HasNulls: true",
},
],
"label": "line2",
},
{
"children": [
{
"label": "number",
},
{
"label": "HasNulls: false",
},
],
"label": "zip",
},
],
"label": "address",
},
{
"children": [
{
"label": "string",
},
{
"label": "HasNulls: false",
},
],
"label": "orderId",
},
],
"label": "Schema",
"onClick": [Function],
},
],
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
@@ -1047,7 +895,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1071,6 +919,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New Container",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Database",
"onClick": [Function],
"styleClass": "deleteDatabaseMenuItem",
},
],
"iconSrc": <DatabaseRegular
fontSize={16}
@@ -1085,7 +939,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
]
`;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only (mirrored) 1`] = `
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
[
{
"children": [
@@ -1099,7 +953,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"onClick": [Function],
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1120,7 +974,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"onClick": [Function],
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1156,7 +1010,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"onClick": [Function],
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1192,7 +1046,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"onClick": [Function],
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1354,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1457,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1591,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1771,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -1945,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -2043,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -2177,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
@@ -2357,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem",
},
],
"iconSrc": <EyeRegular
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,

View File

@@ -82,7 +82,6 @@ jest.mock("Explorer/Tree/Trigger", () => {
jest.mock("Common/DatabaseAccountUtility", () => {
return {
isPublicInternetAccessAllowed: () => true,
isGlobalSecondaryIndexEnabled: () => false,
};
});
@@ -135,15 +134,6 @@ const baseCollection = {
kind: "hash",
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([]),
userDefinedFunctions: ko.observableArray([]),
triggers: ko.observableArray([]),
@@ -373,28 +363,18 @@ describe("createDatabaseTreeNodes", () => {
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
[
"the SQL API, on Fabric read-only (mirrored)",
"the SQL API, on Fabric read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: {
isReadOnly: true,
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
} as FabricContext<CosmosDbArtifactType>,
},
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Fabric non read-only (native)",
"the SQL API, on Fabric non read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: {
isReadOnly: false,
artifactType: CosmosDbArtifactType.NATIVE,
} as FabricContext<CosmosDbArtifactType>,
},
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Portal",

View File

@@ -1,4 +1,4 @@
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import TabsBase from "Explorer/Tabs/TabsBase";
@@ -6,7 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { getItemName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useTabs } from "hooks/useTabs";
@@ -23,13 +23,12 @@ import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode";
export const shouldShowScriptNodes = (): boolean => {
return !isFabric() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
};
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />; //check icon
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
const updatedSampleTree: TreeNode = {
@@ -220,7 +219,7 @@ export const buildCollectionNode = (
): TreeNode => {
let children: TreeNode[];
// Flat Tree for Fabric
if (!isFabricMirrored()) {
if (configContext.platform !== Platform.Fabric) {
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
}
@@ -229,7 +228,7 @@ export const buildCollectionNode = (
children: children,
className: "collectionNode",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
iconSrc: collection.materializedViewDefinition() ? GlobalSecondaryIndexCollectionIcon : TreeCollectionIcon,
iconSrc: TreeCollectionIcon,
onClick: () => {
useSelectedNode.getState().setSelectedNode(collection);
collection.openTab();
@@ -318,7 +317,7 @@ const buildCollectionNodeChildren = (
children.push({
id,
label: database.isDatabaseShared() || isServerlessAccount() || isFabricNative() ? "Settings" : "Scale & Settings",
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),
isSelected: () =>
useSelectedNode

View File

@@ -1,11 +1,11 @@
{
"MaterializedViewsBuilderDescription": "Provision a materialized views builder for your Azure Cosmos DB account. Materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materialized view definition.",
"MaterializedViewsBuilder": "Materialized views builder",
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
"MaterializedViewsBuilder": "Materializedviews Builder",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
"LearnAboutMaterializedViews": "Learn more about materialized views.",
"DeprovisioningDetailsText": "Learn more about materialized views.",
"MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.",
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
"DeprovisioningDetailsText": "Learn more about materializedviews.",
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
@@ -14,58 +14,35 @@
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Materialized views builder resource is being created.",
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Materialized views builder resource will be provisioned.",
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Materialized views builder resource provisioned.",
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Materialized views builder resource provisioning failed.",
"UpdateMessage": "Materialized views builder resource is being updated.",
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Materialized views builder resource will be updated.",
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Materialized views builder resource updated.",
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Materialized views builder resource update failed.",
"DeleteMessage": "Materialized views builder resource is being deleted.",
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Materialized views builder resource will be deleted.",
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Materialized views builder resource deleted.",
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Materialized views builder resource deletion failed.",
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the materialized views builder resource depends on the SKU selection, number of instances per region, and number of regions.",
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the CPU and memory usage for the materialized views builder instances in ",
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"ResizingDecisionText": "To understand if the materialized views builder is the right size, ",
"ResizingDecisionLink": "learn more about materialized views builder sizing.",
"WarningBannerOnUpdate": "Adding or modifying materialized views builder instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the materialized views builder, your materialized views will not be updated with new source changes anymore. Materialized views builder is compute in your account that performs read operations on source containers for any updates and applies them on materialized views as per the materialized view definition.",
"GlobalsecondaryindexesBuilderDescription": "Provision a global secondary indexes builder for your Azure Cosmos DB account. The global secondary indexes builder is compute in your account that performs read operations on source collections for any updates and populates the global secondary indexes as per their definition.",
"GlobalsecondaryindexesBuilder": "Global secondary indexes builder",
"LearnAboutGlobalSecondaryIndexes": "Learn more about global secondary indexes.",
"GlobalsecondaryindexesDeprovisioningDetailsText": "Learn more about global secondary indexes.",
"GlobalsecondaryindexesBuilderPricing": "Learn more about global secondary indexes pricing.",
"GlobalsecondaryindexesCreateMessage": "Global secondary indexes builder resource is being created.",
"GlobalsecondaryindexesCreateInitializeMessage": "Global secondary indexes builder resource will be provisioned.",
"GlobalsecondaryindexesCreateSuccesseMessage": "Global secondary indexes builder resource provisioned.",
"GlobalsecondaryindexesCreateFailureMessage": "Global secondary indexes builder resource provisioning failed.",
"GlobalsecondaryindexesUpdateMessage": "Global secondary indexes builder resource is being updated.",
"GlobalsecondaryindexesUpdateInitializeMessage": "Global secondary indexes builder resource will be updated.",
"GlobalsecondaryindexesUpdateSuccesseMessage": "Global secondary indexes builder resource updated.",
"GlobalsecondaryindexesUpdateFailureMessage": "Global secondary indexes builder resource update failed.",
"GlobalsecondaryindexesDeleteMessage": "Global secondary indexes builder resource is being deleted.",
"GlobalsecondaryindexesDeleteInitializeMessage": "Global secondary indexes builder resource will be deleted.",
"GlobalsecondaryindexesDeleteSuccesseMessage": "Global secondary indexes builder resource deleted.",
"GlobalsecondaryindexesDeleteFailureMessage": "Global secondary indexes builder resource deletion failed.",
"GlobalsecondaryindexesCostText": "Hourly cost of the global secondary indexes builder resource depends on the SKU selection, number of instances per region, and number of regions.",
"GlobalsecondaryindexesMetricsText": "Monitor the CPU and memory usage for the global secondary indexes builder instances in ",
"GlobalsecondaryindexesResizingDecisionText": "To understand if the global secondary indexes builder is the right size, ",
"GlobalsecondaryindexesesizingDecisionLink": "learn more about global secondary indexes builder sizing.",
"GlobalsecondaryindexesWarningBannerOnUpdate": "Adding or modifying global secondary indexes builder instances may affect your bill.",
"GlobalsecondaryindexesWarningBannerOnDelete": "After deprovisioning the global secondary indexes builder, your global secondary indexes will no longer be updated with new source changes. Global secondary indexes builder is compute in your account that performs read operations on source containers for any updates and applies them on global secondary indexes as per their definition."
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
}

View File

@@ -2,14 +2,16 @@
import "./ReactDevTools";
// CSS Dependencies
import { initializeIcons, loadTheme } from "@fluentui/react";
import { initializeIcons, loadTheme, useTheme } from "@fluentui/react";
import { FluentProvider, makeStyles, webDarkTheme, webLightTheme } from "@fluentui/react-components";
import { Platform } from "ConfigContext";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
import "allotment/dist/style.css";
import "bootstrap/dist/css/bootstrap.css";
import { useCarousel } from "hooks/useCarousel";
import React from "react";
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js";
@@ -19,7 +21,7 @@ import "../externals/jquery.dataTables.min.css";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import { SidePanel } from "Explorer/Panes/PanelContainerComponent";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { SidebarContainer } from "Explorer/Sidebar";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
@@ -46,6 +48,7 @@ import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -54,21 +57,32 @@ import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Libs/jquery";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { isDarkMode } from "./hooks/useTheme";
// Initialize icons before React is loaded
initializeIcons(undefined, { disableWarnings: true });
initializeIcons();
const useStyles = makeStyles({
root: {
height: "100vh",
width: "100vw",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)"
}
});
const App: React.FunctionComponent = () => {
const App = (): JSX.Element => {
const config = useConfig();
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
const styles = useStyles();
const theme = useTheme();
const config = useConfig();
if (config?.platform === Platform.Fabric) {
loadTheme(appThemeFabric);
import("../less/documentDBFabric.less");
@@ -81,51 +95,111 @@ const App: React.FunctionComponent = () => {
}
return (
<KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
<CommandBar container={explorer} />
{/* Collections Tree and Tabs - Begin */}
<SidebarContainer explorer={explorer} />
{/* Collections Tree and Tabs - End */}
<div id="Main" className={styles.root}>
<KeyboardShortcutRoot>
<div
className="flexContainer"
style={{
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)"
}}
aria-hidden="false"
data-test="DataExplorerRoot"
>
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
id="divExplorer"
className="flexContainer hideOverflows"
style={{
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)"
}}
>
<NotificationConsole />
<div id="freeTierTeachingBubble"> </div>
{/* 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>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
</KeyboardShortcutRoot>
</div>
);
};
const Root: React.FC = () => {
// Force dark theme
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
// 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");
ReactDOM.render(<App />, mainElement);
if (mainElement) {
ReactDOM.render(<Root />, mainElement);
}
function LoadingExplorer(): JSX.Element {
const styles = useStyles();
return (
<div className="splashLoaderContainer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
<div className={styles.root}>
<div className="splashLoaderContainer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
</div>
);

View File

@@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes";
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
import {
FetchPricesResponse,
MaterializedViewsBuilderServiceResource,
PriceMapAndCurrencyCode,
RegionsResponse,
MaterializedViewsBuilderServiceResource,
UpdateMaterializedViewsBuilderRequestParameters,
} from "./MaterializedViewsBuilderTypes";
@@ -123,23 +123,11 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<Ref
if (response.properties.status === ResourceStatus.Running.toString()) {
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateMessage" : "CreateMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteMessage" : "DeleteMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
} else {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateMessage" : "UpdateMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
}
} catch {
//TODO differentiate between different failures

View File

@@ -29,20 +29,17 @@ import {
updateMaterializedViewsBuilderResource,
} from "./MaterializedViewsBuilder.rp";
import { userContext } from "../../UserContext";
const costPerHourDefaultValue: Description = {
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
textTKey: "CostText",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
const metricsStringValue: Description = {
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesMetricsText" : "MetricsText",
textTKey: "MetricsText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.Metrics),
@@ -79,8 +76,7 @@ const onNumberOfInstancesChange = (
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} as Description,
hidden: false,
@@ -120,8 +116,7 @@ const onEnableMaterializedViewsBuilderChange = (
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} as Description,
hidden: false,
@@ -134,17 +129,10 @@ const onEnableMaterializedViewsBuilderChange = (
} else {
currentValues.set("warningBanner", {
value: {
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesWarningBannerOnDelete" : "WarningBannerOnDelete",
textTKey: "WarningBannerOnDelete",
link: {
href:
userContext.apiType === "SQL"
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
textTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesDeprovisioningDetailsText"
: "DeprovisioningDetailsText",
href: "https://aka.ms/cosmos-db-materializedviews",
textTKey: "DeprovisioningDetailsText",
},
} as Description,
hidden: false,
@@ -194,19 +182,18 @@ const getInstancesMax = async (): Promise<number> => {
};
const NumberOfInstancesDropdownInfo: Info = {
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText",
messageTKey: "ResizingDecisionText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink",
textTKey: "ResizingDecisionLink",
},
};
const ApproximateCostDropDownInfo: Info = {
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
@@ -281,20 +268,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "DeleteInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesDeleteInitializeMessage"
: "DeleteInitializeMessage",
messageTKey: "DeleteInitializeMessage",
},
success: {
titleTKey: "DeleteSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage",
messageTKey: "DeleteSuccesseMessage",
},
failure: {
titleTKey: "DeleteFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage",
messageTKey: "DeleteFailureMessage",
},
},
};
@@ -307,20 +289,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "UpdateInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesUpdateInitializeMessage"
: "UpdateInitializeMessage",
messageTKey: "UpdateInitializeMessage",
},
success: {
titleTKey: "UpdateSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage",
messageTKey: "UpdateSuccesseMessage",
},
failure: {
titleTKey: "UpdateFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage",
messageTKey: "UpdateFailureMessage",
},
},
};
@@ -334,20 +311,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "CreateInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesCreateInitializeMessage"
: "CreateInitializeMessage",
messageTKey: "CreateInitializeMessage",
},
success: {
titleTKey: "CreateSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage",
messageTKey: "CreateSuccesseMessage",
},
failure: {
titleTKey: "CreateFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage",
messageTKey: "CreateFailureMessage",
},
},
};
@@ -394,17 +366,11 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
@Values({
description: {
textTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesBuilderDescription"
: "MaterializedViewsBuilderDescription",
textTKey: "MaterializedViewsBuilderDescription",
type: DescriptionType.Text,
link: {
href:
userContext.apiType === "SQL"
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
textTKey: userContext.apiType === "SQL" ? "LearnAboutGlobalSecondaryIndexes" : "LearnAboutMaterializedViews",
href: "https://aka.ms/cosmos-db-materializedviews",
textTKey: "LearnAboutMaterializedViews",
},
},
})
@@ -412,7 +378,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
@OnChange(onEnableMaterializedViewsBuilderChange)
@Values({
labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder",
labelTKey: "MaterializedViewsBuilder",
trueLabelTKey: "Provisioned",
falseLabelTKey: "Deprovisioned",
})

View File

@@ -10,7 +10,6 @@ export enum AppStateComponentNames {
MostRecentActivity = "MostRecentActivity",
QueryCopilot = "QueryCopilot",
DataExplorerAction = "DataExplorerAction",
SelectedRegionalEndpoint = "SelectedRegionalEndpoint",
}
// Subcomponent for DataExplorerAction

View File

@@ -1,18 +1,16 @@
// Data Explorer specific actions. No need to keep this in sync with the one in Portal.
// Some of the enums names are used in Fabric. Please do not rename them.
export enum Action {
CollapseTreeNode,
CreateCollection, // Used in Fabric. Please do not rename.
CreateGlobalSecondaryIndex,
CreateDocument, // Used in Fabric. Please do not rename.
CreateCollection,
CreateDocument,
CreateStoredProcedure,
CreateTrigger,
CreateUDF,
DeleteCollection, // Used in Fabric. Please do not rename.
DeleteCollection,
DeleteDatabase,
DeleteDocument,
ExpandTreeNode,
ExecuteQuery, // Used in Fabric. Please do not rename.
ExecuteQuery,
HasFeature,
GetVNETServices,
InitializeAccountLocationFromResourceGroup,
@@ -121,7 +119,6 @@ export enum Action {
NotebooksGalleryPublishedCount,
SelfServe,
ExpandAddCollectionPaneAdvancedSection,
ExpandAddGlobalSecondaryIndexPaneAdvancedSection,
SchemaAnalyzerClickAnalyze,
SelfServeComponent,
LaunchQuickstart,
@@ -145,7 +142,6 @@ export enum Action {
ReadPersistedTabState,
SavePersistedTabState,
DeletePersistedTabState,
UploadDocuments, // Used in Fabric. Please do not rename.
}
export const ActionModifiers = {

View File

@@ -111,8 +111,6 @@ export interface UserContext {
readonly isReplica?: boolean;
collectionCreationDefaults: CollectionCreationDefaults;
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
readonly selectedRegionalEndpoint?: string;
readonly writeEnabledInSelectedRegion?: boolean;
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
readonly dataPlaneRbacEnabled?: boolean;

View File

@@ -35,13 +35,6 @@ describe("Query Utils", () => {
version: 2,
};
};
const generatePartitionKeysForPaths = (paths: string[]): DataModels.PartitionKey => {
return {
paths: paths,
kind: "Hash",
version: 2,
};
};
describe("buildDocumentsQueryPartitionProjections()", () => {
it("should return empty string if partition key is undefined", () => {
@@ -96,18 +89,6 @@ describe("Query Utils", () => {
expect(query).toContain("c.id");
});
it("should always include {} for any missing partition keys", () => {
const query = QueryUtils.buildDocumentsQuery(
"",
["a", "b", "c"],
generatePartitionKeysForPaths(["/a", "/b", "/c"]),
[],
);
expect(query).toContain('IIF(IS_DEFINED(c["a"]), c["a"], {})');
expect(query).toContain('IIF(IS_DEFINED(c["b"]), c["b"], {})');
expect(query).toContain('IIF(IS_DEFINED(c["c"]), c["c"], {})');
});
});
describe("queryPagesUntilContentPresent()", () => {
@@ -220,6 +201,18 @@ describe("Query Utils", () => {
expect(expectedPartitionKeyValues).toContain(documentContent["Category"]);
});
it("should extract no partition key values in the case nested partition key", () => {
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.Hash,
paths: ["/Location.type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
documentContent,
singlePartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(0);
});
it("should extract all partition key values for hierarchical and nested partition keys", () => {
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
@@ -232,52 +225,5 @@ describe("Query Utils", () => {
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual(["United States", "Point"]);
});
it("if any partition key is null or empty string, the partitionKeyValues shall match", () => {
const newDocumentContent = {
...documentContent,
...{
Country: null,
Location: {
type: "",
coordinates: [-121.49, 46.206],
},
},
};
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
paths: ["/Country", "/Location/type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
newDocumentContent,
mixedPartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual([null, ""]);
});
it("if any partition key doesn't exist, it should still set partitionkey value as {}", () => {
const newDocumentContent = {
...documentContent,
...{
Country: null,
Location: {
coordinates: [-121.49, 46.206],
},
},
};
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
paths: ["/Country", "/Location/type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
newDocumentContent,
mixedPartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual([null, {}]);
});
});
});

View File

@@ -47,7 +47,6 @@ export function buildDocumentsQueryPartitionProjections(
for (const index in partitionKey.paths) {
// TODO: Handle "/" in partition key definitions
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
const isSystemPartitionKey: boolean = partitionKey.systemKey || false;
let projectedProperty = "";
projectedProperties.forEach((property: string) => {
@@ -62,13 +61,8 @@ export function buildDocumentsQueryPartitionProjections(
projectedProperty += `[${projection}]`;
}
});
const fullAccess = `${collectionAlias}${projectedProperty}`;
if (!isSystemPartitionKey) {
const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`;
projections.push(wrappedProjection);
} else {
projections.push(fullAccess);
}
projections.push(`${collectionAlias}${projectedProperty}`);
}
return projections.join(",");
@@ -124,7 +118,7 @@ export const extractPartitionKeyValues = (
documentContent: any,
partitionKeyDefinition: PartitionKeyDefinition,
): PartitionKey[] => {
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0 || partitionKeyDefinition.systemKey) {
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
return undefined;
}
@@ -136,8 +130,6 @@ export const extractPartitionKeyValues = (
if (value !== undefined) {
partitionKeyValues.push(value);
} else {
partitionKeyValues.push({});
}
});

View File

@@ -1,10 +0,0 @@
import create, { UseStore } from "zustand";
interface ClientWriteEnabledState {
clientWriteEnabled: boolean;
setClientWriteEnabled: (writeEnabled: boolean) => void;
}
export const useClientWriteEnabled: UseStore<ClientWriteEnabledState> = create((set) => ({
clientWriteEnabled: true,
setClientWriteEnabled: (clientWriteEnabled: boolean) => set({ clientWriteEnabled }),
}));

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