diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c84975bd..ec652466c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 58cedfdda..6f034c018 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -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; -} +} \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index 1395c9482..976bd03fd 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -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(); } diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less new file mode 100644 index 000000000..df099585b --- /dev/null +++ b/less/documentDBFabric.less @@ -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%); +} \ No newline at end of file diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 51df6e94f..80d665e62 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -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; diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index 3781af8de..e3968c112 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -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 ) : userContext.features.enableKoResourceTree ? (
+ ) : configContext.platform === Platform.Fabric ? ( + ) : ( - // Uncomment the following line to use the fluent ui tree - // )}
{/* Collections Window - End */} diff --git a/src/Common/StyleConstants.ts b/src/Common/StyleConstants.ts new file mode 100644 index 000000000..81742d8ce --- /dev/null +++ b/src/Common/StyleConstants.ts @@ -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; + } +} diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 18346e838..6a753a280 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -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 { console.error(`Invalid platform query parameter: ${platform}`); break; case Platform.Portal: + case Platform.Fabric: case Platform.Hosted: case Platform.Emulator: updateConfigContext({ platform }); diff --git a/src/Controls/Heatmap/Heatmap.ts b/src/Controls/Heatmap/Heatmap.ts index 0b9c3b796..6d2fa7bfb 100644 --- a/src/Controls/Heatmap/Heatmap.ts +++ b/src/Controls/Heatmap/Heatmap.ts @@ -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"; diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index b5baa8ac5..468a15f11 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -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: () => { diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 236dea5c3..4909e0863 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -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"; diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index b2c5c7c73..fdb3105af 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -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, diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index 135f390f9..5a9d45cfe 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -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 = { - rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` }, + rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` }, }; return ( diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 658502b57..d0cc6c342 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -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() diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 4e4f32e2c..a9c4bbd66 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -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 = ({ container }: Props) => { ); } + const rootStyle = + configContext.platform === Platform.Fabric + ? { + root: { + backgroundColor: "transparent", + padding: "0px 14px 0px 14px", + }, + } + : { + root: { + backgroundColor: backgroundColor, + }, + }; + return (
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index c497d295d..0da9fbc7f 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -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 { diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index c3172da63..fb5260201 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -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 }, }, }, diff --git a/src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx b/src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx index 3b00a20a0..3beacfda0 100644 --- a/src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx @@ -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; diff --git a/src/Explorer/QueryCopilot/CopilotCarousel.tsx b/src/Explorer/QueryCopilot/CopilotCarousel.tsx index 990f79c36..d61924faf 100644 --- a/src/Explorer/QueryCopilot/CopilotCarousel.tsx +++ b/src/Explorer/QueryCopilot/CopilotCarousel.tsx @@ -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"; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 787a3011d..d731bdf58 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -138,12 +138,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind? /> )} - - {useObservable(tab?.tabTitle || getReactTabTitle())} - + {useObservable(tab?.tabTitle || getReactTabTitle())} {tabKind !== ReactTabKind.Home && ( diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 860ac0d2c..0ec19dc59 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -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) => 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(showScriptsMenus); this.showTriggers = ko.observable(showScriptsMenus); this.showUserDefinedFunctions = ko.observable(showScriptsMenus); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index abc9a0785..9430bc1cb 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -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 = ({ 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 => { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index ffaa64778..cd3727b9a 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -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 { diff --git a/src/Explorer/Tree2/ResourceTree.tsx b/src/Explorer/Tree2/ResourceTree.tsx index d07ff39fe..7ef6c56c3 100644 --- a/src/Explorer/Tree2/ResourceTree.tsx +++ b/src/Explorer/Tree2/ResourceTree.tsx @@ -89,8 +89,8 @@ export const ResourceTree2: React.FC = ({ 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) => ( 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 = ( diff --git a/src/Explorer/Tree2/useDatabaseTreeNodes.ts b/src/Explorer/Tree2/useDatabaseTreeNodes.ts index ce1a0d9ef..412ebbeff 100644 --- a/src/Explorer/Tree2/useDatabaseTreeNodes.ts +++ b/src/Explorer/Tree2/useDatabaseTreeNodes.ts @@ -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", diff --git a/src/Main.tsx b/src/Main.tsx index 3982e866b..acea9ae0c 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -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 (
+
{/* Main Command Bar - Start */} @@ -135,6 +143,19 @@ const App: React.FunctionComponent = () => { ReactDOM.render(, document.body); +function LoadFabricOverrides(): JSX.Element { + if (configContext.platform === Platform.Fabric) { + const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform")); + return ( +
}> + + + ); + } else { + return <>; + } +} + function LoadingExplorer(): JSX.Element { return (
diff --git a/src/Platform/Fabric/FabricPlatform.tsx b/src/Platform/Fabric/FabricPlatform.tsx new file mode 100644 index 000000000..267584725 --- /dev/null +++ b/src/Platform/Fabric/FabricPlatform.tsx @@ -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 <>; +} diff --git a/src/Platform/Fabric/FabricTheme.tsx b/src/Platform/Fabric/FabricTheme.tsx new file mode 100644 index 000000000..5288d91c4 --- /dev/null +++ b/src/Platform/Fabric/FabricTheme.tsx @@ -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", + }, +}); diff --git a/src/Platform/Hosted/Components/AccountSwitcher.tsx b/src/Platform/Hosted/Components/AccountSwitcher.tsx index c1085e0fc..d5690e848 100644 --- a/src/Platform/Hosted/Components/AccountSwitcher.tsx +++ b/src/Platform/Hosted/Components/AccountSwitcher.tsx @@ -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"; diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index 3639bd113..575093bc2 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -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 => { + 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 = ({ setEncryptedToken, login, @@ -44,16 +57,8 @@ export const ConnectExplorer: React.FunctionComponent = ({ 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); }} > diff --git a/src/SelfServe/SelfServeStyles.tsx b/src/SelfServe/SelfServeStyles.tsx index 58688b505..5563286a5 100644 --- a/src/SelfServe/SelfServeStyles.tsx +++ b/src/SelfServe/SelfServeStyles.tsx @@ -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 } }; diff --git a/src/Utils/WindowUtils.ts b/src/Utils/WindowUtils.ts index 1ddd451e8..8776796f7 100644 --- a/src/Utils/WindowUtils.ts +++ b/src/Utils/WindowUtils.ts @@ -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; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 4492fe8e9..119e419cf 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -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); } } };