Compare commits

...

28 Commits

Author SHA1 Message Date
Steve Faulkner
35213a77e2 Force token refresh 2021-01-07 14:13:41 -06:00
Steve Faulkner
d1ac8eb077 WIP 2021-01-05 01:14:17 -06:00
Steve Faulkner
d9156c47d0 Fix test login 2021-01-04 10:51:44 -06:00
Steve Faulkner
a844042580 Fix tables 2021-01-04 10:09:24 -06:00
Steve Faulkner
1238d30f95 Fix format 2021-01-03 23:55:58 -06:00
Steve Faulkner
684cbfe4a0 more fixes 2021-01-03 23:54:37 -06:00
Steve Faulkner
5b6b4d3583 More cleanup 2021-01-03 23:41:47 -06:00
Steve Faulkner
e05a78e96a WIP 2021-01-03 23:23:42 -06:00
Steve Faulkner
33f7ae1e6d WIP, checkpoint 2021-01-03 23:07:26 -06:00
Steve Faulkner
2089a8881d Add file 2021-01-03 21:52:20 -06:00
Steve Faulkner
2e1665f093 WIP 2021-01-03 21:49:50 -06:00
Steve Faulkner
f6d6222e5c more WIP 2021-01-02 22:18:20 -06:00
Steve Faulkner
2147b10361 Refactor Components 2021-01-02 20:47:54 -06:00
Steve Faulkner
09ac1d1552 WIP 2021-01-02 20:29:37 -06:00
Steve Faulkner
b81cef2a03 WIP 2021-01-02 19:19:26 -06:00
Steve Faulkner
731999c4e8 WIP 2021-01-02 19:19:19 -06:00
Steve Faulkner
aba583abd8 Checkpoint 2021-01-02 19:00:49 -06:00
Steve Faulkner
2e10b96678 Checkpoint 2021-01-02 17:15:18 -06:00
Steve Faulkner
5652f29d03 WIP 2021-01-01 14:36:29 -06:00
Steve Faulkner
15cb4a8fc4 Checkpoint 2021-01-01 00:09:38 -06:00
Steve Faulkner
bf30c3190a WIP 2020-12-31 15:05:34 -06:00
Steve Faulkner
585f75bc91 Checkpoint 2020-12-30 19:12:22 -06:00
Steve Faulkner
7116f25ce4 checkpoint 2020-12-30 13:18:03 -06:00
Steve Faulkner
5f5d9176af WIP 2020-12-28 20:55:44 -06:00
Steve Faulkner
ac2d645fda Add files from previous commit 2020-12-28 18:48:30 -06:00
Steve Faulkner
12a44fdd42 MSAL 2.0 checkpoint 2020-12-28 18:48:19 -06:00
Steve Faulkner
13dbcb6453 Merge branch 'master' into hosted-msal 2020-12-28 11:40:03 -06:00
Steve Faulkner
e41230e8c4 WIP 2020-12-26 20:01:42 -06:00
56 changed files with 3296 additions and 7133 deletions

View File

@@ -42,7 +42,6 @@ src/Contracts/ViewModels.ts
src/Controls/Heatmap/Heatmap.test.ts src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts src/Controls/Heatmap/Heatmap.ts
src/Controls/Heatmap/HeatmapDatatypes.ts src/Controls/Heatmap/HeatmapDatatypes.ts
src/Definitions/adal.d.ts
src/Definitions/datatables.d.ts src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts src/Definitions/gif.d.ts
src/Definitions/globals.d.ts src/Definitions/globals.d.ts
@@ -242,9 +241,6 @@ src/Platform/Hosted/Authorization.ts
src/Platform/Hosted/DataAccessUtility.ts src/Platform/Hosted/DataAccessUtility.ts
src/Platform/Hosted/ExplorerFactory.ts src/Platform/Hosted/ExplorerFactory.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.ts
src/Platform/Hosted/HostedUtils.test.ts
src/Platform/Hosted/HostedUtils.ts
src/Platform/Hosted/Main.ts src/Platform/Hosted/Main.ts
src/Platform/Hosted/Maint.test.ts src/Platform/Hosted/Maint.test.ts
src/Platform/Hosted/NotificationsClient.ts src/Platform/Hosted/NotificationsClient.ts

View File

