Compare commits

...

29 Commits

Author SHA1 Message Date
Sampath
610b559469 defect1722595 Bug 1722595: [Screen readers Azure Cosmos DB Scale& Settings: Screen reader (NVDA) is not announcing status message which is displayed on the screen after radio button is selected under scale tab. 2022-12-23 18:39:13 +05:30
Sampath
6defcfb7e5 update snapshot 2022-12-23 18:16:16 +05:30
Sampath
ad953ffa1d InfoTooltip changes 2022-12-23 17:51:17 +05:30
Sampath
511b0bd8c0 Revert "commit for tooltip in defect 1704149"
This reverts commit 44766e8213.
2022-12-23 17:17:04 +05:30
Sampath
44766e8213 commit for tooltip in defect 1704149 2022-12-23 17:14:03 +05:30
Sampath
60d666df1e Revert "changes in infotooltip for defct"
This reverts commit ca9833e208.
2022-12-22 20:52:26 +05:30
Sampath
ca9833e208 changes in infotooltip for defct 2022-12-22 19:44:51 +05:30
Sampath
042ad24cc6 changes to addcollectionpanel.test.tsx snp file 2022-12-20 02:56:51 +05:30
Sampath
445427ecec format fixes 2022-12-20 02:00:52 +05:30
Sampath
6d19aaab43 committing changes for the keyboard navigation 2022-12-20 00:41:17 +05:30
Sampath
7fbecfff1a Revert "html,css changes corected after reversion"
This reverts commit 712e0e0c1e.
2022-12-20 00:36:31 +05:30
Sampath
712e0e0c1e html,css changes corected after reversion 2022-12-19 18:52:12 +05:30
Sampath
6aa229462b Revert "keyboard navigation for defects 1722611,1722618"
This reverts commit 9383609a22.
2022-12-19 18:45:53 +05:30
Sampath
990d86ffc6 Fixes for keyboard navigation of add new clause,edit,remove property,insert filter line, remove filter line 2022-12-19 18:27:52 +05:30
MokireddySampath
9383609a22 keyboard navigation for defects 1722611,1722618 2022-12-19 12:41:08 +05:30
vchske
2f32a676d0 Fixes issue when CRUD with no parition key for all APIs (#1362)
* Fixes issue where empty partition key is treated like nonexistent key for Tables API

* Updated format

* Refactor fix

* Prettier

* Fixes issue when CRUD with no parition key for all APIs

* Format fix

* Refactor to explicitly check for Tables
2022-12-13 11:36:39 -08:00
victor-meng
950c8ee470 Fix "Change network settings" button send the wrong message type (#1360) 2022-12-09 15:48:23 -08:00
vchske
b0eaac5b84 Fixes issue where empty partition key is treated like nonexistent key… (#1359) 2022-12-09 15:14:46 -08:00
victor-meng
952491a3ad Empty commit to trigger new build (#1351) 2022-11-14 13:46:27 -08:00
victor-meng
5dde66b032 Add check and guidance for networking settings (#1348) 2022-11-10 10:46:55 -08:00
jawelton74
5b1db2778c Fix typo in 'tutorial' in Quick start. (#1346) 2022-10-27 09:59:14 -07:00
Armando Trejo Oliver
1213788f9c Remove share-link feature (#1345)
* Remove share-link feature

Cosmos DB supports sharing access to an account data using AAD/RBAC now so this feature is unnecessary.

* Fix format issues

* Fix unit tests

* undo package changes
2022-10-20 10:58:37 -07:00
victor-meng
1ce3adff0f Hide launch quick start button and postgres teaching bubbles if account is a replica (#1344) 2022-10-19 17:12:41 -07:00
victor-meng
00eb07da11 Add retries when checking if account is allowed to access phoenix (#1342) 2022-10-19 17:12:24 -07:00
victor-meng
afe59c1589 Postgres fixes (#1341) 2022-10-11 16:03:58 -07:00
victor-meng
53b5ebd39c Add firewall notification in quickstart tab (#1337) 2022-10-10 19:30:52 -07:00
sunghyunkang1111
5b365e642f show introductory video and password reset for first time try postgresql (#1338) (#1340) 2022-10-10 18:53:54 -05:00
victor-meng
333b3de587 Add new message type for opening postgres networking blade (#1336) 2022-10-06 14:25:10 -07:00
victor-meng
e909ac43f4 Integrate PSQL shell in quick start guide (#1333) 2022-10-06 11:32:19 -07:00
49 changed files with 891 additions and 588 deletions

BIN
images/firewallRule.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -3,337 +3,337 @@
@import "../Common/Constants"; @import "../Common/Constants";
.query-panel { .query-panel {
display: table; display: table;
display: none; display: none;
width: 100%; width: 100%;
border-top: 1px solid #DDDDDD; border-top: 1px solid #dddddd;
/*[{environment-commandbar-toolbar-separator}]*/ /*[{environment-commandbar-toolbar-separator}]*/
background-color: #ffffff; background-color: #ffffff;
/*[{plugin-background-color}]*/ /*[{plugin-background-color}]*/
padding: 2px 0px 0px 2px; padding: 2px 0px 0px 2px;
resize: vertical; resize: vertical;
} }
.query-panel .row { .query-panel .row {
display: table-row; display: table-row;
} }
.query-panel .row .cell { .query-panel .row .cell {
display: table-cell; display: table-cell;
} }
.query-panel.transition-in { .query-panel.transition-in {
display: table; display: table;
top: 0px; top: 0px;
-webkit-transition: top 2s linear; -webkit-transition: top 2s linear;
-ms-transition: top 2s linear; -ms-transition: top 2s linear;
-moz-transition: top 2s linear; -moz-transition: top 2s linear;
-khtml-transition: top 2s linear; -khtml-transition: top 2s linear;
-o-transition: top 2s linear; -o-transition: top 2s linear;
transition: top 2s linear; transition: top 2s linear;
} }
.query-builder { .query-builder {
width:100%; width: 100%;
padding-right: @DefaultSpace; padding-right: @DefaultSpace;
border-bottom: 1px solid @BaseMedium; border-bottom: 1px solid @BaseMedium;
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;
} }
.query-builder-toolbar { .query-builder-toolbar {
background-color: #ffffff; background-color: #ffffff;
/*[{plugin-background-color}]*/ /*[{plugin-background-color}]*/
min-width: 600px; min-width: 600px;
height: 30px; height: 30px;
border-bottom: 1px solid #DDDDDD; border-bottom: 1px solid #dddddd;
/*[1px solid {environment-commandbar-toolbar-separator}]*/ /*[1px solid {environment-commandbar-toolbar-separator}]*/
} }
.query-builder-toolbar .query-toolbar-group { .query-builder-toolbar .query-toolbar-group {
display: inline-block; display: inline-block;
height: 24px; height: 24px;
margin: 2px 0px; margin: 2px 0px;
vertical-align: middle; vertical-align: middle;
} }
.query-builder-toolbar .query-toolbar-group .query-toolbar-button { .query-builder-toolbar .query-toolbar-group .query-toolbar-button {
min-width: 0px; min-width: 0px;
padding: 0px; padding: 0px;
margin-left: 2px; margin-left: 2px;
background-color: transparent; background-color: transparent;
border: solid transparent; border: solid transparent;
} }
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active { .query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
outline: 2px solid dodgerblue; outline: 2px solid dodgerblue;
/*[2px solid {common-common-controls-button-border-hover}]*/ /*[2px solid {common-common-controls-button-border-hover}]*/
} }
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover { .query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
background-color: #CCCEDB; background-color: #cccedb;
/*[{common-controls-button-hover-background}]*/ /*[{common-controls-button-hover-background}]*/
} }
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active { .query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
background-color: #E6E7ED; background-color: #e6e7ed;
/*[{common-controls-inner-tab-active-background}]*/ /*[{common-controls-inner-tab-active-background}]*/
outline: none outline: none;
} }
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled, .query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled { .query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
background-color: #ffffff; background-color: #ffffff;
/*[{common-controls-button-disabled-background}]*/ /*[{common-controls-button-disabled-background}]*/
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
outline: none; outline: none;
opacity: 0.4; opacity: 0.4;
} }
.tableContainer { .tableContainer {
overflow: hidden; overflow: hidden;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();
} }
.tablesQueryTab{ .tablesQueryTab {
padding-left: @MediumSpace; padding-left: @MediumSpace;
width: 100%; width: 100%;
margin-bottom:@LargeSpace; margin-bottom: @LargeSpace;
} }
.entity-error-Img { .entity-error-Img {
width: @WarningErrorIconSize; width: @WarningErrorIconSize;
height: @WarningErrorIconSize; height: @WarningErrorIconSize;
margin: @DefaultSpace 0px 0px @SmallSpace; margin: @DefaultSpace 0px 0px @SmallSpace;
} }
.query-editor-panel { .query-editor-panel {
margin-right: 16px; margin-right: 16px;
margin-left: 16px; margin-left: 16px;
margin-top: 25px; margin-top: 25px;
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
cursor: default; cursor: default;
} }
.query-editor-text { .query-editor-text {
width: 100%; width: 100%;
margin: 2px; margin: 2px;
border: solid 1px #A9ACB3; border: solid 1px #a9acb3;
/*[{plugin-textbox-disabled-color}]*/ /*[{plugin-textbox-disabled-color}]*/
resize: none; resize: none;
margin-top: -39px; margin-top: -39px;
background-color: #ddd; background-color: #ddd;
padding: 5px; padding: 5px;
} }
.error-bar { .error-bar {
padding: @LargeSpace 34px @MediumSpace 24px; padding: @LargeSpace 34px @MediumSpace 24px;
} }
.error-message { .error-message {
background-color: @BaseLow; background-color: @BaseLow;
padding: @DefaultSpace; padding: @DefaultSpace;
display: inline-flex; display: inline-flex;
} }
.error-text { .error-text {
padding-left: @MediumSpace; padding-left: @MediumSpace;
} }
.query-editor-text-invalid { .query-editor-text-invalid {
width: 100%; width: 100%;
margin: 2px; margin: 2px;
border: 1px solid #e51400; border: 1px solid #e51400;
resize: none; resize: none;
margin-top: -30px; margin-top: -30px;
} }
.query-editor-panel .warning-bar { .query-editor-panel .warning-bar {
width: 100%; width: 100%;
height: 20px; height: 20px;
background-color: #ffffff; background-color: #ffffff;
/*[{plugin-background-color}]*/ /*[{plugin-background-color}]*/
position: absolute; position: absolute;
top: -24px; top: -24px;
} }
.query-editor-panel .warning-bar .warning-message { .query-editor-panel .warning-bar .warning-message {
display: inline-flex; display: inline-flex;
padding-top: 2px; padding-top: 2px;
vertical-align: middle; vertical-align: middle;
} }
.query-editor-panel .warning-bar .warning-message .warning-text { .query-editor-panel .warning-bar .warning-message .warning-text {
margin-left: 2px; margin-left: 2px;
} }
.advanced-options-panel{ .advanced-options-panel {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;
} }
.advanced-options-panel .advanced-heading .advanced-title { .advanced-options-panel .advanced-heading .advanced-title {
display: inline-flex; display: inline-flex;
margin-left: 27px; margin-left: 27px;
margin-top: 10px; margin-top: 10px;
cursor: default; cursor: default;
} }
.advanced-options-panel .advanced-options { .advanced-options-panel .advanced-options {
margin-left: 32px; margin-left: 32px;
margin-top: 5px; margin-top: 5px;
border: 1px solid transparent; border: 1px solid transparent;
} }
hr { hr {
margin-top: 10px; margin-top: 10px;
margin-bottom: 12px; margin-bottom: 12px;
border: 0; border: 0;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button { input::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
.advanced-options-panel .advanced-options .top .top-input { .advanced-options-panel .advanced-options .top .top-input {
width: 100px; width: 100px;
word-spacing: normal; word-spacing: normal;
color: #1E1E1E; color: #1e1e1e;
/*[{common-controls-button-foreground}]*/ /*[{common-controls-button-foreground}]*/
border: 1px solid #CCCEDB; border: 1px solid #cccedb;
/*[1px solid {plugin-textbox-border-color}]*/ /*[1px solid {plugin-textbox-border-color}]*/
height: 20px; height: 20px;
margin-left: 8px; margin-left: 8px;
} }
.advanced-options-panel .advanced-options .top .invalid-top { .advanced-options-panel .advanced-options .top .invalid-top {
color: red; color: red;
} }
.advanced-options-panel .advanced-options .select { .advanced-options-panel .advanced-options .select {
margin-top: 18px; margin-top: 18px;
display: inline-flex; display: inline-flex;
} }
.advanced-options-icon { .advanced-options-icon {
margin-left: 2px; margin-left: 2px;
vertical-align: sub; vertical-align: sub;
} }
.advanced-options-panel .advanced-options .select .select-options-text { .advanced-options-panel .advanced-options .select .select-options-text {
margin-left: 4px; margin-left: 4px;
} }
.advanced-options-panel .advanced-options .select .select-options-link { .advanced-options-panel .advanced-options .select .select-options-link {
margin-left: 4px; margin-left: 4px;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
} }
.query-panel .row .column-headers .Field { .query-panel .row .column-headers .Field {
padding-left: 95px; padding-left: 95px;
padding-right: 0px; padding-right: 0px;
padding-bottom: 6px; padding-bottom: 6px;
} }
.clause-table { .clause-table {
border-spacing: 0px; border-spacing: 0px;
display: table; display: table;
width: 100%; width: 100%;
margin-top: -3px; margin-top: -3px;
} }
.clause-table-row { .clause-table-row {
display: row; display: row;
margin-bottom: 10px; margin-bottom: 10px;
} }
.clause-table-cell { .clause-table-cell {
display: table-cell; display: table-cell;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
} }
.action-column>button, .action-column > button,
.group-control-header>button, .group-control-header > button,
.group-indicator-column>button { .group-indicator-column > button {
min-width: 20px; min-width: 20px;
width: 20px; width: 20px;
padding: 0px; padding: 0px;
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
cursor: pointer; cursor: pointer;
} }
.group-control-header>button:disabled { .group-control-header > button:disabled {
min-width: 20px; min-width: 20px;
width: 20px; width: 20px;
padding: 0px; padding: 0px;
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
outline: none; outline: none;
opacity: 0.4; opacity: 0.4;
cursor: default; cursor: default;
} }
.clause-table-field { .clause-table-field {
width: 100%; width: 100%;
border: 1px solid #bbbbbb; border: 1px solid #bbbbbb;
} }
.clause-table-cell button { .clause-table-cell button {
height: 20px; height: 20px;
} }
.clause-table-cell input[type="checkbox"] { .clause-table-cell input[type="checkbox"] {
padding: 0px; padding: 0px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.and-or-svg { .and-or-svg {
margin-top: -8px; margin-top: -8px;
margin-right: -5px; margin-right: -5px;
} }
.scroll-box { .scroll-box {
border-bottom: 1px transparent #DDD; border-bottom: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/ /*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #DDD; border-top: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/ /*[1px solid {plugin-table-border-color}]*/
max-height: 20vh; max-height: 20vh;
width: 100%; width: 100%;
} }
.scrollable { .scrollable {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
} }
.and-or-column, .and-or-column,
.and-or-header { .and-or-header {
min-width: 65px; min-width: 65px;
padding-right: 10px; padding-right: 10px;
padding-left: 5px; padding-left: 5px;
} }
.operator-column, .operator-column,
.operator-header { .operator-header {
min-width: 65px; min-width: 65px;
padding-right: 10px; padding-right: 10px;
} }
.field-header, .field-header,
.field-column { .field-column {
min-width: 125px; min-width: 125px;
padding-right: 10px; padding-right: 10px;
} }
.type-header, .type-header,
.type-column { .type-column {
min-width: 85px; min-width: 85px;
} }
.and-or-column, .and-or-column,
@@ -345,41 +345,41 @@ input::-webkit-inner-spin-button {
.type-header, .type-header,
.type-column, .type-column,
.action-header { .action-header {
padding-right: 10px; padding-right: 10px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.value-header, .value-header,
.value-column, .value-column,
.time-column { .time-column {
min-width: 230px; min-width: 230px;
padding: 0px 4px 0px 0px; padding: 0px 4px 0px 0px;
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;
} }
.group-control-header, .group-control-header,
.group-control-column { .group-control-column {
min-width: 25px; min-width: 25px;
text-align: right; text-align: right;
} }
.group-indicator-table { .group-indicator-table {
border-spacing: 0px; border-spacing: 0px;
min-height: 24px min-height: 24px;
} }
.group-indicator-column { .group-indicator-column {
min-width: 21px; min-width: 21px;
padding: 0px; padding: 0px;
border-style: none; border-style: none;
height: 29px; height: 29px;
} }
.clause-table-cell.action-column, .clause-table-cell.action-column,
.clause-table-cell.action-header { .clause-table-cell.action-header {
min-width: 60px; min-width: 60px;
padding-left: @SmallSpace; padding-left: @SmallSpace;
} }
.action-header, .action-header,
@@ -388,15 +388,14 @@ input::-webkit-inner-spin-button {
.operator-header, .operator-header,
.value-header, .value-header,
.and-or-header { .and-or-header {
padding-right: 4px; padding-right: 4px;
padding-bottom: 5px; padding-bottom: 5px;
} }
.header-background { .header-background {
background-color: #ffffff; background-color: #ffffff;
} }
/*.type-header { /*.type-header {
padding-right: 4px; padding-right: 4px;
} }
@@ -410,112 +409,111 @@ input::-webkit-inner-spin-button {
}*/ }*/
.clause-table-field[readonly] { .clause-table-field[readonly] {
background-color: #EEEEF2; background-color: #eeeef2;
/*[{plugin-table-header-background-color}]*/ /*[{plugin-table-header-background-color}]*/
border: 1px solid #CCCEDB; border: 1px solid #cccedb;
/*[{plugin-table-border-color}]*/ /*[{plugin-table-border-color}]*/
} }
.addClause-title { .addClause-title {
/*[{common-common-controls-button-border-hover}]*/ /*[{common-common-controls-button-border-hover}]*/
cursor: pointer; cursor: pointer;
margin-left: -5px; margin-left: -5px;
} }
.addClause { .addClause {
width: 125px; width: 125px;
padding: 8px 0px 5px 5px; padding: 8px 0px 5px 5px;
border: 1px solid #fff; border: 1px solid #fff;
margin-left: 5px; margin-left: 5px;
} }
.addClause:hover { .addClause:hover {
.hover(); .hover();
} }
.addClause:active { .addClause:active {
.active(); .active();
border: 1px dashed @AccentMedium; border: 1px dashed @AccentMedium;
} }
.clause-table-row { .clause-table-row {
min-width: 550px; min-width: 550px;
width: 100%; width: 100%;
} }
.clause-table-field field-column { .clause-table-field field-column {
min-width: 75px; min-width: 75px;
height: 30px; height: 30px;
width: 100%; width: 100%;
} }
.clause-table-field field-input { .clause-table-field field-input {
min-width: 54px; min-width: 54px;
margin-left: -78px; margin-left: -78px;
height: 25px; height: 25px;
border: none; border: none;
} }
.query-panel .row .spacing { .query-panel .row .spacing {
padding-bottom: 6px; padding-bottom: 6px;
} }
.query-panel .divider.horizontal { .query-panel .divider.horizontal {
height: 10px; height: 10px;
width: 100% width: 100%;
} }
.inline-div { .inline-div {
display: inline display: inline;
} }
.querybuilder-addpropertyImg, .querybuilder-addpropertyImg,
.querybuilder-cancelImg { .querybuilder-cancelImg {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin-left: 3px; margin-left: 3px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.addclauseProperty-Img { .addclauseProperty-Img {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin-bottom: 5px; margin-bottom: 5px;
margin-left: 12px; margin-left: 12px;
} }
.entity-Add-Cancel { .entity-Add-Cancel {
padding: @DefaultSpace @SmallSpace @SmallSpace; // padding: @DefaultSpace @SmallSpace @SmallSpace;
cursor: pointer; cursor: pointer;
} }
.entity-Add-Cancel:hover { .entity-Add-Cancel:hover {
.hover(); .hover();
} }
.entity-Add-Cancel:active { .entity-Add-Cancel:active {
.active(); .active();
} }
.query-builder-isDisabled { .query-builder-isDisabled {
border: 1px solid #CCCEDB; border: 1px solid #cccedb;
color: #ccc; color: #ccc;
} }
.edit-value-text { .edit-value-text {
padding-left: @DefaultSpace; padding-left: @DefaultSpace;
} }
.expand-triangle { .expand-triangle {
width: 10px; width: 10px;
height: 10px; height: 10px;
} }
.expand-triangle-right { .expand-triangle-right {
margin-bottom: 5px; margin-bottom: 5px;
} }
/* /*
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.clause-table { .clause-table {
@@ -524,4 +522,4 @@ input::-webkit-inner-spin-button {
width: 100%; width: 100%;
padding-top: 10px; padding-top: 10px;
} }
}*/ }*/

View File

@@ -73,6 +73,17 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
const sectionStackTokens: IStackTokens = { childrenGap: 12 }; const sectionStackTokens: IStackTokens = { childrenGap: 12 };
const handleKeyPress = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
onEditEntity();
}
};
const handleKeyPressdelete = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
onDeleteEntity();
}
};
const getEntityValueType = (): string => { const getEntityValueType = (): string => {
const { Int, Smallint, Tinyint } = CassandraType; const { Int, Smallint, Tinyint } = CassandraType;
const { Double, Int32, Int64 } = TableType; const { Double, Int32, Int64 } = TableType;
@@ -126,12 +137,28 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
/> />
{!isEntityValueDisable && ( {!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip"> <TooltipHost content="Edit property" id="editTooltip">
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} /> <Image
{...imageProps}
src={EditIcon}
alt="editEntity"
id="editEntity"
onClick={onEditEntity}
tabIndex={0}
onKeyPress={handleKeyPress}
/>
</TooltipHost> </TooltipHost>
)} )}
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && ( {isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
<TooltipHost content="Delete property" id="deleteTooltip"> <TooltipHost content="Delete property" id="deleteTooltip">
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} /> <Image
{...imageProps}
src={DeleteIcon}
alt="delete entity"
id="deleteEntity"
onClick={onDeleteEntity}
tabIndex={0}
onKeyPress={handleKeyPressdelete}
/>
</TooltipHost> </TooltipHost>
)} )}
</Stack> </Stack>

View File

@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
return ( return (
<span> <span>
<TooltipHost content={children}> <TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} /> <Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</span> </span>
); );

View File

@@ -4,6 +4,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => { export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName(); const entityName: string = getEntityName();
@@ -13,7 +14,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
await client() await client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue) .item(documentId.id(), getPartitionKeyValue(documentId))
.delete(); .delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`); logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,12 @@
import { userContext } from "UserContext";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const getPartitionKeyValue = (documentId: DocumentId) => {
if (userContext.apiType === "Tables" && documentId.partitionKeyValue?.length === 0) {
return "";
}
if (documentId.partitionKeyValue?.length === 0) {
return undefined;
}
return documentId.partitionKeyValue;
};

View File

@@ -6,6 +6,7 @@ import { HttpHeaders } from "../Constants";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => { export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName(); const entityName = getEntityName();
@@ -21,8 +22,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
const response = await client() const response = await client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
// use undefined if the partitionKeyValue is empty .item(documentId.id(), getPartitionKeyValue(documentId))
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.read(options); .read(options);
return response?.resource; return response?.resource;

View File

@@ -6,6 +6,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const updateDocument = async ( export const updateDocument = async (
collection: CollectionBase, collection: CollectionBase,
@@ -25,7 +26,7 @@ export const updateDocument = async (
const response = await client() const response = await client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue) .item(documentId.id(), getPartitionKeyValue(documentId))
.replace(newDocument, options); .replace(newDocument, options);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);

View File

@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number }; capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[]; locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
publicNetworkAccess?: string;
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {
@@ -566,6 +568,16 @@ export interface ContainerConnectionInfo {
//need to add ram and rom info //need to add ram and rom info
} }
export interface PostgresFirewallRule {
id: string;
name: string;
type: string;
properties: {
startIpAddress: string;
endIpAddress: string;
};
}
export enum PhoenixErrorType { export enum PhoenixErrorType {
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded", MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded", MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",

View File

@@ -36,6 +36,8 @@ export enum MessageTypes {
CloseTab, CloseTab,
OpenQuickstartBlade, OpenQuickstartBlade,
OpenPostgreSQLPasswordReset, OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
} }
@@ -397,6 +396,7 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string; dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
isPostgresAccount?: boolean; isPostgresAccount?: boolean;
isReplica?: boolean;
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array // TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
connectionStringParams?: any; connectionStringParams?: any;
flights?: readonly string[]; flights?: readonly string[];

View File

@@ -2,6 +2,7 @@
* Wrapper around Notebook server terminal * Wrapper around Notebook server terminal
*/ */
import { useTerminal } from "hooks/useTerminal";
import postRobot from "post-robot"; import postRobot from "post-robot";
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
@@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void { handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow; this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
useTerminal.getState().setTerminal(this.terminalWindow);
this.sendPropsToTerminalFrame(); this.sendPropsToTerminalFrame();
} }
@@ -75,7 +77,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) { } else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint; terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) { } else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return (this.props.databaseAccount?.properties as any).postgresqlEndpoint; return this.props.databaseAccount?.properties.postgresqlEndpoint;
} }
if (terminalEndpoint) { if (terminalEndpoint) {

View File

@@ -1,9 +1,12 @@
import { mergeStyleSets } from "@fluentui/merge-styles";
import { import {
Checkbox, Checkbox,
ChoiceGroup, ChoiceGroup,
FontIcon, FontIcon,
IChoiceGroupOption, IChoiceGroupOption,
IColumn, IColumn,
Icon,
initializeIcons,
Label, Label,
Link, Link,
MessageBar, MessageBar,
@@ -82,7 +85,7 @@ interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean; spendAckChecked: boolean;
exceedFreeTierThroughput: boolean; exceedFreeTierThroughput: boolean;
} }
initializeIcons();
export class ThroughputInputAutoPilotV3Component extends React.Component< export class ThroughputInputAutoPilotV3Component extends React.Component<
ThroughputInputAutoPilotV3Props, ThroughputInputAutoPilotV3Props,
ThroughputInputAutoPilotV3State ThroughputInputAutoPilotV3State
@@ -620,13 +623,20 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
if (this.IsComponentDirty().isDiscardable) { if (this.IsComponentDirty().isDiscardable) {
warningMessage = saveThroughputWarningMessage; warningMessage = saveThroughputWarningMessage;
} }
const classNames = mergeStyleSets({
orange: [{ color: "orange" }],
});
return ( return (
<> <>
{warningMessage && ( {warningMessage && (
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}> // <MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
// {warningMessage}
// </MessageBar>
<div className="warningonmanual" role="alert" aria-atomic="true">
<Icon iconName="WarningSolid" className={classNames.orange} />
{warningMessage} {warningMessage}
</MessageBar> </div>
)} )}
</> </>
); );

View File

@@ -8,14 +8,15 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
} }
} }
> >
<StyledMessageBar <div
messageBarIconProps={ aria-atomic="true"
Object { className="warningonmanual"
"className": "messageBarWarningIcon", role="alert"
"iconName": "WarningSolid",
}
}
> >
<Icon
className="orange-53"
iconName="WarningSolid"
/>
<Text <Text
styles={ styles={
Object { Object {
@@ -28,7 +29,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
> >
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
</Text> </Text>
</StyledMessageBar> </div>
<Stack> <Stack>
<StyledLabelBase <StyledLabelBase
id="settingsV2RadioButtonLabelId" id="settingsV2RadioButtonLabelId"

View File

@@ -344,13 +344,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
onMouseLeave={[Function]} onMouseLeave={[Function]}
> >
<StyledIconBase <StyledIconBase
ariaLabel="Info" ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0} tabIndex={0}
> >
<IconBase <IconBase
ariaLabel="Info" ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
styles={[Function]} styles={[Function]}
@@ -630,7 +630,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
} }
> >
<i <i
aria-label="Info" aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon root-57" className="panelInfoIcon root-57"
data-icon-name="Info" data-icon-name="Info"
role="img" role="img"
@@ -1331,13 +1331,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
onMouseLeave={[Function]} onMouseLeave={[Function]}
> >
<StyledIconBase <StyledIconBase
ariaLabel="Info" ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0} tabIndex={0}
> >
<IconBase <IconBase
ariaLabel="Info" ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
styles={[Function]} styles={[Function]}
@@ -1617,7 +1617,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
} }
> >
<i <i
aria-label="Info" aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon root-57" className="panelInfoIcon root-57"
data-icon-name="Info" data-icon-name="Info"
role="img" role="img"

View File

@@ -186,9 +186,7 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (userContext.apiType !== "Postgres") { this.refreshExplorer();
this.refreshExplorer();
}
} }
public async initiateAndRefreshNotebookList(): Promise<void> { public async initiateAndRefreshNotebookList(): Promise<void> {
@@ -1249,9 +1247,11 @@ export default class Explorer {
} }
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken if (userContext.apiType !== "Postgres") {
? this.refreshDatabaseForResourceToken() userContext.authType === AuthType.ResourceToken
: this.refreshAllDatabases(); ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount

View File

@@ -85,14 +85,11 @@ export function createStaticCommandBarButtons(
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" || userContext.apiType === "Cassandra"
userContext.apiType === "Postgres"
) { ) {
notebookButtons.push(createDivider()); notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container)); notebookButtons.push(createOpenCassandraTerminalButton(container));
} else if (userContext.apiType === "Postgres") {
notebookButtons.push(createOpenPsqlTerminalButton(container));
} else { } else {
notebookButtons.push(createOpenMongoTerminalButton(container)); notebookButtons.push(createOpenMongoTerminalButton(container));
} }
@@ -612,16 +609,7 @@ function createStaticCommandBarButtonsForResourceToken(
} }
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] { export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const postgreShellLabel = "Open PostgreSQL Shell"; const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
const openPostgreShellBtn = {
iconSrc: HostedTerminalIcon,
iconAlt: postgreShellLabel,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo),
commandButtonLabel: postgreShellLabel,
hasPopup: false,
disabled: false,
ariaLabel: postgreShellLabel,
};
return [openPostgreShellBtn]; return [openPostgreShellBtn];
} }

View File

@@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
} }
const firstWriteLocation = const firstWriteLocation =
databaseAccount?.properties?.writeLocations && userContext.apiType === "Postgres"
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase(); ? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
try { try {
@@ -313,7 +314,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) { if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) { if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true; isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures === true; isPhoenixFeatures =
isPublicInternetAllowed &&
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
} else { } else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed; isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
} }

View File

@@ -1,17 +1,9 @@
jest.mock("../hooks/useFullScreenURLs");
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
import { OpenFullScreen } from "./OpenFullScreen"; import { OpenFullScreen } from "./OpenFullScreen";
it("renders the correct URLs", () => { it("renders the correct URLs", () => {
(useFullScreenURLs as jest.Mock).mockReturnValue({
readWrite: "read and write url",
read: "read only url",
});
render(<OpenFullScreen />); render(<OpenFullScreen />);
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url"); expect(screen.getByText("Open")).toBeDefined();
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
}); });

View File

@@ -1,66 +1,26 @@
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react"; import { PrimaryButton, Stack, Text } from "@fluentui/react";
import copyToClipboard from "clipboard-copy";
import * as React from "react"; import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
export const OpenFullScreen: React.FunctionComponent = () => { export const OpenFullScreen: React.FunctionComponent = () => {
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
const result = useFullScreenURLs();
if (!result) {
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
}
const readWriteUrl = `https://cosmos.azure.com/?key=${result.readWrite}`;
const readUrl = `https://cosmos.azure.com/?key=${result.read}`;
return ( return (
<> <>
<Stack tokens={{ childrenGap: 10 }}> <div style={{ padding: "34px" }}>
<Text> <Stack tokens={{ childrenGap: 10 }}>
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only <Text>
access urls below to share with others. For security purposes, the URLs grant time-bound access to the Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
account. When access expires, you can reconnect, using a valid connection string for the account. Microsoft account or a connection string.
</Text> </Text>
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} /> <Stack horizontal tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 10 }}> <PrimaryButton
<DefaultButton onClick={() => {
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"} window.open("https://cosmos.azure.com/", "_blank");
onClick={() => { }}
copyToClipboard(readWriteUrl); text="Open"
setIsReadWriteUrlCopy(true); iconProps={{ iconName: "OpenInNewWindow" }}
}} />
text={isReadWriteUrlCopy ? "Copied" : "Copy"} </Stack>
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readWriteUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack> </Stack>
<TextField label="Read Only" readOnly defaultValue={readUrl} /> </div>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
setIsReadUrlCopy(true);
copyToClipboard(readUrl);
}}
text={isReadUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
</> </>
); );
}; };

View File

@@ -242,6 +242,11 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
submitButtonText: getButtonLabel(userContext.apiType), submitButtonText: getButtonLabel(userContext.apiType),
onSubmit, onSubmit,
}; };
const handlekeypressaddentity = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
addNewEntity();
}
};
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
@@ -284,7 +289,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
); );
})} })}
{userContext.apiType !== "Cassandra" && ( {userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy"> <Stack
horizontal
onClick={addNewEntity}
className="addButtonEntiy"
tabIndex={0}
onKeyPress={handlekeypressaddentity}
>
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" /> <Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text> <Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack> </Stack>

View File

@@ -29,10 +29,14 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
className="addButtonEntiy" className="addButtonEntiy"
horizontal={true} horizontal={true}
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]}
tabIndex={0}
> >
<div <div
className="ms-Stack addButtonEntiy css-53" className="ms-Stack addButtonEntiy css-53"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]}
tabIndex={0}
> >
<StyledImageBase <StyledImageBase
alt="Add Entity" alt="Add Entity"

