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>
This commit is contained in:
Vsevolod Kukol 2023-09-15 17:33:27 +02:00 committed by GitHub
parent c2d2ff3dee
commit 379395567c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 675 additions and 84 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

@ -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();
}

207
less/documentDBFabric.less Normal file
View File

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

@ -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 {
@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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);
}
}
};