@@ -43,6 +43,7 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error", eqeqeq: "error",
"react/display-name": "off",
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {

1963
externals/adal.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ body {
height: 100%; height: 100%;
:focus { :focus {
.focus() .focus();
} }
} }
@@ -41,7 +41,7 @@ body {
border-radius: 3px; border-radius: 3px;
&:before { &:before {
content:""; content: "";
position: absolute; position: absolute;
top: -10px; top: -10px;
right: 13px; right: 13px;
@@ -53,13 +53,13 @@ body {
} }
&:after { &:after {
content:""; content: "";
position: absolute; position: absolute;
top: -9px; top: -9px;
right: 14px; right: 14px;
border-width: 0 9px 9px; border-width: 0 9px 9px;
border-style: solid; border-style: solid;
border-color: #FFF rgba(0, 0, 0, 0); border-color: #fff rgba(0, 0, 0, 0);
display: block; display: block;
width: 0; width: 0;
} }
@@ -88,7 +88,6 @@ body {
height: 100%; height: 100%;
width: 100%; width: 100%;
.urlContainer { .urlContainer {
margin-left: @DefaultSpace; margin-left: @DefaultSpace;
@@ -155,7 +154,7 @@ body {
} }
.urlTokenTooltiptext { .urlTokenTooltiptext {
bottom:28px; bottom: 28px;
width: 250px; width: 250px;
.tooltipText(); .tooltipText();
@@ -179,7 +178,8 @@ body {
.active(); .active();
} }
&:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext { &:focus .urlTokenCopyTooltiptext,
&:focus .urlTokenCopyTooltiptext {
.tooltipVisible(); .tooltipVisible();
} }
@@ -217,7 +217,7 @@ body {
.shareLink { .shareLink {
width: 300px; width: 300px;
background-color: #FFFFFF; background-color: #ffffff;
border: 1px solid @BaseMedium; border: 1px solid @BaseMedium;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -423,7 +423,7 @@ body {
.ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons { .ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons {
border: 1px solid @BaseMedium; border: 1px solid @BaseMedium;
box-shadow:0 0 @DefaultSpace @BoxShadow; box-shadow: 0 0 @DefaultSpace @BoxShadow;
padding: 0px; padding: 0px;
.ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar { .ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar {
@@ -722,7 +722,8 @@ stored-procedure-tab {
@ToggleHeight: 30px; @ToggleHeight: 30px;
@ToggleWidth: 180px; @ToggleWidth: 180px;
.results-container, .errors-container { .results-container,
.errors-container {
padding: @MediumSpace 0px 0px @MediumSpace; padding: @MediumSpace 0px 0px @MediumSpace;
height: 100%; height: 100%;
.flex-display(); .flex-display();
@@ -829,8 +830,8 @@ notification-console {
} }
.flexContainer { .flexContainer {
height:100%; height: 100%;
width:100%; width: 100%;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();
} }
@@ -862,8 +863,8 @@ notification-console {
.topSelected:hover { .topSelected:hover {
border-left: 4px solid @AccentMediumHigh; border-left: 4px solid @AccentMediumHigh;
background: #666666!important; background: #666666 !important;
cursor: default!important; cursor: default !important;
} }
#Quickstart:hover span.activemenu, #Quickstart:hover span.activemenu,
@@ -934,19 +935,19 @@ menuQuickStart {
.content { .content {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
transition: all .4s ease-in-out; transition: all 0.4s ease-in-out;
-ms-transition: all .4s ease-in-out; -ms-transition: all 0.4s ease-in-out;
-webkit-transition: all .4s ease-in-out; -webkit-transition: all 0.4s ease-in-out;
-moz-transition: all .4s ease-in-out; -moz-transition: all 0.4s ease-in-out;
height: 100vh; height: 100vh;
} }
.mini { .mini {
width: 0%; width: 0%;
float: left; float: left;
transition: all .4s ease-in-out; transition: all 0.4s ease-in-out;
-webkit-transition: all .4s ease-in-out; -webkit-transition: all 0.4s ease-in-out;
-moz-transition: all .4s ease-in-out; -moz-transition: all 0.4s ease-in-out;
height: 100vh; height: 100vh;
background-color: white; background-color: white;
} }
@@ -1096,13 +1097,13 @@ menuQuickStart {
cursor: pointer; cursor: pointer;
} }
#tbodycontent>tr>td { #tbodycontent > tr > td {
border-bottom: 1px solid #CCCCCC; border-bottom: 1px solid #cccccc;
border-top: none; border-top: none;
padding: 6px; padding: 6px;
} }
#tbodycontent>tr:last-child>td { #tbodycontent > tr:last-child > td {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
@@ -1138,8 +1139,8 @@ menuQuickStart {
} }
.loadErrorIcon { .loadErrorIcon {
width:128px; width: 128px;
height:128px; height: 128px;
} }
.loadErrorDetailsLink { .loadErrorDetailsLink {
@@ -1151,7 +1152,7 @@ menuQuickStart {
padding-top: 28px; padding-top: 28px;
padding-left: @MediumSpace; padding-left: @MediumSpace;
height: 100%; height: 100%;
width:100%; width: 100%;
overflow: hidden; overflow: hidden;
.flex-display(); .flex-display();
@@ -1163,7 +1164,7 @@ menuQuickStart {
diff-editor { diff-editor {
padding-top: 28px; padding-top: 28px;
height: 100%; height: 100%;
width:100%; width: 100%;
overflow: hidden; overflow: hidden;
.flex-display(); .flex-display();
@@ -1205,7 +1206,7 @@ menuQuickStart {
.gridRowSelected:hover { .gridRowSelected:hover {
cursor: default; cursor: default;
.hover() .hover();
} }
.gridRowHighlighted { .gridRowHighlighted {
@@ -1213,7 +1214,7 @@ menuQuickStart {
border-width: 2px; border-width: 2px;
} }
.table-hover>tbody>tr:hover { .table-hover > tbody > tr:hover {
.hover(); .hover();
} }
@@ -1240,7 +1241,7 @@ menuQuickStart {
border-top: 1px solid #eee; border-top: 1px solid #eee;
margin-left: -17px; margin-left: -17px;
width: 100%; width: 100%;
color: 1px solid #53575B; color: 1px solid #53575b;
} }
.partitioning-btn { .partitioning-btn {
@@ -1308,7 +1309,7 @@ menuQuickStart {
.collid-white { .collid-white {
width: 100%; width: 100%;
border: solid 1px #DDD; border: solid 1px #ddd;
} }
.plusimg-but { .plusimg-but {
@@ -1438,7 +1439,7 @@ p {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.createNewDatabaseOrUseExistingRadio:nth-child(n+2) { .createNewDatabaseOrUseExistingRadio:nth-child(n + 2) {
margin-left: @LargeSpace; margin-left: @LargeSpace;
} }
@@ -1631,7 +1632,6 @@ p {
margin-left: -32px; margin-left: -32px;
} }
/* Variant of paddingspan3 without the margins */ /* Variant of paddingspan3 without the margins */
.contextual-pane .paddingspan3b { .contextual-pane .paddingspan3b {
@@ -1644,7 +1644,7 @@ p {
} }
.contextual-pane hr { .contextual-pane hr {
border: 1px solid #53575B; border: 1px solid #53575b;
margin-right: 20px; margin-right: 20px;
} }
@@ -1818,11 +1818,11 @@ label {
.datalist-arrow:focus:after, .datalist-arrow:focus:after,
.datalist-arrow:active:after { .datalist-arrow:active:after {
background: #1EBBEE; background: #1ebbee;
} }
input::-webkit-calendar-picker-indicator::after { input::-webkit-calendar-picker-indicator::after {
content: '\276F'; content: "\276F";
right: 0; right: 0;
top: -8%; top: -8%;
display: block; display: block;
@@ -1836,7 +1836,7 @@ input::-webkit-calendar-picker-indicator::after {
} }
.datalist-arrow:after:hover { .datalist-arrow:after:hover {
content: '\276F'; content: "\276F";
position: absolute; position: absolute;
right: 1px; right: 1px;
top: 6%; top: 6%;
@@ -1848,7 +1848,7 @@ input::-webkit-calendar-picker-indicator::after {
color: #fff; color: #fff;
text-align: center; text-align: center;
pointer-events: none; pointer-events: none;
background-color: #1EBBEE; background-color: #1ebbee;
} }
.Introline3 { .Introline3 {
@@ -1902,26 +1902,26 @@ input::-webkit-calendar-picker-indicator::after {
border: none; border: none;
} }
.qslevel>li>a { .qslevel > li > a {
border: none !important; border: none !important;
} }
.qslevel>li.active { .qslevel > li.active {
border-bottom: 4px solid #767676; border-bottom: 4px solid #767676;
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 8px; padding-top: 8px;
background-color: #F2F2F2; background-color: #f2f2f2;
} }
.navTabHeight { .navTabHeight {
height: 31px; height: 31px;
} }
.qslevel>li.active>a, .qslevel > li.active > a,
.qslevel>li>a:focus, .qslevel > li > a:focus,
.nav.nav-tabs.qslevel>li>a:hover { .nav.nav-tabs.qslevel > li > a:hover {
border: none; border: none;
border-radius: 0; border-radius: 0;
background-color: transparent !important; background-color: transparent !important;
@@ -1941,16 +1941,16 @@ input::-webkit-calendar-picker-indicator::after {
padding-left: 20px; padding-left: 20px;
} }
.numberheading>p { .numberheading > p {
padding-top: 10px; padding-top: 10px;
font-size: 12px !important; font-size: 12px !important;
} }
.numberheading>ul { .numberheading > ul {
padding-top: 10px; padding-top: 10px;
} }
.numberheading>ul>li>a { .numberheading > ul > li > a {
font-size: 12px !important; font-size: 12px !important;
} }
@@ -1958,7 +1958,7 @@ input::-webkit-calendar-picker-indicator::after {
padding-bottom: 60px; padding-bottom: 60px;
} }
.step1>input { .step1 > input {
font-size: 12px; font-size: 12px;
} }
@@ -1978,7 +1978,7 @@ input::-webkit-calendar-picker-indicator::after {
.atags { .atags {
color: @AccentMediumHigh; color: @AccentMediumHigh;
font-weight: 400; font-weight: 400;
cursor: pointer cursor: pointer;
} }
.qsmenuicons { .qsmenuicons {
@@ -2041,7 +2041,7 @@ a:link {
text-decoration: none; text-decoration: none;
} }
.nav>li>a:focus { .nav > li > a:focus {
background-color: white; background-color: white;
outline: none; outline: none;
} }
@@ -2218,10 +2218,10 @@ a:link {
.documentsGridHeaderContainer { .documentsGridHeaderContainer {
padding-left: 5px; padding-left: 5px;
width: 100%; width: 100%;
border-bottom: 1px solid #CCCCCC; border-bottom: 1px solid #cccccc;
} }
.documentsGridHeaderContainer>table { .documentsGridHeaderContainer > table {
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;
border-collapse: unset; border-collapse: unset;
@@ -2234,7 +2234,7 @@ a:link {
position: sticky; position: sticky;
top: 0; top: 0;
background-color: #fff !important; background-color: #fff !important;
border-bottom: 1px solid #CCCCCC !important; border-bottom: 1px solid #cccccc !important;
} }
} }
@@ -2275,14 +2275,14 @@ a:link {
overflow-x: hidden; overflow-x: hidden;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
width:100%; width: 100%;
} }
.tabdocuments>.tabdocumentsGridElement { .tabdocuments > .tabdocumentsGridElement {
width: 50%; width: 50%;
} }
.tabdocuments>.evenlySpacedHeader { .tabdocuments > .evenlySpacedHeader {
width: 30%; width: 30%;
} }
@@ -2316,7 +2316,7 @@ td a:hover {
cursor: pointer; cursor: pointer;
} }
.loadMore>a:focus { .loadMore > a:focus {
outline: 1px dotted; outline: 1px dotted;
} }
@@ -2346,7 +2346,7 @@ td a:hover {
} }
.table-fixed tbody td, .table-fixed tbody td,
.table-fixed thead>tr>th { .table-fixed thead > tr > th {
float: left; float: left;
border-bottom-width: 0; border-bottom-width: 0;
} }
@@ -2383,52 +2383,52 @@ a:link {
color: #393939; color: #393939;
} }
.tab [type=radio] { .tab [type="radio"] {
display: none; display: none;
} }
.tabcontent { .tabcontent {
clear:both; clear: both;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
padding: @MediumSpace 0px; padding: @MediumSpace 0px;
} }
.tab [type=radio]:checked~label { .tab [type="radio"]:checked ~ label {
border: 1px solid #0072c6; border: 1px solid #0072c6;
background-color: @AccentMediumHigh; background-color: @AccentMediumHigh;
color: white; color: white;
z-index: 2; z-index: 2;
} }
.tab [type=radio]:checked~label:hover { .tab [type="radio"]:checked ~ label:hover {
border: 1px solid @AccentMediumHigh; border: 1px solid @AccentMediumHigh;
background-color: @AccentMediumHigh; background-color: @AccentMediumHigh;
color: white; color: white;
z-index: 2; z-index: 2;
} }
.tab [type=radio]:checked~label:active { .tab [type="radio"]:checked ~ label:active {
border: 1px solid #0072c6; border: 1px solid #0072c6;
background-color: #0072c6; background-color: #0072c6;
color: white; color: white;
z-index: 2; z-index: 2;
} }
.tab [type=radio]:checked~label~.tabcontent { .tab [type="radio"]:checked ~ label ~ .tabcontent {
z-index: 1; z-index: 1;
display: initial; display: initial;
} }
.tab [type=radio]:not(:checked)~label:hover { .tab [type="radio"]:not(:checked) ~ label:hover {
border: 1px solid #969696; border: 1px solid #969696;
background-color: #969696; background-color: #969696;
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.tab [type=radio]:not(:checked)~label~.tabcontent { .tab [type="radio"]:not(:checked) ~ label ~ .tabcontent {
display: none; display: none;
} }
@@ -2437,10 +2437,10 @@ a:link {
} }
.atagdetails { .atagdetails {
padding-left: 55px!important; padding-left: 55px !important;
} }
.contextual-pane-in .form-errors+img { .contextual-pane-in .form-errors + img {
display: block; display: block;
position: absolute; position: absolute;
top: 92px; top: 92px;
@@ -2532,8 +2532,8 @@ a:link {
border: none; border: none;
} }
.queryButton{ .queryButton {
margin-left:@LargeSpace; margin-left: @LargeSpace;
} }
.hrline1 { .hrline1 {
@@ -2631,9 +2631,9 @@ a:link {
cursor: col-resize; cursor: col-resize;
} }
.nav-tabs>li.active>.tabNavContentContainer, .nav-tabs > li.active > .tabNavContentContainer,
.nav-tabs>li.active>.tabNavContentContainer:focus, .nav-tabs > li.active > .tabNavContentContainer:focus,
.nav-tabs>li.active>.tabNavContentContainer:hover { .nav-tabs > li.active > .tabNavContentContainer:hover {
color: #555; color: #555;
cursor: default; cursor: default;
background-color: @BaseLight; background-color: @BaseLight;
@@ -2645,7 +2645,7 @@ a:link {
width: @ActiveTabWidth; width: @ActiveTabWidth;
} }
.nav-tabs>li.active:focus>.tabNavContentContainer { .nav-tabs > li.active:focus > .tabNavContentContainer {
.focus(); .focus();
} }
@@ -2732,7 +2732,7 @@ a:link {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
flex-grow: 1 flex-grow: 1;
} }
.tabIconSection { .tabIconSection {
@@ -2799,7 +2799,7 @@ a:link {
height: 100%; height: 100%;
} }
.nav-tabs>li>a:active { .nav-tabs > li > a:active {
background-color: #e0e0e0; background-color: #e0e0e0;
width: 100%; width: 100%;
border: 1px solid @AccentMediumHigh; border: 1px solid @AccentMediumHigh;
@@ -2822,17 +2822,17 @@ a:link {
} }
.tabCommandDisabled { .tabCommandDisabled {
color: #CCCCCC; color: #cccccc;
cursor: default; cursor: default;
background-color: #FFFFFF; background-color: #ffffff;
} }
.tabCommandDisabled:active { .tabCommandDisabled:active {
border: 1px solid #FFFFFF; border: 1px solid #ffffff;
} }
.tabCommandDisabled:hover { .tabCommandDisabled:hover {
background-color: #FFFFFF; background-color: #ffffff;
} }
#explorerNotificationConsole { #explorerNotificationConsole {
@@ -2857,7 +2857,7 @@ a:link {
} }
.uniqueTooltiptext { .uniqueTooltiptext {
bottom:28px; bottom: 28px;
.tooltipText(); .tooltipText();
} }
@@ -2940,13 +2940,13 @@ settings-pane {
} }
.linkDarkBackground { .linkDarkBackground {
color: @AccentExtraHigh color: @AccentExtraHigh;
} }
.linkDarkBackground:hover, .linkDarkBackground:hover,
.linkDarkBackground:active, .linkDarkBackground:active,
.linkDarkBackground:focus { .linkDarkBackground:focus {
color: @AccentHigh color: @AccentHigh;
} }
.library-add-button { .library-add-button {
@@ -2962,11 +2962,11 @@ settings-pane {
margin-left: 1em; margin-left: 1em;
} }
#deletecollectionconfirmationpane .paneMainContent > div:not(:first-child){ #deletecollectionconfirmationpane .paneMainContent > div:not(:first-child) {
margin-top: 12px; margin-top: 12px;
} }
#deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child){ #deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child) {
margin-top: 12px; margin-top: 12px;
} }
@@ -2978,12 +2978,12 @@ settings-pane {
margin-top: @SmallSpace; margin-top: @SmallSpace;
} }
.enableAnalyticalStorageRadio:nth-child(n+2) { .enableAnalyticalStorageRadio:nth-child(n + 2) {
margin-left: @LargeSpace; margin-left: @LargeSpace;
} }
.enableAnalyticalStorageRadioLabel { .enableAnalyticalStorageRadioLabel {
padding: 0px padding: 0px;
} }
} }
@@ -2992,19 +2992,19 @@ settings-pane {
font-weight: 600; font-weight: 600;
} }
.button.enabled{ .button.enabled {
background: #FFF; background: #fff;
border-radius: 2px; border-radius: 2px;
color: #323130; color: #323130;
padding: 3px 20px; padding: 3px 20px;
border: 1px solid #8A8886; border: 1px solid #8a8886;
} }
.button.disabled{ .button.disabled {
background: #F3F2F1; background: #f3f2f1;
border: 0px solid #8A8886; border: 0px solid #8a8886;
border-radius: 2px; border-radius: 2px;
color: #A19F9D; color: #a19f9d;
padding: 3px 20px; padding: 3px 20px;
} }
@@ -3017,13 +3017,13 @@ settings-pane {
} }
.warningErrorContent a { .warningErrorContent a {
color: @AccentMediumHigh color: @AccentMediumHigh;
} }
.infoBoxContent a { .infoBoxContent a {
color: @AccentMediumHigh color: @AccentMediumHigh;
} }
.collapsibleSection :hover{ .collapsibleSection :hover {
cursor: pointer; cursor: pointer;
} }

56
package-lock.json generated
View File

@@ -275,6 +275,27 @@
} }
} }
}, },
"@azure/msal-browser": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.8.0.tgz",
"integrity": "sha512-I6n7EQnwsZXgKPOLlS5X48jhzUNUFwMVm180wDBA/pwEkUy8ei6zWiPMBfWaMSxz9uNx9WHaEhgAyhJy0ze3AQ==",
"requires": {
"@azure/msal-common": "^2.0.0"
}
},
"@azure/msal-common": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-2.0.0.tgz",
"integrity": "sha512-d1RNcJb+P1EGzMHtgbZoVlHLQWjlVfr504jywNk9YEfoq8Hw3BxJ0wepu+1w0hc64D8zG0wljcvHaIH1jTn2SA==",
"requires": {
"debug": "^4.1.1"
}
},
"@azure/msal-react": {
"version": "1.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-1.0.0-alpha.1.tgz",
"integrity": "sha512-8BftMP1DyXf7/Fa7mxi14/fmHBdDGDUONmE8sm1T6w7ERJyY1RN7PZgdnUOcYcqj2xMnxfz9++8HsrzMrtMc0Q=="
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.10.4", "version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@@ -5443,12 +5464,6 @@
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
}, },
"adal-angular": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/adal-angular/-/adal-angular-1.0.15.tgz",
"integrity": "sha1-8qnvgvNYxEToMUKs5l0yJ6RBBDs=",
"dev": true
},
"agent-base": { "agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -15313,6 +15328,15 @@
"tinyqueue": "^1.1.0" "tinyqueue": "^1.1.0"
} }
}, },
"match-sorter": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.0.2.tgz",
"integrity": "sha512-SDRLNlWof9GnAUEyhKP0O5525MMGXUGt+ep4MrrqQ2StAh3zjvICVZseiwg7Zijn3GazpJDiwuRr/mFDHd92NQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"remove-accents": "0.4.2"
}
},
"matchdep": { "matchdep": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
@@ -15791,9 +15815,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"msal": { "msal": {
"version": "1.4.3", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/msal/-/msal-1.4.3.tgz", "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.4.tgz",
"integrity": "sha512-C90MhgzcBuTSR2BOQ/LQryY1CZVESQLJDdmRDWSsaVde+zwZ2iXD0fWw7zeBd5TzfUCiJEXZVs4lFJ8d/IGbiQ==", "integrity": "sha512-aOBD/L6jAsizDFzKxxvXxH0FEDjp6Inr3Ufi/Y2o7KCFKN+akoE2sLeszEb/0Y3VxHxK0F0ea7xQ/HHTomKivw==",
"requires": { "requires": {
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
@@ -17760,6 +17784,15 @@
"warning": "^4.0.2" "warning": "^4.0.2"
} }
}, },
"react-query": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.5.5.tgz",
"integrity": "sha512-WYZcHcAs5K5lPGT6CI8fz3lU62S8IfZhvB1K4aZH27wg9T6CWei+y7IRyZwti9X18LX134O4olgEuNth9LEX+w==",
"requires": {
"@babel/runtime": "^7.5.5",
"match-sorter": "^6.0.2"
}
},
"react-redux": { "react-redux": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz",
@@ -18152,6 +18185,11 @@
"superagent-proxy": "^2.0.0" "superagent-proxy": "^2.0.0"
} }
}, },
"remove-accents": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U="
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",

View File

@@ -6,8 +6,10 @@
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.9.0",
"@azure/identity": "1.1.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0",
"@azure/msal-browser": "2.8.0",
"@azure/msal-react": "1.0.0-alpha.1",
"@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
@@ -69,6 +71,7 @@
"knockout": "3.5.1", "knockout": "3.5.1",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.18.1", "monaco-editor": "0.18.1",
"msal": "1.4.4",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1", "office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0", "p-retry": "4.2.0",
@@ -83,6 +86,7 @@
"react-dom": "16.9.0", "react-dom": "16.9.0",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-query": "3.5.5",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
@@ -128,7 +132,6 @@
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1", "@typescript-eslint/parser": "4.0.1",
"adal-angular": "1.0.15",
"axe-puppeteer": "1.1.0", "axe-puppeteer": "1.1.0",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",

View File

@@ -2,5 +2,6 @@ export enum AuthType {
AAD = "aad", AAD = "aad",
EncryptedToken = "encryptedtoken", EncryptedToken = "encryptedtoken",
MasterKey = "masterkey", MasterKey = "masterkey",
ResourceToken = "resourcetoken" ResourceToken = "resourcetoken",
ConnectionString = "connectionstring"
} }

View File

@@ -587,11 +587,3 @@ export interface MemoryUsageInfo {
freeKB: number; freeKB: number;
totalKB: number; totalKB: number;
} }
export interface resourceTokenConnectionStringProperties {
accountEndpoint: string;
collectionId: string;
databaseId: string;
partitionKey?: string;
resourceToken: string;
}

View File

@@ -1,383 +0,0 @@
// Type definitions for adal-angular 1.0.1.1
// Project: https://github.com/AzureAD/azure-activedirectory-library-for-js#readme
// Definitions by: Daniel Perez Alvarez <https://github.com/unindented>
// Anthony Ciccarello <https://github.com/aciccarello>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
//This is a customized version of adal on top of version 1.0.1 which does not support multi tenant
// Customized version add tenantId to stored tokens so when tenant change, adal will refetch instead of read from sessionStorage
// In module contexts the class constructor function is the exported object
// export = AuthenticationContext;
// This class is defined globally in not in a module context
declare class AuthenticationContext {
instance: string;
config: AuthenticationContext.Options;
callback: AuthenticationContext.TokenCallback;
popUp: boolean;
isAngular: boolean;
/**
* Enum for request type
*/
REQUEST_TYPE: AuthenticationContext.RequestType;
RESPONSE_TYPE: AuthenticationContext.ResponseType;
CONSTANTS: AuthenticationContext.Constants;
constructor(options: AuthenticationContext.Options);
/**
* Initiates the login process by redirecting the user to Azure AD authorization endpoint.
*/
login(): void;
/**
* Returns whether a login is in progress.
*/
loginInProgress(): boolean;
/**
* Gets token for the specified resource from the cache.
* @param resource A URI that identifies the resource for which the token is requested.
* @param tenantId tenant Id.
*/
getCachedToken(resource: string, tenantId: string): string;
/**
* If user object exists, returns it. Else creates a new user object by decoding `id_token` from the cache.
*/
getCachedUser(): AuthenticationContext.UserInfo;
/**
* Adds the passed callback to the array of callbacks for the specified resource.
* @param resource A URI that identifies the resource for which the token is requested.
* @param expectedState A unique identifier (guid).
* @param callback The callback provided by the caller. It will be called with token or error.
*/
registerCallback(
expectedState: string,
resource: string,
callback: AuthenticationContext.TokenCallback,
tenantId: string
): void;
/**
* Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token.
* @param resource Resource URI identifying the target resource.
* @param callback The callback provided by the caller. It will be called with token or error.
*/
acquireToken(resource: string, tenantId: string, callback: AuthenticationContext.TokenCallback): void;
/**
* Acquires token (interactive flow using a popup window) by sending request to AAD to obtain a new token.
* @param resource Resource URI identifying the target resource.
* @param extraQueryParameters Query parameters to add to the authentication request.
* @param claims Claims to add to the authentication request.
* @param callback The callback provided by the caller. It will be called with token or error.
*/
acquireTokenPopup(
resource: string,
tenantId: string,
extraQueryParameters: string | null | undefined,
claims: string | null | undefined,
callback: AuthenticationContext.TokenCallback
): void;
/**
* Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the authentication request constructor will be called.
* @param resource Resource URI identifying the target resource.
* @param extraQueryParameters Query parameters to add to the authentication request.
* @param claims Claims to add to the authentication request.
*/
acquireTokenRedirect(
resource: string,
tenantId: string,
extraQueryParameters?: string | null,
claims?: string | null
): void;
/**
* Redirects the browser to Azure AD authorization endpoint.
* @param urlNavigate URL of the authorization endpoint.
*/
promptUser(urlNavigate: string): void;
/**
* Clears cache items.
*/
clearCache(): void;
/**
* Clears cache items for a given resource.
* @param resource Resource URI identifying the target resource.
*/
clearCacheForResource(resource: string): void;
/**
* Redirects user to logout endpoint. After logout, it will redirect to `postLogoutRedirectUri` if added as a property on the config object.
*/
logOut(): void;
/**
* Calls the passed in callback with the user object or error message related to the user.
* @param callback The callback provided by the caller. It will be called with user or error.
*/
getUser(callback: AuthenticationContext.UserCallback): void;
/**
* Checks if the URL fragment contains access token, id token or error description.
* @param hash Hash passed from redirect page.
*/
isCallback(hash: string): boolean;
/**
* Gets login error.
*/
getLoginError(): string;
/**
* Creates a request info object from the URL fragment and returns it.
*/
getRequestInfo(hash: string): AuthenticationContext.RequestInfo;
/**
* Saves token or error received in the response from AAD in the cache. In case of `id_token`, it also creates the user object.
*/
saveTokenFromHash(requestInfo: AuthenticationContext.RequestInfo): void;
/**
* Gets resource for given endpoint if mapping is provided with config.
* @param endpoint Resource URI identifying the target resource.
*/
getResourceForEndpoint(resource: string): string;
/**
* This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the callbacks with the result.
* @param hash Hash fragment of URL. Defaults to `window.location.hash`.
*/
handleWindowCallback(hash?: string): void;
/**
* Checks the logging Level, constructs the log message and logs it. Users need to implement/override this method to turn on logging.
* @param level Level can be set 0, 1, 2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively.
* @param message Message to log.
* @param error Error to log.
*/
log(level: AuthenticationContext.LoggingLevel, message: string, error: any): void;
/**
* Logs messages when logging level is set to 0.
* @param message Message to log.
* @param error Error to log.
*/
error(message: string, error: any): void;
/**
* Logs messages when logging level is set to 1.
* @param message Message to log.
*/
warn(message: string): void;
/**
* Logs messages when logging level is set to 2.
* @param message Message to log.
*/
info(message: string): void;
/**
* Logs messages when logging level is set to 3.
* @param message Message to log.
*/
verbose(message: string): void;
/**
* Logs Pii messages when Logging Level is set to 0 and window.piiLoggingEnabled is set to true.
* @param message Message to log.
* @param error Error to log.
*/
errorPii(message: string, error: any): void;
/**
* Logs Pii messages when Logging Level is set to 1 and window.piiLoggingEnabled is set to true.
* @param message Message to log.
*/
warnPii(message: string): void;
/**
* Logs messages when Logging Level is set to 2 and window.piiLoggingEnabled is set to true.
* @param message Message to log.
*/
infoPii(message: string): void;
/**
* Logs messages when Logging Level is set to 3 and window.piiLoggingEnabled is set to true.
* @param message Message to log.
*/
verbosePii(message: string): void;
}
declare namespace AuthenticationContext {
function inject(config: Options): AuthenticationContext;
type LoggingLevel = 0 | 1 | 2 | 3;
type RequestType = "LOGIN" | "RENEW_TOKEN" | "UNKNOWN";
type ResponseType = "id_token token" | "token";
interface RequestInfo {
/**
* Object comprising of fields such as id_token/error, session_state, state, e.t.c.
*/
parameters: any;
/**
* Request type.
*/
requestType: RequestType;
/**
* Whether state is valid.
*/
stateMatch: boolean;
/**
* Unique guid used to match the response with the request.
*/
stateResponse: string;
/**
* Whether `requestType` contains `id_token`, `access_token` or error.
*/
valid: boolean;
}
interface UserInfo {
/**
* Username assigned from UPN or email.
*/
userName: string;
/**
* Properties parsed from `id_token`.
*/
profile: any;
}
type TokenCallback = (errorDesc: string | null, token: string | null, error: any) => void;
type UserCallback = (errorDesc: string | null, user: UserInfo | null) => void;
/**
* Configuration options for Authentication Context
*/
interface Options {
/**
* Client ID assigned to your app by Azure Active Directory.
*/
clientId: string;
/**
* Endpoint at which you expect to receive tokens.Defaults to `window.location.href`.
*/
redirectUri?: string;
/**
* Azure Active Directory instance. Defaults to `https://login.microsoftonline.com/`.
*/
instance?: string;
/**
* Your target tenant. Defaults to `common`.
*/
tenant?: string;
/**
* Query parameters to add to the authentication request.
*/
extraQueryParameter?: string;
/**
* Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits).
*/
correlationId?: string;
/**
* User defined function of handling the navigation to Azure AD authorization endpoint in case of login.
*/
displayCall?: (url: string) => void;
/**
* Set this to true to enable login in a popup winodow instead of a full redirect. Defaults to `false`.
*/
popUp?: boolean;
/**
* Set this to the resource to request on login. Defaults to `clientId`.
*/
loginResource?: string;
/**
* Set this to redirect the user to a custom login page.
*/
localLoginUrl?: string;
/**
* Redirects to start page after login. Defaults to `true`.
*/
navigateToLoginRequestUrl?: boolean;
/**
* Set this to redirect the user to a custom logout page.
*/
logOutUri?: string;
/**
* Redirects the user to postLogoutRedirectUri after logout. Defaults to `redirectUri`.
*/
postLogoutRedirectUri?: string;
/**
* Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to `sessionStorage`.
*/
cacheLocation?: "localStorage" | "sessionStorage";
/**
* Array of keywords or URIs. Adal will attach a token to outgoing requests that have these keywords or URIs.
*/
endpoints?: { [resource: string]: string };
/**
* Array of keywords or URIs. Adal will not attach a token to outgoing requests that have these keywords or URIs.
*/
anonymousEndpoints?: string[];
/**
* If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds.
*/
expireOffsetSeconds?: number;
/**
* The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out. Defaults to 6 seconds.
*/
loadFrameTimeout?: number;
/**
* Callback to be invoked when a token is acquired.
*/
callback?: TokenCallback;
}
interface LoggingConfig {
level: LoggingLevel;
log: (message: string) => void;
piiLoggingEnabled: boolean;
}
/**
* Enum for storage constants
*/
interface Constants {
ACCESS_TOKEN: "access_token";
EXPIRES_IN: "expires_in";
ID_TOKEN: "id_token";
ERROR_DESCRIPTION: "error_description";
SESSION_STATE: "session_state";
STORAGE: {
TOKEN_KEYS: "adal.token.keys";
ACCESS_TOKEN_KEY: "adal.access.token.key";
EXPIRATION_KEY: "adal.expiration.key";
STATE_LOGIN: "adal.state.login";
STATE_RENEW: "adal.state.renew";
NONCE_IDTOKEN: "adal.nonce.idtoken";
SESSION_STATE: "adal.session.state";
USERNAME: "adal.username";
IDTOKEN: "adal.idtoken";
ERROR: "adal.error";
ERROR_DESCRIPTION: "adal.error.description";
LOGIN_REQUEST: "adal.login.request";
LOGIN_ERROR: "adal.login.error";
RENEW_STATUS: "adal.token.renew.status";
};
RESOURCE_DELIMETER: "|";
LOADFRAME_TIMEOUT: "6000";
TOKEN_RENEW_STATUS_CANCELED: "Canceled";
TOKEN_RENEW_STATUS_COMPLETED: "Completed";
TOKEN_RENEW_STATUS_IN_PROGRESS: "In Progress";
LOGGING_LEVEL: {
ERROR: 0;
WARN: 1;
INFO: 2;
VERBOSE: 3;
};
LEVEL_STRING_MAP: {
0: "ERROR:";
1: "WARNING:";
2: "INFO:";
3: "VERBOSE:";
};
POPUP_WIDTH: 483;
POPUP_HEIGHT: 600;
}
}
// declare global {
// interface Window {
// Logging: AuthenticationContext.LoggingConfig;
// }
// }

View File

@@ -1,159 +0,0 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
import { AccountKind } from "../../../Common/Constants";
const createBlankProps = (): AccountSwitchComponentProps => {
return {
authType: null,
displayText: "",
accounts: [],
selectedAccountName: null,
isLoadingAccounts: false,
onAccountChange: jest.fn(),
subscriptions: [],
selectedSubscriptionId: null,
isLoadingSubscriptions: false,
onSubscriptionChange: jest.fn()
};
};
const createBlankAccount = (): DatabaseAccount => {
return {
id: "",
kind: AccountKind.Default,
name: "",
properties: null,
location: "",
tags: null,
type: ""
};
};
const createBlankSubscription = (): Subscription => {
return {
subscriptionId: "",
displayName: "",
authorizationSource: "",
state: "",
subscriptionPolicies: null,
tenantId: "",
uniqueDisplayName: ""
};
};
const createFullProps = (): AccountSwitchComponentProps => {
const props = createBlankProps();
props.authType = AuthType.AAD;
const account1 = createBlankAccount();
account1.name = "account1";
const account2 = createBlankAccount();
account2.name = "account2";
const account3 = createBlankAccount();
account3.name = "superlongaccountnamestringtest";
props.accounts = [account1, account2, account3];
props.selectedAccountName = "account2";
const sub1 = createBlankSubscription();
sub1.displayName = "sub1";
sub1.subscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
const sub2 = createBlankSubscription();
sub2.displayName = "subsubsubsubsubsubsub2";
sub2.subscriptionId = "b20b3e93-0185-4326-8a9c-d44bac276b6b";
props.subscriptions = [sub1, sub2];
props.selectedSubscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
return props;
};
describe("test render", () => {
it("renders no auth type -> handle error in code", () => {
const props = createBlankProps();
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// Encrypted Token
it("renders auth security token, with selected account name", () => {
const props = createBlankProps();
props.authType = AuthType.EncryptedToken;
props.selectedAccountName = "testaccount";
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// AAD
it("renders auth aad, with all information", () => {
const props = createFullProps();
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders auth aad all dropdown menus", () => {
const props = createFullProps();
const wrapper = mount(<AccountSwitchComponent {...props} />);
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
wrapper.find("button.accountSwitchButton").simulate("click");
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(true);
expect(wrapper.exists("div.accountSwitchSubscriptionDropdown")).toBe(true);
wrapper.find("DropdownBase.accountSwitchSubscriptionDropdown").simulate("click");
// Click will dismiss the first contextual menu in enzyme. Need to dig deeper to achieve below test
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(true);
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(2);
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(false);
// expect(wrapper.exists("div.accountSwitchAccountDropdown")).toBe(true);
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(true);
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(3);
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(false);
// wrapper.find("button.accountSwitchButton").simulate("click");
// expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
wrapper.unmount();
});
});
// describe("test function", () => {
// it("switch subscription function", () => {
// const props = createFullProps();
// const wrapper = mount(<AccountSwitchComponent {...props} />);
// wrapper.find("button.accountSwitchButton").simulate("click");
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
// wrapper
// .find("button.ms-Dropdown-item")
// .at(1)
// .simulate("click");
// expect(props.onSubscriptionChange).toBeCalled();
// expect(props.onSubscriptionChange).toHaveBeenCalled();
// wrapper.unmount();
// });
// it("switch account", () => {
// const props = createFullProps();
// const wrapper = mount(<AccountSwitchComponent {...props} />);
// wrapper.find("button.accountSwitchButton").simulate("click");
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// wrapper
// .find("button.ms-Dropdown-item")
// .at(0)
// .simulate("click");
// expect(props.onAccountChange).toBeCalled();
// expect(props.onAccountChange).toHaveBeenCalled();
// wrapper.unmount();
// });
// });

View File

@@ -1,177 +0,0 @@
import { AuthType } from "../../../AuthType";
import { StyleConstants } from "../../../Common/Constants";
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
import * as React from "react";
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
export interface AccountSwitchComponentProps {
authType: AuthType;
selectedAccountName: string;
accounts: DatabaseAccount[];
isLoadingAccounts: boolean;
onAccountChange: (newAccount: DatabaseAccount) => void;
selectedSubscriptionId: string;
subscriptions: Subscription[];
isLoadingSubscriptions: boolean;
onSubscriptionChange: (newSubscription: Subscription) => void;
displayText?: string;
}
export class AccountSwitchComponent extends React.Component<AccountSwitchComponentProps> {
public render(): JSX.Element {
return this.props.authType === AuthType.AAD ? this._renderSwitchDropDown() : this._renderAccountName();
}
private _renderSwitchDropDown(): JSX.Element {
const { displayText, selectedAccountName } = this.props;
const menuProps: IContextualMenuProps = {
directionalHintFixed: true,
className: "accountSwitchContextualMenu",
items: [
{
key: "switchSubscription",
onRender: this._renderSubscriptionDropdown.bind(this)
},
{
key: "switchAccount",
onRender: this._renderAccountDropDown.bind(this)
}
]
};
const buttonStyles: IButtonStyles = {
root: {
fontSize: StyleConstants.DefaultFontSize,
height: 40,
padding: 0,
paddingLeft: 10,
marginRight: 5,
backgroundColor: StyleConstants.BaseDark,
color: StyleConstants.BaseLight
},
rootHovered: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootFocused: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootPressed: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootExpanded: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
textContainer: {
flexGrow: "initial"
}
};
const buttonProps: IButtonProps = {
text: displayText || selectedAccountName,
menuProps: menuProps,
styles: buttonStyles,
className: "accountSwitchButton",
id: "accountSwitchButton"
};
return <DefaultButton {...buttonProps} />;
}
private _renderSubscriptionDropdown(): JSX.Element {
const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props;
const options: IDropdownOption[] = subscriptions.map(sub => {
return {
key: sub.subscriptionId,
text: sub.displayName,
data: sub
};
});
const placeHolderText = isLoadingSubscriptions
? "Loading subscriptions"
: !options || !options.length
? "No subscriptions found in current directory"
: "Select subscription from list";
const dropdownProps: IDropdownProps = {
label: "Subscription",
className: "accountSwitchSubscriptionDropdown",
options: options,
onChange: this._onSubscriptionDropdownChange,
defaultSelectedKey: selectedSubscriptionId,
placeholder: placeHolderText,
styles: {
callout: "accountSwitchSubscriptionDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
private _onSubscriptionDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
if (!option) {
return;
}
this.props.onSubscriptionChange(option.data);
};
private _renderAccountDropDown(): JSX.Element {
const { accounts, selectedAccountName, isLoadingAccounts } = this.props;
const options: IDropdownOption[] = accounts.map(account => {
return {
key: account.name,
text: account.name,
data: account
};
});
// Fabric UI will also try to select the first non-disabled option from dropdown.
// Add a option to prevent pop the message when user click on dropdown on first time.
options.unshift({
key: "select from list",
text: "Select Cosmos DB account from list",
data: undefined
});
const placeHolderText = isLoadingAccounts
? "Loading Cosmos DB accounts"
: !options || !options.length
? "No Cosmos DB accounts found"
: "Select Cosmos DB account from list";
const dropdownProps: IDropdownProps = {
label: "Cosmos DB Account Name",
className: "accountSwitchAccountDropdown",
options: options,
onChange: this._onAccountDropdownChange,
defaultSelectedKey: selectedAccountName,
placeholder: placeHolderText,
styles: {
callout: "accountSwitchAccountDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
private _onAccountDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
if (!option) {
return;
}
this.props.onAccountChange(option.data);
};
private _renderAccountName(): JSX.Element {
const { displayText, selectedAccountName } = this.props;
return <span className="accountNameHeader">{displayText || selectedAccountName}</span>;
}
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
export class AccountSwitchComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<AccountSwitchComponentProps>;
public renderComponent(): JSX.Element {
return <AccountSwitchComponent {...this.parameters()} />;
}
}

View File

@@ -1,71 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test render renders auth aad, with all information 1`] = `
<CustomizedDefaultButton
className="accountSwitchButton"
id="accountSwitchButton"
menuProps={
Object {
"className": "accountSwitchContextualMenu",
"directionalHintFixed": true,
"items": Array [
Object {
"key": "switchSubscription",
"onRender": [Function],
},
Object {
"key": "switchAccount",
"onRender": [Function],
},
],
}
}
styles={
Object {
"root": Object {
"backgroundColor": undefined,
"color": undefined,
"fontSize": undefined,
"height": 40,
"marginRight": 5,
"padding": 0,
"paddingLeft": 10,
},
"rootExpanded": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootFocused": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootHovered": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootPressed": Object {
"backgroundColor": undefined,
"color": undefined,
},
"textContainer": Object {
"flexGrow": "initial",
},
}
}
text="account2"
/>
`;
exports[`test render renders auth security token, with selected account name 1`] = `
<span
className="accountNameHeader"
>
testaccount
</span>
`;
exports[`test render renders no auth type -> handle error in code 1`] = `
<span
className="accountNameHeader"
/>
`;

View File

@@ -38,7 +38,7 @@ export interface CommandButtonComponentProps {
/** /**
* Label for the button * Label for the button
*/ */
commandButtonLabel: string; commandButtonLabel?: string;
/** /**
* True if this button opens a tab or pane, false otherwise. * True if this button opens a tab or pane, false otherwise.

View File

@@ -1260,16 +1260,6 @@ export default class Explorer {
$("#contextSwitchPrompt").dialog("open"); $("#contextSwitchPrompt").dialog("open");
} }
public displayConnectExplorerForm(): void {
$("#divExplorer").hide();
$("#connectExplorer").css("display", "flex");
}
public hideConnectExplorerForm(): void {
$("#connectExplorer").hide();
$("#divExplorer").show();
}
public isReadWriteToggled: () => boolean = (): boolean => { public isReadWriteToggled: () => boolean = (): boolean => {
return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite;
}; };
@@ -1843,7 +1833,7 @@ export default class Explorer {
if (inputs != null) { if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly // This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development" && configContext.platform === Platform.Portal) {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
} }
@@ -1878,16 +1868,6 @@ export default class Explorer {
subscriptionType: inputs.subscriptionType, subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId quotaId: inputs.quotaId
}); });
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
{
resourceId: this.databaseAccount && this.databaseAccount().id,
dataExplorerArea: Constants.Areas.ResourceTree,
databaseAccount: this.databaseAccount && this.databaseAccount()
},
inputs.loadDatabaseAccountTimestamp
);
this.isAccountReady(true); this.isAccountReady(true);
} }
} }

View File

@@ -1,3 +1,4 @@
import "./MeControlComponent.less";
import * as React from "react"; import * as React from "react";
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button"; import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/ConnectionStringParser"; import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
@@ -48,9 +48,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase {
}; };
private _shouldShowContextSwitchPrompt(): boolean { private _shouldShowContextSwitchPrompt(): boolean {
const inputMetadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const inputMetadata: DataModels.AccessInputMetadata = parseConnectionString(this.accessKey());
this.accessKey()
);
const apiKind: DataModels.ApiKind = const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()); this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean = const hasOpenedTabs: boolean =

File diff suppressed because it is too large Load Diff

140
src/HostedExplorer.tsx Normal file
View File

@@ -0,0 +1,140 @@
import { useBoolean } from "@uifabric/react-hooks";
import { initializeIcons } from "office-ui-fabric-react";
import * as React from "react";
import { render } from "react-dom";
import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
import { DatabaseAccount } from "./Contracts/DataModels";
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
import { MeControl } from "./Platform/Hosted/Components/MeControl";
import "./Platform/Hosted/ConnectScreen.less";
import "./Shared/appInsights";
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import { useAADAuth } from "./hooks/useAADAuth";
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
initializeIcons();
const App: React.FunctionComponent = () => {
// For handling encrypted portal tokens sent via query paramter
const params = new URLSearchParams(window.location.search);
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
const encryptedTokenMetadata = usePortalAccessToken(encryptedToken);
// For showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>();
const ref = React.useRef<HTMLIFrameElement>();
React.useEffect(() => {
// If ref.current is undefined no iframe has been rendered
if (ref.current) {
// In hosted mode, we can set global properties directly on the child iframe.
// This is not possible in the portal where the iframes have different origins
const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame;
// AAD authenticated uses ALWAYS using AAD authType
if (isLoggedIn) {
frameWindow.hostedConfig = {
authType: AuthType.AAD,
databaseAccount,
authorizationToken: armToken
};
} else if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = {
authType: AuthType.EncryptedToken,
encryptedToken,
encryptedTokenMetadata
};
} else if (authType === AuthType.ConnectionString) {
frameWindow.hostedConfig = {
authType: AuthType.ConnectionString,
encryptedToken,
encryptedTokenMetadata,
masterKey: extractMasterKeyfromConnectionString(connectionString)
};
} else if (authType === AuthType.ResourceToken) {
frameWindow.hostedConfig = {
authType: AuthType.ResourceToken,
resourceToken: connectionString
};
}
}
}, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]);
const showAccount = (isLoggedIn && databaseAccount) || (encryptedTokenMetadata && encryptedTokenMetadata);
return (
<>
<header>
<div className="items" role="menubar">
<div className="cosmosDBTitle">
<span
className="title"
onClick={() => window.open("https://portal.azure.com", "_blank")}
tabIndex={0}
title="Go to Azure Portal"
>
Microsoft Azure
</span>
<span className="accontSplitter" /> <span className="serviceTitle">Cosmos DB</span>
{(isLoggedIn || encryptedTokenMetadata?.accountName) && (
<img className="chevronRight" src={ChevronRight} alt="account separator" />
)}
{isLoggedIn && (
<span className="accountSwitchComponentContainer">
<AccountSwitcher armToken={armToken} setDatabaseAccount={setDatabaseAccount} />
</span>
)}
{!isLoggedIn && encryptedTokenMetadata?.accountName && (
<span className="accountSwitchComponentContainer">
<span className="accountNameHeader">{encryptedTokenMetadata?.accountName}</span>
</span>
)}
</div>
<FeedbackCommandButton />
<div className="meControl">
{isLoggedIn ? (
<MeControl {...{ graphToken, openPanel, logout, account }} />
) : (
<SignInButton {...{ login }} />
)}
</div>
</div>
</header>
{showAccount && (
// Ideally we would import and render data explorer like any other React component, however
// because it still has a significant amount of Knockout code, this would lead to memory leaks.
// Knockout does not have a way to tear down all of its binding and listeners with a single method.
// It's possible this can be changed once all knockout code has been removed.
<iframe
// Setting key is needed so React will re-render this element on any account change
key={databaseAccount?.id || encryptedTokenMetadata?.accountName}
ref={ref}
id="explorerMenu"
name="explorer"
className="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Hosted"
></iframe>
)}
{!isLoggedIn && !encryptedTokenMetadata && (
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
)}
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />}
</>
);
};
render(<App />, document.getElementById("App"));

View File

@@ -0,0 +1,32 @@
import { AuthType } from "./AuthType";
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
export interface HostedExplorerChildFrame extends Window {
hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken;
}
interface AAD {
authType: AuthType.AAD;
databaseAccount: DatabaseAccount;
authorizationToken: string;
}
interface ConnectionString {
authType: AuthType.ConnectionString;
// Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy
encryptedToken: string;
encryptedTokenMetadata: AccessInputMetadata;
// Master key is currently only used by Graph API. All other APIs use encrypted tokens and proxy with connection string
masterKey?: string;
}
interface EncryptedToken {
authType: AuthType.EncryptedToken;
encryptedToken: string;
encryptedTokenMetadata: AccessInputMetadata;
}
interface ResourceToken {
authType: AuthType.ResourceToken;
resourceToken: string;
}

View File

@@ -44,7 +44,6 @@ import "./Libs/jquery";
import "bootstrap/dist/js/npm"; import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js"; import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js"; import "../externals/jquery-ui.min.js";
import "../externals/adal.js";
import "promise-polyfill/src/polyfill"; import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill"; import "abort-controller/polyfill";
import "whatwg-fetch"; import "whatwg-fetch";
@@ -56,19 +55,11 @@ import "url-polyfill/url-polyfill.min";
initializeIcons(); initializeIcons();
import * as ko from "knockout";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
import * as Emulator from "./Platform/Emulator/Main";
import Hosted from "./Platform/Hosted/Main";
import * as Portal from "./Platform/Portal/Main";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { applyExplorerBindings } from "./applyExplorerBindings"; import { applyExplorerBindings } from "./applyExplorerBindings";
import { initializeConfiguration, Platform } from "./ConfigContext"; import { configContext, initializeConfiguration, Platform } from "./ConfigContext";
import Explorer from "./Explorer/Explorer"; import Explorer from "./Explorer/Explorer";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
@@ -78,49 +69,195 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg"; import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import { updateUserContext } from "./UserContext";
import AuthHeadersUtil from "./Platform/Hosted/Authorization";
import { CollectionCreation } from "./Shared/Constants";
import { extractFeatures } from "./Platform/Hosted/extractFeatures";
import { emulatorAccount } from "./Platform/Emulator/emulatorAccount";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import {
getDatabaseAccountKindFromExperience,
getDatabaseAccountPropertiesFromMetadata
} from "./Platform/Hosted/HostedUtils";
import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility";
import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
import { AccountKind, DefaultAccountExperience } from "./Common/Constants";
// TODO: Encapsulate and reuse all global variables as environment variables // const accountResourceId =
window.authType = AuthType.AAD; // authType === AuthType.EncryptedToken
// ? Main._databaseAccountId
// : authType === AuthType.AAD && account
// ? account.id
// : "";
// const subscriptionId: string =
// accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
// const resourceGroup: string =
// accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
useEffect(() => { useEffect(() => {
initializeConfiguration().then(config => { initializeConfiguration().then(config => {
let explorer: Explorer;
if (config.platform === Platform.Hosted) { if (config.platform === Platform.Hosted) {
try { const win = (window as unknown) as HostedExplorerChildFrame;
Hosted.initializeExplorer().then( explorer = new Explorer();
(explorer: Explorer) => { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
applyExplorerBindings(explorer); // TODO: Remove window.authType
Hosted.configureTokenValidationDisplayPrompt(explorer); window.authType = AuthType.EncryptedToken;
}, // Impossible to tell if this is a try cosmos sub using an encrypted token
(error: unknown) => { explorer.isTryCosmosDBSubscription(false);
try { updateUserContext({
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess(); accessToken: encodeURIComponent(win.hostedConfig.encryptedToken)
window.dataExplorer = uninitializedExplorer; });
ko.applyBindings(uninitializedExplorer);
BindingHandlersRegisterer.registerBindingHandlers(); const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
if (window.authType !== AuthType.AAD) { win.hostedConfig.encryptedTokenMetadata.apiKind
uninitializedExplorer.isRefreshingExplorer(false);
uninitializedExplorer.displayConnectExplorerForm();
}
} catch (e) {
console.log(e);
}
console.error(error);
}
); );
} catch (e) { explorer.initDataExplorerWithFrameInputs({
console.log(e); databaseAccount: {
id: "",
// id: Main._databaseAccountId,
name: win.hostedConfig.encryptedTokenMetadata.accountName,
kind: getDatabaseAccountKindFromExperience(apiExperience),
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
tags: []
},
subscriptionId: undefined,
resourceGroup: undefined,
masterKey: undefined,
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
authorizationToken: undefined,
features: extractFeatures(),
csmEndpoint: undefined,
dnsSuffix: undefined,
serverId: AuthHeadersUtil.serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
});
explorer.isAccountReady(true);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
window.authType = AuthType.ResourceToken;
// Resource tokens can only be used with SQL API
const apiExperience: string = DefaultAccountExperience.DocumentDB;
const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken);
updateUserContext({
resourceToken: parsedResourceToken.resourceToken
});
return explorer.initDataExplorerWithFrameInputs({
databaseAccount: {
id: "",
name: parsedResourceToken.accountEndpoint,
kind: AccountKind.GlobalDocumentDB,
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
tags: { defaultExperience: apiExperience }
},
subscriptionId: undefined,
resourceGroup: undefined,
masterKey: undefined,
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
authorizationToken: undefined,
features: extractFeatures(),
csmEndpoint: undefined,
dnsSuffix: undefined,
serverId: AuthHeadersUtil.serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
isAuthWithresourceToken: true
});
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
window.authType = AuthType.EncryptedToken;
// Impossible to tell if this is a try cosmos sub using an encrypted token
explorer.isTryCosmosDBSubscription(false);
updateUserContext({
accessToken: encodeURIComponent(win.hostedConfig.encryptedToken)
});
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
win.hostedConfig.encryptedTokenMetadata.apiKind
);
explorer.initDataExplorerWithFrameInputs({
databaseAccount: {
id: "",
// id: Main._databaseAccountId,
name: win.hostedConfig.encryptedTokenMetadata.accountName,
kind: getDatabaseAccountKindFromExperience(apiExperience),
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
tags: []
},
subscriptionId: undefined,
resourceGroup: undefined,
masterKey: win.hostedConfig.masterKey,
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
authorizationToken: undefined,
features: extractFeatures(),
csmEndpoint: undefined,
dnsSuffix: undefined,
serverId: AuthHeadersUtil.serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
});
explorer.isAccountReady(true);
} else if (win.hostedConfig.authType === AuthType.AAD) {
window.authType = AuthType.AAD;
const account = win.hostedConfig.databaseAccount;
const accountResourceId = account.id;
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
updateUserContext({
databaseAccount: win.hostedConfig.databaseAccount
});
explorer.initDataExplorerWithFrameInputs({
databaseAccount: account,
subscriptionId,
resourceGroup,
masterKey: "",
hasWriteAccess: true, //TODO: 425017 - support read access
authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`,
features: extractFeatures(),
csmEndpoint: undefined,
dnsSuffix: undefined,
serverId: AuthHeadersUtil.serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
});
explorer.isAccountReady(true);
} }
} else if (config.platform === Platform.Emulator) { } else if (config.platform === Platform.Emulator) {
window.authType = AuthType.MasterKey; window.authType = AuthType.MasterKey;
const explorer = Emulator.initializeExplorer(); explorer = new Explorer();
applyExplorerBindings(explorer); explorer.databaseAccount(emulatorAccount);
explorer.isAccountReady(true);
} else if (config.platform === Platform.Portal) { } else if (config.platform === Platform.Portal) {
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {}); explorer = new Explorer();
const explorer = Portal.initializeExplorer();
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {}); // In development mode, try to load the iframe message from session storage.
applyExplorerBindings(explorer); // This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
if (initMessage) {
const message = JSON.parse(initMessage);
console.warn("Loaded cached portal iframe message from session storage");
console.dir(message);
explorer.initDataExplorerWithFrameInputs(message);
} }
}
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
}
applyExplorerBindings(explorer);
}); });
}, []); }, []);
@@ -177,7 +314,7 @@ const App: React.FunctionComponent = () => {
aria-label="Share url link" aria-label="Share url link"
className="shareLink" className="shareLink"
type="text" type="text"
read-only read-only={true}
data-bind="value: shareAccessUrl" data-bind="value: shareAccessUrl"
/> />
<span <span

View File

@@ -1,25 +0,0 @@
import Explorer from "../../Explorer/Explorer";
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
export function initializeExplorer(): Explorer {
const explorer = new Explorer();
explorer.databaseAccount({
name: "",
id: "",
location: "",
type: "",
kind: AccountKind.DocumentDB,
tags: {
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
},
properties: {
documentEndpoint: "",
tableEndpoint: "",
gremlinEndpoint: "",
cassandraEndpoint: ""
}
});
explorer.isAccountReady(true);
return explorer;
}

View File

@@ -0,0 +1,18 @@
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
export const emulatorAccount = {
name: "",
id: "",
location: "",
type: "",
kind: AccountKind.DocumentDB,
tags: {
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
},
properties: {
documentEndpoint: "",
tableEndpoint: "",
gremlinEndpoint: "",
cassandraEndpoint: ""
}
};

View File

@@ -1,180 +0,0 @@
import AuthHeadersUtil from "./Authorization";
import * as Constants from "../../Common/Constants";
import * as Logger from "../../Common/Logger";
import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels";
import { configContext } from "../../ConfigContext";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
// TODO: 421864 - add a fetch wrapper
export abstract class ArmResourceUtils {
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
private static readonly _armApiVersion: string = configContext.ARM_API_VERSION;
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
// TODO: 422867 - return continuation token instead of read through
public static async listTenants(): Promise<Array<Tenant>> {
let tenants: Array<Tenant> = [];
try {
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea);
let nextLink = `${ArmResourceUtils._armEndpoint}/tenants?api-version=2017-08-01`;
while (nextLink) {
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
const result: TenantListResult =
response.status === 204 || response.status === 304 ? null : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
tenants = [...tenants, ...result.value];
}
return tenants;
} catch (error) {
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listTenants");
throw error;
}
}
// TODO: 422867 - return continuation token instead of read through
public static async listSubscriptions(tenantId?: string): Promise<Array<Subscription>> {
let subscriptions: Array<Subscription> = [];
try {
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
let nextLink = `${ArmResourceUtils._armEndpoint}/subscriptions?api-version=${ArmResourceUtils._armApiVersion}`;
while (nextLink) {
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
const result: SubscriptionListResult =
response.status === 204 || response.status === 304 ? null : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
const validSubscriptions = result.value.filter(
sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue"
);
subscriptions = [...subscriptions, ...validSubscriptions];
}
return subscriptions;
} catch (error) {
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listSubscriptions");
throw error;
}
}
// TODO: 422867 - return continuation token instead of read through
public static async listCosmosdbAccounts(
subscriptionIds: string[],
tenantId?: string
): Promise<Array<DatabaseAccount>> {
if (!subscriptionIds || !subscriptionIds.length) {
return Promise.reject("No subscription passed in");
}
let accounts: Array<DatabaseAccount> = [];
try {
const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'";
const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`;
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
let nextLink = `${ArmResourceUtils._armEndpoint}/resources?api-version=${ArmResourceUtils._armApiVersion}&${urlFilter}`;
while (nextLink) {
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
const result: AccountListResult =
response.status === 204 || response.status === 304 ? null : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
accounts = [...accounts, ...result.value];
}
return accounts;
} catch (error) {
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listAccounts");
throw error;
}
}
public static async getCosmosdbAccount(cosmosdbResourceId: string, tenantId?: string): Promise<DatabaseAccount> {
if (!cosmosdbResourceId) {
return Promise.reject("No Cosmos DB resource id passed in");
}
try {
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
const url = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}?api-version=${Constants.ArmApiVersions.documentDB}`;
const response: Response = await fetch(url, { headers: fetchHeaders });
const result: DatabaseAccount = response.status === 204 || response.status === 304 ? null : await response.json();
if (!response.ok) {
throw result;
}
return result;
} catch (error) {
throw error;
}
}
public static async getCosmosdbKeys(cosmosdbResourceId: string, tenantId?: string): Promise<AccountKeys> {
if (!cosmosdbResourceId) {
return Promise.reject("No Cosmos DB resource id passed in");
}
try {
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
const readWriteKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/listKeys?api-version=${Constants.ArmApiVersions.documentDB}`;
const readOnlyKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/readOnlyKeys?api-version=${Constants.ArmApiVersions.documentDB}`;
let response: Response = await fetch(readWriteKeysUrl, { headers: fetchHeaders, method: "POST" });
if (response.status === Constants.HttpStatusCodes.Forbidden) {
// fetch read only keys for readers
response = await fetch(readOnlyKeysUrl, { headers: fetchHeaders, method: "POST" });
}
const result: AccountKeys =
response.status === Constants.HttpStatusCodes.NoContent ||
response.status === Constants.HttpStatusCodes.NotModified
? null
: await response.json();
if (!response.ok) {
throw result;
}
return result;
} catch (error) {
Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAccountKeys");
throw error;
}
}
public static async getAuthToken(tenantId?: string): Promise<string> {
try {
const token = await AuthHeadersUtil.getAccessToken(ArmResourceUtils._armAuthArea, tenantId);
return token;
} catch (error) {
Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAuthToken");
throw error;
}
}
private static async _getAuthHeader(authArea: string, tenantId?: string): Promise<Headers> {
const token = await AuthHeadersUtil.getAccessToken(authArea, tenantId);
let fetchHeaders = new Headers();
fetchHeaders.append("authorization", `Bearer ${token}`);
return fetchHeaders;
}
}
interface TenantListResult {
nextLink: string;
value: Tenant[];
}
interface SubscriptionListResult {
nextLink: string;
value: Subscription[];
}
interface AccountListResult {
nextLink: string;
value: DatabaseAccount[];
}