View File

@@ -0,0 +1,105 @@
export const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
CREATE SCHEMA cosmosdb_tutorial;
SET search_path to cosmosdb_tutorial;
CREATE TABLE github_users
(
user_id bigint,
url text,
login text,
avatar_url text,
gravatar_id text,
display_login text
);
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
CREATE INDEX event_type_index ON github_events (event_type);
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
`;
export const newTableCommandForDisplay = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
CREATE SCHEMA cosmosdb_tutorial;
-- Using schema created for tutorial
SET search_path to cosmosdb_tutorial;
CREATE TABLE github_users
(
user_id bigint,
url text,
login text,
avatar_url text,
gravatar_id text,
display_login text
);
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
--Create indexes on events table
CREATE INDEX event_type_index ON github_events (event_type);
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);`;
export const distributeTableCommand = `SET search_path to cosmosdb_tutorial;
SELECT create_distributed_table('github_users', 'user_id');
SELECT create_distributed_table('github_events', 'user_id');
`;
export const distributeTableCommandForDisplay = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
SELECT create_distributed_table('github_users', 'user_id');
SELECT create_distributed_table('github_events', 'user_id');`;
export const loadDataCommand = `SET search_path to cosmosdb_tutorial;
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);
`;
export const loadDataCommandForDisplay = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
-- download users and store in table
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);`;
export const queryCommand = `SET search_path to cosmosdb_tutorial;
SELECT count(*) FROM github_users;
SELECT created_at, event_type, repo->>'name' AS repo_name
FROM github_events
WHERE user_id = 3861633;
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;
`;
export const queryCommandForDisplay = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
-- count all rows (across shards)
SELECT count(*) FROM github_users;
-- Find all events for a single user.
SELECT created_at, event_type, repo->>'name' AS repo_name
FROM github_events
WHERE user_id = 3861633;
-- Find the number of commits on the master branch per hour
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;`;

View File

@@ -94,7 +94,7 @@ const getDescriptionText = (page: number): string => {
case 1: case 1:
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. "; return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
case 2: case 2:
return "Launch the quickstart for a tutotrial to learn how to create a database, add sample data, connect to a sample app and more."; return "Launch the quickstart for a tutorial to learn how to create a database, add sample data, connect to a sample app and more.";
case 3: case 3:
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer."; return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
default: default:

View File

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

View File

@@ -11,6 +11,17 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import {
distributeTableCommand,
distributeTableCommandForDisplay,
loadDataCommand,
loadDataCommandForDisplay,
newTableCommand,
newTableCommandForDisplay,
queryCommand,
queryCommandForDisplay,
} from "Explorer/Quickstart/PostgreQuickstartCommands";
import { useTerminal } from "hooks/useTerminal";
import React, { useState } from "react"; import React, { useState } from "react";
import Youtube from "react-youtube"; import Youtube from "react-youtube";
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg"; import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
@@ -35,65 +46,6 @@ enum GuideSteps {
export const QuickstartGuide: React.FC = (): JSX.Element => { export const QuickstartGuide: React.FC = (): JSX.Element => {
const [currentStep, setCurrentStep] = useState<number>(0); const [currentStep, setCurrentStep] = useState<number>(0);
const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
CREATE SCHEMA cosmosdb_tutorial;
-- Using schema created for tutorial
SET search_path to cosmosdb_tutorial;
CREATE TABLE github_users
(
user_id bigint,
url text,
login text,
avatar_url text,
gravatar_id text,
display_login text
);
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
--Create indexes on events table
CREATE INDEX event_type_index ON github_events (event_type);
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops); `;
const distributeTableCommand = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
SELECT create_distributed_table('github_users', 'user_id');
SELECT create_distributed_table('github_events', 'user_id'); `;
const loadDataCommand = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
-- download users and store in table
\\COPY github_users FROM PROGRAM 'curl https://examples.citusdata.com/users.csv' WITH (FORMAT CSV)
\\COPY github_events FROM PROGRAM 'curl https://examples.citusdata.com/events.csv' WITH (FORMAT CSV) `;
const queryCommand = `-- Using schema created for the tutorial
SET search_path to cosmosdb_tutorial;
-- count all rows (across shards)
SELECT count(*) FROM github_users;
-- Find all events for a single user.
SELECT created_at, event_type, repo->>'name' AS repo_name
FROM github_events
WHERE user_id = 3861633;
-- Find the number of commits on the master branch per hour
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour; `;
const onCopyBtnClicked = (selector: string): void => { const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector); const textfield: HTMLInputElement = document.querySelector(selector);
@@ -143,14 +95,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}> <Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}> <Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
<Text variant="xxLarge">Quick start guide</Text> <Text variant="xxLarge">Quick start guide</Text>
<Text variant="medium">Gettings started in Cosmos DB</Text>
{currentStep < 5 && ( {currentStep < 5 && (
<Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}> <Pivot
style={{ marginTop: 10, width: "100%" }}
selectedKey={GuideSteps[currentStep]}
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
>
<PivotItem <PivotItem
headerText="Login" headerText="Login"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)} onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
itemKey={GuideSteps[0]} itemKey={GuideSteps[0]}
onClick={() => setCurrentStep(0)} onClick={() => {
setCurrentStep(0);
}}
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text> <Text>
@@ -158,8 +115,12 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<br /> <br />
<br /> <br />
To begin, please enter the cluster&apos;s password in the PostgreSQL terminal. To begin, please enter the cluster&apos;s password in the PostgreSQL terminal.
<br />
<br />
Note: If you navigate out of the Quick Start tab (PostgreSQL Shell), the session will be closed and
all ongoing commands might be interrupted.
</Text> </Text>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@@ -169,15 +130,20 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => setCurrentStep(1)} onClick={() => setCurrentStep(1)}
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text>Lets create two tables github_users and github_events in cosmosdb_tutorial schema.</Text> <Text>Let&apos;s create two tables github_users and github_events in cosmosdb_tutorial schema.</Text>
<DefaultButton style={{ marginTop: 16, width: 150 }}>Create new table</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 150 }}
onClick={() => useTerminal.getState().sendMessage(newTableCommand)}
>
Create new table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="newTableCommand" id="newTableCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={newTableCommand} defaultValue={newTableCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@@ -194,7 +160,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#newTableCommand")} onClick={() => onCopyBtnClicked("#newTableCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@@ -210,14 +176,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<br /> <br />
We are choosing user_id as the distribution column for our sample dataset. We are choosing user_id as the distribution column for our sample dataset.
</Text> </Text>
<DefaultButton style={{ marginTop: 16, width: 200 }}>Create distributed table</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 200 }}
onClick={() => useTerminal.getState().sendMessage(distributeTableCommand)}
>
Create distributed table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="distributeTableCommand" id="distributeTableCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={distributeTableCommand} defaultValue={distributeTableCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@@ -234,7 +205,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#distributeTableCommand")} onClick={() => onCopyBtnClicked("#distributeTableCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@@ -245,14 +216,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text>Let&apos;s load the two tables with a sample dataset generated from the GitHub API.</Text> <Text>Let&apos;s load the two tables with a sample dataset generated from the GitHub API.</Text>
<DefaultButton style={{ marginTop: 16, width: 110 }}>Load data</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
>
Load data
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="loadDataCommand" id="loadDataCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={loadDataCommand} defaultValue={loadDataCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@@ -269,7 +245,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#loadDataCommand")} onClick={() => onCopyBtnClicked("#loadDataCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@@ -282,14 +258,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Text> <Text>
Congratulations on creating and distributing your tables. Now, it&apos;s time to run your first query! Congratulations on creating and distributing your tables. Now, it&apos;s time to run your first query!
</Text> </Text>
<DefaultButton style={{ marginTop: 16, width: 115 }}>Try queries</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 115 }}
onClick={() => useTerminal.getState().sendMessage(queryCommand)}
>
Try queries
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="queryCommand" id="queryCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={queryCommand} defaultValue={queryCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@@ -306,7 +287,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#queryCommand")} onClick={() => onCopyBtnClicked("#queryCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="k_EanjMtaPg" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
</Pivot> </Pivot>

View File

@@ -14,10 +14,6 @@
padding-right: 16px; padding-right: 16px;
max-width: 1168px; max-width: 1168px;
> * {
justify-content: space-between;
}
> .title { > .title {
position: relative; // To attach FeaturePanelLauncher as absolute position: relative; // To attach FeaturePanelLauncher as absolute
color: @BaseHigh; color: @BaseHigh;

View File

@@ -186,7 +186,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
headline="Create your password" headline="Create your password"
target={"#mainButton-quickstartDescription"} target={"#mainButton-quickstartDescription"}
hasCloseButton hasCloseButton
onDismiss={() => usePostgres.getState().setShowResetPasswordBubble(false)} onDismiss={() => {
localStorage.setItem(userContext.databaseAccount.id, "true");
usePostgres.getState().setShowResetPasswordBubble(false);
}}
calloutProps={{ calloutProps={{
directionalHint: DirectionalHint.bottomRightEdge, directionalHint: DirectionalHint.bottomRightEdge,
directionalHintFixed: true, directionalHintFixed: true,
@@ -197,14 +200,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
primaryButtonProps={{ primaryButtonProps={{
text: "Create", text: "Create",
onClick: () => { onClick: () => {
localStorage.setItem(userContext.databaseAccount.id, "true");
sendMessage({ sendMessage({
type: MessageTypes.OpenQuickstartBlade, type: MessageTypes.OpenPostgreSQLPasswordReset,
}); });
usePostgres.getState().setShowResetPasswordBubble(false); usePostgres.getState().setShowResetPasswordBubble(false);
}, },
}} }}
> >
This password will be used to connect to the database. If you haven&apos;t changed your password yet, change it now.
</TeachingBubble> </TeachingBubble>
)} )}
</div> </div>
@@ -300,7 +304,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public createMainItems(): SplashScreenItem[] { public createMainItems(): SplashScreenItem[] {
const heroes: SplashScreenItem[] = []; const heroes: SplashScreenItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Postgres") { if (
userContext.apiType === "SQL" ||
userContext.apiType === "Mongo" ||
(userContext.apiType === "Postgres" && !userContext.isReplica)
) {
const launchQuickstartBtn = { const launchQuickstartBtn = {
id: "quickstartDescription", id: "quickstartDescription",
iconSrc: QuickStartIcon, iconSrc: QuickStartIcon,
@@ -329,7 +337,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: PowerShellIcon, iconSrc: PowerShellIcon,
title: "PostgreSQL Shell", title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQLs shell interface", description: "Create table and interact with data using PostgreSQLs shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo), onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
}; };
heroes.push(postgreShellBtn); heroes.push(postgreShellBtn);
} else { } else {
@@ -347,10 +355,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const connectBtn = { const connectBtn = {
iconSrc: ConnectIcon, iconSrc: ConnectIcon,
title: userContext.apiType === "Postgres" ? "Connect with PG Admin" : "Connect", title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
description: description:
userContext.apiType === "Postgres" userContext.apiType === "Postgres"
? "Prefer PGadmin? Find your connection strings here" ? "Prefer pgAdmin? Find your connection strings here"
: "Prefer using your own choice of tooling? Find the connection string you need to connect", : "Prefer using your own choice of tooling? Find the connection string you need to connect",
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect), onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
}; };

View File

@@ -516,7 +516,7 @@ export default class QueryBuilderViewModel {
}; };
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => { public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.key === "Enter" || event.key === "Space") {
this.addClauseIndex(this.clauseArray().length - 1); this.addClauseIndex(this.clauseArray().length - 1);
event.stopPropagation(); event.stopPropagation();
return false; return false;

View File

@@ -120,7 +120,7 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
</Stack> </Stack>
<Label>Secure connections</Label> <Label>Secure connections</Label>
<Text> <Text style={{ marginBottom: 8 }}>
Only secure connections are supported. For production use cases, we recommend using the &apos;verify-full&apos; Only secure connections are supported. For production use cases, we recommend using the &apos;verify-full&apos;
mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and
provide it when connecting to the database.{" "} provide it when connecting to the database.{" "}
@@ -128,6 +128,18 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
Learn more Learn more
</Link> </Link>
</Text> </Text>
<Label>Connect with pgAdmin</Label>
<Text>
Refer to our{" "}
<Link
href="https://learn.microsoft.com/en-us/azure/postgresql/hyperscale/howto-connect?tabs=pgadmin"
target="_blank"
>
guide
</Link>{" "}
to help you connect via pgAdmin.
</Text>
</div> </div>
); );
}; };

