Compare commits

...

17 Commits

Author SHA1 Message Date
MokireddySampath
c32b859f83 Update QuickstartCarousel.tsx 2023-09-27 19:55:47 +05:30
MokireddySampath
5358333384 Update QuickstartCarousel.tsx 2023-09-27 19:50:18 +05:30
Sampath
e45abde0bf File name changed, since the video is not playing in the portal's data explorer blade 2023-09-26 09:45:28 +05:30
Sampath
551bc0f973 "Dummy video has been added and instead of using the reactyoutube library using hte native
video player "
2023-09-25 12:16:06 +05:30
Armando Trejo Oliver
bb82915cc6 Add wildcard to allow any fabric test origin (#1617) 2023-09-24 17:53:55 -07:00
bogercraig
c59d31f4c0 File Upload Tooltip (#1621)
* Added tooltip to the filename and status columns.  There is an InfoTooltip function that can reduce the boilerplate.

* Changed tooltip to be constructed with the DetailsList column and changed orientation to rightCenter.
That orientation prevents issues with topCenter orientation getting off center from the width of the cell.
Also looks a bit nicer, but will get feedback.

* Updated formatting.

* Removing uneccessary column formatting.

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-09-20 19:34:22 -04:00
sunghyunkang1111
8fa2721eab Update graph endpoint (#1623)
* Update graph endpoint

* Update graph endpoint
2023-09-20 10:39:37 -05:00
vchske
754354dbf9 Adds unit tests for vcore quickstart feature code (#1618)
* Safety checkin

* Adding vcoremongo to Main

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Safety checkin

* Adding vcoremongo to Main

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Safety commit

* Integrating mongo shell

* Safety checkin

* Safety commit

* Enable mongo shell in its own tab

* Adding message

* Integrated mongo shell

* Moving Juno endpoint back to prod

* Fixed command bar unit tests

* Fixing spelling

* Adds unit tests for vcore quickstart feature code

* Fix lint
2023-09-19 17:12:41 -07:00
vchske
ae5811306b Minor QOL changes (#1619) 2023-09-19 17:06:40 -07:00
Armando Trejo Oliver
f84deea9bc Fix query copilot help links (#1620) 2023-09-19 14:40:57 -07:00
Predrag Klepic
260c99e15c [Query Copilot V2] Explanation bubble added buttons (#1609)
* Fixing naming convention

* Additional implementation for Explanation

* Added snaps

* Removing snapshots

* re-updated snapshots

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-09-19 10:17:51 +02:00
v-darkora
c1c12019da Add feature flag for fixed output editor height or calculated height (#1606) 2023-09-18 11:30:17 +02:00
Armando Trejo Oliver
4e5358185f Add Fabric edog origin to allowed ancestors (#1616) 2023-09-16 19:45:07 -07:00
Vsevolod Kukol
b4bc93ac03 Fix active tabs border on Fabric (#1614) 2023-09-15 22:15:33 +02:00
Vsevolod Kukol
c61788198f Allow iframe within Fabric Extension iframe (#1613) 2023-09-15 21:44:44 +02:00
Vsevolod Kukol
379395567c Initial Fabric support (#1607)
* Add Platform.Fabric to run in context of Fabric

* Use separate StyleConstants

We want to have more flexibility with Styles at runtime
but Constants depend on ConfigContext and therefore
get loaded very early at startup.

* Add Fabric specific styles and Fluent theme

documentDBFabric.less contains all styles for Fabric.
We use React.lazy to import them conditionally at
runtime preventing webpack from preprocessing
them into main.css.

* Restyle CommandBar for Fabric
with more roundness and native colors.

* Disable Notebooks when running in Fabric

* Disable Synapse and Scripts commands for Fabric

* Fix code formatting issues

* Fetch encrypted token from sessionStorage for fabric platform

* Fix Tabs style

* Dark refresh icons for Fabric

* Use new ResourceTree2 for Fabric

* Fluent tree should have a fixed width
otherwise the action buttons jump around on hover.

* Disable remaining Script actions in Fabric

* Revert accidentally committed change
which broke a test

* Fix cross-origin error second try

* Adjust @FabrixBoxMargin css

* Hide Database Scale node on Fabric

* Remove all Collection child nodes on Fabric

* Add a comment about why we need FabricPlatform.tsx

* Fix equality checks

* Fix more Colors for Fabric

* Switch resource tree to "medium" size

* Fix formatting again

* Fix formatting again

* Disable no-var-requires error on some intended var import.

* Increase memory limit for build

* Use standard Segoe UI font for Fabric

* Improve Tabs design for Fabric

* Fix active Tab style bug in Portal
introduced with 39a7765aef

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2023-09-15 17:33:27 +02:00
Laurent Nguyen
c2d2ff3dee Update CSP header in web.config to allow iframe within fabric (#1611) 2023-09-15 06:58:15 +02:00
64 changed files with 1265 additions and 220 deletions

View File

@@ -85,6 +85,8 @@ jobs:
path: .cache
key: ${{ runner.os }}-build-cache
- run: npm run pack:prod
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v2

View File

@@ -23,5 +23,6 @@
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.preferences.importModuleSpecifier": "non-relative"
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@@ -10,6 +10,7 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@NoColor: "brightness(0) saturate(100%)";
@xSmallFontSize: 4px;
@smallFontSize: 8px;
@@ -147,14 +148,41 @@
// CommandBar
@CommandBarButtonHeight: 40px;
/**********************************************************************************
Portal Consts
/**********************************************************************************/
@PortalAccentMediumHigh: #0058ad;
@PortalAccentMedium: #004e87;
@PortalAccentLight: #eef7ff;
@PortalAccentAccentExtra: #ddf0ff;
/**********************************************************************************
Fabric Consts
/**********************************************************************************/
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
@FabricBoxMargin: 4px 3px 4px 3px;
@FabricAccentMediumHigh: #0c695a;
@FabricAccentMedium: #117865;
@FabricAccentLight: #f5f5f5;
@FabricAccentExtra: #ebebeb;
@FabricButtonBorderRadius: 4px;
/**********************************************************************************
Common Flex Property
/**********************************************************************************/
.flex-display(@display: flex) {
display: ~"-webkit-@{display}";
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display: ~"-ms-@{display}"; // IE11
display:~"-webkit-@{display}";
display:~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display:~"-ms-@{display}"; // IE11
display: @display;
}
@@ -168,13 +196,15 @@
High contrast mode active
**************************************************************************************/
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
@media all and (-ms-high-contrast: none),
(-ms-high-contrast: active) {
.selectedRadio,
.selectedRadio:hover,
.selectedRadio:active,
.selectedRadio.dirty,
.tab [type="radio"]:checked ~ label,
.tab [type="radio"]:checked ~ label:hover {
.tab [type="radio"]:checked~label,
.tab [type="radio"]:checked~label:hover {
-ms-high-contrast-adjust: none;
-webkit-text-fill-color: HighlightText;
color: HighlightText;
@@ -183,6 +213,7 @@
}
.queryMetricsSummaryTuple {
th,
td {
&:nth-child(2) {
@@ -302,4 +333,4 @@
width: 0;
height: 0;
border-color: @InfoPointerColor transparent;
}
}

View File

@@ -2646,6 +2646,11 @@ a:link {
width: @ActiveTabWidth;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0,120,212,1);
}
.nav-tabs > li.active:focus > .tabNavContentContainer {
.focus();
}

211
less/documentDBFabric.less Normal file
View File

@@ -0,0 +1,211 @@
@import "./Common/Constants";
html {
font-family: @FabricFont;
}
body {
font-family: @FabricFont;
background-color: #f5f5f5;
}
a {
color: @FabricAccentMedium;
text-decoration: none;
}
a:hover,
a:focus {
color: @FabricAccentMediumHigh;
text-decoration: underline;
}
#divExplorer {
background-color: #f5f5f5;
}
.resourceTreeAndTabs {
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 4px;
background-color: #ffffff;
}
.tabsManagerContainer {
background-color: #fafafa
}
.nav-tabs-margin {
padding-top: 8px;
background-color: #fafafa
}
.commandBarContainer {
background-color: #ffffff;
border-bottom: none;
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
padding-top: 2px;
}
.dividerContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
.flex-display();
span {
border-left: @ButtonBorderWidth solid @BaseMedium;
margin: 0 10px 0 10px;
}
}
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
border-bottom: 2px solid #e0e0e0;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
border-bottom: 2px solid @FabricAccentMedium;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText {
border-bottom: 0px none transparent;
}
.tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
&:hover {
background-color: transparent;
border-color: transparent;
}
.tab_Content {
border-right: 0px none transparent;
margin: 0px @SmallSpace 0px @SmallSpace;
width: calc(@TabsWidth - (@SmallSpace * 2));
padding-bottom: @SmallSpace;
.statusIconContainer {
margin-left: 0px;
}
.tabIconSection {
.cancelButton {
padding: 0px 0px 0px @SmallSpace;
&:hover {
background-color: transparent;
}
&:focus {
background-color: transparent;
}
&:active {
background-color: transparent;
}
}
}
}
}
.resourceTree {
padding: 12px;
}
.accordion {
.accordionItemContainer {
.accordionItemHeader {
border-radius: 4px;
}
}
}
.treeComponent {
.nodeItem {
&:focus {
outline: 2px @FabricAccentMedium;
}
.treeNodeHeader {
padding: 5px 5px;
border-radius: 4px;
&:hover {
background-color: @FabricAccentLight;
.treeMenuEllipsis {
opacity: 1;
}
}
&.showingMenu {
background-color: #eee;
}
}
.selected {
&>.treeNodeHeader {
background-color: @FabricAccentExtra;
}
}
}
}
.dataExplorerErrorConsoleContainer {
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
width: auto;
align-self: auto;
}
.filterbtnstyle {
background: #fff;
color: #000;
border: solid 1px #d1d1d1;
border-radius: 4px;
}
.filterbtnstyle:hover {
background: @FabricAccentLight;
color: #000;
border: solid 1px #d1d1d1;
}
.filterbtnstyle:active {
background: @FabricAccentLight;
color: #000;
border: solid 1px #d1d1d1;
}
.filterbtnstyle:focus {
background: #fff;
color: #000;
border: solid 1px #d1d1d1;
}
.gridRowSelected .tabdocumentsGridElement:hover {
background-color: @FabricAccentLight !important;
}
.refreshcol {
filter: brightness(0) saturate(100%);
}
.refreshcol1 {
filter: brightness(0) saturate(100%);
}
.fileImportImg img {
filter: brightness(0) saturate(100%);
}

View File

@@ -199,6 +199,7 @@
"pack:fast": "webpack --mode development --progress",
"copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest",
"test:debug": "jest --runInBand",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
"watch": "npm run start",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",

View File

@@ -365,9 +365,6 @@ export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;

View File

@@ -5,8 +5,10 @@ import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { Platform, configContext } from "./../ConfigContext";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
@@ -76,10 +78,10 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : configContext.platform === Platform.Fabric ? (
<ResourceTree2 container={container} />
) : (
<ResourceTree container={container} />
// Uncomment the following line to use the fluent ui tree
// <ResourceTree2 container={container} />
)}
</div>
{/* Collections Window - End */}

View File

@@ -0,0 +1,18 @@
import { Platform, configContext } from "../ConfigContext";
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export function updateStyles(): void {
if (configContext.platform === Platform.Fabric) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
StyleConstants.AccentAccentExtra = StyleConstants.FabricAccentMediumHigh;
} else {
StyleConstants.AccentMediumHigh = StyleConstants.PortalAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.PortalAccentMedium;
StyleConstants.AccentLight = StyleConstants.PortalAccentLight;
StyleConstants.AccentAccentExtra = StyleConstants.PortalAccentMediumHigh;
}
}

View File

@@ -16,6 +16,7 @@ export enum Platform {
Portal = "Portal",
Hosted = "Hosted",
Emulator = "Emulator",
Fabric = "Fabric",
}
export interface ConfigContext {
@@ -67,7 +68,7 @@ let configContext: Readonly<ConfigContext> = {
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.windows.net",
GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
@@ -187,6 +188,7 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
console.error(`Invalid platform query parameter: ${platform}`);
break;
case Platform.Portal:
case Platform.Fabric:
case Platform.Hosted:
case Platform.Emulator:
updateConfigContext({ platform });

View File

@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { StyleConstants } from "../../Common/Constants";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { StyleConstants } from "../../Common/StyleConstants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";

View File

@@ -18,6 +18,7 @@ import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
@@ -99,7 +100,10 @@ export const createCollectionContextMenuButton = (
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
if (
configContext.platform !== Platform.Fabric &&
(userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
) {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {

View File

@@ -32,6 +32,20 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
},
};
const testPostgresAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
postgresqlEndpoint: "https://testPostgresEndpoint.azure.com/",
},
};
const testVCoreMongoAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
vcoreMongoEndpoint: "https://testVCoreMongoEndpoint.azure.com/",
},
};
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
@@ -50,6 +64,18 @@ const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInf
forwardingId: "Id",
};
const testPostgresNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/postgresql",
forwardingId: "Id",
};
const testVCoreMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongovcore",
forwardingId: "Id",
};
describe("NotebookTerminalComponent", () => {
it("renders terminal", () => {
const props: NotebookTerminalComponentProps = {
@@ -94,4 +120,27 @@ describe("NotebookTerminalComponent", () => {
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders Postgres shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testPostgresAccount,
notebookServerInfo: testPostgresNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders vCore Mongo shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testVCoreMongoAccount,
notebookServerInfo: testVCoreMongoNotebookServerInfo,
tabId: undefined,
username: "username",
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookTerminalComponent renders Postgres shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
<div
className="notebookTerminalContainer"
@@ -47,3 +59,15 @@ exports[`NotebookTerminalComponent renders terminal 1`] = `
/>
</div>
`;
exports[`NotebookTerminalComponent renders vCore Mongo shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;

View File

@@ -23,9 +23,9 @@ import * as React from "react";
import * as _ from "underscore";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { QueriesClient } from "../../../Common/QueriesClient";
import { StyleConstants } from "../../../Common/StyleConstants";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -23,7 +23,8 @@ import {
Text,
} from "@fluentui/react";
import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { Urls } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { hoursInAMonth } from "../../../Shared/Constants";
import {
computeRUUsagePriceHourly,

View File

@@ -18,6 +18,7 @@ import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squa
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -237,7 +238,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
private renderContextMenuButton(node: TreeNode): JSX.Element {
const menuItemLabel = "More";
const buttonStyles: Partial<IButtonStyles> = {
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` },
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
};
return (

View File

@@ -1,7 +1,7 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform } from "ConfigContext";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { IGalleryItem } from "Juno/JunoClient";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
@@ -1343,9 +1343,10 @@ export default class Explorer {
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures;
configContext.platform !== Platform.Fabric &&
(userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures);
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()

View File

@@ -8,7 +8,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType, StyleConstants } from "../../../Common/Constants";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -84,15 +86,27 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
}
const rootStyle =
configContext.platform === Platform.Fabric
? {
root: {
backgroundColor: "transparent",
padding: "0px 14px 0px 14px",
},
}
: {
root: {
backgroundColor: backgroundColor,
},
};
return (
<div className="commandBarContainer">
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
farItems={uiFabricControlButtons}
styles={{
root: { backgroundColor: backgroundColor },
}}
styles={rootStyle}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
</div>

View File

@@ -343,6 +343,31 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Open Postgres and vCore Mongo buttons", () => {
const openPostgresShellButtonLabel = "Open PSQL shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (vcore) shell";
beforeAll(() => {
mockExplorer = {} as Explorer;
});
it("creates Postgres shell button", () => {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
const openPostgresShellButton = buttons.find(
(button) => button.commandButtonLabel === openPostgresShellButtonLabel
);
expect(openPostgresShellButton).toBeDefined();
});
it("creates vCore Mongo shell button", () => {
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
const openVCoreMongoShellButton = buttons.find(
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel
);
expect(openVCoreMongoShellButton).toBeDefined();
});
});
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";

View File

@@ -54,7 +54,11 @@ export function createStaticCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
if (
configContext.platform !== Platform.Fabric &&
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
@@ -257,7 +261,9 @@ export function createDivider(): CommandButtonComponentProps {
}
function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
return (
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {

View File

@@ -9,7 +9,9 @@ import {
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { PoolIdType, StyleConstants } from "../../../Common/Constants";
import { PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -24,11 +26,14 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const hoverColor =
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
};
return btns
@@ -68,6 +73,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
height: buttonHeightPx,
paddingRight: 0,
paddingLeft: 0,
borderRadius: configContext.platform == Platform.Fabric ? StyleConstants.FabricButtonBorderRadius : "0px",
minWidth: 24,
marginLeft: isSplit ? 0 : 5,
marginRight: isSplit ? 0 : 5,
@@ -79,17 +85,17 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
splitButtonMenuButton: {
backgroundColor: backgroundColor,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight },
":hover": { backgroundColor: hoverColor },
},
width: 16,
},
label: { fontSize: StyleConstants.mediumFontSize },
rootHovered: { backgroundColor: StyleConstants.AccentLight },
rootPressed: { backgroundColor: StyleConstants.AccentLight },
rootHovered: { backgroundColor: hoverColor },
rootPressed: { backgroundColor: hoverColor },
splitButtonMenuButtonExpanded: {
backgroundColor: StyleConstants.AccentExtra,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight },
":hover": { backgroundColor: hoverColor },
},
},
splitButtonDivider: {
@@ -120,7 +126,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
selectors: {
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
".ms-ContextualMenu-icon": { width: 16, height: 16 },
},
},

View File

@@ -3,7 +3,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { StyleConstants } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
interface Props {
lastSaved?: Date | null;

View File

@@ -1,8 +1,15 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react";
import {
DetailsList,
DetailsListLayoutMode,
DirectionalHint,
IColumn,
SelectionMode,
TooltipHost,
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -74,12 +81,21 @@ export const UploadItemsPane: FunctionComponent = () => {
];
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
let fieldContent: string;
const tooltipId = `tooltip-${index}-${column.key}`;
switch (column.key) {
case "status":
return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
break;
default:
return item.fileName;
fieldContent = item.fileName;
}
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
{fieldContent}
</TooltipHost>
);
};
return (

View File

@@ -11,8 +11,9 @@ import {
Stack,
Text,
} from "@fluentui/react";
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
import { QueryCopilotSampleDatabaseId } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { StyleConstants } from "Common/StyleConstants";
import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";

View File

@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
<Link href="https://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -97,7 +97,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>

View File

@@ -102,7 +102,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
Ask Copilot to generate a query by describing the query in your words.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
href="https://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>
@@ -138,7 +138,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
href="https://aka.ms/cdb-copilot-preview-terms"
>
Read preview terms
</StyledLinkBase>
@@ -174,7 +174,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
href="https://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>

View File

@@ -434,7 +434,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
}}
>
Learn about{" "}
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
@@ -448,7 +448,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
Read preview terms
</Link>
{showErrorMessageBar && (

View File

@@ -0,0 +1,69 @@
import { Text } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { ExplanationButton } from "./ExplanationButton";
describe("Explanation Button", () => {
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
useQueryCopilot.getState().showExplanationBubble = true;
useQueryCopilot.getState().shouldIncludeInMessages = false;
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it("should render explanation bubble with generated comments", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.find("Stack")).toHaveLength(1);
expect(wrapper.find("Text")).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
it("should render 'Explain this query' link", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const mockSetChatMessages = jest.fn();
const mockSetIsGeneratingExplanation = jest.fn();
const mockSetShouldIncludeInMessages = jest.fn();
const mockSetShowExplanationBubble = jest.fn();
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
useQueryCopilot.getState().setShowExplanationBubble = mockSetShowExplanationBubble;
const wrapper = shallow(<ExplanationButton />);
const textElement = wrapper.find(Text);
textElement.simulate("click");
expect(mockSetChatMessages).toHaveBeenCalledWith([
...initialStoreState.chatMessages,
{ source: 0, message: "Explain this query to me" },
]);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
expect(mockSetShowExplanationBubble).toHaveBeenCalledWith(false);
jest.advanceTimersByTime(3000);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockSetChatMessages).toHaveBeenCalled();
});
it("should render nothing when conditions are not met", () => {
useQueryCopilot.getState().showExplanationBubble = false;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -2,12 +2,13 @@ import { Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const ExplanationBubble: React.FC = (): JSX.Element => {
export const ExplanationButton: React.FC = (): JSX.Element => {
const {
showExplanationBubble,
isGeneratingQuery,
chatMessages,
setChatMessages,
generatedQuery,
generatedQueryComments,
isGeneratingExplanation,
setIsGeneratingExplanation,
@@ -24,7 +25,7 @@ export const ExplanationBubble: React.FC = (): JSX.Element => {
setTimeout(() => {
if (useQueryCopilot.getState().shouldIncludeInMessages) {
setIsGeneratingExplanation(false);
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments }]);
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments, sqlQuery: generatedQuery }]);
}
}, 3000);
};

View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Button should render explanation bubble with generated comments 1`] = `
<Stack
style={
Object {
"alignItems": "center",
"display": "flex",
"margin": "5px",
"padding": "5px 5px 5px 50px",
}
}
>
<Text
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Explain this query to me
</Text>
</Stack>
`;
exports[`Explanation Button should render nothing when conditions are not met 1`] = `""`;

View File

@@ -0,0 +1,26 @@
import { Stack, Text } from "@fluentui/react";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
import React from "react";
export const ExplanationBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
return (
<Stack
horizontalAlign="start"
verticalAlign="start"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
<Text>{copilotMessage.message}</Text>
<FeedbackButtons sqlQuery={copilotMessage.sqlQuery} />
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
AI-generated content may be incorrect
</Text>
</Stack>
);
};

View File

@@ -1,69 +1,17 @@
import { Text } from "@fluentui/react";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { ExplanationBubble } from "./ExplanationBubble";
describe("Explanation Bubble", () => {
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
useQueryCopilot.getState().showExplanationBubble = true;
useQueryCopilot.getState().shouldIncludeInMessages = false;
jest.useFakeTimers();
});
describe("Explanation Bubble snapshot tests", () => {
it("should render", () => {
const mockCopilotMessage: CopilotMessage = {
source: 2,
message: "Mock message",
};
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
const wrapper = shallow(<ExplanationBubble copilotMessage={mockCopilotMessage} />);
it("should render explanation bubble with generated comments", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const wrapper = shallow(<ExplanationBubble />);
expect(wrapper.find("Stack")).toHaveLength(1);
expect(wrapper.find("Text")).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
it("should render 'Explain this query' link", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const mockSetChatMessages = jest.fn();
const mockSetIsGeneratingExplanation = jest.fn();
const mockSetShouldIncludeInMessages = jest.fn();
const mockSetShowExplanationBubble = jest.fn();
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
useQueryCopilot.getState().setShowExplanationBubble = mockSetShowExplanationBubble;
const wrapper = shallow(<ExplanationBubble />);
const textElement = wrapper.find(Text);
textElement.simulate("click");
expect(mockSetChatMessages).toHaveBeenCalledWith([
...initialStoreState.chatMessages,
{ source: 0, message: "Explain this query to me" },
]);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
expect(mockSetShowExplanationBubble).toHaveBeenCalledWith(false);
jest.advanceTimersByTime(3000);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockSetChatMessages).toHaveBeenCalled();
});
it("should render nothing when conditions are not met", () => {
useQueryCopilot.getState().showExplanationBubble = false;
const wrapper = shallow(<ExplanationBubble />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,32 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Bubble should render explanation bubble with generated comments 1`] = `
exports[`Explanation Bubble snapshot tests should render 1`] = `
<Stack
horizontalAlign="start"
style={
Object {
"alignItems": "center",
"display": "flex",
"margin": "5px",
"padding": "5px 5px 5px 50px",
"backgroundColor": "white",
"borderRadius": "8px",
"margin": "5px 10px",
"textAlign": "start",
}
}
tokens={
Object {
"childrenGap": 8,
"padding": 8,
}
}
verticalAlign="start"
>
<Text>
Mock message
</Text>
<FeedbackButtons />
<Text
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
"fontSize": "10px",
"fontWeight": 400,
"lineHeight": "14px",
}
}
>
Explain this query to me
AI-generated content may be incorrect
</Text>
</Stack>
`;
exports[`Explanation Bubble should render nothing when conditions are not met 1`] = `""`;

View File

@@ -2,18 +2,31 @@ import { Stack, Text } from "@fluentui/react";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
import { userContext } from "UserContext";
import React, { useState } from "react";
export const OutputBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
const [windowHeight, setWindowHeight] = useState<string>();
const textHeightWithPadding = 16;
const calculateQueryWindowHeight = (): string => {
const calculatedHeight = document.getElementById("outputBubble")?.clientHeight * (3 / 5);
return `${calculatedHeight}px`;
const outputWidth = document.getElementById("outputBubble")?.clientWidth;
const responseLength = copilotMessage.sqlQuery.length;
if (outputWidth > responseLength) {
return `${textHeightWithPadding * 3}px`;
} else {
const neededLines = Math.ceil(responseLength / outputWidth);
return `${neededLines * textHeightWithPadding}px`;
}
};
React.useEffect(() => {
setWindowHeight(calculateQueryWindowHeight());
if (userContext.features.copilotChatFixedMonacoEditorHeight) {
setWindowHeight(`${textHeightWithPadding * 5}px`);
} else {
setWindowHeight(calculateQueryWindowHeight());
}
}, []);
return (

View File

@@ -65,7 +65,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>
@@ -87,7 +87,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
<Link href="https://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
</Stack>
@@ -109,7 +109,7 @@ export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
<Text>
Copilot is setup on a sample database we have configured for you at no cost
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
<Link href="https://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>

View File

@@ -1,6 +1,7 @@
import { Stack } from "@fluentui/react";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplanationBubble";
import { ExplanationButton } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/Button/ExplanationButton";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
@@ -42,27 +43,34 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
}}
>
<WelcomeBubble />
{chatMessages.map((message, index) =>
message.source === 0 || message.source === 2 ? (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: message.source === 0 ? "#E0E7FF" : "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
) : (
<OutputBubble key={index} copilotMessage={message} />
)
)}
{chatMessages.map((message, index) => {
switch (message.source) {
case 0:
return (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "#E0E7FF",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
);
case 1:
return <OutputBubble key={index} copilotMessage={message} />;
case 2:
return <ExplanationBubble key={index} copilotMessage={message} />;
default:
return <></>;
}
})}
<RetrievingBubble />
<ExplanationBubble />
<ExplanationButton />
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
</Stack>

View File

@@ -102,7 +102,7 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
>
Read preview terms

View File

@@ -4,7 +4,6 @@ import { traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react";
import Youtube from "react-youtube";
import Image1 from "../../../images/CarouselImage1.svg";
import Image2 from "../../../images/CarouselImage2.svg";
@@ -79,7 +78,12 @@ const getHeaderText = (page: number): string => {
const getContent = (page: number): JSX.Element => {
switch (page) {
case 1:
return <Youtube videoId="Jvgh64rvdXU" onPlay={() => traceSuccess(Action.PlayCarouselVideo)} />;
return (
<video controls width="640" height="360" controlsList="nofullscreen nodownload ">
<source src="src/Explorer/Quickstart/Videos/Cosmos-db-turorial.mp4" type="video/mp4"></source>
Your browser does not support the video tag.
</video>
);
case 2:
return <Image style={{ width: 640 }} src={Image1} />;
case 3:

Binary file not shown.

View File

@@ -0,0 +1,91 @@
import { DatabaseAccount, FirewallRule } from "Contracts/DataModels";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { updateUserContext } from "UserContext";
import { mockFunction } from "Utils/JestUtils";
import { armRequest } from "Utils/arm/request";
import React from "react";
jest.mock("Utils/arm/request");
const armRequestMock = mockFunction(armRequest);
describe("CheckFirewallRule tests", () => {
const apiVersion = "2023-03-15-preview";
const rulePredicate = (rule: FirewallRule) =>
rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255";
let isAllPublicIPAddressEnabled: boolean;
const setIsAllPublicIPAddressEnabled = jest.fn((value) => (isAllPublicIPAddressEnabled = value));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const useStateMock: any = (initState: any) => [initState, setIsAllPublicIPAddressEnabled];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
beforeAll(() => {
updateUserContext({
databaseAccount: {
id: "testResourceId",
} as DatabaseAccount,
});
});
it("returns 'all public IP addresses' is enabled for account with the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce({
value: [
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "0.0.0.0",
endIpAddress: "255.255.255.255",
},
},
],
});
await checkFirewallRules(apiVersion, rulePredicate, setIsAllPublicIPAddressEnabled);
expect(isAllPublicIPAddressEnabled).toBe(true);
});
it("returns 'all public IP addresses' is NOT enabled for account without the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce([
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "10.10.10.10",
endIpAddress: "10.10.10.10",
},
},
]);
await checkFirewallRules(apiVersion, rulePredicate, setIsAllPublicIPAddressEnabled);
expect(isAllPublicIPAddressEnabled).toBe(false);
});
it("sets message for account without the proper firewall rule", async () => {
armRequestMock.mockResolvedValueOnce([
{
id: "resourceId",
name: "AllowAll",
type: "Microsoft.DocumentDB/mongoClusters/firewallRules",
properties: {
provisioningState: "Succeeded",
startIpAddress: "0.0.0.0",
endIpAddress: "255.255.255.255",
},
},
]);
const warningMessage = "This is a warning message";
let warningMessageResult: string;
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
await checkFirewallRules(apiVersion, rulePredicate, undefined, warningMessageFunc, warningMessage);
expect(warningMessageResult).toEqual(warningMessage);
});
});

View File

@@ -138,12 +138,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
/>
)}
</span>
<span
className="tabNavText"
style={active ? { fontWeight: "bolder", borderBottom: "2px solid rgba(0,120,212,1)" } : {}}
>
{useObservable(tab?.tabTitle || getReactTabTitle())}
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
{tabKind !== ReactTabKind.Home && (
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />

View File

@@ -37,6 +37,7 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
@@ -205,7 +206,8 @@ export default class Collection implements ViewModels.Collection {
.map((node) => <Trigger>node);
});
const showScriptsMenus: boolean = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const showScriptsMenus: boolean =
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);

View File

@@ -39,6 +39,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@@ -69,7 +70,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
shallow
);
const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const showScriptNodes =
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
const pseudoDirPath = "PsuedoDir";
const buildGalleryCallout = (): JSX.Element => {

View File

@@ -40,6 +40,7 @@ import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@@ -249,7 +250,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
* @param container
*/
private static showScriptNodes(container: Explorer): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
return (
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {

View File

@@ -89,8 +89,8 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
aria-label="CosmosDB resources"
openItems={openItems}
onOpenChange={handleOpenChange}
size="small"
style={{ height: "100%" }}
size="medium"
style={{ height: "100%", width: "290px" }}
>
{[dataNodeTree].map((node) => (
<TreeNode2Component

View File

@@ -17,6 +17,7 @@ import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
export const buildCollectionNode = (
database: ViewModels.Database,
@@ -25,6 +26,46 @@ export const buildCollectionNode = (
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void
): TreeNode2 => {
let children: TreeNode2[];
// Flat Tree for Fabric
if (configContext.platform !== Platform.Fabric) {
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
}
return {
label: collection.id(),
iconSrc: CollectionIcon,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => {
useSelectedNode.getState().setSelectedNode(collection);
collection.openTab();
// push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
onExpanded: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
};
const buildCollectionNodeChildren = (
database: ViewModels.Database,
collection: ViewModels.Collection,
isNotebookEnabled: boolean,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void
): TreeNode2[] => {
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const children: TreeNode2[] = [];
children.push({
@@ -110,27 +151,7 @@ export const buildCollectionNode = (
});
}
return {
label: collection.id(),
iconSrc: CollectionIcon,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => {
useSelectedNode.getState().setSelectedNode(collection);
},
onExpanded: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
return children;
};
const buildStoredProcedureNode = (

View File

@@ -9,6 +9,7 @@ import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFacto
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boolean): TreeNode2[] => {
const databases = useDatabases((state) => state.databases);
@@ -35,7 +36,7 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
};
if (database.isDatabaseShared()) {
if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) {
databaseNode.children.push({
id: database.isSampleDB ? "sampleScaleSettings" : "",
label: "Scale",

View File

@@ -1,5 +1,5 @@
// CSS Dependencies
import { initializeIcons } from "@fluentui/react";
import { initializeIcons, loadTheme } from "@fluentui/react";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
@@ -26,6 +26,8 @@ import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import * as StyleConstants from "./Common/StyleConstants";
import { configContext, Platform } from "ConfigContext";
import "../less/documentDB.less";
import "../less/forms.less";
import "../less/infobox.less";
@@ -57,6 +59,7 @@ import "./Libs/jquery";
import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
initializeIcons();
@@ -67,6 +70,10 @@ const App: React.FunctionComponent = () => {
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
const config = useConfig();
if (config?.platform === Platform.Fabric) {
loadTheme(appThemeFabric);
}
StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform);
const toggleLeftPaneExpanded = () => {
@@ -84,6 +91,7 @@ const App: React.FunctionComponent = () => {
return (
<div className="flexContainer" aria-hidden="false">
<LoadFabricOverrides />
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
@@ -135,6 +143,19 @@ const App: React.FunctionComponent = () => {
ReactDOM.render(<App />, document.body);
function LoadFabricOverrides(): JSX.Element {
if (configContext.platform === Platform.Fabric) {
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
return (
<React.Suspense fallback={<div></div>}>
<FabricStyle />
</React.Suspense>
);
} else {
return <></>;
}
}
function LoadingExplorer(): JSX.Element {
return (
<div className="splashLoaderContainer">

View File

@@ -0,0 +1,7 @@
import React from "react";
import "../../../less/documentDBFabric.less";
// This is a dummy export, allowing us to conditionally import documentDBFabric.less
// by lazy-importing this in Main.tsx (see LoadFabricOverrides() there)
export default function InitFabric() {
return <></>;
}

View File

@@ -0,0 +1,208 @@
import { Theme, createTheme } from "@fluentui/react";
export const appThemeFabric: Theme = createTheme({
palette: {
/**
* Color code for themeDarker.
*/
themeDarker: "#033f38",
/**
* Color code for themeDark.
*/
themeDark: "#0a5c50",
/**
* Color code for themeDarkAlt.
*/
themeDarkAlt: "#0c695a",
/**
* Color code for themePrimary.
*/
themePrimary: "#117865",
/**
* Color code for themeSecondary.
*/
themeSecondary: "#1f937e",
/**
* Color code for themeTertiary.
*/
themeTertiary: "#52c7aa",
/**
* Color code for themeLight.
*/
themeLight: "#9ee0cb",
/**
* Color code for themeLighter.
*/
themeLighter: "#c0ecdd",
/**
* Color code for themeLighterAlt.
*/
themeLighterAlt: "#e3f7ef",
/**
* Color code for the strongest color, which is black in the default theme.
* This is a very light color in inverted themes.
*/
black: "#000000",
/**
* Color code for blackTranslucent40.
*/
blackTranslucent40: "rgba(0, 0, 0, 0.4)",
/**
* Color code for neutralDark.
*/
neutralDark: "#141414",
/**
* Color code for neutralPrimary.
*/
neutralPrimary: "#242424",
/**
* Color code for neutralPrimaryAlt.
*/
neutralPrimaryAlt: "#383838",
/**
* Color code for neutralSecondary.
*/
neutralSecondary: "#5c5c5c",
/**
* Color code for neutralSecondaryAlt.
*/
neutralSecondaryAlt: "#858585",
/**
* Color code for neutralTertiary.
*/
neutralTertiary: "#9e9e9e",
/**
* Color code for neutralTertiaryAlt.
*/
neutralTertiaryAlt: "#c7c7c7",
/**
* Color code for neutralQuaternary.
*/
neutralQuaternary: "#d1d1d1",
/**
* Color code for neutralQuaternaryAlt.
*/
neutralQuaternaryAlt: "#e0e0e0",
/**
* Color code for neutralLight.
*/
neutralLight: "#ebebeb",
/**
* Color code for neutralLighter.
*/
neutralLighter: "#f5f5f5",
/**
* Color code for neutralLighterAlt.
*/
neutralLighterAlt: "#fafafa",
/**
* Color code for the accent.
*/
accent: "#117865",
/**
* Color code for the softest color, which is white in the default theme. This is a very dark color in dark themes.
* This is the page background.
*/
white: "#ffffff",
/**
* Color code for whiteTranslucent40
*/
whiteTranslucent40: "rgba(255, 255, 255, 0.4)",
/**
* Color code for yellowDark.
*/
yellowDark: "#d39300",
/**
* Color code for yellow.
*/
yellow: "#fde300",
/**
* Color code for yellowLight.
*/
yellowLight: "#fef7b2",
/**
* Color code for orange.
*/
orange: "#f7630c",
/**
* Color code for orangeLight.
*/
orangeLight: "#f98845",
/**
* Color code for orangeLighter.
*/
orangeLighter: "#fdcfb4",
/**
* Color code for redDark.
*/
redDark: "#750b1c",
/**
* Color code for red.
*/
red: "#d13438",
/**
* Color code for magentaDark.
*/
magentaDark: "#6b0043",
/**
* Color code for magenta.
*/
magenta: "#bf0077",
/**
* Color code for magentaLight.
*/
magentaLight: "#d957a8",
/**
* Color code for purpleDark.
*/
purpleDark: "#401b6c",
/**
* Color code for purple.
*/
purple: "#5c2e91",
/**
* Color code for purpleLight.
*/
purpleLight: "#c6b1de",
/**
* Color code for blueDark.
*/
blueDark: "#003966",
/**
* Color code for blueMid.
*/
blueMid: "#004e8c",
/**
* Color code for blue.
*/
blue: "#0078d4",
/**
* Color code for blueLight.
*/
blueLight: "#3a96dd",
/**
* Color code for tealDark.
*/
tealDark: "#006666",
/**
* Color code for teal.
*/
teal: "#038387",
/**
* Color code for tealLight.
*/
tealLight: "#00b7c3",
/**
* Color code for greenDark.
*/
greenDark: "#0b6a0b",
/**
* Color code for green.
*/
green: "#107c10",
/**
* Color code for greenLight.
*/
greenLight: "#13a10e",
},
});

View File

@@ -4,7 +4,7 @@
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react";
import * as React from "react";
import { FunctionComponent, useEffect, useState } from "react";
import { StyleConstants } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../hooks/useSubscriptions";

View File

@@ -1,7 +1,7 @@
import { useBoolean } from "@fluentui/react-hooks";
import * as React from "react";
import ErrorImage from "../../../../images/error.svg";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg";
import { AuthType } from "../../../AuthType";
import { HttpHeaders } from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext";
@@ -16,6 +16,19 @@ interface Props {
setAuthType: (authType: AuthType) => void;
}
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
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());
return decodeURIComponent(result.readWrite || result.read);
};
export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken,
login,
@@ -44,16 +57,8 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
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());
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
const encryptedToken = await fetchEncryptedToken(connectionString);
setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString);
}}
>

View File

@@ -40,6 +40,7 @@ export type Features = {
readonly copilotVersion?: string;
readonly disableCopilotPhoenixGateaway: boolean;
readonly enableCopilotFullSchema: boolean;
readonly copilotChatFixedMonacoEditorHeight: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -112,6 +113,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotVersion: get("copilotversion") ?? "v1.0",
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
};
}

View File

@@ -1,5 +1,5 @@
import { IButtonStyles, ICommandBarStyles, ISeparatorStyles, IStackTokens } from "@fluentui/react";
import { StyleConstants } from "../Common/Constants";
import { StyleConstants } from "../Common/StyleConstants";
export const commandBarItemStyles: IButtonStyles = { root: { paddingLeft: 20 } };

View File

@@ -55,6 +55,17 @@ export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
"https://localhost:1234",
];
export const PortalBackendIPs: { [key: string]: string[] } = {
"https://main.documentdb.ext.azure.com": ["104.42.195.92", "40.76.54.131"],
// DE doesn't talk to prod2 (main2) but it might be added
//"https://main2.documentdb.ext.azure.com": ["104.42.196.69"],
"https://main.documentdb.ext.azure.cn": ["139.217.8.252"],
"https://main.documentdb.ext.azure.us": ["52.244.48.71"],
// Add ussec and usnat when endpoint address is known:
//ussec: ["29.26.26.67", "29.26.26.66"],
//usnat: ["7.28.202.68"],
};
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn",
@@ -67,7 +78,7 @@ export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localho
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];

4
src/Utils/JestUtils.ts Normal file
View File

@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mockFunction<T extends (...args: any[]) => any>(fn: T): jest.MockedFunction<T> {
return fn as jest.MockedFunction<T>;
}

View File

@@ -0,0 +1,106 @@
import { resetConfigContext, updateConfigContext } from "ConfigContext";
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
import { updateUserContext } from "UserContext";
import { PortalBackendIPs } from "Utils/EndpointValidation";
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
describe("NetworkUtility tests", () => {
describe("getNetworkSettingsWarningMessage", () => {
const publicAccessMessagePart = "Please enable public access to proceed";
const accessMessagePart = "Please allow access from Azure Portal to proceed";
// validEnpoints are a subset of those from Utils/EndpointValidation/PortalBackendIPs
const validEndpoints = [
"https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn",
"https://main.documentdb.ext.azure.us",
];
let warningMessageResult: string;
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
beforeEach(() => {
warningMessageResult = undefined;
});
afterEach(() => {
resetConfigContext();
});
it("should return no message when publicNetworkAccess is enabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toBeUndefined();
});
it("should return publicAccessMessage when publicNetworkAccess is disabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Disabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toContain(publicAccessMessagePart);
});
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, () => {
validEndpoints.forEach(async (endpoint) => {
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: PortalBackendIPs[endpoint].map((ip: string) => ({ ipAddressOrRange: ip } as IpRule)),
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
BACKEND_ENDPOINT: endpoint,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toBeUndefined();
});
});
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", () => {
validEndpoints.forEach(async (endpoint) => {
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
BACKEND_ENDPOINT: endpoint,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toContain(accessMessagePart);
});
});
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
// tests are omitted here and included in CheckFirewallRules.test.ts
});
});

View File

@@ -1,15 +1,7 @@
import { configContext } from "ConfigContext";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
const PortalIPs: { [key: string]: string[] } = {
prod1: ["104.42.195.92", "40.76.54.131"],
prod2: ["104.42.196.69"],
mooncake: ["139.217.8.252"],
blackforest: ["51.4.229.218"],
fairfax: ["52.244.48.71"],
ussec: ["29.26.26.67", "29.26.26.66"],
usnat: ["7.28.202.68"],
};
import { PortalBackendIPs } from "Utils/EndpointValidation";
export const getNetworkSettingsWarningMessage = async (
setStateFunc: (warningMessage: string) => void
@@ -28,6 +20,7 @@ export const getNetworkSettingsWarningMessage = async (
setStateFunc,
accessMessage
);
return;
} else if (userContext.apiType === "VCoreMongo") {
checkFirewallRules(
"2023-03-01-preview",
@@ -38,6 +31,7 @@ export const getNetworkSettingsWarningMessage = async (
setStateFunc,
accessMessage
);
return;
} else if (accountProperties) {
// public network access is disabled
if (
@@ -45,13 +39,14 @@ export const getNetworkSettingsWarningMessage = async (
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
) {
setStateFunc(publicAccessMessage);
return;
}
const ipRules = accountProperties.ipRules;
// public network access is NOT set to "All networks"
if (ipRules.length > 0) {
if (ipRules?.length > 0) {
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
const portalIPs = PortalIPs[userContext.portalEnv];
const portalIPs = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
let numberOfMatches = 0;
ipRules.forEach((ipRule) => {
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {

View File

@@ -1,3 +1,5 @@
import { Platform, configContext } from "./../ConfigContext";
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window.
try {
@@ -5,7 +7,11 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
if (currentWindow.parent === currentWindow) {
return undefined;
}
if (currentWindow.parent === currentWindow.top) {
if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) {
// in Fabric data explorer is inside an extension iframe, so we have two parent iframes
return currentWindow;
}
if (configContext.platform !== Platform.Fabric && currentWindow.parent === currentWindow.top) {
return currentWindow;
}
currentWindow = currentWindow.parent;

View File

@@ -1,6 +1,8 @@
import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer";
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { fetchAccessData } from "hooks/usePortalAccessToken";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react";
import { AuthType } from "../AuthType";
@@ -60,6 +62,26 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
} else if (platform === Platform.Portal) {
const explorer = await configurePortal();
setExplorer(explorer);
} else if (platform === Platform.Fabric) {
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
const connectionString = sessionStorage.getItem("connectionString");
if (!connectionString) {
console.error("No connection string found in session storage");
return;
}
const encryptedToken = await fetchEncryptedToken(connectionString);
// TODO Duplicated from useTokenMetadata
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
const win = (window as unknown) as HostedExplorerChildFrame;
win.hostedConfig = {
authType: AuthType.EncryptedToken,
encryptedToken,
encryptedTokenMetadata,
};
const explorer = await configureHosted();
setExplorer(explorer);
}
}
};

View File

@@ -57,4 +57,22 @@ describe("shouldShowQueryPageOptions()", () => {
});
expect(userContext.apiType).toBe("Mongo");
});
it("should be 'Postgres' for Postgres API", () => {
updateUserContext({
databaseAccount: {
kind: "Postgres",
} as DatabaseAccount,
});
expect(userContext.apiType).toBe("Postgres");
});
it("should be 'VCoreMongo' for vCore Mongo", () => {
updateUserContext({
databaseAccount: {
kind: "VCoreMongo",
} as DatabaseAccount,
});
expect(userContext.apiType).toBe("VCoreMongo");
});
});

View File

@@ -21,7 +21,7 @@
<clear />
<add name="X-Xss-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net" />
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net" />
</customHeaders>
<redirectHeaders>
<clear />