View File

@@ -1,5 +1,3 @@
import "expose-loader?AuthenticationContext!../../../externals/adal";
import Q from "q"; import Q from "q";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
@@ -14,29 +12,6 @@ import { userContext } from "../../UserContext";
export default class AuthHeadersUtil { export default class AuthHeadersUtil {
public static serverId: string = Constants.ServerIds.productionPortal; public static serverId: string = Constants.ServerIds.productionPortal;
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT;
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT;
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT;
private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION;
private static _authContext: AuthenticationContext = new AuthenticationContext({
instance: AuthHeadersUtil._aadEndpoint,
clientId: AuthHeadersUtil._firstPartyAppId,
postLogoutRedirectUri: window.location.origin,
endpoints: {
aad: AuthHeadersUtil._aadEndpoint,
graph: AuthHeadersUtil._graphEndpoint,
armAuthArea: AuthHeadersUtil._armAuthArea,
armEndpoint: AuthHeadersUtil._armEndpoint,
arcadiaEndpoint: AuthHeadersUtil._arcadiaEndpoint
},
tenant: undefined,
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
});
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> { public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>(); const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`; const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
@@ -118,154 +93,6 @@ export default class AuthHeadersUtil {
}); });
} }
public static isUserSignedIn(): boolean {
const user = AuthHeadersUtil._authContext.getCachedUser();
return !!user;
}
public static getCachedUser(): AuthenticationContext.UserInfo {
if (this.isUserSignedIn()) {
return AuthHeadersUtil._authContext.getCachedUser();
}
return undefined;
}
public static signIn() {
if (!AuthHeadersUtil.isUserSignedIn()) {
AuthHeadersUtil._authContext.login();
}
}
public static signOut() {
AuthHeadersUtil._authContext.logOut();
}
/**
* Process token from oauth after login or get cached
*/
public static processTokenResponse() {
const isCallback = AuthHeadersUtil._authContext.isCallback(window.location.hash);
if (isCallback && !AuthHeadersUtil._authContext.getLoginError()) {
AuthHeadersUtil._authContext.handleWindowCallback();
}
}
/**
* Get auth token to access apis (Graph, ARM)
*
* @param authEndpoint Default to ARM endpoint
* @param tenantId if tenant id provided, tenant id will set at global. Can be reset with 'common'
*/
public static async getAccessToken(
authEndpoint: string = AuthHeadersUtil._armAuthArea,
tenantId?: string
): Promise<string> {
const AuthorizationType: string = (<any>window).authType;
if (AuthorizationType === AuthType.EncryptedToken) {
// setting authorization header to an undefined value causes the browser to exclude
// the header, which is expected here
throw new Error("auth type is encrypted token, should not get access token");
}
return new Promise<string>(async (resolve, reject) => {
if (tenantId) {
// if tenant id passed in, we will use this tenant id for all the rest calls until next tenant id passed in
AuthHeadersUtil._authContext.config.tenant = tenantId;
}
AuthHeadersUtil._authContext.acquireToken(
authEndpoint,
AuthHeadersUtil._authContext.config.tenant,
(errorResponse: any, token: any) => {
if (errorResponse && typeof errorResponse === "string") {
if (errorResponse.indexOf("login is required") >= 0 || errorResponse.indexOf("AADSTS50058") === 0) {
// Handle error AADSTS50058: A silent sign-in request was sent but no user is signed in.
// The user's cached token is invalid, hence we let the user login again.
AuthHeadersUtil._authContext.login();
return;
}
if (
this._isMultifactorAuthRequired(errorResponse) ||
errorResponse.indexOf("AADSTS53000") > -1 ||
errorResponse.indexOf("AADSTS65001") > -1
) {
// Handle error AADSTS50079 and AADSTS50076: User needs to use multifactor authentication and acquireToken fails silent. Redirect
// Handle error AADSTS53000: User needs to use compliant device to access resource when Conditional Access Policy is set up for user.
AuthHeadersUtil._authContext.acquireTokenRedirect(
authEndpoint,
AuthHeadersUtil._authContext.config.tenant
);
return;
}
}
if (errorResponse || !token) {
Logger.logError(errorResponse, "Hosted/Authorization/_getAuthHeader");
reject(errorResponse);
return;
}
resolve(token);
}
);
});
}
public static async getPhotoFromGraphAPI(): Promise<Blob> {
const token = await this.getAccessToken(AuthHeadersUtil._graphEndpoint);
const headers = new Headers();
headers.append("Authorization", `Bearer ${token}`);
try {
const response: Response = await fetch(
`${AuthHeadersUtil._graphEndpoint}/me/thumbnailPhoto?api-version=${AuthHeadersUtil._graphApiVersion}`,
{
method: "GET",
headers: headers
}
);
if (!response.ok) {
throw response;
}
return response.blob();
} catch (err) {
return new Blob();
}
}
private static async _getTenant(subId: string): Promise<string | undefined> {
if (subId) {
try {
// Follow https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-manager-api-authentication.md
// TenantId will be returned in the header of the response.
const response: Response = await fetch(
`https://management.core.windows.net/subscriptions/${subId}?api-version=2015-01-01`
);
if (!response.ok) {
throw response;
}
} catch (reason) {
if (reason.status === 401) {
const authUrl: string = reason.headers
.get("www-authenticate")
.split(",")[0]
.split("=")[1];
// Fetch the tenant GUID ID and the length should be 36.
const tenantId: string = authUrl.substring(authUrl.lastIndexOf("/") + 1, authUrl.lastIndexOf("/") + 37);
return Promise.resolve(tenantId);
}
}
}
return Promise.resolve(undefined);
}
private static _isMultifactorAuthRequired(errorResponse: string): boolean {
for (const code of ["AADSTS50079", "AADSTS50076"]) {
if (errorResponse.indexOf(code) === 0) {
return true;
}
}
return false;
}
private static _generateResourceUrl(): string { private static _generateResourceUrl(): string {
const databaseAccount = userContext.databaseAccount; const databaseAccount = userContext.databaseAccount;
const subscriptionId: string = userContext.subscriptionId; const subscriptionId: string = userContext.subscriptionId;

View File

@@ -0,0 +1,139 @@
// TODO: Renable this rule for the file or turn it off everywhere
/* eslint-disable react/display-name */
import { StyleConstants } from "../../../Common/Constants";
import * as React from "react";
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
import { useSubscriptions } from "../../../hooks/useSubscriptions";
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
import { DatabaseAccount } from "../../../Contracts/DataModels";
const buttonStyles: IButtonStyles = {
root: {
fontSize: StyleConstants.DefaultFontSize,
height: 40,
padding: 0,
paddingLeft: 10,
marginRight: 5,
backgroundColor: StyleConstants.BaseDark,
color: StyleConstants.BaseLight
},
rootHovered: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootFocused: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootPressed: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootExpanded: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
textContainer: {
flexGrow: "initial"
}
};
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
const cachedDatabaseAccountName = localStorage.getItem("cachedDatabaseAccountName");
interface Props {
armToken: string;
setDatabaseAccount: (account: DatabaseAccount) => void;
}
export const AccountSwitcher: React.FunctionComponent<Props> = ({ armToken, setDatabaseAccount }: Props) => {
const subscriptions = useSubscriptions(armToken);
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>(cachedDatabaseAccountName);
React.useEffect(() => {
if (accounts && selectedAccountName) {
const account = accounts.find(account => account.name === selectedAccountName);
// Only set a new account if one is found
if (account) {
setDatabaseAccount(account);
}
}
}, [accounts, selectedAccountName]);
const menuProps: IContextualMenuProps = {
directionalHintFixed: true,
className: "accountSwitchContextualMenu",
items: [
{
key: "switchSubscription",
onRender: () => {
const dropdownProps: IDropdownProps = {
label: "Subscription",
className: "accountSwitchSubscriptionDropdown",
options: subscriptions.map(sub => {
return {
key: sub.subscriptionId,
text: sub.displayName,
data: sub
};
}),
onChange: (event, option) => {
const subscriptionId = String(option.key);
setSelectedSubscriptionId(subscriptionId);
localStorage.setItem("cachedSubscriptionId", subscriptionId);
},
defaultSelectedKey: selectedSubscriptionId,
placeholder: "Select subscription from list",
styles: {
callout: "accountSwitchSubscriptionDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
},
{
key: "switchAccount",
onRender: (item, dismissMenu) => {
const dropdownProps: IDropdownProps = {
label: "Cosmos DB Account Name",
className: "accountSwitchAccountDropdown",
options: accounts.map(account => ({
key: account.name,
text: account.name,
data: account
})),
onChange: (event, option) => {
const accountName = String(option.key);
setSelectedAccoutName(String(option.key));
localStorage.setItem("cachedDatabaseAccountName", accountName);
dismissMenu();
},
defaultSelectedKey: selectedAccountName,
placeholder: "No Cosmos DB accounts found",
styles: {
callout: "accountSwitchAccountDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
}
]
};
return (
<DefaultButton
text={selectedAccountName || "Select Database Account"}
menuProps={menuProps}
styles={buttonStyles}
className="accountSwitchButton"
id="accountSwitchButton"
/>
);
};

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import { useBoolean } from "@uifabric/react-hooks";
import { HttpHeaders } from "../../../Common/Constants";
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
import { configContext } from "../../../ConfigContext";
import { AuthType } from "../../../AuthType";
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
interface Props {
connectionString: string;
login: () => void;
setEncryptedToken: (token: string) => void;
setConnectionString: (connectionString: string) => void;
setAuthType: (authType: AuthType) => void;
}
export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken,
login,
setAuthType,
connectionString,
setConnectionString
}: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
return (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
<div className="connectExplorerFormContainer">
<div className="connectExplorer">
<p className="connectExplorerContent">
<img src="images/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" />
</p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isFormVisible ? (
<form
id="connectWithConnectionString"
onSubmit={async event => {
event.preventDefault();
if (isResourceTokenConnectionString(connectionString)) {
setAuthType(AuthType.ResourceToken);
return;
}
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
// This API has a quirk where it must be parsed twice
const result: GenerateTokenResponse = JSON.parse(await response.json());
console.log(result.readWrite || result.read);
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
setAuthType(AuthType.ConnectionString);
}}
>
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
<p className="connectExplorerContent">
<input
className="inputToken"
type="text"
required
placeholder="Please enter a connection string"
value={connectionString}
onChange={event => {
setConnectionString(event.target.value);
}}
/>
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
<img className="errorImg" src="images/error.svg" alt="Error notification" />
<span className="errorDetails"></span>
</span>
</p>
<p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" />
</p>
<p className="switchConnectTypeText" onClick={login}>
Sign In with Azure Account
</p>
</form>
) : (
<div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
<p className="switchConnectTypeText" onClick={showForm}>
Connect to your account with connection string
</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,39 @@
import { Panel, PanelType, ChoiceGroup } from "office-ui-fabric-react";
import * as React from "react";
import { useDirectories } from "../../../hooks/useDirectories";
interface Props {
isOpen: boolean;
dismissPanel: () => void;
tenantId: string;
armToken: string;
switchTenant: (tenantId: string) => void;
}
export const DirectoryPickerPanel: React.FunctionComponent<Props> = ({
isOpen,
dismissPanel,
armToken,
tenantId,
switchTenant
}: Props) => {
const directories = useDirectories(armToken);
return (
<Panel
type={PanelType.medium}
headerText="Select Directory"
isOpen={isOpen}
onDismiss={dismissPanel}
closeButtonAriaLabel="Close"
>
<ChoiceGroup
options={directories.map(dir => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))}
selectedKey={tenantId}
onChange={async (event, option) => {
switchTenant(option.key);
dismissPanel();
}}
/>
</Panel>
);
};

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
import FeedbackIcon from "../../../../images/Feedback.svg";
export const FeedbackCommandButton: React.FunctionComponent = () => {
return (
<div className="feedbackConnectSettingIcons">
<CommandButtonComponent
id="commandbutton-feedback"
iconSrc={FeedbackIcon}
iconAlt="feeback button"
onCommandClick={() =>
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
}
ariaLabel="feeback button"
tooltipText="Send feedback"
hasPopup={true}
disabled={false}
/>
</div>
);
};

View File

@@ -0,0 +1,68 @@
import {
FocusZone,
DefaultButton,
DirectionalHint,
Persona,
PersonaInitialsColor,
PersonaSize
} from "office-ui-fabric-react";
import * as React from "react";
import { Account } from "msal";
import { useGraphPhoto } from "../../../hooks/useGraphPhoto";
interface Props {
graphToken: string;
account: Account;
openPanel: () => void;
logout: () => void;
}
export const MeControl: React.FunctionComponent<Props> = ({ openPanel, logout, account, graphToken }: Props) => {
const photo = useGraphPhoto(graphToken);
return (
<FocusZone>
<DefaultButton
id="mecontrolHeader"
className="mecontrolHeaderButton"
menuProps={{
className: "mecontrolContextualMenu",
isBeakVisible: false,
directionalHintFixed: true,
directionalHint: DirectionalHint.bottomRightEdge,
calloutProps: {
minPagePadding: 0
},
items: [
{
key: "SwitchDirectory",
text: "Switch Directory",
onClick: openPanel
},
{
key: "SignOut",
text: "Sign Out",
onClick: logout
}
]
}}
styles={{
rootHovered: { backgroundColor: "#393939" },
rootFocused: { backgroundColor: "#393939" },
rootPressed: { backgroundColor: "#393939" },
rootExpanded: { backgroundColor: "#393939" }
}}
>
<Persona
imageUrl={photo}
text={account?.name}
secondaryText={account?.userName}
showSecondaryText={true}
showInitialsUntilImageLoads={true}
initialsColor={PersonaInitialsColor.teal}
size={PersonaSize.size28}
className="mecontrolHeaderPersona"
/>
</DefaultButton>
</FocusZone>
);
};

View File

@@ -0,0 +1,21 @@
import { DefaultButton } from "office-ui-fabric-react";
import * as React from "react";
interface Props {
login: () => void;
}
export const SignInButton: React.FunctionComponent<Props> = ({ login }: Props) => {
return (
<DefaultButton
className="mecontrolSigninButton"
text="Sign In"
onClick={login}
styles={{
rootHovered: { backgroundColor: "#393939", color: "#fff" },
rootFocused: { backgroundColor: "#393939", color: "#fff" },
rootPressed: { backgroundColor: "#393939", color: "#fff" }
}}
/>
);
};

View File

@@ -0,0 +1,101 @@
.connectExplorerContainer {
height: 100%;
width: 100%;
}
.connectExplorerContainer .connectExplorerFormContainer {
display: -webkit-flex;
display: -ms-flexbox;
display: -ms-flex;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
height: 100%;
width: 100%;
}
.connectExplorerContainer .connectExplorer {
text-align: center;
display: -webkit-flex;
display: -ms-flexbox;
display: -ms-flex;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
justify-content: center;
height: 100%;
margin-bottom: 60px;
}
.connectExplorerContainer .connectExplorer .welcomeText {
font-size: 14px;
color: #393939;
margin: 8px 8px 16px 8px;
}
.connectExplorerContainer .connectExplorer .switchConnectTypeText {
margin: 8px;
font-size: 12px;
color: #0058ad;
cursor: pointer;
}
.connectExplorerContainer .connectExplorer .connectStringText {
font-size: 12px;
color: #393939;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent {
margin: 8px;
color: #393939;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken {
width: 300px;
padding: 0px 4px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken::placeholder {
font-style: italic;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip {
position: relative;
display: inline-block;
padding-left: 4px;
vertical-align: top;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip:hover .errorDetails {
visibility: visible;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails {
bottom: 24px;
width: 145px;
visibility: hidden;
background-color: #393939;
color: #ffffff;
position: absolute;
z-index: 1;
left: -10px;
padding: 6px;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails:after {
border-width: 10px 10px 0px 10px;
bottom: -8px;
content: "";
position: absolute;
right: 100%;
border-style: solid;
left: 12px;
width: 0;
height: 0;
border-color: #3b3b3b transparent;
}
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorImg {
height: 14px;
width: 14px;
}
.filterbtnstyle {
background: #0058ad;
width: 90px;
height: 25px;
color: white;
border: solid 1px;
}

View File

@@ -1,12 +1,12 @@
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { ConnectionStringParser } from "./ConnectionStringParser"; import { parseConnectionString } from "./ConnectionStringParser";
describe("ConnectionStringParser", () => { describe("ConnectionStringParser", () => {
const mockAccountName: string = "Test"; const mockAccountName: string = "Test";
const mockMasterKey: string = "some-key"; const mockMasterKey: string = "some-key";
it("should parse a valid sql account connection string", () => { it("should parse a valid sql account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};` `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};`
); );
@@ -15,7 +15,7 @@ describe("ConnectionStringParser", () => {
}); });
it("should parse a valid mongo account connection string", () => { it("should parse a valid mongo account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255` `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255`
); );
@@ -24,7 +24,7 @@ describe("ConnectionStringParser", () => {
}); });
it("should parse a valid compute mongo account connection string", () => { it("should parse a valid compute mongo account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255` `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255`
); );
@@ -33,7 +33,7 @@ describe("ConnectionStringParser", () => {
}); });
it("should parse a valid graph account connection string", () => { it("should parse a valid graph account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;` `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`
); );
@@ -42,7 +42,7 @@ describe("ConnectionStringParser", () => {
}); });
it("should parse a valid table account connection string", () => { it("should parse a valid table account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;` `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`
); );
@@ -51,7 +51,7 @@ describe("ConnectionStringParser", () => {
}); });
it("should parse a valid cassandra account connection string", () => { it("should parse a valid cassandra account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString(
`AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};` `AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};`
); );
@@ -60,15 +60,13 @@ describe("ConnectionStringParser", () => {
}); });
it("should fail to parse an invalid connection string", () => { it("should fail to parse an invalid connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const metadata = parseConnectionString("some-rogue-connection-string");
"some-rogue-connection-string"
);
expect(metadata).toBe(undefined); expect(metadata).toBe(undefined);
}); });
it("should fail to parse an empty connection string", () => { it("should fail to parse an empty connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(""); const metadata = parseConnectionString("");
expect(metadata).toBe(undefined); expect(metadata).toBe(undefined);
}); });

View File

@@ -1,37 +1,36 @@
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels"; import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels";
export class ConnectionStringParser { export function parseConnectionString(connectionString: string): AccessInputMetadata {
public static parseConnectionString(connectionString: string): DataModels.AccessInputMetadata { if (connectionString) {
if (!!connectionString) {
try { try {
const accessInput: DataModels.AccessInputMetadata = {} as DataModels.AccessInputMetadata; const accessInput = {} as AccessInputMetadata;
const connectionStringParts = connectionString.split(";"); const connectionStringParts = connectionString.split(";");
connectionStringParts.forEach((connectionStringPart: string) => { connectionStringParts.forEach((connectionStringPart: string) => {
if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1]; accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
accessInput.apiKind = DataModels.ApiKind.SQL; accessInput.apiKind = ApiKind.SQL;
} else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) { } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo); const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
accessInput.accountName = matches && matches.length > 1 && matches[2]; accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = DataModels.ApiKind.MongoDB; accessInput.apiKind = ApiKind.MongoDB;
} else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) { } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute); const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
accessInput.accountName = matches && matches.length > 1 && matches[2]; accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute; accessInput.apiKind = ApiKind.MongoDBCompute;
} else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) { } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
Constants.EndpointsRegex.cassandra.forEach(regex => { Constants.EndpointsRegex.cassandra.forEach(regex => {
if (RegExp(regex).test(connectionStringPart)) { if (RegExp(regex).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(regex)[1]; accessInput.accountName = connectionStringPart.match(regex)[1];
accessInput.apiKind = DataModels.ApiKind.Cassandra; accessInput.apiKind = ApiKind.Cassandra;
} }
}); });
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) { } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1]; accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
accessInput.apiKind = DataModels.ApiKind.Table; accessInput.apiKind = ApiKind.Table;
} else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) { } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
accessInput.apiKind = DataModels.ApiKind.Graph; accessInput.apiKind = ApiKind.Graph;
} }
}); });
@@ -46,5 +45,4 @@ export class ConnectionStringParser {
} }
return undefined; return undefined;
}
} }