View File

@@ -70,24 +70,19 @@
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody> <tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
</table> </table>
</div> </div>
<div <button
class="addClause" data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }"
role="button" style="border: none; background: none"
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
tabindex="0"
> >
<div class="addClause-heading"> <div class="addClause" data-bind=" ">
<span class="clause-table addClause-title"> <div class="addClause-heading">
<img <span class="clause-table addClause-title">
class="addclauseProperty-Img" <img class="addclauseProperty-Img" style="margin-bottom: 5px" src="/Add-property.svg" />
style="margin-bottom: 5px" <span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
src="/Add-property.svg" </span>
alt="Add new clause" </div>
/>
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
</span>
</div> </div>
</div> </button>
</div> </div>
</div> </div>
<!-- Tables Query Tab Query Helper - End--> <!-- Tables Query Tab Query Helper - End-->
@@ -168,22 +163,20 @@
<script type="text/html" id="queryClause-template"> <script type="text/html" id="queryClause-template">
<tr class="clause-table-row"> <tr class="clause-table-row">
<td class="clause-table-cell action-column"> <td class="clause-table-cell action-column">
<span <button
class="entity-Add-Cancel"
role="button"
tabindex="0"
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}" data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
> >
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" /> <span class="entity-Add-Cancel" role="button">
</span> <img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
<span </span>
class="entity-Add-Cancel" </button>
role="button" <button
tabindex="0"
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}" data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
> >
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" /> <span class="entity-Add-Cancel" role="button">
</span> <img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
</span>
</button>
</td> </td>
<td class="clause-table-cell group-control-column"> <td class="clause-table-cell group-control-column">
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" /> <input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />

View File

@@ -1,11 +1,15 @@
import { Spinner, SpinnerSize, Stack } from "@fluentui/react"; import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels"; import { configContext } from "ConfigContext";
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent"; import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide"; import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
import React, { useEffect } from "react"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import React, { useEffect, useState } from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
interface QuickstartTabProps { interface QuickstartTabProps {
explorer: Explorer; explorer: Explorer;
@@ -13,14 +17,42 @@ interface QuickstartTabProps {
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => { export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo); const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
forwardingId: notebookServerInfo.forwardingId,
});
const checkFirewallRules = async (): Promise<void> => {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2020-10-05-privatepreview",
});
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
const isEnabled = firewallRules.some(
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
);
setIsAllPublicIPAddressEnabled(isEnabled);
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) {
setTimeout(checkFirewallRules, 30000);
}
};
useEffect(() => {
checkFirewallRules();
});
useEffect(() => { useEffect(() => {
explorer.allocateContainer(); explorer.allocateContainer();
}, []); }, []);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`,
forwardingId: notebookServerInfo.forwardingId,
});
return ( return (
<Stack style={{ width: "100%" }} horizontal> <Stack style={{ width: "100%" }} horizontal>
@@ -28,15 +60,24 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
<QuickstartGuide /> <QuickstartGuide />
</Stack> </Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}> <Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{notebookServerInfo?.notebookServerEndpoint && ( {!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent <NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()} notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount} databaseAccount={userContext.databaseAccount}
tabId="EmbbedTerminal" tabId="QuickstartPSQLShell"
/> />
)} )}
{!notebookServerInfo?.notebookServerEndpoint && ( {isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner> <Stack style={{ margin: "auto 0" }}>
<Text block style={{ margin: "auto" }}>
Connecting to the PostgreSQL shell.
</Text>
<Text block style={{ margin: "auto" }}>
If the cluster was just created, this could take up to a minute.
</Text>
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
</Stack>
)} )}
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -1,3 +1,6 @@
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels"; import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
@@ -21,10 +24,24 @@ interface TabsProps {
} }
export const Tabs = ({ explorer }: TabsProps): JSX.Element => { export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs(); const { openedTabs, openedReactTabs, activeTab, activeReactTab, showNetworkSettingsWarning } = useTabs();
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
{showNetworkSettingsWarning && (
<MessageBar
messageBarType={MessageBarType.warning}
actions={
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
Change network settings
</MessageBarButton>
}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
>
The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure
Portal to proceed.
</MessageBar>
)}
<div id="content" className="flexContainer hideOverflows"> <div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin"> <div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">

View File

@@ -1,6 +1,9 @@
import { Spinner, SpinnerSize } from "@fluentui/react"; import { Spinner, SpinnerSize } from "@fluentui/react";
import { configContext } from "ConfigContext";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { armRequest } from "Utils/arm/request";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -26,10 +29,15 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
constructor( constructor(
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
private getDatabaseAccount: () => DataModels.DatabaseAccount, private getDatabaseAccount: () => DataModels.DatabaseAccount,
private getTabId: () => string private getTabId: () => string,
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
) {} ) {}
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
if (!this.isAllPublicIPAddressesEnabled()) {
return <QuickstartFirewallNotification />;
}
return this.parameters() ? ( return this.parameters() ? (
<NotebookTerminalComponent <NotebookTerminalComponent
notebookServerInfo={this.getNotebookServerInfo()} notebookServerInfo={this.getNotebookServerInfo()}
@@ -46,25 +54,33 @@ export default class TerminalTab extends TabsBase {
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> '; public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
private container: Explorer; private container: Explorer;
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter; private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
constructor(options: TerminalTabOptions) { constructor(options: TerminalTabOptions) {
super(options); super(options);
this.container = options.container; this.container = options.container;
this.isAllPublicIPAddressesEnabled = ko.observable(true);
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options), () => this.getNotebookServerInfo(options),
() => userContext?.databaseAccount, () => userContext?.databaseAccount,
() => this.tabId () => this.tabId,
this.isAllPublicIPAddressesEnabled
); );
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if ( if (
this.isTemplateReady() && this.isTemplateReady() &&
useNotebook.getState().isNotebookEnabled && useNotebook.getState().isNotebookEnabled &&
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
this.isAllPublicIPAddressesEnabled()
) { ) {
return true; return true;
} }
return false; return false;
}); });
if (options.kind === ViewModels.TerminalKind.Postgres) {
this.checkPostgresFirewallRules();
}
} }
public getContainer(): Explorer { public getContainer(): Explorer {
@@ -110,4 +126,25 @@ export default class TerminalTab extends TabsBase {
forwardingId: info.forwardingId, forwardingId: info.forwardingId,
}; };
} }
private async checkPostgresFirewallRules(): Promise<void> {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2020-10-05-privatepreview",
});
const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || [];
const isEnabled = firewallRules.some(
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
);
this.isAllPublicIPAddressesEnabled(isEnabled);
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
if (!isEnabled) {
setTimeout(() => this.checkPostgresFirewallRules(), 30000);
}
}
} }

View File

@@ -1160,23 +1160,6 @@ export default class Collection implements ViewModels.Collection {
this.onDocumentDBDocumentsClick(); this.onDocumentDBDocumentsClick();
} }
/**
* Get correct collection label depending on account API
*/
public getLabel(): string {
if (userContext.apiType === "Tables") {
return "Entities";
} else if (userContext.apiType === "Cassandra") {
return "Rows";
} else if (userContext.apiType === "Gremlin") {
return "Graph";
} else if (userContext.apiType === "Mongo") {
return "Documents";
}
return "Items";
}
public getDatabase(): ViewModels.Database { public getDatabase(): ViewModels.Database {
return useDatabases.getState().findDatabaseWithId(this.databaseId); return useDatabases.getState().findDatabaseWithId(this.databaseId);
} }

View File

@@ -1,5 +1,6 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { getItemName } from "Utils/APITypeUtils";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
@@ -497,7 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => { const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
const children: TreeNode[] = []; const children: TreeNode[] = [];
children.push({ children.push({
label: collection.getLabel(), label: getItemName(),
id: collection.isSampleCollection ? "sampleItems" : "", id: collection.isSampleCollection ? "sampleItems" : "",
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();

View File

@@ -1,6 +1,7 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { getItemName } from "Utils/APITypeUtils";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg";
@@ -254,7 +255,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode { private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
const children: TreeNode[] = []; const children: TreeNode[] = [];
children.push({ children.push({
label: collection.getLabel(), label: getItemName(),
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent

View File

@@ -45,7 +45,11 @@ export class PhoenixClient {
} }
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> { public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
return this.executeContainerAssignmentOperation(provisionData, "allocate"); return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
retries: 4,
maxTimeout: 20000,
minTimeout: 20000,
});
} }
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> { public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
@@ -80,9 +84,12 @@ export class PhoenixClient {
} }
const phoenixError = responseJson as IPhoenixError; const phoenixError = responseJson as IPhoenixError;
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
throw new Error(this.ConvertToForbiddenErrorString(phoenixError)); if (phoenixError.message === "Sequence contains no elements") {
throw Error("Phoenix container allocation failed, please try again later.");
}
throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
} }
throw new Error(phoenixError.message); throw new AbortError(phoenixError.message);
} catch (error) { } catch (error) {
error.status = response?.status; error.status = response?.status;
throw error; throw error;

View File

@@ -1,31 +0,0 @@
import * as Constants from "../../Common/Constants";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { userContext } from "../../UserContext";
export default class AuthHeadersUtil {
public static async generateEncryptedToken(readOnly: boolean = false): Promise<DataModels.GenerateTokenResponse> {
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
const headers: any = { authorization: userContext.authorizationToken };
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
const response = await fetch(url, { method: "POST", headers });
const result = await response.json();
// This API has a quirk where the response must be parsed to JSON twice
return JSON.parse(result);
}
private static _generateResourceUrl(): string {
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
const sid = subscriptionId || "";
const rg = resourceGroup || "";
const dba = databaseAccount?.name || "";
const resourceUrl = encodeURIComponent(accountEndpoint);
const rid = "";
const rtype = "";
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
}
}

View File

@@ -2,7 +2,7 @@
* JupyterLab applications based on jupyterLab components * JupyterLab applications based on jupyterLab components
*/ */
import { ServerConnection, TerminalManager } from "@jupyterlab/services"; import { ServerConnection, TerminalManager } from "@jupyterlab/services";
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal"; import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
import { Terminal } from "@jupyterlab/terminal"; import { Terminal } from "@jupyterlab/terminal";
import { Panel, Widget } from "@phosphor/widgets"; import { Panel, Widget } from "@phosphor/widgets";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
@@ -46,7 +46,7 @@ export class JupyterLabAppFactory {
} }
} }
public async createTerminalApp(serverSettings: ServerConnection.ISettings) { public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
const manager = new TerminalManager({ const manager = new TerminalManager({
serverSettings: serverSettings, serverSettings: serverSettings,
}); });
@@ -68,7 +68,7 @@ export class JupyterLabAppFactory {
if (!term) { if (!term) {
console.error("Failed starting terminal"); console.error("Failed starting terminal");
return; return undefined;
} }
term.title.closable = false; term.title.closable = false;
@@ -81,6 +81,9 @@ export class JupyterLabAppFactory {
// Attach the widget to the dom. // Attach the widget to the dom.
Widget.attach(panel, document.body); Widget.attach(panel, document.body);
// Switch focus to the terminal
term.activate();
// Handle resize events. // Handle resize events.
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
panel.update(); panel.update();
@@ -90,5 +93,7 @@ export class JupyterLabAppFactory {
window.addEventListener("unload", () => { window.addEventListener("unload", () => {
panel.dispose(); panel.dispose();
}); });
return session;
} }
} }

View File

@@ -1,4 +1,5 @@
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
import "@jupyterlab/terminal/style/index.css"; import "@jupyterlab/terminal/style/index.css";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import postRobot from "post-robot"; import postRobot from "post-robot";
@@ -41,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
return ServerConnection.makeSettings(options); return ServerConnection.makeSettings(options);
}; };
const initTerminal = async (props: TerminalProps) => { const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
// Initialize userContext (only properties which are needed by TelemetryProcessor) // Initialize userContext (only properties which are needed by TelemetryProcessor)
updateUserContext({ updateUserContext({
subscriptionId: props.subscriptionId, subscriptionId: props.subscriptionId,
@@ -55,10 +56,12 @@ const initTerminal = async (props: TerminalProps) => {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try { try {
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings); const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
return session;
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
return undefined;
} }
}; };
@@ -70,6 +73,7 @@ const closeTab = (tabId: string): void => {
}; };
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
let session: ITerminalConnection | undefined;
postRobot.on( postRobot.on(
"props", "props",
{ {
@@ -80,7 +84,22 @@ const main = async (): Promise<void> => {
// Typescript definition for event is wrong. So read props by casting to <any> // Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as TerminalProps; const props = (event as any).data as TerminalProps;
await initTerminal(props); session = await initTerminal(props);
}
);
postRobot.on(
"sendMessage",
{
window: window.parent,
domain: window.location.origin,
},
async (event) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (event as any).data as IMessage;
if (session) {
session.send(message);
}
} }
); );
}; };

View File

@@ -67,6 +67,7 @@ interface UserContext {
partitionKey?: string; partitionKey?: string;
}; };
readonly postgresConnectionStrParams?: PostgresConnectionStrParams; readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
readonly isReplica?: boolean;
collectionCreationDefaults: CollectionCreationDefaults; collectionCreationDefaults: CollectionCreationDefaults;
} }
@@ -109,16 +110,19 @@ function updateUserContext(newContext: Partial<UserContext>): void {
); );
if (!localStorage.getItem(newContext.databaseAccount.id)) { if (!localStorage.getItem(newContext.databaseAccount.id)) {
if (newContext.apiType === "Postgres") { if (newContext.isTryCosmosDBSubscription || isNewAccount) {
if (newContext.apiType === "Postgres" && !newContext.isReplica) {
usePostgres.getState().setShowResetPasswordBubble(true);
usePostgres.getState().setShowPostgreTeachingBubble(true);
} else {
useCarousel.getState().setShouldOpen(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
traceOpen(Action.OpenCarousel);
}
} else if (newContext.apiType === "Postgres") {
usePostgres.getState().setShowPostgreTeachingBubble(true); usePostgres.getState().setShowPostgreTeachingBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true"); localStorage.setItem(newContext.databaseAccount.id, "true");
} }
if (userContext.isTryCosmosDBSubscription || isNewAccount) {
useCarousel.getState().setShouldOpen(true);
usePostgres.getState().setShowResetPasswordBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
traceOpen(Action.OpenCarousel);
}
} }
} }
Object.assign(userContext, newContext); Object.assign(userContext, newContext);

View File

@@ -74,3 +74,18 @@ export const getApiShortDisplayName = (): string => {
return "Table API"; return "Table API";
} }
}; };
export const getItemName = (): string => {
switch (userContext.apiType) {
case "Tables":
return "Entities";
case "Cassandra":
return "Rows";
case "Gremlin":
return "Graph";
case "Mongo":
return "Documents";
default:
return "Items";
}
};

View File

@@ -0,0 +1,40 @@
import { userContext } from "UserContext";
const PortalIPs: { [key: string]: string[] } = {
prod1: ["104.42.195.92", "40.76.54.131"],
prod2: ["104.42.196.69"],
mooncake: ["139.217.8.252"],
blackforest: ["51.4.229.218"],
fairfax: ["52.244.48.71"],
ussec: ["29.26.26.67", "29.26.26.66"],
usnat: ["7.28.202.68"],
};
export const doNetworkSettingsAllowDataExplorerAccess = (): boolean => {
const accountProperties = userContext.databaseAccount?.properties;
if (!accountProperties) {
return false;
}
// public network access is disabled
if (accountProperties.publicNetworkAccess !== "Enabled") {
return false;
}
const ipRules = accountProperties.ipRules;
// public network access is set to "All networks"
if (ipRules.length === 0) {
return true;
}
const portalIPs = PortalIPs[userContext.portalEnv];
let numberOfMatches = 0;
ipRules.forEach((ipRule) => {
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
numberOfMatches++;
}
});
return numberOfMatches === portalIPs.length;
};

View File

@@ -1,18 +0,0 @@
import { useEffect, useState } from "react";
import { GenerateTokenResponse } from "../Contracts/DataModels";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
export function useFullScreenURLs(): GenerateTokenResponse | undefined {
const [state, setState] = useState<GenerateTokenResponse>();
useEffect(() => {
Promise.all([AuthHeadersUtil.generateEncryptedToken(), AuthHeadersUtil.generateEncryptedToken(true)]).then(
([readWriteResponse, readOnlyResponse]) =>
setState({
readWrite: readWriteResponse.readWrite,
read: readOnlyResponse.read,
})
);
}, []);
return state;
}

View File

@@ -1,5 +1,6 @@
import { useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { doNetworkSettingsAllowDataExplorerAccess } from "Utils/NetworkUtility";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, Flights } from "../Common/Constants"; import { AccountKind, Flights } from "../Common/Constants";
@@ -100,7 +101,11 @@ async function configureHosted(): Promise<Explorer> {
} }
if (event.data?.type === MessageTypes.CloseTab) { if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); if (event.data?.data?.tabId === "QuickstartPSQLShell") {
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
} else {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
}
} }
}, },
false false
@@ -290,7 +295,11 @@ async function configurePortal(): Promise<Explorer> {
} else if (shouldForwardMessage(message, event.origin)) { } else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message); sendMessage(message);
} else if (event.data?.type === MessageTypes.CloseTab) { } else if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); if (event.data?.data?.tabId === "QuickstartPSQLShell") {
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
} else {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
}
} }
}, },
false false
@@ -356,7 +365,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
}); });
if (inputs.isPostgresAccount) { if (inputs.isPostgresAccount) {
updateUserContext({ apiType: "Postgres" }); updateUserContext({ apiType: "Postgres", isReplica: !!inputs.isReplica });
if (inputs.connectionStringParams) { if (inputs.connectionStringParams) {
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension // TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
@@ -373,6 +382,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
} }
} }
const isDataExplorerAccessAllowed = doNetworkSettingsAllowDataExplorerAccess();
useTabs.getState().setShowNetworkSettingsWarning(!isDataExplorerAccessAllowed);
if (inputs.features) { if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features))); Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
} }

View File

@@ -9,6 +9,7 @@ interface TabsState {
openedReactTabs: ReactTabKind[]; openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined; activeTab: TabsBase | undefined;
activeReactTab: ReactTabKind | undefined; activeReactTab: ReactTabKind | undefined;
showNetworkSettingsWarning: boolean;
activateTab: (tab: TabsBase) => void; activateTab: (tab: TabsBase) => void;
activateNewTab: (tab: TabsBase) => void; activateNewTab: (tab: TabsBase) => void;
activateReactTab: (tabkind: ReactTabKind) => void; activateReactTab: (tabkind: ReactTabKind) => void;
@@ -20,6 +21,7 @@ interface TabsState {
closeAllNotebookTabs: (hardClose: boolean) => void; closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void; openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void; closeReactTab: (tabKind: ReactTabKind) => void;
setShowNetworkSettingsWarning: (showWarning: boolean) => void;
} }
export enum ReactTabKind { export enum ReactTabKind {
@@ -33,6 +35,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedReactTabs: [ReactTabKind.Home], openedReactTabs: [ReactTabKind.Home],
activeTab: undefined, activeTab: undefined,
activeReactTab: ReactTabKind.Home, activeReactTab: ReactTabKind.Home,
showNetworkSettingsWarning: false,
activateTab: (tab: TabsBase): void => { activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab, activeReactTab: undefined }); set({ activeTab: tab, activeReactTab: undefined });
@@ -142,4 +145,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
set({ openedReactTabs: updatedOpenedReactTabs }); set({ openedReactTabs: updatedOpenedReactTabs });
}, },
setShowNetworkSettingsWarning: (showWarning: boolean) => set({ showNetworkSettingsWarning: showWarning }),
})); }));

26
src/hooks/useTerminal.ts Normal file
View File

@@ -0,0 +1,26 @@
import postRobot from "post-robot";
import create, { UseStore } from "zustand";
interface TerminalState {
terminalWindow: Window;
setTerminal: (terminalWindow: Window) => void;
sendMessage: (message: string) => void;
}
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
terminalWindow: undefined,
setTerminal: (terminalWindow: Window) => {
set({ terminalWindow });
},
sendMessage: (message: string) => {
const terminalWindow = get().terminalWindow;
postRobot.send(
terminalWindow,
"sendMessage",
{ type: "stdin", content: [message] },
{
domain: window.location.origin,
}
);
},
}));

View File

@@ -82,7 +82,6 @@
"./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/Explorer/Tree/AccessibleVerticalList.ts",
"./src/GitHub/GitHubConnector.ts", "./src/GitHub/GitHubConnector.ts",
"./src/HostedExplorerChildFrame.ts", "./src/HostedExplorerChildFrame.ts",
"./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.test.tsx",
"./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/MeControl.tsx",
"./src/Platform/Hosted/Components/SignInButton.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx",
@@ -126,7 +125,6 @@
"./src/Utils/WindowUtils.ts", "./src/Utils/WindowUtils.ts",
"./src/hooks/useConfig.ts", "./src/hooks/useConfig.ts",
"./src/hooks/useDirectories.tsx", "./src/hooks/useDirectories.tsx",
"./src/hooks/useFullScreenURLs.tsx",
"./src/hooks/useGraphPhoto.tsx", "./src/hooks/useGraphPhoto.tsx",
"./src/hooks/useNotebookSnapshotStore.ts", "./src/hooks/useNotebookSnapshotStore.ts",
"./src/hooks/usePortalAccessToken.tsx", "./src/hooks/usePortalAccessToken.tsx",
@@ -165,4 +163,4 @@
"src/Terminal/**/*", "src/Terminal/**/*",
"src/Utils/arm/**/*" "src/Utils/arm/**/*"
] ]
} }