diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.less b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.less new file mode 100644 index 000000000..8d1940cbe --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.less @@ -0,0 +1,20 @@ +.featurePanelComponentContainer { + width: 800px; + + .urlContainer { + padding: 10px; + margin-bottom: 10px; + white-space: nowrap; + overflow: auto; + } + + .options { + padding: 10px; + overflow: auto; + height: 100%; + } + + .checkboxRow { + width: 390px; + } +} \ No newline at end of file diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.test.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.test.tsx new file mode 100644 index 000000000..61e0a82c2 --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.test.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; + +import { FeaturePanelComponent } from "./FeaturePanelComponent"; + +describe("Feature panel", () => { + it("renders all flags", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx new file mode 100644 index 000000000..eb3dee367 --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -0,0 +1,252 @@ +import * as React from "react"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import { Dropdown, IDropdownOption, IDropdownStyles } from "office-ui-fabric-react/lib/Dropdown"; +import { Checkbox } from "office-ui-fabric-react/lib/Checkbox"; +import { TextField, ITextFieldStyles } from "office-ui-fabric-react/lib/TextField"; +import { DefaultButton } from "office-ui-fabric-react"; +import "./FeaturePanelComponent.less"; + +export const FeaturePanelComponent: React.FunctionComponent = () => { + // Initial conditions + const originalParams = new URLSearchParams(window.location.search); + const urlParams = new Map(); // Params with lowercase keys + originalParams.forEach((value: string, key: string) => urlParams.set(key.toLocaleLowerCase(), value)); + + const baseUrlOptions = [ + { key: "https://localhost:1234/explorer.html", text: "localhost:1234" }, + { key: "https://cosmos.azure.com/explorer.html", text: "cosmos.azure.com" }, + { key: "https://portal.azure.com", text: "portal" } + ]; + + const platformOptions = [ + { key: "Hosted", text: "Hosted" }, + { key: "Portal", text: "Portal" }, + { key: "Emulator", text: "Emulator" }, + { key: "", text: "None" } + ]; + + // React hooks to keep state + const [baseUrl, setBaseUrl] = React.useState( + baseUrlOptions.find(o => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0] + ); + const [platform, setPlatform] = React.useState( + urlParams.has("platform") + ? platformOptions.find(o => o.key === urlParams.get("platform")) || platformOptions[0] + : platformOptions[0] + ); + + const booleanFeatures: { + key: string; + label: string; + value: string; + disabled?: () => boolean; + reactState?: [boolean, React.Dispatch>]; + onChange?: (_?: React.FormEvent, checked?: boolean) => void; + }[] = [ + { key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" }, + { key: "feature.enablerupm", label: "Enable RUPM", value: "true" }, + { key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" }, + { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, + { key: "feature.enablettl", label: "Enable TTL", value: "true" }, + { key: "feature.enablegallery", label: "Enable Notebook Gallery", value: "true" }, + { key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" }, + { + key: "feature.enablefixedcollectionwithsharedthroughput", + label: "Enable fixed collection with shared throughput", + value: "true" + }, + { key: "feature.ttl90days", label: "TTL 90 days", value: "true" }, + { key: "feature.enablenotebooks", label: "Enable notebooks", value: "true" }, + { + key: "feature.customportal", + label: "Force Production portal (portal only)", + value: "false", + disabled: (): boolean => baseUrl.key !== "https://portal.azure.com" + }, + { key: "feature.enablespark", label: "Enable Synapse", value: "true" }, + { key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" } + ]; + + const stringFeatures: { + key: string; + label: string; + placeholder: string; + disabled?: () => boolean; + reactState?: [string, React.Dispatch>]; + onChange?: (_: React.FormEvent, newValue?: string) => void; + }[] = [ + { key: "feature.notebookserverurl", label: "Notebook server URL", placeholder: "https://notebookserver" }, + { key: "feature.notebookservertoken", label: "Notebook server token", placeholder: "" }, + { key: "feature.notebookbasepath", label: "Notebook base path", placeholder: "" }, + { key: "key", label: "Auth key", placeholder: "" }, + { + key: "dataExplorerSource", + label: "Data Explorer Source (portal only)", + placeholder: "https://localhost:1234/explorer.html", + disabled: (): boolean => baseUrl.key !== "https://portal.azure.com" + }, + { key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" } + ]; + + booleanFeatures.forEach( + f => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false)) + ); + stringFeatures.forEach( + f => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) : undefined)) + ); + + const buildUrl = (): string => { + const fragments = (platform.key === "" ? [] : [`platform=${platform.key}`]) + .concat(booleanFeatures.map(f => (f.reactState[0] ? `${f.key}=${f.value}` : ""))) + .concat(stringFeatures.map(f => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : ""))) + .filter(v => v && v.length > 0); + + const paramString = fragments.length < 1 ? "" : `?${fragments.join("&")}`; + return `${baseUrl.key}${paramString}`; + }; + + const onChangeBaseUrl = (event: React.FormEvent, option?: IDropdownOption): void => { + setBaseUrl(option); + }; + + const onChangePlatform = (event: React.FormEvent, option?: IDropdownOption): void => { + setPlatform(option); + }; + + booleanFeatures.forEach( + f => + (f.onChange = (ev?: React.FormEvent, checked?: boolean): void => { + f.reactState[1](checked); + }) + ); + + stringFeatures.forEach( + f => + (f.onChange = (event: React.FormEvent, newValue?: string): void => { + f.reactState[1](newValue); + }) + ); + + const onNotebookShortcut = (): void => { + booleanFeatures.find(f => f.key === "feature.enablenotebooks").reactState[1](true); + stringFeatures + .find(f => f.key === "feature.notebookserverurl") + .reactState[1]("https://localhost:10001/12345/notebook/"); + stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token"); + stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1]("."); + setPlatform(platformOptions.find(o => o.key === "Hosted")); + }; + + const onPortalLocalDEShortcut = (): void => { + setBaseUrl(baseUrlOptions.find(o => o.key === "https://portal.azure.com")); + setPlatform(platformOptions.find(o => o.key === "Portal")); + stringFeatures.find(f => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html"); + }; + + const onReset = (): void => { + booleanFeatures.forEach(f => f.reactState[1](false)); + stringFeatures.forEach(f => f.reactState[1]("")); + }; + + const stackTokens = { childrenGap: 10 }; + const dropdownStyles: Partial = { dropdown: { width: 200 } }; + const textFieldStyles: Partial = { fieldGroup: { width: 300 } }; + + // Show in 2 columns to keep it compact + let halfSize = Math.ceil(booleanFeatures.length / 2); + const leftBooleanFeatures = booleanFeatures.slice(0, halfSize); + const rightBooleanFeatures = booleanFeatures.slice(halfSize, booleanFeatures.length); + + halfSize = Math.ceil(stringFeatures.length / 2); + const leftStringFeatures = stringFeatures.slice(0, halfSize); + const rightStringFeatures = stringFeatures.slice(halfSize, stringFeatures.length); + + const anchorOptions = { + href: buildUrl(), + target: "_blank", + rel: "noopener" + }; + + return ( + + + {buildUrl()} + + + + Notebooks on localhost + Portal points to local DE + Reset + + + + + + + + {leftBooleanFeatures.map(f => ( + + ))} + + + {rightBooleanFeatures.map(f => ( + + ))} + + + + + {leftStringFeatures.map(f => ( + + ))} + + + {rightStringFeatures.map(f => ( + + ))} + + + + + ); +}; diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.less b/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.less new file mode 100644 index 000000000..05e76056c --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.less @@ -0,0 +1,18 @@ +.featurePanelLauncherContainer { + .featurePanelLauncherModal { + overflow-y: hidden; + display: flex; + flex-direction: column; + } + .ms-Dialog-main { + overflow: hidden; + } +} + +.activePatch { + position: absolute; + height: 20px; + width: 20px; + top: 20px; + left: -20px; +} \ No newline at end of file diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.tsx new file mode 100644 index 000000000..cb5340875 --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; +import { FeaturePanelComponent } from "./FeaturePanelComponent"; +import { getTheme, mergeStyleSets, FontWeights, Modal, IconButton, IIconProps } from "office-ui-fabric-react"; +import "./FeaturePanelLauncher.less"; + +// Modal wrapper +export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element => { + const [isModalOpen, showModal] = React.useState(false); + + const onActivate = (event: React.MouseEvent): void => { + if (!event.shiftKey || !event.ctrlKey) { + return; + } + event.stopPropagation(); + showModal(true); + }; + + const theme = getTheme(); + const contentStyles = mergeStyleSets({ + container: { + display: "flex", + flexFlow: "column nowrap", + alignItems: "stretch" + }, + header: [ + // tslint:disable-next-line:deprecation + theme.fonts.xLargePlus, + { + flex: "1 1 auto", + borderTop: `4px solid ${theme.palette.themePrimary}`, + color: theme.palette.neutralPrimary, + display: "flex", + alignItems: "center", + fontWeight: FontWeights.semibold, + padding: "12px 12px 14px 24px" + } + ], + body: { + flex: "4 4 auto", + overflowY: "hidden", + marginBottom: 40, + height: "100%", + display: "flex" + } + }); + + const iconButtonStyles = { + root: { + color: theme.palette.neutralPrimary, + marginLeft: "auto", + marginTop: "4px", + marginRight: "2px" + }, + rootHovered: { + color: theme.palette.neutralDark + } + }; + const cancelIcon: IIconProps = { iconName: "Cancel" }; + const hideModal = (): void => showModal(false); + + return ( + + + + Data Explorer Launcher + + + + + + + + ); +}; diff --git a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap new file mode 100644 index 000000000..3b1c08f62 --- /dev/null +++ b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap @@ -0,0 +1,312 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Feature panel renders all flags 1`] = ` + + + + https://localhost:1234/explorer.html?platform=Hosted + + + + + + Notebooks on localhost + + + Portal points to local DE + + + Reset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/src/Explorer/SplashScreen/SplashScreenComponent.less b/src/Explorer/SplashScreen/SplashScreenComponent.less index 887bc02b1..38ee7fd0b 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponent.less +++ b/src/Explorer/SplashScreen/SplashScreenComponent.less @@ -19,6 +19,7 @@ } >.title { + position: relative; // To attach FeaturePanelLauncher as absolute color: @BaseHigh; font-size: 48px; padding-left: 0px; diff --git a/src/Explorer/SplashScreen/SplashScreenComponent.tsx b/src/Explorer/SplashScreen/SplashScreenComponent.tsx index 8f940c4ec..8a452ed7d 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponent.tsx +++ b/src/Explorer/SplashScreen/SplashScreenComponent.tsx @@ -5,6 +5,7 @@ import * as React from "react"; import * as Constants from "../../Common/Constants"; import { Link } from "office-ui-fabric-react/lib/Link"; +import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher"; export interface SplashScreenItem { iconSrc: string; @@ -29,7 +30,10 @@ export class SplashScreenComponent extends React.Component - Welcome to Cosmos DB + + Welcome to Cosmos DB + + Globally distributed, multi-model database service for any scale {this.props.mainItems.map((item: SplashScreenItem) => (