View File

@@ -1,24 +1,10 @@
import Main from "./Main"; import { isResourceTokenConnectionString, parseResourceTokenConnectionString } from "./ResourceTokenUtils";
describe("Main", () => {
it("correctly detects feature flags", () => {
// Search containing non-features, with Camelcase keys and uri encoded values
const params = new URLSearchParams(
"?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey"
);
const features = Main.extractFeatures(params);
expect(features).toEqual({
notebookserverurl: "https://localhost:10001/12345/notebook",
notebookservertoken: "token",
enablenotebooks: "true"
});
});
describe("parseResourceTokenConnectionString", () => {
it("correctly parses resource token connection string", () => { it("correctly parses resource token connection string", () => {
const connectionString = const connectionString =
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
const properties = Main.parseResourceTokenConnectionString(connectionString); const properties = parseResourceTokenConnectionString(connectionString);
expect(properties).toEqual({ expect(properties).toEqual({
accountEndpoint: "fakeEndpoint", accountEndpoint: "fakeEndpoint",
@@ -32,7 +18,7 @@ describe("Main", () => {
it("correctly parses resource token connection string with partition key", () => { it("correctly parses resource token connection string with partition key", () => {
const connectionString = const connectionString =
"type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;"; "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;";
const properties = Main.parseResourceTokenConnectionString(connectionString); const properties = parseResourceTokenConnectionString(connectionString);
expect(properties).toEqual({ expect(properties).toEqual({
accountEndpoint: "fakeEndpoint", accountEndpoint: "fakeEndpoint",
@@ -43,3 +29,16 @@ describe("Main", () => {
}); });
}); });
}); });
describe("isResourceToken", () => {
it("valid resource connection string", () => {
const connectionString =
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
expect(isResourceTokenConnectionString(connectionString)).toBe(true);
});
it("non-resource connection string", () => {
const connectionString = "AccountEndpoint=https://stfaul-sql.documents.azure.com:443/;AccountKey=foo;";
expect(isResourceTokenConnectionString(connectionString)).toBe(false);
});
});

View File

@@ -0,0 +1,43 @@
export interface ParsedResourceTokenConnectionString {
accountEndpoint: string;
collectionId: string;
databaseId: string;
partitionKey?: string;
resourceToken: string;
}
export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString {
let accountEndpoint: string;
let collectionId: string;
let databaseId: string;
let partitionKey: string;
let resourceToken: string;
const connectionStringParts = connectionString.split(";");
connectionStringParts.forEach((part: string) => {
if (part.startsWith("type=resource")) {
resourceToken = part + ";";
} else if (part.startsWith("AccountEndpoint=")) {
accountEndpoint = part.substring(16);
} else if (part.startsWith("DatabaseId=")) {
databaseId = part.substring(11);
} else if (part.startsWith("CollectionId=")) {
collectionId = part.substring(13);
} else if (part.startsWith("PartitionKey=")) {
partitionKey = part.substring(13);
} else if (part !== "") {
resourceToken += part + ";";
}
});
return {
accountEndpoint,
collectionId,
databaseId,
partitionKey,
resourceToken
};
}
export function isResourceTokenConnectionString(connectionString: string): boolean {
return connectionString && connectionString.includes("type=resource");
}

View File

@@ -1,9 +1,9 @@
import { AccessInputMetadata } from "../../Contracts/DataModels"; import { AccessInputMetadata } from "../../Contracts/DataModels";
import { HostedUtils } from "./HostedUtils"; import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils";
describe("getDatabaseAccountPropertiesFromMetadata", () => { describe("getDatabaseAccountPropertiesFromMetadata", () => {
it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => { it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => {
let mongoComputeAccount: AccessInputMetadata = { const mongoComputeAccount: AccessInputMetadata = {
accountName: "compute-batch2", accountName: "compute-batch2",
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255", apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
apiKind: 5, apiKind: 5,
@@ -11,21 +11,21 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => {
expiryTimestamp: "1234", expiryTimestamp: "1234",
mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/" mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/"
}; };
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({ expect(getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
mongoEndpoint: mongoComputeAccount.mongoEndpoint, mongoEndpoint: mongoComputeAccount.mongoEndpoint,
documentEndpoint: mongoComputeAccount.documentEndpoint documentEndpoint: mongoComputeAccount.documentEndpoint
}); });
}); });
it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => { it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => {
let mongoAccount: AccessInputMetadata = { const mongoAccount: AccessInputMetadata = {
accountName: "compute-batch2", accountName: "compute-batch2",
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255", apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
apiKind: 1, apiKind: 1,
documentEndpoint: "https://compute-batch2.documents.azure.com:443/", documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
expiryTimestamp: "1234" expiryTimestamp: "1234"
}; };
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({ expect(getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
documentEndpoint: mongoAccount.documentEndpoint documentEndpoint: mongoAccount.documentEndpoint
}); });
}); });

View File

@@ -3,8 +3,7 @@ import * as DataModels from "../../Contracts/DataModels";
import { AccessInputMetadata } from "../../Contracts/DataModels"; import { AccessInputMetadata } from "../../Contracts/DataModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
export class HostedUtils { export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): unknown {
static getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): any {
let properties = { documentEndpoint: metadata.documentEndpoint }; let properties = { documentEndpoint: metadata.documentEndpoint };
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind); const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
@@ -31,5 +30,22 @@ export class HostedUtils {
} }
} }
return properties; return properties;
} }
export function getDatabaseAccountKindFromExperience(apiExperience: string): string {
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
return Constants.AccountKind.MongoDB;
}
if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) {
return Constants.AccountKind.MongoDB;
}
return Constants.AccountKind.GlobalDocumentDB;
}
export function extractMasterKeyfromConnectionString(connectionString: string): string {
// Only Gremlin uses the actual master key for connection to cosmos
const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
} }

View File

@@ -1,598 +0,0 @@
import * as Constants from "../../Common/Constants";
import AuthHeadersUtil from "./Authorization";
import Q from "q";
import {
AccessInputMetadata,
AccountKeys,
ApiKind,
DatabaseAccount,
GenerateTokenResponse,
resourceTokenConnectionStringProperties
} from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { CollectionCreation } from "../../Shared/Constants";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { HostedUtils } from "./HostedUtils";
import { sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { SubscriptionUtilMappings } from "../../Shared/Constants";
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
import Explorer from "../../Explorer/Explorer";
import { updateUserContext } from "../../UserContext";
import { configContext } from "../../ConfigContext";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export default class Main {
private static _databaseAccountId: string;
private static _encryptedToken: string;
private static _accessInputMetadata: AccessInputMetadata;
private static _features: { [key: string]: string };
// For AAD, Need to post message to hosted frame to do the auth
// Use local deferred variable as work around until we find better solution
private static _getAadAccessDeferred: Q.Deferred<Explorer>;
private static _explorer: Explorer;
public static isUsingEncryptionToken(): boolean {
const params = new URLSearchParams(window.parent.location.search);
if ((!!params && params.has("key")) || Main._hasCachedEncryptedKey()) {
return true;
}
return false;
}
public static initializeExplorer(): Q.Promise<Explorer> {
window.addEventListener("message", this._handleMessage.bind(this), false);
this._features = {};
const params = new URLSearchParams(window.parent.location.search);
const deferred: Q.Deferred<Explorer> = Q.defer<Explorer>();
let authType: string = null;
// Encrypted token flow
if (!!params && params.has("key")) {
Main._encryptedToken = encodeURIComponent(params.get("key"));
SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
authType = AuthType.EncryptedToken;
} else if (Main._hasCachedEncryptedKey()) {
Main._encryptedToken = SessionStorageUtility.getEntryString(StorageKey.EncryptedKeyToken);
authType = AuthType.EncryptedToken;
}
// Aad flow
if (AuthHeadersUtil.isUserSignedIn()) {
authType = AuthType.AAD;
}
if (params) {
this._features = Main.extractFeatures(params);
}
(<any>window).authType = authType;
if (!authType) {
return Q.reject("Sign in needed");
}
const explorer: Explorer = this._instantiateExplorer();
if (authType === AuthType.EncryptedToken) {
sendMessage({
type: MessageTypes.UpdateAccountSwitch,
props: {
authType: AuthType.EncryptedToken,
displayText: "Loading..."
}
});
updateUserContext({
accessToken: Main._encryptedToken
});
Main._getAccessInputMetadata(Main._encryptedToken).then(
() => {
const expiryTimestamp: number =
Main._accessInputMetadata && parseInt(Main._accessInputMetadata.expiryTimestamp);
if (authType === AuthType.EncryptedToken && (isNaN(expiryTimestamp) || expiryTimestamp <= 0)) {
return deferred.reject("Token expired");
}
Main._initDataExplorerFrameInputs(explorer);
deferred.resolve(explorer);
},
(error: any) => {
console.error(error);
deferred.reject(error);
}
);
} else if (authType === AuthType.AAD) {
sendMessage({
type: MessageTypes.GetAccessAadRequest
});
if (this._getAadAccessDeferred != null) {
// already request aad access, don't duplicate
return Q(null);
}
this._explorer = explorer;
this._getAadAccessDeferred = Q.defer<Explorer>();
return this._getAadAccessDeferred.promise.finally(() => {
this._getAadAccessDeferred = null;
});
} else {
Main._initDataExplorerFrameInputs(explorer);
deferred.resolve(explorer);
}
return deferred.promise;
}
public static extractFeatures(params: URLSearchParams): { [key: string]: string } {
const featureParamRegex = /feature.(.*)/i;
const features: { [key: string]: string } = {};
params.forEach((value: string, param: string) => {
if (featureParamRegex.test(param)) {
const matches: string[] = param.match(featureParamRegex);
if (matches.length > 0) {
features[matches[1].toLowerCase()] = value;
}
}
});
return features;
}
public static configureTokenValidationDisplayPrompt(explorer: Explorer): void {
const authType: AuthType = (<any>window).authType;
if (
!explorer ||
!Main._encryptedToken ||
!Main._accessInputMetadata ||
Main._accessInputMetadata.expiryTimestamp == null ||
authType !== AuthType.EncryptedToken
) {
return;
}
Main._showGuestAccessTokenRenewalPromptInMs(explorer, parseInt(Main._accessInputMetadata.expiryTimestamp));
}
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
let accountEndpoint: string;
let collectionId: string;
let databaseId: string;
let partitionKey: string;
let resourceToken: string;
const connectionStringParts = connectionString.split(";");
connectionStringParts.forEach((part: string) => {
if (part.startsWith("type=resource")) {
resourceToken = part + ";";
} else if (part.startsWith("AccountEndpoint=")) {
accountEndpoint = part.substring(16);
} else if (part.startsWith("DatabaseId=")) {
databaseId = part.substring(11);
} else if (part.startsWith("CollectionId=")) {
collectionId = part.substring(13);
} else if (part.startsWith("PartitionKey=")) {
partitionKey = part.substring(13);
} else if (part !== "") {
resourceToken += part + ";";
}
});
return {
accountEndpoint,
collectionId,
databaseId,
partitionKey,
resourceToken
};
}
public static renewExplorerAccess = (explorer: Explorer, connectionString: string): Q.Promise<void> => {
if (!connectionString) {
console.error("Missing or invalid connection string input");
Q.reject("Missing or invalid connection string input");
}
if (Main._isResourceToken(connectionString)) {
return Main._renewExplorerAccessWithResourceToken(explorer, connectionString);
}
const deferred: Q.Deferred<void> = Q.defer<void>();
AuthHeadersUtil.generateUnauthenticatedEncryptedTokenForConnectionString(connectionString).then(
(encryptedToken: GenerateTokenResponse) => {
if (!encryptedToken || !encryptedToken.readWrite) {
deferred.reject("Encrypted token is empty or undefined");
}
Main._encryptedToken = encryptedToken.readWrite;
window.authType = AuthType.EncryptedToken;
updateUserContext({
accessToken: Main._encryptedToken
});
Main._getAccessInputMetadata(Main._encryptedToken).then(
() => {
if (explorer.isConnectExplorerVisible()) {
explorer.notificationConsoleData([]);
explorer.hideConnectExplorerForm();
}
if (Main._accessInputMetadata.apiKind != ApiKind.Graph) {
// do not save encrypted token for graphs because we cannot extract master key in the client
SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
window.parent &&
window.parent.history.replaceState(
{ encryptedToken: encryptedToken },
"",
`?key=${Main._encryptedToken}${(window.parent && window.parent.location.hash) || ""}`
); // replace query params if any
} else {
SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken);
window.parent &&
window.parent.history.replaceState(
{ encryptedToken: encryptedToken },
"",
`?${(window.parent && window.parent.location.hash) || ""}`
); // replace query params if any
}
const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString);
Main.configureTokenValidationDisplayPrompt(explorer);
Main._setExplorerReady(explorer, masterKey);
deferred.resolve();
},
(error: any) => {
console.error(error);
deferred.reject(error);
}
);
},
(error: any) => {
deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`);
}
);
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
};
public static getUninitializedExplorerForGuestAccess(): Explorer {
const explorer = Main._instantiateExplorer();
if (window.authType === AuthType.AAD) {
this._explorer = explorer;
}
(<any>window).dataExplorer = explorer;
return explorer;
}
private static _initDataExplorerFrameInputs(
explorer: Explorer,
masterKey?: string /* master key extracted from connection string if available */,
account?: DatabaseAccount,
authorizationToken?: string /* access key */
): void {
const serverId: string = AuthHeadersUtil.serverId;
const authType: string = (<any>window).authType;
const accountResourceId =
authType === AuthType.EncryptedToken
? Main._databaseAccountId
: authType === AuthType.AAD && account
? account.id
: "";
const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
const resourceGroup: string = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
explorer.isTryCosmosDBSubscription(SubscriptionUtilMappings.FreeTierSubscriptionIds.indexOf(subscriptionId) >= 0);
if (authorizationToken && authorizationToken.indexOf("Bearer") !== 0) {
// Portal sends the auth token with bearer suffix, so we prepend the same to be consistent
authorizationToken = `Bearer ${authorizationToken}`;
}
if (authType === AuthType.EncryptedToken) {
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
Main._accessInputMetadata.apiKind
);
sendMessage({
type: MessageTypes.UpdateAccountSwitch,
props: {
authType: AuthType.EncryptedToken,
selectedAccountName: Main._accessInputMetadata.accountName
}
});
return explorer.initDataExplorerWithFrameInputs({
databaseAccount: {
id: Main._databaseAccountId,
name: Main._accessInputMetadata.accountName,
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
tags: { defaultExperience: apiExperience }
},
subscriptionId,
resourceGroup,
masterKey,
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
authorizationToken: undefined,
features: this._features,
csmEndpoint: undefined,
dnsSuffix: null,
serverId: serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
});
}
if (authType === AuthType.AAD) {
const inputs: DataExplorerInputsFrame = {
databaseAccount: account,
subscriptionId,
resourceGroup,
masterKey,
hasWriteAccess: true, //TODO: 425017 - support read access
authorizationToken,
features: this._features,
csmEndpoint: undefined,
dnsSuffix: null,
serverId: serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
};
return explorer.initDataExplorerWithFrameInputs(inputs);
}
if (authType === AuthType.ResourceToken) {
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
Main._accessInputMetadata.apiKind
);
return explorer.initDataExplorerWithFrameInputs({
databaseAccount: {
id: Main._databaseAccountId,
name: Main._accessInputMetadata.accountName,
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
tags: { defaultExperience: apiExperience }
},
subscriptionId,
resourceGroup,
masterKey,
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
authorizationToken: undefined,
features: this._features,
csmEndpoint: undefined,
dnsSuffix: null,
serverId: serverId,
extensionEndpoint: configContext.BACKEND_ENDPOINT,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
quotaId: undefined,
addCollectionDefaultFlight: explorer.flight(),
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
isAuthWithresourceToken: true
});
}
throw new Error(`Unsupported AuthType ${authType}`);
}
private static _instantiateExplorer(): Explorer {
const explorer = new Explorer();
// workaround to resolve cyclic refs with view
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
// Hosted needs click to dismiss any menu
if (window.authType === AuthType.AAD) {
window.addEventListener(
"click",
() => {
sendMessage({
type: MessageTypes.ExplorerClickEvent
});
},
true
);
}
return explorer;
}
private static _showGuestAccessTokenRenewalPromptInMs(explorer: Explorer, interval: number): void {
if (interval != null && !isNaN(interval)) {
setTimeout(() => {
explorer.displayGuestAccessTokenRenewalPrompt();
}, interval);
}
}
private static _hasCachedEncryptedKey(): boolean {
return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken);
}
private static _getDatabaseAccountKindFromExperience(apiExperience: string): string {
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
return Constants.AccountKind.MongoDB;
}
if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) {
return Constants.AccountKind.MongoDB;
}
return Constants.AccountKind.GlobalDocumentDB;
}
private static _getAccessInputMetadata(accessInput: string): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
AuthHeadersUtil.getAccessInputMetadata(accessInput).then(
(metadata: any) => {
Main._accessInputMetadata = metadata;
deferred.resolve();
},
(error: any) => {
deferred.reject(error);
}
);
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
}
private static _getMasterKeyFromConnectionString(connectionString: string): string {
if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) {
// client only needs master key for Graph API
return undefined;
}
const matchedParts: string[] = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
return (matchedParts.length > 1 && matchedParts[1]) || undefined;
}
private static _isResourceToken(connectionString: string): boolean {
return connectionString && connectionString.includes("type=resource");
}
private static _renewExplorerAccessWithResourceToken = (
explorer: Explorer,
connectionString: string
): Q.Promise<void> => {
window.authType = AuthType.ResourceToken;
const properties: resourceTokenConnectionStringProperties = Main.parseResourceTokenConnectionString(
connectionString
);
if (
!properties.accountEndpoint ||
!properties.resourceToken ||
!properties.databaseId ||
!properties.collectionId
) {
console.error("Invalid connection string input");
Q.reject("Invalid connection string input");
}
updateUserContext({
resourceToken: properties.resourceToken,
endpoint: properties.accountEndpoint
});
explorer.resourceTokenDatabaseId(properties.databaseId);
explorer.resourceTokenCollectionId(properties.collectionId);
if (properties.partitionKey) {
explorer.resourceTokenPartitionKey(properties.partitionKey);
}
Main._accessInputMetadata = Main._getAccessInputMetadataFromAccountEndpoint(properties.accountEndpoint);
if (explorer.isConnectExplorerVisible()) {
explorer.notificationConsoleData([]);
explorer.hideConnectExplorerForm();
}
Main._setExplorerReady(explorer);
return Q.resolve();
};
private static _getAccessInputMetadataFromAccountEndpoint = (accountEndpoint: string): AccessInputMetadata => {
const documentEndpoint: string = accountEndpoint;
const result: RegExpMatchArray = accountEndpoint.match("https://([^\\.]+)\\..+");
const accountName: string = result && result[1];
const apiEndpoint: string = accountEndpoint.substring(8);
const apiKind: number = ApiKind.SQL;
return {
accountName,
apiEndpoint,
apiKind,
documentEndpoint,
expiryTimestamp: ""
};
};
private static _setExplorerReady(
explorer: Explorer,
masterKey?: string,
account?: DatabaseAccount,
authorizationToken?: string
) {
Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken);
explorer.isAccountReady.valueHasMutated();
sendMessage("ready");
}
private static _shouldProcessMessage(event: MessageEvent): boolean {
if (typeof event.data !== "object") {
return false;
}
if (event.data["signature"] !== "pcIframe") {
return false;
}
if (!("data" in event.data)) {
return false;
}
if (typeof event.data["data"] !== "object") {
return false;
}
return true;
}
private static _handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (!this._shouldProcessMessage(event)) {
return;
}
const message: any = event.data.data;
if (message.type) {
if (message.type === MessageTypes.GetAccessAadResponse && (message.response || message.error)) {
if (message.response) {
Main._handleGetAccessAadSucceed(message.response);
}
if (message.error) {
Main._handleGetAccessAadFailed(message.error);
}
return;
}
if (message.type === MessageTypes.SwitchAccount && message.account && message.keys) {
Main._handleSwitchAccountSucceed(message.account, message.keys, message.authorizationToken);
return;
}
}
}
private static _handleSwitchAccountSucceed(account: DatabaseAccount, keys: AccountKeys, authorizationToken: string) {
if (!this._explorer) {
console.error("no explorer found");
return;
}
this._explorer.hideConnectExplorerForm();
const masterKey = Main._getMasterKey(keys);
this._explorer.notificationConsoleData([]);
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
}
private static _handleGetAccessAadSucceed(response: [DatabaseAccount, AccountKeys, string]) {
if (!response || response.length < 1) {
return;
}
const account = response[0];
const masterKey = Main._getMasterKey(response[1]);
const authorizationToken = response[2];
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
this._getAadAccessDeferred.resolve(this._explorer);
}
private static _getMasterKey(keys: AccountKeys): string {
return (
keys?.primaryMasterKey ??
keys?.secondaryMasterKey ??
keys?.primaryReadonlyMasterKey ??
keys?.secondaryReadonlyMasterKey
);
}
private static _handleGetAccessAadFailed(error: any) {
this._getAadAccessDeferred.reject(error);
}
}

View File

@@ -0,0 +1,17 @@
import { extractFeatures } from "./extractFeatures";
describe("extractFeatures", () => {
it("correctly detects feature flags", () => {
// Search containing non-features, with Camelcase keys and uri encoded values
const params = new URLSearchParams(
"?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey"
);
const features = extractFeatures(params);
expect(features).toEqual({
notebookserverurl: "https://localhost:10001/12345/notebook",
notebookservertoken: "token",
enablenotebooks: "true"
});
});
});

View File

@@ -0,0 +1,14 @@
export function extractFeatures(params?: URLSearchParams): { [key: string]: string } {
params = params || new URLSearchParams(window.parent.location.search);
const featureParamRegex = /feature.(.*)/i;
const features: { [key: string]: string } = {};
params.forEach((value: string, param: string) => {
if (featureParamRegex.test(param)) {
const matches: string[] = param.match(featureParamRegex);
if (matches.length > 0) {
features[matches[1].toLowerCase()] = value;
}
}
});
return features;
}

View File

@@ -1,23 +0,0 @@
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
import Explorer from "../../Explorer/Explorer";
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
export function initializeExplorer(): Explorer {
const explorer = new Explorer();
// In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
if (initMessage) {
const message = JSON.parse(initMessage);
console.warn("Loaded cached portal iframe message from session storage");
console.dir(message);
explorer.initDataExplorerWithFrameInputs(message);
}
}
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
return explorer;
}

View File

@@ -1,11 +1,9 @@
import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
if (window.authType === AuthType.EncryptedToken) { if (window.authType === AuthType.EncryptedToken) {
@@ -21,19 +19,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet
} }
} }
export async function getArcadiaAuthToken(
arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT,
tenantId?: string
): Promise<string> {
try {
const token = await AuthHeadersUtil.getAccessToken(arcadiaEndpoint, tenantId);
return token;
} catch (error) {
Logger.logError(getErrorMessage(error), "AuthorizationUtils/getArcadiaAuthToken");
throw error;
}
}
export function decryptJWTToken(token: string) { export function decryptJWTToken(token: string) {
if (!token) { if (!token) {
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");

View File

@@ -1,24 +0,0 @@
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import * as UserUtils from "./UserUtils";
describe("UserUtils", () => {
it("getFullName works in regular data explorer (inside portal)", () => {
const user: AuthenticationContext.UserInfo = {
userName: "userName",
profile: {
name: "name"
}
};
AuthHeadersUtil.getCachedUser = jest.fn().mockReturnValue(user);
expect(UserUtils.getFullName()).toBe("name");
});
it("getFullName works in fullscreen data explorer (outside portal)", () => {
jest.mock("./AuthorizationUtils", () => {
(): { name: string } => ({ name: "name" });
});
expect(UserUtils.getFullName()).toBe("name");
});
});

View File

@@ -1,17 +1,8 @@
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import { decryptJWTToken } from "./AuthorizationUtils"; import { decryptJWTToken } from "./AuthorizationUtils";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export function getFullName(): string { export function getFullName(): string {
let fullName: string;
const user = AuthHeadersUtil.getCachedUser();
if (user) {
fullName = user.profile.name;
} else {
const authToken = userContext.authorizationToken; const authToken = userContext.authorizationToken;
const props = decryptJWTToken(authToken); const props = decryptJWTToken(authToken);
fullName = props.name; return props.name;
}
return fullName;
} }

View File

@@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer";
export const applyExplorerBindings = (explorer: Explorer) => { export const applyExplorerBindings = (explorer: Explorer) => {
if (!!explorer) { if (!!explorer) {
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
sendMessage("ready");
window.dataExplorer = explorer; window.dataExplorer = explorer;
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
ko.applyBindings(explorer); ko.applyBindings(explorer);
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
sendMessage("ready");
$("#divExplorer").show(); $("#divExplorer").show();
} }
}; };

96
src/hooks/useAADAuth.ts Normal file
View File

@@ -0,0 +1,96 @@
import * as React from "react";
import { useBoolean } from "@uifabric/react-hooks";
import { UserAgentApplication, Account } from "msal";
const msal = new UserAgentApplication({
cache: {
cacheLocation: "localStorage"
},
auth: {
authority: "https://login.microsoftonline.com/common",
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
}
});
const cachedAccount = msal.getAllAccounts()?.[0];
const cachedTenantId = localStorage.getItem("cachedTenantId");
interface ReturnType {
isLoggedIn: boolean;
graphToken: string;
armToken: string;
login: () => void;
logout: () => void;
tenantId: string;
account: Account;
switchTenant: (tenantId: string) => void;
}
export function useAADAuth(): ReturnType {
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
Boolean(cachedAccount && cachedTenantId) || false
);
const [account, setAccount] = React.useState<Account>(cachedAccount);
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
const [graphToken, setGraphToken] = React.useState<string>();
const [armToken, setArmToken] = React.useState<string>();
const login = React.useCallback(async () => {
const response = await msal.loginPopup();
setLoggedIn();
setAccount(response.account);
setTenantId(response.tenantId);
localStorage.setItem("cachedTenantId", response.tenantId);
}, []);
const logout = React.useCallback(() => {
setLoggedOut();
localStorage.removeItem("cachedTenantId");
msal.logout();
}, []);
const switchTenant = React.useCallback(
async id => {
const response = await msal.loginPopup({
authority: `https://login.microsoftonline.com/${id}`
});
setTenantId(response.tenantId);
setAccount(response.account);
},
[account, tenantId]
);
React.useEffect(() => {
if (account && tenantId) {
Promise.all([
msal.acquireTokenSilent({
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
forceRefresh: true,
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ["https://graph.windows.net//.default"]
}),
msal.acquireTokenSilent({
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
forceRefresh: true,
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ["https://management.azure.com//.default"]
})
]).then(([graphTokenResponse, armTokenResponse]) => {
setGraphToken(graphTokenResponse.accessToken);
setArmToken(armTokenResponse.accessToken);
});
}
}, [account, tenantId]);
return {
account,
tenantId,
isLoggedIn,
graphToken,
armToken,
login,
logout,
switchTenant
};
}

View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import { DatabaseAccount } from "../Contracts/DataModels";
interface AccountListResult {
nextLink: string;
value: DatabaseAccount[];
}
export async function fetchDatabaseAccounts(
subscriptionIds: string[],
accessToken: string
): Promise<DatabaseAccount[]> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
if (!subscriptionIds || !subscriptionIds.length) {
return Promise.reject("No subscription passed in");
}
let accounts: Array<DatabaseAccount> = [];
const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'";
const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`;
let nextLink = `https://management.azure.com/resources?api-version=2020-01-01&${urlFilter}`;
while (nextLink) {
const response: Response = await fetch(nextLink, { headers });
const result: AccountListResult =
response.status === 204 || response.status === 304 ? undefined : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
accounts = [...accounts, ...result.value];
}
return accounts;
}
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] {
const [state, setState] = useState<DatabaseAccount[]>();
useEffect(() => {
if (subscriptionId && armToken) {
fetchDatabaseAccounts([subscriptionId], armToken).then(response => setState(response));
}
}, [subscriptionId, armToken]);
return state || [];
}

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { Tenant } from "../Contracts/DataModels";
interface TenantListResult {
nextLink: string;
value: Tenant[];
}
export async function fetchDirectories(accessToken: string): Promise<Tenant[]> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
let tenents: Array<Tenant> = [];
let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`;
while (nextLink) {
const response = await fetch(nextLink, { headers });
const result: TenantListResult =
response.status === 204 || response.status === 304 ? undefined : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
tenents = [...tenents, ...result.value];
}
return tenents;
}
export function useDirectories(armToken: string): Tenant[] {
const [state, setState] = useState<Tenant[]>();
useEffect(() => {
if (armToken) {
fetchDirectories(armToken).then(response => setState(response));
}
}, [armToken]);
return state || [];
}

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
headers.append("Content-Type", "image/jpg");
const options = {
method: "GET",
headers: headers
};
return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options)
.then(response => response.blob())
.catch(error => console.log(error));
}
export function useGraphPhoto(graphToken: string): string {
const [photo, setPhoto] = useState<string>();
useEffect(() => {
if (graphToken) {
fetchPhoto(graphToken).then(response => setPhoto(URL.createObjectURL(response)));
}
}, [graphToken]);
return photo;
}

View File

@@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import { ApiEndpoints } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import { AccessInputMetadata } from "../Contracts/DataModels";
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
const headers = new Headers();
// Portal encrypted token API quirk: The token header must be URL encoded
headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken));
const options = {
method: "GET",
headers: headers
};
return (
fetch(url, options)
.then(response => response.json())
// Portal encrypted token API quirk: The response is double JSON encoded
.then(json => JSON.parse(json))
.catch(error => console.log(error))
);
}
export function usePortalAccessToken(token: string): AccessInputMetadata {
const [state, setState] = useState<AccessInputMetadata>();
useEffect(() => {
if (token) {
fetchAccessData(token).then(response => setState(response));
}
}, [token]);
return state;
}

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import { Subscription } from "../Contracts/DataModels";
interface SubscriptionListResult {
nextLink: string;
value: Subscription[];
}
export async function fetchSubscriptions(accessToken: string): Promise<Subscription[]> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
let subscriptions: Array<Subscription> = [];
let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`;
while (nextLink) {
const response = await fetch(nextLink, { headers });
const result: SubscriptionListResult =
response.status === 204 || response.status === 304 ? undefined : await response.json();
if (!response.ok) {
throw result;
}
nextLink = result.nextLink;
const validSubscriptions = result.value.filter(
sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue"
);
subscriptions = [...subscriptions, ...validSubscriptions];
}
return subscriptions;
}
export function useSubscriptions(armToken: string): Subscription[] {
const [state, setState] = useState<Subscription[]>();
useEffect(() => {
if (armToken) {
fetchSubscriptions(armToken).then(response => setState(response));
}
}, [armToken]);
return state || [];
}

View File

@@ -7,59 +7,6 @@
</head> </head>
<body> <body>
<header> <div id="App"></div>
<div class="items" role="menubar">
<div class="cosmosDBTitle">
<span
class="title"
data-bind="click: openAzurePortal, event: { keypress: onOpenAzurePortalKeyPress }"
tabindex="0"
title="Go to Azure Portal"
>Microsoft Azure</span
>
<span class="accontSplitter"></span> <span class="serviceTitle">Cosmos DB</span>
<img
class="chevronRight"
src="/chevron-right.svg"
alt="account separator"
data-bind="visible: isAccountActive"
/>
<span
class="accountSwitchComponentContainer"
data-bind="react: accountSwitchComponentAdapter, visible: isAccountActive"
></span>
</div>
<div class="feedbackConnectSettingIcons" data-bind="react: controlBarComponentAdapter"></div>
<div class="meControl" data-bind="react: meControlComponentAdapter"></div>
</div>
</header>
<!-- TODO display after introducing multiple account access -->
<nav class="fixedleftpane" style="display: none;">
<div class="fixedLeftPaneIcons"><img src="/Hamburger.svg" alt="Expand" /></div>
<div class="fixedLeftPaneIcons"><img src="/Connect.svg" alt="Connect to an account" /></div>
<div
class="fixedLeftPaneIcons"
data-bind="click: explorer_click, css:{ topSelected: navigationSelection() === 'explorer' }"
>
<img src="/HostedExplorer.svg" alt="Open Data Explorer" />
</div>
</nav>
<switch-directory-pane params="{data: switchDirectoryPane}"></switch-directory-pane>
<!-- TODO generate version number dynamically -->
<iframe
id="explorerMenu"
name="explorer"
class="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Hosted"
data-bind="visible: navigationSelection() === 'explorer'"
>
</iframe>
<div data-bind="react: firewallWarningComponentAdapter"></div>
<div data-bind="react: dialogComponentAdapter"></div>
</body> </body>
</html> </html>

View File

@@ -9,13 +9,13 @@ export async function login(connectionString: string): Promise<Frame> {
return page.mainFrame(); return page.mainFrame();
} }
// log in with connection string // log in with connection string
await page.waitFor("div > p.switchConnectTypeText", { visible: true });
await page.click("div > p.switchConnectTypeText");
const connStr = connectionString;
await page.type("input[class='inputToken']", connStr);
await page.click("input[value='Connect']");
const handle = await page.waitForSelector("iframe"); const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame(); const frame = await handle.contentFrame();
await frame.waitFor("div > p.switchConnectTypeText", { visible: true });
await frame.click("div > p.switchConnectTypeText");
const connStr = connectionString;
await frame.type("input[class='inputToken']", connStr);
await frame.click("input[value='Connect']");
return frame; return frame;
} }

View File

@@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"downlevelIteration": true, "downlevelIteration": true,
"module": "esnext", "module": "esnext",
"target": "es5", "target": "es2017",
"lib": ["es5", "es6", "dom", "webworker.importscripts"], "lib": ["es5", "es6", "dom", "webworker.importscripts"],
"jsx": "react", "jsx": "react",
"moduleResolution": "node", "moduleResolution": "node",

View File

@@ -78,7 +78,7 @@ const ModulesRule = {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
cacheDirectory: ".cache/babel", cacheDirectory: ".cache/babel",
presets: [["@babel/preset-env", { targets: { ie: "11" }, useBuiltIns: false }]] presets: [["@babel/preset-env", { targets: { chrome: "80" }, useBuiltIns: false }]]
} }
} }
], ],
@@ -132,8 +132,8 @@ module.exports = function(env = {}, argv = {}) {
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: "index.html", filename: "index.html",
template: "src/index.html", template: "src/hostedExplorer.html",
chunks: ["index"] chunks: ["hostedExplorer"]
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: "hostedExplorer.html", filename: "hostedExplorer.html",
@@ -182,7 +182,7 @@ module.exports = function(env = {}, argv = {}) {
main: "./src/Main.tsx", main: "./src/Main.tsx",
index: "./src/Index.ts", index: "./src/Index.ts",
quickstart: "./src/quickstart.ts", quickstart: "./src/quickstart.ts",
hostedExplorer: "./src/HostedExplorer.ts", hostedExplorer: "./src/HostedExplorer.tsx",
testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts", testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts",
heatmap: "./src/Controls/Heatmap/Heatmap.ts", heatmap: "./src/Controls/Heatmap/Heatmap.ts",
terminal: "./src/Terminal/index.ts", terminal: "./src/Terminal/index.ts",