From c5f76ac2a91c908107ee9350f0322c8a1e73fed2 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 12 May 2021 07:16:13 -0700 Subject: [PATCH 01/12] Fix isFixedCollectionWithSharedThroughputSupported flag (#774) --- src/Explorer/Explorer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 0fa1f5634..80e136cc9 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -34,6 +34,7 @@ import { updateUserContext, userContext } from "../UserContext"; import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils"; import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { stringToBlob } from "../Utils/BlobUtils"; +import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; @@ -368,7 +369,7 @@ export default class Explorer { return false; } - return userContext.apiType === "Mongo"; + return isCapabilityEnabled("EnableMongo"); }); this.isServerlessEnabled = ko.computed( From 66281447dfe358c6ecc59fe45269eb5b5265b7c1 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Wed, 12 May 2021 11:49:25 -0500 Subject: [PATCH 02/12] Pass undefined analyticalTTL if Synapse is disabled (#778) --- src/Explorer/Panes/AddCollectionPanel.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 1837133b5..6220e0099 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -921,6 +921,10 @@ export class AddCollectionPanel extends React.Component Date: Wed, 12 May 2021 13:41:44 -0500 Subject: [PATCH 03/12] Add Mongo 3.2 End to End Test (#779) --- .github/workflows/ci.yml | 1 + test/mongo/container32.spec.ts | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/mongo/container32.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d63657b1d..3b96b256b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,6 +141,7 @@ jobs: - ./test/graph/container.spec.ts - ./test/sql/container.spec.ts - ./test/mongo/container.spec.ts + - ./test/mongo/container32.spec.ts - ./test/selfServe/selfServeExample.spec.ts - ./test/notebooks/upload.spec.ts - ./test/sql/resourceToken.spec.ts diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts new file mode 100644 index 000000000..e3949385e --- /dev/null +++ b/test/mongo/container32.spec.ts @@ -0,0 +1,37 @@ +import { jest } from "@jest/globals"; +import "expect-playwright"; +import { safeClick } from "../utils/safeClick"; +import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +jest.setTimeout(240000); + +test("Mongo CRUD", async () => { + const databaseId = generateDatabaseNameWithTimestamp(); + const containerId = generateUniqueName("container"); + + await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); + await page.waitForSelector("iframe"); + const explorer = page.frame({ + name: "explorer", + }); + + // Create new database and collection + await explorer.click('[data-test="New Collection"]'); + await explorer.fill('[aria-label="New database id"]', databaseId); + await explorer.fill('[aria-label="Collection id"]', containerId); + await explorer.fill('[aria-label="Shard key"]', "pk"); + await explorer.click("#sidePanelOkButton"); + await safeClick(explorer, `.nodeItem >> text=${databaseId}`); + await safeClick(explorer, `.nodeItem >> text=${containerId}`); + // Delete database and collection + await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); + await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); + await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); + await explorer.click('[aria-label="Submit"]'); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); + await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); + await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); + await explorer.click("#sidePanelOkButton"); + await expect(explorer).not.toHaveText(".dataResourceTree", databaseId); + await expect(explorer).not.toHaveText(".dataResourceTree", containerId); +}); From 2f6dbd83f3124a9ce54bd0b58e61b893c1defd63 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 12 May 2021 11:56:24 -0700 Subject: [PATCH 04/12] Fix throughput input component and add database panel (#773) --- .../ThroughputInput/ThroughputInput.test.tsx | 14 - .../ThroughputInput/ThroughputInput.tsx | 15 +- src/Explorer/Panes/AddCollectionPanel.tsx | 26 +- .../AddDatabasePanel/AddDatabasePanel.tsx | 256 ++++++------------ .../AddDatabasePanel.test.tsx.snap | 160 ++++++----- src/Utils/CapabilityUtils.ts | 3 + src/hooks/useSidePanel.ts | 2 +- 7 files changed, 177 insertions(+), 299 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx index 7b59ae003..be6a8636f 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx @@ -19,18 +19,4 @@ describe("ThroughputInput Pane", () => { it("should render Default properly", () => { expect(wrapper).toMatchSnapshot(); }); - - it("test Autoscale Mode select", () => { - wrapper.setProps({ isAutoscaleSelected: true }); - expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toBe( - "Estimate your required RU/s with capacity calculator." - ); - expect(wrapper.find('[aria-label="maxRUDescription"]').at(0).text()).toContain("Max RU/s"); - }); - - it("test Manual Mode select", () => { - wrapper.setProps({ isAutoscaleSelected: false }); - expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toContain("Estimate your required RU/s with"); - expect(wrapper.find('[aria-label="capacityLink"]').at(0).text()).toContain("capacity calculator"); - }); }); diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 3991c0cbd..b0c029d73 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -17,8 +17,6 @@ export interface ThroughputInputProps { setThroughputValue: (throughput: number) => void; setIsAutoscale: (isAutoscale: boolean) => void; onCostAcknowledgeChange: (isAcknowledged: boolean) => void; - isAutoscaleSelected?: boolean; - throughput?: number; } export const ThroughputInput: FunctionComponent = ({ @@ -27,12 +25,16 @@ export const ThroughputInput: FunctionComponent = ({ setThroughputValue, setIsAutoscale, isSharded, - isAutoscaleSelected = true, - throughput = AutoPilotUtils.minAutoPilotThroughput, onCostAcknowledgeChange, }: ThroughputInputProps) => { + const [isAutoscaleSelected, setIsAutoScaleSelected] = useState(true); + const [throughput, setThroughput] = useState(AutoPilotUtils.minAutoPilotThroughput); const [isCostAcknowledged, setIsCostAcknowledged] = useState(false); const [throughputError, setThroughputError] = useState(""); + + setIsAutoscale(isAutoscaleSelected); + setThroughputValue(throughput); + const getThroughputLabelText = (): string => { let throughputHeaderText: string; if (isAutoscaleSelected) { @@ -49,6 +51,7 @@ export const ThroughputInput: FunctionComponent = ({ const onThroughputValueChange = (newInput: string): void => { const newThroughput = parseInt(newInput); + setThroughput(newThroughput); setThroughputValue(newThroughput); if (!isSharded && newThroughput > 10000) { setThroughputError("Unsharded collections support up to 10,000 RUs"); @@ -82,9 +85,13 @@ export const ThroughputInput: FunctionComponent = ({ const handleOnChangeMode = (event: React.ChangeEvent, mode: string): void => { if (mode === "Autoscale") { + setThroughput(AutoPilotUtils.minAutoPilotThroughput); + setIsAutoScaleSelected(true); setThroughputValue(AutoPilotUtils.minAutoPilotThroughput); setIsAutoscale(true); } else { + setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400); + setIsAutoScaleSelected(false); setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400); setIsAutoscale(false); } diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 6220e0099..0af7c3225 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -25,7 +25,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getCollectionName } from "../../Utils/APITypeUtils"; -import { isCapabilityEnabled } from "../../Utils/CapabilityUtils"; +import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils"; import { getUpsellMessage } from "../../Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; @@ -182,7 +182,7 @@ export class AddCollectionPanel extends React.Component - {!this.isServerlessAccount() && ( + {!isServerlessAccount() && ( )} - {!this.isServerlessAccount() && this.state.isSharedThroughputChecked && ( + {!isServerlessAccount() && this.state.isSharedThroughputChecked && ( (this.newDatabaseThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} @@ -398,7 +396,7 @@ export class AddCollectionPanel extends React.Component) => { if ( userContext.apiType !== "Mongo" && - this.state.partitionKey === "" && + !this.state.partitionKey && !event.target.value.startsWith("/") ) { this.setState({ partitionKey: "/" + event.target.value }); @@ -410,7 +408,7 @@ export class AddCollectionPanel extends React.Component )} - {!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( + {!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( (this.collectionThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} @@ -755,14 +751,8 @@ export class AddCollectionPanel extends React.Component capability.name === Constants.CapabilityNames.EnableServerless - ); - } - private getSharedThroughputDefault(): boolean { - return userContext.subscriptionType !== SubscriptionType.EA && !this.isServerlessAccount(); + return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount(); } private getFreeTierIndexingText(): string { @@ -800,7 +790,7 @@ export class AddCollectionPanel extends React.Component = ({ closePanel, openNotificationConsole, }: AddDatabasePaneProps) => { + let throughput: number; + let isAutoscaleSelected: boolean; + let isCostAcknowledged: boolean; const { subscriptionType } = userContext; - const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled()); - const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => { - if (isAutoPilotSelected && maxAutoPilotThroughputSet) { - return { - maxThroughput: maxAutoPilotThroughputSet * 1, - }; - } - return undefined; - }; - const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; @@ -52,61 +45,14 @@ export const AddDatabasePanel: FunctionComponent = ({ } is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`; const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; - const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState(getSharedThroughputDefault); + const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState( + subscriptionType !== SubscriptionType.EA && !isServerlessAccount() + ); const [formErrorsDetails, setFormErrorsDetails] = useState(); const [formErrors, setFormErrors] = useState(""); - - const [isAutoPilotSelected, setIsAutoPilotSelected] = useState(container.isAutoscaleDefaultEnabled()); - - const throughputDefaults = container.collectionCreationDefaults.throughput; - const [throughput, setThroughput] = useState( - isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared - ); - - const [throughputSpendAck, setThroughputSpendAck] = useState(false); - - const canRequestSupport = () => { - if ( - configContext.platform !== Platform.Emulator && - !userContext.isTryCosmosDBSubscription && - configContext.platform !== Platform.Portal - ) { - const offerThroughput: number = throughput; - return offerThroughput <= 100000; - } - - return false; - }; - const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; - const upsellMessage: string = PricingUtils.getUpsellMessage( - userContext.portalEnv, - isFreeTierAccount, - container.isFirstResourceCreated(), - false - ); - - const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing; - - const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details"; - const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput; - - const canConfigureThroughput = !container.isServerlessEnabled(); - const showUpsellMessage = () => { - if (container.isServerlessEnabled()) { - return false; - } - - if (isFreeTierAccount) { - return databaseCreateNewShared; - } - - return true; - }; const [isExecuting, setIsExecuting] = useState(false); - useEffect(() => { - setDatabaseCreateNewShared(getSharedThroughputDefault); - }, [subscriptionType]); + const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; const addDatabasePaneMessage = { database: { @@ -126,7 +72,7 @@ export const AddDatabasePanel: FunctionComponent = ({ subscriptionType: SubscriptionType[subscriptionType], subscriptionQuotaId: userContext.quotaId, defaultsCheck: { - throughput: throughput, + throughput, flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, @@ -139,11 +85,9 @@ export const AddDatabasePanel: FunctionComponent = ({ return; } - const offerThroughput: number = _computeOfferThroughput(); - const addDatabasePaneStartMessage = { ...addDatabasePaneMessage, - offerThroughput, + throughput, }; const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage); setFormErrors(""); @@ -153,18 +97,18 @@ export const AddDatabasePanel: FunctionComponent = ({ databaseId: addDatabasePaneStartMessage.database.id, databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, }; - if (isAutoPilotSelected) { - createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput; + if (isAutoscaleSelected) { + createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.throughput; } else { - createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput; + createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.throughput; } createDatabase(createDatabaseParams).then( () => { - _onCreateDatabaseSuccess(offerThroughput, startKey); + _onCreateDatabaseSuccess(throughput, startKey); }, (error: string) => { - _onCreateDatabaseFailure(error, offerThroughput, startKey); + _onCreateDatabaseFailure(error, throughput, startKey); } ); }; @@ -194,48 +138,19 @@ export const AddDatabasePanel: FunctionComponent = ({ TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); }; - const _getThroughput = (): number => { - return isNaN(throughput) ? 0 : Number(throughput); - }; - - const _computeOfferThroughput = (): number => { - if (!canConfigureThroughput) { - return undefined; - } - - return _getThroughput(); - }; - const _isValid = (): boolean => { // TODO add feature flag that disables validation for customers with custom accounts - if (isAutoPilotSelected) { - const autoPilot = _isAutoPilotSelectedAndWhatTier(); - if ( - !autoPilot || - !autoPilot.maxThroughput || - !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) - ) { + if (isAutoscaleSelected) { + if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) { setFormErrors( `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` ); return false; } } - const throughput = _getThroughput(); - if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) { - setFormErrors(`Please acknowledge the estimated daily spend.`); - return false; - } - - const autoscaleThroughput = maxAutoPilotThroughputSet * 1; - - if ( - isAutoPilotSelected && - autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !throughputSpendAck - ) { - setFormErrors(`Please acknowledge the estimated monthly spend.`); + if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { + setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`); return false; } @@ -250,7 +165,7 @@ export const AddDatabasePanel: FunctionComponent = ({ ); const props: RightPaneFormProps = { - expandConsole: container.expandConsole, + expandConsole: openNotificationConsole, formError: formErrors, formErrorDetail: formErrorsDetails, isExecuting, @@ -260,81 +175,66 @@ export const AddDatabasePanel: FunctionComponent = ({ return ( -
- {showUpsellMessage && formErrors === "" && ( - + )} +
+
+ + * + {databaseIdLabel} + {databaseIdTooltipText} + + + - )} -
-
-

- * - {databaseIdLabel} - {databaseIdTooltipText} -

- + setDatabaseCreateNewShared(!databaseCreateNewShared)} /> + {databaseLevelThroughputTooltipText} + -
- setDatabaseCreateNewShared(!databaseCreateNewShared)} - />{" "} - {databaseLevelThroughputTooltipText} -
- {databaseCreateNewShared && ( -
- setThroughput(throughput)} - setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)} - onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)} - /> - - {canRequestSupport() && ( -

- - Contact support{" "} - - for more than {throughputDefaults.unlimitedmax?.toLocaleString()} RU/s. -

- )} -
- )} -
+ {!isServerlessAccount() && databaseCreateNewShared && ( + (throughput = newThroughput)} + setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)} + onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} + /> + )}
diff --git a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap index bdc4f1526..2c5343436 100644 --- a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap +++ b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap @@ -9,95 +9,87 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` submitButtonText="OK" >
- -
-
-

- - * - - - Database id - - - A database is a logical container of one or more collections - -

- + + + * + + + Database id + + + A database is a logical container of one or more collections + + + + + -
- - - - Provisioned throughput at the database level will be shared across all collections within the database. - -
-
- -
-
+ title="Provision shared throughput" + /> + + Provisioned throughput at the database level will be shared across all collections within the database. + + +
diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 8e04e044c..040766219 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -1,4 +1,7 @@ +import * as Constants from "../Common/Constants"; import { userContext } from "../UserContext"; export const isCapabilityEnabled = (capabilityName: string): boolean => userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName); + +export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); diff --git a/src/hooks/useSidePanel.ts b/src/hooks/useSidePanel.ts index 67cb1b200..e5d93c84b 100644 --- a/src/hooks/useSidePanel.ts +++ b/src/hooks/useSidePanel.ts @@ -18,7 +18,7 @@ export const useSidePanel = (): SidePanelHooks => { setHeaderText(headerText); setPanelContent(panelContent); setIsPanelOpen(true); - setOnCloseCallback({ callback: onClose }); + !!onClose && setOnCloseCallback({ callback: onClose }); }; const closeSidePanel = (): void => { From 14e58e551964a784f4987fe03397552493b0343c Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 12 May 2021 17:12:03 -0700 Subject: [PATCH 05/12] Batch of small fixes for RightPaneForm and AddDatabasePane components (#780) --- .../ThroughputInput/ThroughputInput.test.tsx | 10 +++++ .../ThroughputInput/ThroughputInput.tsx | 2 +- .../ThroughputInput.test.tsx.snap | 2 + src/Explorer/Explorer.tsx | 2 +- .../AddDatabasePanel/AddDatabasePanel.tsx | 3 -- .../Panes/PanelInfoErrorComponent.tsx | 38 +++++++++---------- .../RightPaneForm/RightPaneForm.test.tsx | 1 - .../Panes/RightPaneForm/RightPaneForm.tsx | 21 +++++----- .../__snapshots__/RightPaneForm.test.tsx.snap | 8 ---- .../Panes/SettingsPane/SettingsPane.tsx | 5 +-- .../__snapshots__/SettingsPane.test.tsx.snap | 2 - .../Panes/UploadFilePane/UploadFilePane.tsx | 7 +--- .../Panes/UploadItemsPane/UploadItemsPane.tsx | 7 +--- .../UploadItemsPane.test.tsx.snap | 1 - 14 files changed, 44 insertions(+), 65 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx index be6a8636f..899a87329 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx @@ -19,4 +19,14 @@ describe("ThroughputInput Pane", () => { it("should render Default properly", () => { expect(wrapper).toMatchSnapshot(); }); + + it("should switch mode properly", () => { + wrapper.find('[aria-label="Manual mode"]').simulate("change"); + expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe( + "Container throughput (400 - unlimited RU/s)" + ); + + wrapper.find('[aria-label="Autoscale mode"]').simulate("change"); + expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)"); + }); }); diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index b0c029d73..8b20d32db 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -101,7 +101,7 @@ export const ThroughputInput: FunctionComponent = ({
- + {getThroughputLabelText()} {PricingUtils.getRuToolTipText()} diff --git a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap index cf430a1e0..4b0afb1ee 100644 --- a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap +++ b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap @@ -25,6 +25,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = ` *  this.expandConsole()} closePanel={this.closeSidePanel} /> ); diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 6b8fbe08c..e1de2613b 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -48,7 +48,6 @@ export const AddDatabasePanel: FunctionComponent = ({ const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState( subscriptionType !== SubscriptionType.EA && !isServerlessAccount() ); - const [formErrorsDetails, setFormErrorsDetails] = useState(); const [formErrors, setFormErrors] = useState(""); const [isExecuting, setIsExecuting] = useState(false); @@ -128,7 +127,6 @@ export const AddDatabasePanel: FunctionComponent = ({ setIsExecuting(false); const errorMessage = getErrorMessage(error); setFormErrors(errorMessage); - setFormErrorsDetails(errorMessage); const addDatabasePaneFailedMessage = { ...addDatabasePaneMessage, offerThroughput, @@ -167,7 +165,6 @@ export const AddDatabasePanel: FunctionComponent = ({ const props: RightPaneFormProps = { expandConsole: openNotificationConsole, formError: formErrors, - formErrorDetail: formErrorsDetails, isExecuting, submitButtonText: "OK", onSubmit, diff --git a/src/Explorer/Panes/PanelInfoErrorComponent.tsx b/src/Explorer/Panes/PanelInfoErrorComponent.tsx index 2ae836f64..50f6745ea 100644 --- a/src/Explorer/Panes/PanelInfoErrorComponent.tsx +++ b/src/Explorer/Panes/PanelInfoErrorComponent.tsx @@ -8,7 +8,6 @@ export interface PanelInfoErrorProps { link?: string; linkText?: string; openNotificationConsole?: () => void; - formError?: boolean; } export const PanelInfoErrorComponent: React.FunctionComponent = ({ @@ -18,7 +17,6 @@ export const PanelInfoErrorComponent: React.FunctionComponent { let icon: JSX.Element; if (messageType === "error") { @@ -30,25 +28,23 @@ export const PanelInfoErrorComponent: React.FunctionComponent - {icon} - - - {message} - {link && linkText && ( - - {linkText} - - )} - - {showErrorDetails && ( - - More details - + + {icon} + + + {message} + {link && linkText && ( + + {linkText} + )} - - - ) + + {showErrorDetails && ( + + More details + + )} + + ); }; diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx index a2f3423e1..f35df1caa 100644 --- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx +++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx @@ -9,7 +9,6 @@ const expandConsole = jest.fn(); const props = { expandConsole, formError: "", - formErrorDetail: "", isExecuting: false, submitButtonText: "Load", onSubmit, diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx index 774399b23..7f8e14134 100644 --- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx +++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx @@ -1,12 +1,11 @@ import React, { FunctionComponent, ReactNode } from "react"; import { PanelFooterComponent } from "../PanelFooterComponent"; -import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent"; +import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelLoadingScreen } from "../PanelLoadingScreen"; export interface RightPaneFormProps { expandConsole: () => void; formError: string; - formErrorDetail: string; isExecuting: boolean; onSubmit: () => void; submitButtonText: string; @@ -17,7 +16,6 @@ export interface RightPaneFormProps { export const RightPaneForm: FunctionComponent = ({ expandConsole, formError, - formErrorDetail, isExecuting, onSubmit, submitButtonText, @@ -29,18 +27,17 @@ export const RightPaneForm: FunctionComponent = ({ onSubmit(); }; - const panelInfoErrorProps: PanelInfoErrorProps = { - messageType: "error", - message: formError, - formError: formError !== "", - showErrorDetails: formErrorDetail !== "", - openNotificationConsole: expandConsole, - }; - return ( <> -
+ {formError && ( + + )} {children} {!isSubmitButtonHidden && } diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap index 30a9e3107..3438c3398 100644 --- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap +++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap @@ -4,18 +4,10 @@ exports[`Right Pane Form should render Default properly 1`] = ` -
= ({ expandConsole, closePanel, }: SettingsPaneProps) => { - const [formErrors, setFormErrors] = useState(""); const [isExecuting, setIsExecuting] = useState(false); const [pageOption, setPageOption] = useState( LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage @@ -50,7 +49,6 @@ export const SettingsPane: FunctionComponent = ({ const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const handlerOnSubmit = (e: MouseEvent) => { - setFormErrors(""); setIsExecuting(true); LocalStorageUtility.setEntryNumber( @@ -104,8 +102,7 @@ export const SettingsPane: FunctionComponent = ({ const genericPaneProps: RightPaneFormProps = { expandConsole, - formError: formErrors, - formErrorDetail: "", + formError: "", isExecuting, submitButtonText: "Apply", onSubmit: () => handlerOnSubmit(undefined), diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index a7eb35a83..0af71bf15 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -4,7 +4,6 @@ exports[`Settings Pane should render Default properly 1`] = ` = ({ const [files, setFiles] = useState(); const [formErrors, setFormErrors] = useState(""); - const [formErrorsDetails, setFormErrorsDetails] = useState(""); const [isExecuting, setIsExecuting] = useState(false); const submit = () => { setFormErrors(""); - setFormErrorsDetails(""); if (!files || files.length === 0) { - setFormErrors("No file specified"); - setFormErrorsDetails("No file specified. Please input a file."); + setFormErrors("No file specified. Please input a file."); logConsoleError(`${errorMessage} -- No file specified. Please input a file.`); return; } @@ -49,7 +46,6 @@ export const UploadFilePane: FunctionComponent = ({ }, (error: string) => { setFormErrors(errorMessage); - setFormErrorsDetails(`${errorMessage}: ${error}`); logConsoleError(`${errorMessage} ${file.name}: ${error}`); } ) @@ -85,7 +81,6 @@ export const UploadFilePane: FunctionComponent = ({ const genericPaneProps: RightPaneFormProps = { expandConsole, formError: formErrors, - formErrorDetail: formErrorsDetails, isExecuting: isExecuting, submitButtonText: "Upload", onSubmit: submit, diff --git a/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx b/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx index e2ef3ddbc..b6a62ee5c 100644 --- a/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx +++ b/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx @@ -15,15 +15,14 @@ export const UploadItemsPane: FunctionComponent = ({ explo const [files, setFiles] = useState(); const [uploadFileData, setUploadFileData] = useState([]); const [formError, setFormError] = useState(""); - const [formErrorDetail, setFormErrorDetail] = useState(""); const [isExecuting, setIsExecuting] = useState(); const onSubmit = () => { setFormError(""); if (!files || files.length === 0) { - setFormError("No files specified"); - setFormErrorDetail("No files were specified. Please input at least one file."); + setFormError("No files were specified. Please input at least one file."); logConsoleError("Could not upload items -- No files were specified. Please input at least one file."); + return; } const selectedCollection = explorer.findSelectedCollection(); @@ -40,7 +39,6 @@ export const UploadItemsPane: FunctionComponent = ({ explo (error: Error) => { const errorMessage = getErrorMessage(error); setFormError(errorMessage); - setFormErrorDetail(errorMessage); } ) .finally(() => { @@ -55,7 +53,6 @@ export const UploadItemsPane: FunctionComponent = ({ explo const genericPaneProps: RightPaneFormProps = { expandConsole: () => explorer.expandConsole(), formError, - formErrorDetail, isExecuting: isExecuting, submitButtonText: "Upload", onSubmit, diff --git a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap index c635538f7..5c34e367c 100644 --- a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap +++ b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap @@ -4,7 +4,6 @@ exports[`Upload Items Pane should render Default properly 1`] = ` From d76aaca0ddc9781f090432a6d2b89cdc260cdf5e Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 12 May 2021 17:13:15 -0700 Subject: [PATCH 06/12] Improve lazy load database/collection offer logic (#768) --- src/Explorer/Tree/Collection.ts | 5 ++++- src/Explorer/Tree/Database.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index f92871f27..b7820d35d 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -90,6 +90,7 @@ export default class Collection implements ViewModels.Collection { public storedProceduresFocused: ko.Observable; public userDefinedFunctionsFocused: ko.Observable; public triggersFocused: ko.Observable; + private isOfferRead: boolean; constructor(container: Explorer, databaseId: string, data: DataModels.Collection) { this.nodeKind = "Collection"; @@ -201,6 +202,7 @@ export default class Collection implements ViewModels.Collection { this.isStoredProceduresExpanded = ko.observable(false); this.isUserDefinedFunctionsExpanded = ko.observable(false); this.isTriggersExpanded = ko.observable(false); + this.isOfferRead = false; } public expandCollapseCollection() { @@ -1143,7 +1145,7 @@ export default class Collection implements ViewModels.Collection { } public async loadOffer(): Promise { - if (!this.container.isServerlessEnabled() && !this.offer()) { + if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) { const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { databaseName: this.databaseId, collectionName: this.id(), @@ -1158,6 +1160,7 @@ export default class Collection implements ViewModels.Collection { try { this.offer(await readCollectionOffer(params)); this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id())); + this.isOfferRead = true; TelemetryProcessor.traceSuccess( Action.LoadOffers, diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index 84911515b..f9cc2e605 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -30,6 +30,7 @@ export default class Database implements ViewModels.Database { public isDatabaseShared: ko.Computed; public selectedSubnodeKind: ko.Observable; public junoClient: JunoClient; + private isOfferRead: boolean; constructor(container: Explorer, data: any) { this.nodeKind = "Database"; @@ -45,6 +46,7 @@ export default class Database implements ViewModels.Database { return this.offer && !!this.offer(); }); this.junoClient = new JunoClient(); + this.isOfferRead = false; } public onSettingsClick = () => { @@ -214,12 +216,13 @@ export default class Database implements ViewModels.Database { } public async loadOffer(): Promise { - if (!this.container.isServerlessEnabled() && !this.offer()) { + if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) { const params: DataModels.ReadDatabaseOfferParams = { databaseId: this.id(), databaseResourceId: this.self, }; this.offer(await readDatabaseOffer(params)); + this.isOfferRead = true; } } From 2d506f03124d4ef2595529d2d5e0fc831dcbb9d1 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 13 May 2021 05:53:10 +0530 Subject: [PATCH 07/12] Add Files to TypeScript Strict Mode (#777) --- src/Common/Tooltip/InfoTooltip.tsx | 2 +- src/Explorer/Controls/Dialog.tsx | 2 +- src/Explorer/Controls/Header/GalleryHeaderComponent.tsx | 4 ++-- src/Explorer/Notebook/NotebookRenderer/Prompt.tsx | 2 +- .../Notebook/NotebookRenderer/decorators/CellLabeler.tsx | 7 +++---- src/hooks/useGraphPhoto.tsx | 2 +- tsconfig.strict.json | 9 ++++++++- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Common/Tooltip/InfoTooltip.tsx b/src/Common/Tooltip/InfoTooltip.tsx index c16e22ce8..480aa9020 100644 --- a/src/Common/Tooltip/InfoTooltip.tsx +++ b/src/Common/Tooltip/InfoTooltip.tsx @@ -5,7 +5,7 @@ export interface TooltipProps { children: string; } -export const InfoTooltip: React.FunctionComponent = ({ children }: TooltipProps) => { +export const InfoTooltip: React.FunctionComponent = ({ children }: TooltipProps) => { return ( diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index 779cdb049..3481e4a1f 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -103,7 +103,7 @@ export const Dialog: FunctionComponent = ({ text: secondaryButtonText, onClick: onSecondaryButtonClick, } - : undefined; + : {}; return ( diff --git a/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx b/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx index 4f5489d98..a3c7cc2f7 100644 --- a/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx +++ b/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx @@ -1,5 +1,5 @@ +import { CommandButton, FontIcon, FontWeights, ITextProps, Separator, Stack, Text } from "@fluentui/react"; import * as React from "react"; -import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "@fluentui/react"; export class GalleryHeaderComponent extends React.Component { private static readonly azureText = "Microsoft Azure"; @@ -61,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component { {this.renderHeaderItem( GalleryHeaderComponent.galleryText, - undefined, + () => "", GalleryHeaderComponent.headerItemTextProps )} diff --git a/src/Explorer/Notebook/NotebookRenderer/Prompt.tsx b/src/Explorer/Notebook/NotebookRenderer/Prompt.tsx index af81cd347..b8d95fa67 100644 --- a/src/Explorer/Notebook/NotebookRenderer/Prompt.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/Prompt.tsx @@ -53,7 +53,7 @@ export class PromptPure extends React.Component { } } -const makeMapStateToProps = (state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => { +const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => { const mapStateToProps = (state: CdbAppState) => { const { contentRef, id } = ownProps; const model = selectors.model(state, { contentRef }); diff --git a/src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx b/src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx index 45fc1cde9..cfef39026 100644 --- a/src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx @@ -1,10 +1,9 @@ +import { AppState, ContentRef, DocumentRecordProps, selectors } from "@nteract/core"; +import { RecordOf } from "immutable"; import React from "react"; import { connect } from "react-redux"; import "./CellLabeler.less"; -import { AppState, ContentRef, selectors, DocumentRecordProps } from "@nteract/core"; -import { RecordOf } from "immutable"; - interface ComponentProps { id: string; contentRef: ContentRef; // TODO: Make this per contentRef? @@ -29,7 +28,7 @@ class CellLabeler extends React.Component { } } -const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => { +const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => { const mapStateToProps = (state: AppState) => { const model = selectors.model(state, { contentRef: ownProps.contentRef }); const cellOrder = selectors.notebook.cellOrder(model as RecordOf); diff --git a/src/hooks/useGraphPhoto.tsx b/src/hooks/useGraphPhoto.tsx index 1afdc56cb..e09efeb04 100644 --- a/src/hooks/useGraphPhoto.tsx +++ b/src/hooks/useGraphPhoto.tsx @@ -18,7 +18,7 @@ export async function fetchPhoto(accessToken: string): Promise { } export function useGraphPhoto(graphToken: string): string { - const [photo, setPhoto] = useState(); + const [photo, setPhoto] = useState(""); useEffect(() => { if (graphToken) { diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 15c304aec..7443dd212 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -8,6 +8,13 @@ "noUnusedParameters": true }, "files": [ + "./src/hooks/useGraphPhoto.tsx", + "./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx", + "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", + "./src/Explorer/Controls/Header/GalleryHeaderComponent.tsx", + "./src/Explorer/Controls/Header/GalleryHeaderComponent.tsx", + "./src/Explorer/Controls/Dialog.tsx", + "./src/Common/Tooltip/InfoTooltip.tsx", "./src/AuthType.ts", "./src/Bindings/ReactBindingHandler.ts", "./src/Common/ArrayHashMap.ts", @@ -129,4 +136,4 @@ "src/Terminal/**/*", "src/Utils/arm/**/*" ] -} +} \ No newline at end of file From 8e6d274b117dadd0014ffd57caaea08d802814b1 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 13 May 2021 06:26:48 +0530 Subject: [PATCH 08/12] Add Files to TypeScript Strict (#776) Co-authored-by: Steve Faulkner --- .../Panes/PanelInfoErrorComponent.tsx | 2 +- tsconfig.strict.json | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Explorer/Panes/PanelInfoErrorComponent.tsx b/src/Explorer/Panes/PanelInfoErrorComponent.tsx index 50f6745ea..43fc978e9 100644 --- a/src/Explorer/Panes/PanelInfoErrorComponent.tsx +++ b/src/Explorer/Panes/PanelInfoErrorComponent.tsx @@ -18,7 +18,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent { - let icon: JSX.Element; + let icon: JSX.Element = ; if (messageType === "error") { icon = ; } else if (messageType === "warning") { diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 7443dd212..3ff54e123 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -8,13 +8,6 @@ "noUnusedParameters": true }, "files": [ - "./src/hooks/useGraphPhoto.tsx", - "./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx", - "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", - "./src/Explorer/Controls/Header/GalleryHeaderComponent.tsx", - "./src/Explorer/Controls/Header/GalleryHeaderComponent.tsx", - "./src/Explorer/Controls/Dialog.tsx", - "./src/Common/Tooltip/InfoTooltip.tsx", "./src/AuthType.ts", "./src/Bindings/ReactBindingHandler.ts", "./src/Common/ArrayHashMap.ts", @@ -43,6 +36,7 @@ "./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SubscriptionType.ts", "./src/Contracts/Versions.ts", + "./src/Explorer/Controls/Dialog.tsx", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", @@ -60,12 +54,16 @@ "./src/Explorer/Notebook/NotebookContentClient.ts", "./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", + "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", + "./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", + "./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx", "./src/Explorer/Notebook/NotebookUtil.ts", "./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.tsx", "./src/Explorer/Panes/PaneComponents.ts", "./src/Explorer/Panes/PanelFooterComponent.tsx", + "./src/Explorer/Panes/PanelInfoErrorComponent.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", @@ -81,6 +79,8 @@ "./src/Index.ts", "./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts", "./src/Platform/Hosted/Authorization.ts", + "./src/Platform/Hosted/Components/MeControl.test.tsx", + "./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx", "./src/Platform/Hosted/extractFeatures.test.ts", "./src/Platform/Hosted/extractFeatures.ts", @@ -114,6 +114,7 @@ "./src/Utils/WindowUtils.ts", "./src/hooks/useDirectories.tsx", "./src/hooks/useFullScreenURLs.tsx", + "./src/hooks/useGraphPhoto.tsx", "./src/hooks/useObservable.ts", "./src/i18n.ts", "./src/quickstart.ts", @@ -122,13 +123,16 @@ ], "include": [ "src/CellOutputViewer/transforms/**/*", + "src/Common/Tooltip/**/*", "src/Controls/**/*", "src/Definitions/**/*", "src/Explorer/Controls/ErrorDisplayComponent/**/*", + "src/Explorer/Controls/Header/**/*", "src/Explorer/Controls/RadioSwitchComponent/**/*", "src/Explorer/Controls/ResizeSensorReactComponent/**/*", "src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*", "src/Explorer/Notebook/NotebookComponent/__mocks__/**/*", + "src/Explorer/Panes/RightPaneForm/**/*", "src/Libs/**/*", "src/Localization/**/*", "src/Platform/Emulator/**/*", @@ -136,4 +140,4 @@ "src/Terminal/**/*", "src/Utils/arm/**/*" ] -} \ No newline at end of file +} From d7c62ac7f17501f3c8b85943d3f190ac07ff1cf8 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 13 May 2021 06:33:52 +0530 Subject: [PATCH 09/12] Migrate Collapse/Open Resource Tree to React (#733) Co-authored-by: Steve Faulkner --- less/documentDB.less | 6 +- less/tree.less | 1 + src/Common/CollapsedResourceTree.tsx | 35 ++++++ src/Common/ResourceTree.tsx | 59 ++++++++++ .../SettingsComponent.test.tsx.snap | 8 -- src/Explorer/Explorer.tsx | 23 +--- .../GitHubReposPanel.test.tsx.snap | 2 - .../StringInputPane.test.tsx.snap | 2 - ...eteDatabaseConfirmationPanel.test.tsx.snap | 2 - src/Main.tsx | 106 +++--------------- 10 files changed, 120 insertions(+), 124 deletions(-) create mode 100644 src/Common/CollapsedResourceTree.tsx create mode 100644 src/Common/ResourceTree.tsx diff --git a/less/documentDB.less b/less/documentDB.less index e58206818..5fc2f8571 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2099,7 +2099,7 @@ a:link { display: flex; flex: 1 1 auto; overflow-x: auto; - overflow-y: auto; + overflow-y: hidden; height: 100%; } @@ -3085,3 +3085,7 @@ settings-pane { padding-left: @SmallSpace; } } +.hiddenMain { + visibility: hidden; + height: 0px; +} \ No newline at end of file diff --git a/less/tree.less b/less/tree.less index 56a5ee38e..e60bcf69c 100644 --- a/less/tree.less +++ b/less/tree.less @@ -3,6 +3,7 @@ .resourceTree { height: 100%; + width: 20%; flex: 0 0 auto; .main { height: 100%; diff --git a/src/Common/CollapsedResourceTree.tsx b/src/Common/CollapsedResourceTree.tsx new file mode 100644 index 000000000..ea1ee6a32 --- /dev/null +++ b/src/Common/CollapsedResourceTree.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent } from "react"; +import arrowLeftImg from "../../images/imgarrowlefticon.svg"; + +export interface CollapsedResourceTreeProps { + toggleLeftPaneExpanded: () => void; + isLeftPaneExpanded: boolean; +} + +export const CollapsedResourceTree: FunctionComponent = ({ + toggleLeftPaneExpanded, + isLeftPaneExpanded, +}: CollapsedResourceTreeProps): JSX.Element => { + return ( +
+
+
    +
  • + + Expand + + + + +
  • +
+
+
+ ); +}; diff --git a/src/Common/ResourceTree.tsx b/src/Common/ResourceTree.tsx new file mode 100644 index 000000000..9aae283ea --- /dev/null +++ b/src/Common/ResourceTree.tsx @@ -0,0 +1,59 @@ +import React, { FunctionComponent } from "react"; +import arrowLeftImg from "../../images/imgarrowlefticon.svg"; +import refreshImg from "../../images/refresh-cosmos.svg"; +import { AuthType } from "../AuthType"; +import { userContext } from "../UserContext"; + +export interface ResourceTreeProps { + toggleLeftPaneExpanded: () => void; + isLeftPaneExpanded: boolean; +} + +export const ResourceTree: FunctionComponent = ({ + toggleLeftPaneExpanded, + isLeftPaneExpanded, +}: ResourceTreeProps): JSX.Element => { + return ( +
+ {/* Collections Window - - Start */} +
+ {/* Collections Window Title/Command Bar - Start */} +
+
+ +
+ + Refresh tree + + + Hide + +
+
+
+ {userContext.authType === AuthType.ResourceToken ? ( +
+ ) : ( +
+ )} +
+ {/* Collections Window - End */} +
+ ); +}; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 9eb6d8b7e..d413c0525 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -1051,7 +1051,6 @@ exports[`SettingsComponent renders 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -1118,7 +1117,6 @@ exports[`SettingsComponent renders 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], }, "databaseId": "test", "defaultTtl": [Function], @@ -2163,7 +2161,6 @@ exports[`SettingsComponent renders 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -2230,7 +2227,6 @@ exports[`SettingsComponent renders 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], } } isAutoPilotSelected={false} @@ -3288,7 +3284,6 @@ exports[`SettingsComponent renders 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -3355,7 +3350,6 @@ exports[`SettingsComponent renders 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], }, "databaseId": "test", "defaultTtl": [Function], @@ -4400,7 +4394,6 @@ exports[`SettingsComponent renders 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -4467,7 +4460,6 @@ exports[`SettingsComponent renders 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], } } geospatialConfigType="Geometry" diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 40c04bc61..b3451c0e7 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -134,7 +134,6 @@ export default class Explorer { public databases: ko.ObservableArray; public selectedDatabaseId: ko.Computed; public selectedCollectionId: ko.Computed; - public isLeftPaneExpanded: ko.Observable; public selectedNode: ko.Observable; private resourceTree: ResourceTreeAdapter; @@ -231,6 +230,7 @@ export default class Explorer { }); } }); + this.isNotebooksEnabledForAccount = ko.observable(false); this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); this.isSparkEnabledForAccount = ko.observable(false); @@ -335,7 +335,6 @@ export default class Explorer { } return true; }); - this.isLeftPaneExpanded = ko.observable(true); this.selectedNode = ko.observable(); this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { // Make sure switching tabs restores tabs display @@ -675,16 +674,8 @@ export default class Explorer { this.setIsNotificationConsoleExpanded(true); } - public toggleLeftPaneExpanded() { - this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); - - if (this.isLeftPaneExpanded()) { - document.getElementById("expandToggleLeftPaneButton").focus(); - this.splitter.expandLeft(); - } else { - document.getElementById("collapseToggleLeftPaneButton").focus(); - this.splitter.collapseLeft(); - } + public collapseConsole(): void { + this.setIsNotificationConsoleExpanded(false); } public refreshDatabaseForResourceToken(): Q.Promise { @@ -803,14 +794,6 @@ export default class Explorer { this.refreshNotebookList(); }; - public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.toggleLeftPaneExpanded(); - return false; - } - return true; - }; - // Facade public provideFeedbackEmail = () => { window.open(Constants.Urls.feedbackEmail, "_blank"); diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 223e8e63b..5da784af4 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -1040,7 +1040,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -1107,7 +1106,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], }, "getRepo": [Function], "pinRepo": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index b05197cb9..f779e8b74 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -1030,7 +1030,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isAutoscaleDefaultEnabled": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isHostedDataExplorerEnabled": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -1097,7 +1096,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], } } inProgressMessage="Creating directory " diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 0e7a59725..ace1945cb 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -1030,7 +1030,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "isHostedDataExplorerEnabled": [Function], "isLastCollection": [Function], "isLastNonEmptyDatabase": [Function], - "isLeftPaneExpanded": [Function], "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], @@ -1099,7 +1098,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "activeTab": [Function], "openedTabs": [Function], }, - "toggleLeftPaneExpandedKeyPress": [Function], } } openNotificationConsole={[Function]} diff --git a/src/Main.tsx b/src/Main.tsx index 57ce3b5bd..2abf3d2f5 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -14,8 +14,6 @@ import "../externals/jquery.typeahead.min.js"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/favicon.ico"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; -import arrowLeftImg from "../images/imgarrowlefticon.svg"; -import refreshImg from "../images/refresh-cosmos.svg"; import "../less/documentDB.less"; import "../less/forms.less"; import "../less/infobox.less"; @@ -27,7 +25,8 @@ import "../less/TableStyles/EntityEditor.less"; import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/queryBuilder.less"; import "../less/tree.less"; -import { AuthType } from "./AuthType"; +import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; +import { ResourceTree } from "./Common/ResourceTree"; import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; @@ -54,7 +53,6 @@ import { useSidePanel } from "./hooks/useSidePanel"; import { useTabs } from "./hooks/useTabs"; import "./Libs/jquery"; import "./Shared/appInsights"; -import { userContext } from "./UserContext"; initializeIcons(); @@ -63,6 +61,7 @@ const App: React.FunctionComponent = () => { const [notificationConsoleData, setNotificationConsoleData] = useState(undefined); //TODO: Refactor so we don't need to pass the id to remove a console data const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState(""); + const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState(true); const [dialogProps, setDialogProps] = useState(); const [showDialog, setShowDialog] = useState(false); @@ -92,6 +91,15 @@ const App: React.FunctionComponent = () => { const config = useConfig(); const explorer = useKnockoutExplorer(config?.platform, explorerParams); + const toggleLeftPaneExpanded = () => { + setIsLeftPaneExpanded(!isLeftPaneExpanded); + if (isLeftPaneExpanded) { + document.getElementById("expandToggleLeftPaneButton").focus(); + } else { + document.getElementById("collapseToggleLeftPaneButton").focus(); + } + }; + if (!explorer) { return ; } @@ -107,93 +115,13 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree Expanded - Start */} -
- {/* Collections Window - - Start */} -
- {/* Collections Window Title/Command Bar - Start */} -
-
- -
- - - - - Hide - -
-
-
- {userContext.authType === AuthType.ResourceToken ? ( -
- ) : ( -
- )} -
- {/* Collections Window - End */} -
+ {/* Collections Tree Expanded - End */} {/* Collections Tree Collapsed - Start */} -
-
-
    -
  • - - Expand - - - - -
  • -
-
-
+ {/* Collections Tree Collapsed - End */}
{/* Splitter - Start */} From 404b1fc0f12c369fd6f55ed677ceaefe4a7e1324 Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Thu, 13 May 2021 10:34:09 -0700 Subject: [PATCH 10/12] Prep Schema Analyzer for flighting (#760) * Prepare for flighting Schema Analyzer * Rename SchemaAnalyzerComponent -> SchemaAnalyzer * Only show Schema option if notebooks enabled --- src/CellOutputViewer/CellOutputViewer.less | 13 +- src/Common/Constants.ts | 1 + src/Explorer/Explorer.tsx | 4 +- .../InMemoryContentProvider.ts | 108 ++++++++++++ .../InMemoryContentProviderUtils.ts | 15 ++ .../NotebookContentProvider.ts | 14 +- src/Explorer/Notebook/NotebookManager.tsx | 12 +- .../SchemaAnalyzer.less} | 2 +- .../SchemaAnalyzer.tsx} | 159 +++++++++--------- .../SchemaAnalyzer/SchemaAnalyzerAdapter.tsx | 48 ++++++ .../SchemaAnalyzer/SchemaAnalyzerHeader.tsx | 101 +++++++++++ .../SchemaAnalyzerSplashScreen.tsx | 39 +++++ .../SchemaAnalyzer/SchemaAnalyzerUtils.ts | 44 +++++ .../SchemaAnalyzerComponentAdapter.tsx | 88 ---------- src/Explorer/Tabs/SchemaAnalyzerTab.ts | 26 ++- src/Explorer/Tree/Collection.ts | 2 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 8 +- src/Platform/Hosted/extractFeatures.ts | 2 +- src/Shared/Telemetry/TelemetryConstants.ts | 1 + 19 files changed, 502 insertions(+), 185 deletions(-) create mode 100644 src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts create mode 100644 src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts rename src/Explorer/Notebook/{SchemaAnalyzerComponent/SchemaAnalyzerComponent.less => SchemaAnalyzer/SchemaAnalyzer.less} (65%) rename src/Explorer/Notebook/{SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx => SchemaAnalyzer/SchemaAnalyzer.tsx} (55%) create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts delete mode 100644 src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx diff --git a/src/CellOutputViewer/CellOutputViewer.less b/src/CellOutputViewer/CellOutputViewer.less index 2e79facc7..0be451c1a 100644 --- a/src/CellOutputViewer/CellOutputViewer.less +++ b/src/CellOutputViewer/CellOutputViewer.less @@ -1,10 +1,15 @@ .schema-analyzer-cell-outputs { - padding: 10px; + padding: 10px 2px; } +// Mimic FluentUI8's DocumentCard style .schema-analyzer-cell-output { margin-bottom: 20px; - padding: 10px; - border-radius: 2px; - box-shadow: rgba(0, 0, 0, 13%) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 11%) 0px 0.3px 0.9px 0px; + padding: 14px 20px; + border: 1px solid rgb(237, 235, 233); +} + +.schema-analyzer-cell-output:hover { + border-color: rgb(200, 198, 196); + box-shadow: inset 0 0 0 1px rgb(200, 198, 196) } \ No newline at end of file diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 097c4237f..31f537d8a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -94,6 +94,7 @@ export class Flights { public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; + public static readonly SchemaAnalyzer = "schemaanalyzer"; } export class AfecFeatures { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index b3451c0e7..c909c690a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -163,7 +163,6 @@ export default class Explorer { public isMongoIndexingEnabled: ko.Observable; public canExceedMaximumValue: ko.Computed; public isAutoscaleDefaultEnabled: ko.Observable; - public isSchemaEnabled: ko.Computed; // Notebooks @@ -1048,6 +1047,9 @@ export default class Explorer { if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { this.isMongoIndexingEnabled(true); } + if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) { + userContext.features.enableSchemaAnalyzer = true; + } } public findSelectedCollection(): ViewModels.Collection { diff --git a/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts new file mode 100644 index 000000000..9d979324d --- /dev/null +++ b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts @@ -0,0 +1,108 @@ +import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core"; +import { Observable, of } from "rxjs"; +import { AjaxResponse } from "rxjs/ajax"; +import { HttpStatusCodes } from "../../../../Common/Constants"; +import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils"; +import * as Logger from "../../../../Common/Logger"; + +export interface InMemoryContentProviderParams { + [path: string]: { readonly: boolean; content: IContent }; +} + +// Nteract relies on `errno` property to figure out the kind of failure +// That's why we need a custom wrapper around Error to include `errno` property +class InMemoryContentProviderError extends Error { + constructor(error: string, public errno: number = InMemoryContentProvider.SelfErrorCode) { + super(error); + } +} + +export class InMemoryContentProvider implements IContentProvider { + public static readonly SelfErrorCode = 666; + + constructor(private params: InMemoryContentProviderParams) {} + + public remove(): Observable { + return this.errorResponse("Not implemented", "remove"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public get(_config: ServerConfig, uri: string): Observable { + const item = this.params[uri]; + if (item) { + return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content)); + } + + return this.errorResponse(`${uri} not found`, "get"); + } + + public update(): Observable { + return this.errorResponse("Not implemented", "update"); + } + + public create(): Observable { + return this.errorResponse("Not implemented", "create"); + } + + public save( + _config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars + uri: string, + model: Partial> + ): Observable { + const item = this.params[uri]; + if (item) { + if (!item.readonly) { + Object.assign(item.content, model); + } + return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content)); + } + + return this.errorResponse(`${uri} not found`, "save"); + } + + public listCheckpoints(): Observable { + return this.errorResponse("Not implemented", "listCheckpoints"); + } + + public createCheckpoint(): Observable { + return this.errorResponse("Not implemented", "createCheckpoint"); + } + + public deleteCheckpoint(): Observable { + return this.errorResponse("Not implemented", "deleteCheckpoint"); + } + + public restoreFromCheckpoint(): Observable { + return this.errorResponse("Not implemented", "restoreFromCheckpoint"); + } + + private errorResponse(message: string, functionName: string): Observable { + const error = new InMemoryContentProviderError(message); + Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + private createSuccessAjaxResponse(status: number, content: IContent): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status, + response: content ? content : undefined, + responseText: content ? JSON.stringify(content) : undefined, + responseType: "json", + }; + } + + private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status: error.errno, + response: error, + responseText: getErrorMessage(error), + responseType: "json", + }; + } +} diff --git a/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts new file mode 100644 index 000000000..1a4d6c150 --- /dev/null +++ b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts @@ -0,0 +1,15 @@ +// memory:// +// Custom scheme for in memory content +export const ContentUriPattern = /memory:\/\/([^/]*)/; + +export function fromContentUri(contentUri: string): undefined | string { + const matches = contentUri.match(ContentUriPattern); + if (matches && matches.length > 1) { + return matches[1]; + } + return undefined; +} + +export function toContentUri(path: string): string { + return `memory://${path}`; +} diff --git a/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts b/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts index dd16dc364..a40f30979 100644 --- a/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts +++ b/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts @@ -1,11 +1,17 @@ -import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "@nteract/core"; +import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core"; import { Observable } from "rxjs"; import { AjaxResponse } from "rxjs/ajax"; import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider"; +import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils"; export class NotebookContentProvider implements IContentProvider { - constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {} + constructor( + private inMemoryContentProvider: InMemoryContentProvider, + private gitHubContentProvider: GitHubContentProvider, + private jupyterContentProvider: IContentProvider + ) {} public remove(serverConfig: ServerConfig, path: string): Observable { return this.getContentProvider(path).remove(serverConfig, path); @@ -60,6 +66,10 @@ export class NotebookContentProvider implements IContentProvider { } private getContentProvider(path: string): IContentProvider { + if (InMemoryContentProviderUtils.fromContentUri(path)) { + return this.inMemoryContentProvider; + } + if (GitHubUtils.fromContentUri(path)) { return this.gitHubContentProvider; } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 7be295b51..a0f09e3ea 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -22,13 +22,14 @@ import { getFullName } from "../../Utils/UserUtils"; import Explorer from "../Explorer"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; -// import { GitHubReposPane } from "../Panes/GitHubReposPane"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; +import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { SnapshotRequest } from "./NotebookComponent/types"; import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContentClient } from "./NotebookContentClient"; +import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils"; type NotebookPaneContent = string | ImmutableNotebook; @@ -50,6 +51,7 @@ export default class NotebookManager { public notebookClient: NotebookContainerClient; public notebookContentClient: NotebookContentClient; + private inMemoryContentProvider: InMemoryContentProvider; private gitHubContentProvider: GitHubContentProvider; public gitHubOAuthService: GitHubOAuthService; public gitHubClient: GitHubClient; @@ -63,12 +65,20 @@ export default class NotebookManager { this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); this.gitHubClient = new GitHubClient(this.onGitHubClientError); + this.inMemoryContentProvider = new InMemoryContentProvider({ + [SchemaAnalyzerNotebook.path]: { + readonly: true, + content: SchemaAnalyzerNotebook, + }, + }); + this.gitHubContentProvider = new GitHubContentProvider({ gitHubClient: this.gitHubClient, promptForCommitMsg: this.promptForCommitMsg, }); this.notebookContentProvider = new NotebookContentProvider( + this.inMemoryContentProvider, this.gitHubContentProvider, contents.JupyterContentProvider ); diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less similarity index 65% rename from src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less rename to src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less index 1a077c600..9d42c852f 100644 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less @@ -1,4 +1,4 @@ -.schemaAnalyzerComponent { +.schemaAnalyzer { width: 100%; height: 100%; overflow-y: auto; diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx similarity index 55% rename from src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx rename to src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx index 063050b01..cf9810f38 100644 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx @@ -1,22 +1,26 @@ -import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "@fluentui/react"; -import { ImmutableOutput } from "@nteract/commutable"; +import { Spinner, SpinnerSize, Stack } from "@fluentui/react"; +import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable"; import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core"; import Immutable from "immutable"; import * as React from "react"; import { connect } from "react-redux"; import { Dispatch } from "redux"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import loadTransform from "../NotebookComponent/loadTransform"; import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs"; -import "./SchemaAnalyzerComponent.less"; +import "./SchemaAnalyzer.less"; +import { DefaultFilter, DefaultSampleSize, SchemaAnalyzerHeader } from "./SchemaAnalyzerHeader"; +import { SchemaAnalyzerSplashScreen } from "./SchemaAnalyzerSplashScreen"; -interface SchemaAnalyzerComponentPureProps { +interface SchemaAnalyzerPureProps { contentRef: ContentRef; kernelRef: KernelRef; databaseId: string; collectionId: string; } -interface SchemaAnalyzerComponentDispatchProps { +interface SchemaAnalyzerDispatchProps { runCell: (contentRef: ContentRef, cellId: string) => void; addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void; updateCell: (text: string, id: string, contentRef: ContentRef) => void; @@ -24,25 +28,23 @@ interface SchemaAnalyzerComponentDispatchProps { type OutputType = "rich" | "json"; -interface SchemaAnalyzerComponentState { +interface SchemaAnalyzerState { outputType: OutputType; - filter?: string; isFiltering: boolean; + sampleSize: string; } -type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps & - StateProps & - SchemaAnalyzerComponentDispatchProps; +type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps; -export class SchemaAnalyzerComponent extends React.Component< - SchemaAnalyzerComponentProps, - SchemaAnalyzerComponentState -> { - constructor(props: SchemaAnalyzerComponentProps) { +export class SchemaAnalyzer extends React.Component { + private clickAnalyzeTelemetryStartKey: number; + + constructor(props: SchemaAnalyzerProps) { super(props); this.state = { outputType: "rich", isFiltering: false, + sampleSize: DefaultSampleSize, }; } @@ -50,34 +52,59 @@ export class SchemaAnalyzerComponent extends React.Component< loadTransform(this.props); } - private onFilterTextFieldChange = ( - event: React.FormEvent, - newValue?: string - ): void => { - this.setState({ - filter: newValue, - }); - }; - - private onAnalyzeButtonClick = () => { + private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => { const query = { command: "listSchema", database: this.props.databaseId, collection: this.props.collectionId, outputType: this.state.outputType, - filter: this.state.filter, + filter, + sampleSize, }; - if (this.state.filter) { - this.setState({ - isFiltering: true, - }); - } + this.setState({ + isFiltering: true, + }); this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef); + + this.clickAnalyzeTelemetryStartKey = traceStart(Action.SchemaAnalyzerClickAnalyze, { + database: this.props.databaseId, + collection: this.props.collectionId, + sampleSize, + }); + this.props.runCell(this.props.contentRef, this.props.firstCellId); }; + private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List) => { + /** + * CosmosMongoKernel always returns 1st output as "text/html" + * This output can be an error stack or information about how many documents were sampled + */ + let firstTextHtmlOutput: string; + if (outputs.size > 0 && outputs.get(0).output_type === "execute_result") { + const executeResult = outputs.get(0) as ImmutableExecuteResult; + firstTextHtmlOutput = executeResult.data["text/html"]; + } + + const data = { + database: this.props.databaseId, + collection: this.props.collectionId, + firstTextHtmlOutput, + sampleSize: this.state.sampleSize, + numOfOutputs: outputs.size, + kernelStatus, + }; + + // Only in cases where CosmosMongoKernel runs into an error we get a single output + if (outputs.size === 1) { + traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey); + } else { + traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey); + } + }; + render(): JSX.Element { const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props; if (!id) { @@ -86,31 +113,22 @@ export class SchemaAnalyzerComponent extends React.Component< const isKernelBusy = kernelStatus === "busy"; const isKernelIdle = kernelStatus === "idle"; - const showSchemaOutput = isKernelIdle && outputs.size > 0; + const showSchemaOutput = isKernelIdle && outputs?.size > 0; + + if (showSchemaOutput && this.clickAnalyzeTelemetryStartKey) { + this.traceClickAnalyzeComplete(kernelStatus, outputs); + this.clickAnalyzeTelemetryStartKey = undefined; + } return ( -
- - - - - - - - - - - +
+ + this.setState({ sampleSize })} + onAnalyzeButtonClick={this.onAnalyzeButtonClick} + /> {showSchemaOutput ? ( ) : this.state.isFiltering ? ( - - {isKernelBusy && } - + ) : ( - <> - - - - - Explore your schema - - - - Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set. - - - - - - {isKernelBusy && } - + )}
@@ -229,4 +228,4 @@ const makeMapDispatchToProps = () => { return mapDispatchToProps; }; -export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent); +export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer); diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx new file mode 100644 index 000000000..4eea4afbb --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx @@ -0,0 +1,48 @@ +import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core"; +import * as React from "react"; +import { Provider } from "react-redux"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import { + NotebookComponentBootstrapper, + NotebookComponentBootstrapperOptions, +} from "../NotebookComponent/NotebookComponentBootstrapper"; +import SchemaAnalyzer from "./SchemaAnalyzer"; +import { SchemaAnalyzerNotebook } from "./SchemaAnalyzerUtils"; + +export class SchemaAnalyzerAdapter extends NotebookComponentBootstrapper implements ReactAdapter { + public parameters: unknown; + private kernelRef: KernelRef; + + constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) { + super(options); + + if (!this.contentRef) { + this.contentRef = createContentRef(); + this.kernelRef = createKernelRef(); + + this.getStore().dispatch( + actions.fetchContent({ + filepath: SchemaAnalyzerNotebook.path, + params: {}, + kernelRef: this.kernelRef, + contentRef: this.contentRef, + }) + ); + } + } + + public renderComponent(): JSX.Element { + const props = { + contentRef: this.contentRef, + kernelRef: this.kernelRef, + databaseId: this.databaseId, + collectionId: this.collectionId, + }; + + return ( + + ; + + ); + } +} diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx new file mode 100644 index 000000000..2c91fdcaa --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx @@ -0,0 +1,101 @@ +import { + DefaultButton, + Icon, + IRenderFunction, + ITextFieldProps, + PrimaryButton, + Stack, + TextField, + TooltipHost, +} from "@fluentui/react"; +import * as React from "react"; + +type SchemaAnalyzerHeaderProps = { + isKernelIdle: boolean; + isKernelBusy: boolean; + onSampleSizeUpdated: (sampleSize: string) => void; + onAnalyzeButtonClick: (filter: string, sampleSize: string) => void; +}; + +export const DefaultFilter = ""; +export const DefaultSampleSize = "1000"; +const FilterPlaceholder = "{ field: 'value' }"; +const SampleSizePlaceholder = "1000"; +const MinSampleSize = 1; +const MaxSampleSize = 5000; + +export const SchemaAnalyzerHeader = ({ + isKernelIdle, + isKernelBusy, + onSampleSizeUpdated, + onAnalyzeButtonClick, +}: SchemaAnalyzerHeaderProps): JSX.Element => { + const [filter, setFilter] = React.useState(DefaultFilter); + const [sampleSize, setSampleSize] = React.useState(DefaultSampleSize); + + return ( + + + setFilter(newValue)} + label="Filter" + placeholder={FilterPlaceholder} + disabled={!isKernelIdle} + /> + + + { + const num = Number(newValue); + if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) { + setSampleSize(newValue); + onSampleSizeUpdated(newValue); + } + }} + label="Sample size" + onRenderLabel={onSampleSizeWrapDefaultLabelRenderer} + placeholder={SampleSizePlaceholder} + disabled={!isKernelIdle} + /> + + + { + const sampleSizeToUse = sampleSize || DefaultSampleSize; + setSampleSize(sampleSizeToUse); + onAnalyzeButtonClick(filter, sampleSizeToUse); + }} + disabled={!isKernelIdle} + styles={{ root: { width: 120 } }} + /> + + + { + setFilter(DefaultFilter); + setSampleSize(DefaultSampleSize); + }} + /> + + + ); +}; + +const onSampleSizeWrapDefaultLabelRenderer = ( + props: ITextFieldProps, + defaultRender: IRenderFunction +): JSX.Element => { + return ( + + {defaultRender(props)} + + + + + ); +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx new file mode 100644 index 000000000..a479828e7 --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx @@ -0,0 +1,39 @@ +import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; +import * as React from "react"; + +type SchemaAnalyzerSplashScreenProps = { + isKernelIdle: boolean; + isKernelBusy: boolean; + onAnalyzeButtonClick: () => void; +}; + +export const SchemaAnalyzerSplashScreen = ({ + isKernelIdle, + isKernelBusy, + onAnalyzeButtonClick, +}: SchemaAnalyzerSplashScreenProps): JSX.Element => { + return ( + + + + + + Explore your schema + + + + Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set. + + + + onAnalyzeButtonClick()} + disabled={!isKernelIdle} + /> + + {isKernelBusy && } + + ); +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts new file mode 100644 index 000000000..a422a6a58 --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts @@ -0,0 +1,44 @@ +import { Notebook } from "@nteract/commutable"; +import { IContent } from "@nteract/types"; +import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils"; + +const notebookName = "schema-analyzer-component-notebook.ipynb"; +const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName); +const notebook: Notebook = { + cells: [ + { + cell_type: "code", + metadata: {}, + execution_count: 0, + outputs: [], + source: "", + }, + ], + metadata: { + kernelspec: { + displayName: "Mongo", + language: "mongocli", + name: "mongo", + }, + language_info: { + file_extension: "ipynb", + mimetype: "application/json", + name: "mongo", + version: "1.0", + }, + }, + nbformat: 4, + nbformat_minor: 4, +}; + +export const SchemaAnalyzerNotebook: IContent<"notebook"> = { + name: notebookName, + path: notebookPath, + type: "notebook", + writable: true, + created: "", + last_modified: "", + mimetype: "application/x-ipynb+json", + content: notebook, + format: "json", +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx deleted file mode 100644 index 75b221b88..000000000 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Notebook } from "@nteract/commutable"; -import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core"; -import * as React from "react"; -import { Provider } from "react-redux"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { - NotebookComponentBootstrapper, - NotebookComponentBootstrapperOptions, -} from "../NotebookComponent/NotebookComponentBootstrapper"; -import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent"; - -export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter { - public parameters: unknown; - private kernelRef: KernelRef; - - constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) { - super(options); - - if (!this.contentRef) { - this.contentRef = createContentRef(); - this.kernelRef = createKernelRef(); - - const notebook: Notebook = { - cells: [ - { - cell_type: "code", - metadata: {}, - execution_count: 0, - outputs: [], - source: "", - }, - ], - metadata: { - kernelspec: { - displayName: "Mongo", - language: "mongocli", - name: "mongo", - }, - language_info: { - file_extension: "ipynb", - mimetype: "application/json", - name: "mongo", - version: "1.0", - }, - }, - nbformat: 4, - nbformat_minor: 4, - }; - - const model: IContent<"notebook"> = { - name: "schema-analyzer-component-notebook.ipynb", - path: "schema-analyzer-component-notebook.ipynb", - type: "notebook", - writable: true, - created: "", - last_modified: "", - mimetype: "application/x-ipynb+json", - content: notebook, - format: "json", - }; - - // Request fetching notebook content - this.getStore().dispatch( - actions.fetchContentFulfilled({ - filepath: model.path, - model, - kernelRef: this.kernelRef, - contentRef: this.contentRef, - }) - ); - } - } - - public renderComponent(): JSX.Element { - const props = { - contentRef: this.contentRef, - kernelRef: this.kernelRef, - databaseId: this.databaseId, - collectionId: this.collectionId, - }; - - return ( - - ; - - ); - } -} diff --git a/src/Explorer/Tabs/SchemaAnalyzerTab.ts b/src/Explorer/Tabs/SchemaAnalyzerTab.ts index 0e3aa8418..4bd0079da 100644 --- a/src/Explorer/Tabs/SchemaAnalyzerTab.ts +++ b/src/Explorer/Tabs/SchemaAnalyzerTab.ts @@ -1,13 +1,16 @@ -import { SchemaAnalyzerComponentAdapter } from "../Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter"; +import * as Constants from "../../Common/Constants"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; +import { SchemaAnalyzerAdapter } from "../Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; export default class SchemaAnalyzerTab extends NotebookTabBase { - public readonly html = '
'; - private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter; + public readonly html = '
'; + private schemaAnalyzerAdapter: SchemaAnalyzerAdapter; constructor(options: NotebookTabBaseOptions) { super(options); - this.schemaAnalyzerComponentAdapter = new SchemaAnalyzerComponentAdapter( + this.schemaAnalyzerAdapter = new SchemaAnalyzerAdapter( { contentRef: undefined, notebookClient: NotebookTabBase.clientManager, @@ -17,6 +20,21 @@ export default class SchemaAnalyzerTab extends NotebookTabBase { ); } + public onActivate(): void { + traceSuccess( + Action.Tab, + { + databaseName: this.collection?.databaseId, + collectionName: this.collection?.id, + dataExplorerArea: Constants.Areas.Tab, + tabTitle: "Schema", + }, + this.onLoadStartKey + ); + + super.onActivate(); + } + protected buildCommandBarOptions(): void { this.updateNavbarWithTabsButtons(); } diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index b7820d35d..1239b1257 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -511,7 +511,7 @@ export default class Collection implements ViewModels.Collection { this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default; TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Mongo Schema node", + description: "Schema node", databaseName: this.databaseId, collectionName: this.id(), dataExplorerArea: Constants.Areas.ResourceTree, diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index ed4b6adbe..3d7281acc 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,5 +1,5 @@ -import * as ko from "knockout"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; +import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import DeleteIcon from "../../../images/delete.svg"; @@ -273,7 +273,11 @@ export class ResourceTreeAdapter implements ReactAdapter { contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), }); - if (userContext.apiType === "Mongo" && userContext.features.enableSchemaAnalyzer) { + if ( + userContext.apiType === "Mongo" && + this.container.isNotebookEnabled() && + userContext.features.enableSchemaAnalyzer + ) { children.push({ label: "Schema (Preview)", onClick: collection.onSchemaAnalyzerClick.bind(collection), diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index b7f62e87e..18ff57cc2 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -8,7 +8,7 @@ export type Features = { readonly enableReactPane: boolean; readonly enableRightPanelV2: boolean; readonly enableSchema: boolean; - readonly enableSchemaAnalyzer: boolean; + enableSchemaAnalyzer: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 5500118ce..058937df9 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -116,6 +116,7 @@ export enum Action { NotebooksGalleryPublishedCount, SelfServe, ExpandAddCollectionPaneAdvancedSection, + SchemaAnalyzerClickAnalyze, } export const ActionModifiers = { From a6b82c8340545e57123657a66b51d42b4bffed41 Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Fri, 14 May 2021 02:15:00 +0530 Subject: [PATCH 11/12] Migrate graph style panel to react (#619) Co-authored-by: Steve Faulkner --- .eslintignore | 7 +- src/Contracts/ViewModels.ts | 19 +- src/Explorer/ComponentRegisterer.test.ts | 8 - src/Explorer/ComponentRegisterer.ts | 3 - .../SettingsComponent.test.tsx.snap | 516 ------------------ src/Explorer/Explorer.tsx | 16 - .../D3ForceGraph.test.ts | 9 +- .../GraphExplorerComponent/D3ForceGraph.ts | 157 +++--- .../GraphExplorer.test.tsx | 29 +- .../GraphExplorerComponent/GraphExplorer.tsx | 148 +++-- .../GraphExplorerAdapter.tsx | 74 +++ .../GraphStyleComponent/GraphStyle.test.ts | 51 -- .../GraphStyleComponent.test.tsx | 67 +++ .../GraphStyleComponent.ts | 103 ---- .../GraphStyleComponent.tsx | 131 +++++ .../GraphStyleComponent.test.tsx.snap | 3 + .../graph-style-component.html | 74 --- src/Explorer/OpenActions.test.ts | 1 - src/Explorer/OpenActions.ts | 3 - .../GitHubReposPanel.test.tsx.snap | 129 ----- src/Explorer/Panes/GraphStylingPane.html | 59 -- src/Explorer/Panes/GraphStylingPane.ts | 68 --- .../GraphStylingPanel/GraphStylingPanel.tsx | 37 ++ src/Explorer/Panes/PaneComponents.ts | 11 - .../StringInputPane.test.tsx.snap | 129 ----- ...eteDatabaseConfirmationPanel.test.tsx.snap | 129 ----- src/Explorer/Tabs/GraphTab.tsx | 130 +++-- src/Main.tsx | 1 - 28 files changed, 596 insertions(+), 1516 deletions(-) create mode 100644 src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx delete mode 100644 src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts create mode 100644 src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.test.tsx delete mode 100644 src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts create mode 100644 src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.tsx create mode 100644 src/Explorer/Graph/GraphStyleComponent/__snapshots__/GraphStyleComponent.test.tsx.snap delete mode 100644 src/Explorer/Graph/GraphStyleComponent/graph-style-component.html delete mode 100644 src/Explorer/Panes/GraphStylingPane.html delete mode 100644 src/Explorer/Panes/GraphStylingPane.ts create mode 100644 src/Explorer/Panes/GraphStylingPanel/GraphStylingPanel.tsx diff --git a/.eslintignore b/.eslintignore index f72026dc5..967345584 100644 --- a/.eslintignore +++ b/.eslintignore @@ -84,8 +84,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts -src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts -src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts +# src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts +# src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts @@ -115,7 +115,8 @@ src/Explorer/Panes/CassandraAddCollectionPane.ts src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts -src/Explorer/Panes/GraphStylingPane.ts +# src/Explorer/Panes/GraphStylingPane.ts +# src/Explorer/Panes/NewVertexPane.ts src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/SetupNotebooksPane.ts diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 656b87eb8..9fb242f79 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -206,17 +206,14 @@ export enum NeighborType { BOTH, } -/** - * Set of observable related to graph configuration by user - */ -export interface GraphConfigUiData { - showNeighborType: ko.Observable; - nodeProperties: ko.ObservableArray; - nodePropertiesWithNone: ko.ObservableArray; - nodeCaptionChoice: ko.Observable; - nodeColorKeyChoice: ko.Observable; - nodeIconChoice: ko.Observable; - nodeIconSet: ko.Observable; +export interface IGraphConfigUiData { + showNeighborType: NeighborType; + nodeProperties: string[]; + nodePropertiesWithNone: string[]; + nodeCaptionChoice: string; + nodeColorKeyChoice: string; + nodeIconChoice: string; + nodeIconSet: string; } /** diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts index 1f97cd6f5..16244a16c 100644 --- a/src/Explorer/ComponentRegisterer.test.ts +++ b/src/Explorer/ComponentRegisterer.test.ts @@ -12,18 +12,10 @@ describe("Component Registerer", () => { expect(ko.components.isRegistered("error-display")).toBe(true); }); - it("should register graph-style component", () => { - expect(ko.components.isRegistered("graph-style")).toBe(true); - }); - it("should register json-editor component", () => { expect(ko.components.isRegistered("json-editor")).toBe(true); }); - it("should register graph-styling-pane component", () => { - expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); - }); - it("should register dynamic-list component", () => { expect(ko.components.isRegistered("dynamic-list")).toBe(true); }); diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index ddf9bb991..ff4c4f96e 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -6,12 +6,10 @@ import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDis import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; -import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; import * as PaneComponents from "./Panes/PaneComponents"; ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("error-display", new ErrorDisplayComponent()); -ko.components.register("graph-style", GraphStyleComponent); ko.components.register("editor", new EditorComponent()); ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent()); @@ -21,5 +19,4 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent // Panes ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); -ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index d413c0525..6b841cdb8 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -32,115 +32,6 @@ exports[`SettingsComponent renders 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -1026,26 +917,6 @@ exports[`SettingsComponent renders 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], @@ -1142,115 +1013,6 @@ exports[`SettingsComponent renders 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -2136,26 +1898,6 @@ exports[`SettingsComponent renders 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], @@ -2265,115 +2007,6 @@ exports[`SettingsComponent renders 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -3259,26 +2892,6 @@ exports[`SettingsComponent renders 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], @@ -3375,115 +2988,6 @@ exports[`SettingsComponent renders 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -4369,26 +3873,6 @@ exports[`SettingsComponent renders 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index c909c690a..de0d8402e 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -61,7 +61,6 @@ import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfir import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; -import GraphStylingPane from "./Panes/GraphStylingPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; @@ -151,7 +150,6 @@ export default class Explorer { // Contextual panes public addDatabasePane: AddDatabasePane; - public graphStylingPane: GraphStylingPane; public cassandraAddCollectionPane: CassandraAddCollectionPane; private gitHubClient: GitHubClient; public gitHubOAuthService: GitHubOAuthService; @@ -182,7 +180,6 @@ export default class Explorer { public openDialog: ExplorerParams["openDialog"]; public closeDialog: ExplorerParams["closeDialog"]; - private _panes: ContextualPaneBase[] = []; private _isInitializingNotebooks: boolean; private notebookBasePath: ko.Observable; private _arcadiaManager: ArcadiaResourceManager; @@ -411,13 +408,6 @@ export default class Explorer { container: this, }); - this.graphStylingPane = new GraphStylingPane({ - id: "graphstylingpane", - visible: ko.observable(false), - - container: this, - }); - this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ id: "cassandraaddcollectionpane", visible: ko.observable(false), @@ -433,7 +423,6 @@ export default class Explorer { } }); - this._panes = [this.addDatabasePane, this.graphStylingPane, this.cassandraAddCollectionPane]; this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.isTabsContentExpanded = ko.observable(false); @@ -1058,10 +1047,6 @@ export default class Explorer { : this.selectedNode().collection) as ViewModels.Collection; } - public closeAllPanes(): void { - this._panes.forEach((pane: ContextualPaneBase) => pane.close()); - } - public isRunningOnNationalCloud(): boolean { return ( userContext.portalEnv === "blackforest" || @@ -1855,7 +1840,6 @@ export default class Explorer { public async handleOpenFileAction(path: string): Promise { if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { - this.closeAllPanes(); this._openSetupNotebooksPaneForQuickstart(); } diff --git a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts index 1f434d577..9c1fcdc2f 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts @@ -1,7 +1,7 @@ import * as sinon from "sinon"; -import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph"; -import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData"; import GraphTab from "../../Tabs/GraphTab"; +import { D3Link, D3Node, GraphData } from "../GraphExplorerComponent/GraphData"; +import { D3ForceGraph, D3GraphNodeData, LoadMoreDataAction } from "./D3ForceGraph"; describe("D3ForceGraph", () => { const v1Id = "v1"; @@ -68,7 +68,7 @@ describe("D3ForceGraph", () => { beforeEach(() => { forceGraph = new D3ForceGraph({ - graphConfig: GraphTab.createGraphConfig(), + igraphConfig: GraphTab.createIGraphConfig(), onHighlightedNode: sinon.spy(), onLoadMoreData: (action: LoadMoreDataAction): void => {}, @@ -141,6 +141,7 @@ describe("D3ForceGraph", () => { const mouseoverEvent = document.createEvent("Events"); mouseoverEvent.initEvent("mouseover", true, false); $(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex + expect($(rootNode).find(".node")[0]).toBe(1); // onHighlightedNode is always called once to clear the selection expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true); @@ -150,7 +151,7 @@ describe("D3ForceGraph", () => { expect(onHighlightedNode.id).toEqual(v1Id); }; - forceGraph.updateGraph(newGraph); + forceGraph.updateGraph(newGraph, forceGraph.igraphConfig); }); }); }); diff --git a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts index 05825bf98..b6d8717c4 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts @@ -13,7 +13,7 @@ import _ from "underscore"; import * as Constants from "../../../Common/Constants"; import { NeighborType } from "../../../Contracts/ViewModels"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; -import { GraphConfig } from "../../Tabs/GraphTab"; +import { IGraphConfig } from "./../../Tabs/GraphTab"; import { D3Link, D3Node, GraphData } from "./GraphData"; import { GraphExplorer } from "./GraphExplorer"; @@ -48,21 +48,22 @@ interface ZoomTransform extends Point2D { export interface D3ForceGraphParameters { // Graph to parent - graphConfig: GraphConfig; - onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph - onLoadMoreData: (action: LoadMoreDataAction) => void; + + igraphConfig: IGraphConfig; + onHighlightedNode?: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph + onLoadMoreData?: (action: LoadMoreDataAction) => void; // parent to graph - onInitialized: (instance: GraphRenderer) => void; + onInitialized?: (instance: GraphRenderer) => void; // For unit testing purposes - onGraphUpdated: (timestamp: number) => void; + onGraphUpdated?: (timestamp: number) => void; } export interface GraphRenderer { selectNode(id: string): void; resetZoom(): void; - updateGraph(graphData: GraphData): void; + updateGraph(graphData: GraphData, igraphConfigParam?: IGraphConfig): void; enableHighlight(enable: boolean): void; } @@ -108,7 +109,7 @@ export class D3ForceGraph implements GraphRenderer { private viewCenter: Point2D; // Map a property to a graph node attribute (such as color) - private uniqueValues: (string | number)[]; // keep track of unique values + private uniqueValues: (string | number)[] = []; // keep track of unique values private graphDataWrapper: GraphData; // Communication with outside @@ -119,9 +120,11 @@ export class D3ForceGraph implements GraphRenderer { // outside -> Graph private idToSelect: ko.Observable; // Programmatically select node by id outside graph private isHighlightDisabled: boolean; + public igraphConfig: IGraphConfig; public constructor(params: D3ForceGraphParameters) { this.params = params; + this.igraphConfig = this.params.igraphConfig; this.idToSelect = ko.observable(null); this.errorMsgs = ko.observableArray([]); this.graphDataWrapper = null; @@ -151,7 +154,10 @@ export class D3ForceGraph implements GraphRenderer { this.g.remove(); } - public updateGraph(newGraph: GraphData): void { + public updateGraph(newGraph: GraphData, igraphConfigParam?: IGraphConfig): void { + if (igraphConfigParam) { + this.igraphConfig = igraphConfigParam; + } if (!newGraph || !this.simulation) { return; } @@ -159,7 +165,8 @@ export class D3ForceGraph implements GraphRenderer { this.graphDataWrapper = new GraphData(); this.graphDataWrapper.setData(newGraph); - const key = this.params.graphConfig.nodeColorKey(); + const key = this.igraphConfig.nodeColorKey; + if (key !== GraphExplorer.NONE_CHOICE) { this.updateUniqueValues(key); } @@ -265,20 +272,7 @@ export class D3ForceGraph implements GraphRenderer { }); }); - // Redraw if any of these configs change - this.params.graphConfig.nodeColor.subscribe(this.redrawGraph.bind(this)); - this.params.graphConfig.nodeColorKey.subscribe((key: string) => { - // Compute colormap - this.uniqueValues = []; - this.updateUniqueValues(key); - this.redrawGraph(); - }); - this.params.graphConfig.linkColor.subscribe(() => this.redrawGraph()); - this.params.graphConfig.showNeighborType.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeCaption.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeSize.subscribe(() => this.redrawGraph()); - this.params.graphConfig.linkWidth.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeIconKey.subscribe(() => this.redrawGraph()); + this.redrawGraph(); this.instantiateSimulation(); } // initialize @@ -371,7 +365,10 @@ export class D3ForceGraph implements GraphRenderer { */ private shiftGraph(targetPosition: Point2D): Q.Promise { const deferred: Q.Deferred = Q.defer(); - const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y }; + const offset = { + x: this.width / 2 - targetPosition.x, + y: this.height / 2 - targetPosition.y, + }; this.viewCenter = targetPosition; if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) { @@ -526,7 +523,10 @@ export class D3ForceGraph implements GraphRenderer { .transition() .duration(D3ForceGraph.TRANSITION_STEP3_MS) .attrTween("transform", (d: D3Node) => { - const finalPos = nodeFinalPositionMap.get(d.id) || { x: viewCenter.x, y: viewCenter.y }; + const finalPos = nodeFinalPositionMap.get(d.id) || { + x: viewCenter.x, + y: viewCenter.y, + }; const ix = interpolateNumber(viewCenter.x, finalPos.x); const iy = interpolateNumber(viewCenter.y, finalPos.y); return (t: number) => { @@ -626,10 +626,10 @@ export class D3ForceGraph implements GraphRenderer { this.addNewLinks(); - const nodes = this.simulation.nodes(); + const nodes1 = this.simulation.nodes(); this.redrawGraph(); - this.animateBigBang(nodes, newNodes); + this.animateBigBang(nodes1, newNodes); this.simulation.alpha(1).restart(); this.params.onGraphUpdated(new Date().getTime()); @@ -657,8 +657,8 @@ export class D3ForceGraph implements GraphRenderer { .append("path") .attr("class", "link") .attr("fill", "none") - .attr("stroke-width", this.params.graphConfig.linkWidth()) - .attr("stroke", this.params.graphConfig.linkColor()); + .attr("stroke-width", this.igraphConfig.linkWidth) + .attr("stroke", this.igraphConfig.linkColor); if (D3ForceGraph.useSvgMarkerEnd()) { line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`); @@ -668,7 +668,7 @@ export class D3ForceGraph implements GraphRenderer { .append("use") .attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`) .attr("class", "markerEnd link") - .attr("fill", this.params.graphConfig.linkColor()) + .attr("fill", this.igraphConfig.linkColor) .classed(`${this.getArrowHeadSymbolId()}`, true); } @@ -724,7 +724,7 @@ export class D3ForceGraph implements GraphRenderer { .append("circle") .attr("fill", this.getNodeColor.bind(this)) .attr("class", "main") - .attr("r", this.params.graphConfig.nodeSize()); + .attr("r", this.igraphConfig.nodeSize); var iconGroup = newNodes .append("g") @@ -749,7 +749,7 @@ export class D3ForceGraph implements GraphRenderer { self.onNodeClicked(this.parentNode, d); } }); - var nodeSize = this.params.graphConfig.nodeSize(); + var nodeSize = this.igraphConfig.nodeSize; var bgsize = nodeSize + 1; iconGroup @@ -759,7 +759,7 @@ export class D3ForceGraph implements GraphRenderer { .attr("width", bgsize * 2) .attr("height", bgsize * 2) .attr("fill-opacity", (d: D3Node) => { - return this.params.graphConfig.nodeIconKey() ? 1 : 0; + return this.igraphConfig.nodeIconKey ? 1 : 0; }) .attr("class", "icon-background"); @@ -767,14 +767,13 @@ export class D3ForceGraph implements GraphRenderer { iconGroup .append("svg:image") .attr("xlink:href", (d: D3Node) => { - return D3ForceGraph.computeImageData(d, this.params.graphConfig); + return D3ForceGraph.computeImageData(d, this.igraphConfig); }) .attr("x", -nodeSize) .attr("y", -nodeSize) .attr("height", nodeSize * 2) .attr("width", nodeSize * 2) .attr("class", "icon"); - newNodes .append("text") .attr("class", "caption") @@ -808,7 +807,7 @@ export class D3ForceGraph implements GraphRenderer { .attr("x2", 0) .attr("y2", gaugeYOffset) .style("stroke-width", 1) - .style("stroke", this.params.graphConfig.linkColor()); + .style("stroke", this.igraphConfig.linkColor); parent .append("use") .attr("xlink:href", "#triangleRight") @@ -877,7 +876,7 @@ export class D3ForceGraph implements GraphRenderer { .attr("height", gaugeHeight) .style("fill", "white") .style("stroke-width", 1) - .style("stroke", this.params.graphConfig.linkColor()); + .style("stroke", this.igraphConfig.linkColor); parent .append("rect") .attr("x", (d: D3Node) => { @@ -894,7 +893,7 @@ export class D3ForceGraph implements GraphRenderer { : 0; }) .attr("height", gaugeHeight) - .style("fill", this.params.graphConfig.nodeColor()) + .style("fill", this.igraphConfig.nodeColor) .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); parent .append("text") @@ -971,7 +970,7 @@ export class D3ForceGraph implements GraphRenderer { const self = this; nodeSelection.selectAll(".loadmore").remove(); - var nodeSize = this.params.graphConfig.nodeSize(); + var nodeSize = this.igraphConfig.nodeSize; const rootSelectionG = nodeSelection .filter((d: D3Node) => { return !!d._isRoot && !!d._pagination; @@ -995,7 +994,7 @@ export class D3ForceGraph implements GraphRenderer { this.createLoadMoreControl(missingNeighborNonRootG, nodeSize); // Don't color icons individually, just the definitions - this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); + this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.igraphConfig.nodeColor); } /** @@ -1032,11 +1031,11 @@ export class D3ForceGraph implements GraphRenderer { * @param d */ private getNodeColor(d: D3Node): string { - if (this.params.graphConfig.nodeColorKey()) { - const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey()); + if (this.igraphConfig.nodeColorKey) { + const val = GraphData.getNodePropValue(d, this.igraphConfig.nodeColorKey); return this.lookupColorFromKey(val); } else { - return this.params.graphConfig.nodeColor(); + return this.igraphConfig.nodeColor; } } @@ -1103,12 +1102,12 @@ export class D3ForceGraph implements GraphRenderer { this.graphDataWrapper.getTargetsForId(nodeId) ); } - })(this.params.graphConfig.showNeighborType()); + })(this.igraphConfig.showNeighborType); return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId; }); this.g.selectAll(".link").classed("inactive", (l: D3Link) => { - switch (this.params.graphConfig.showNeighborType()) { + switch (this.igraphConfig.showNeighborType) { case NeighborType.SOURCES_ONLY: return (l.target).id !== nodeId; case NeighborType.TARGETS_ONLY: @@ -1152,7 +1151,7 @@ export class D3ForceGraph implements GraphRenderer { } private retrieveNodeCaption(d: D3Node) { - let key = this.params.graphConfig.nodeCaption(); + let key = this.igraphConfig.nodeCaption; let value: string = d.id || d.label; if (key) { value = GraphData.getNodePropValue(d, key) || ""; @@ -1194,10 +1193,16 @@ export class D3ForceGraph implements GraphRenderer { } private positionLinkEnd(l: D3Link) { - const source: Point2D = { x: (l.source).x, y: (l.source).y }; - const target: Point2D = { x: (l.target).x, y: (l.target).y }; + const source: Point2D = { + x: (l.source).x, + y: (l.source).y, + }; + const target: Point2D = { + x: (l.target).x, + y: (l.target).y, + }; const d1 = D3ForceGraph.calculateControlPoint(source, target); - var radius = this.params.graphConfig.nodeSize() + 3; + var radius = this.igraphConfig.nodeSize + 3; // End const dx = target.x - d1.x; @@ -1210,10 +1215,16 @@ export class D3ForceGraph implements GraphRenderer { } private positionLink(l: D3Link) { - const source: Point2D = { x: (l.source).x, y: (l.source).y }; - const target: Point2D = { x: (l.target).x, y: (l.target).y }; + const source: Point2D = { + x: (l.source).x, + y: (l.source).y, + }; + const target: Point2D = { + x: (l.target).x, + y: (l.target).y, + }; const d1 = D3ForceGraph.calculateControlPoint(source, target); - var radius = this.params.graphConfig.nodeSize() + 3; + var radius = this.igraphConfig.nodeSize + 3; // Start var dx = d1.x - source.x; @@ -1245,13 +1256,13 @@ export class D3ForceGraph implements GraphRenderer { return d._isRoot ? "node root" : "node"; }); - this.applyConfig(this.params.graphConfig); + this.applyConfig(this.igraphConfig); } - private static computeImageData(d: D3Node, config: GraphConfig): string { - let propValue = GraphData.getNodePropValue(d, config.nodeIconKey()) || ""; + private static computeImageData(d: D3Node, config: IGraphConfig): string { + let propValue = GraphData.getNodePropValue(d, config.nodeIconKey) || ""; // Trim leading and trailing spaces to make comparison more forgiving. - let value = config.iconsMap()[propValue.trim()]; + let value = config.iconsMap[propValue.trim()]; if (!value) { return undefined; } @@ -1261,48 +1272,46 @@ export class D3ForceGraph implements GraphRenderer { /** * Update graph according to configuration or use default */ - private applyConfig(config: GraphConfig) { - if (config.nodeIconKey()) { + private applyConfig(config: IGraphConfig) { + if (config.nodeIconKey) { this.g .selectAll(".node .icon") .attr("xlink:href", (d: D3Node) => { return D3ForceGraph.computeImageData(d, config); }) - .attr("x", -config.nodeSize()) - .attr("y", -config.nodeSize()) - .attr("height", config.nodeSize() * 2) - .attr("width", config.nodeSize() * 2) + .attr("x", -config.nodeSize) + .attr("y", -config.nodeSize) + .attr("height", config.nodeSize * 2) + .attr("width", config.nodeSize * 2) .attr("class", "icon"); } else { // clear icons this.g.selectAll(".node .icon").attr("xlink:href", undefined); } this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => { - return config.nodeIconKey() ? 1 : 0; + return config.nodeIconKey ? 1 : 0; }); - this.g.selectAll(".node text.caption").text((d: D3Node) => { return this.retrieveNodeCaption(d); }); - this.g.selectAll(".node circle.main").attr("r", config.nodeSize()); - this.g.selectAll(".node text.caption").attr("dx", config.nodeSize() + 2); + this.g.selectAll(".node circle.main").attr("r", config.nodeSize); + this.g.selectAll(".node text.caption").attr("dx", config.nodeSize + 2); this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this)); // Can't color nodes individually if using defs - this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); + this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", config.nodeColor); + this.g.selectAll(".link").attr("stroke-width", config.linkWidth); - this.g.selectAll(".link").attr("stroke-width", config.linkWidth()); - - this.g.selectAll(".link").attr("stroke", config.linkColor()); + this.g.selectAll(".link").attr("stroke", config.linkColor); if (D3ForceGraph.useSvgMarkerEnd()) { this.svg .select(`#${this.getArrowHeadSymbolId()}-marker`) - .attr("fill", config.linkColor()) - .attr("stroke", config.linkColor()); + .attr("fill", config.linkColor) + .attr("stroke", config.linkColor); } else { - this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor()); + this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor); } // Reset highlight diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index cee9c4c76..1954186c8 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -1,20 +1,20 @@ jest.mock("../../../Common/dataAccess/queryDocuments"); jest.mock("../../../Common/dataAccess/queryDocumentsPage"); -import React from "react"; -import * as sinon from "sinon"; import { mount, ReactWrapper } from "enzyme"; import * as Q from "q"; +import React from "react"; +import * as sinon from "sinon"; import "../../../../externals/jquery.typeahead.min"; -import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeData } from "./GraphExplorer"; -import * as D3ForceGraph from "./D3ForceGraph"; -import { GraphData } from "./GraphData"; -import { TabComponent } from "../../Controls/Tabs/TabComponent"; -import * as DataModels from "../../../Contracts/DataModels"; -import * as StorageUtility from "../../../Shared/StorageUtility"; -import GraphTab from "../../Tabs/GraphTab"; -import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as StorageUtility from "../../../Shared/StorageUtility"; +import { TabComponent } from "../../Controls/Tabs/TabComponent"; +import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; +import GraphTab from "../../Tabs/GraphTab"; +import * as D3ForceGraph from "./D3ForceGraph"; +import { GraphData } from "./GraphData"; +import { GraphAccessor, GraphExplorer, GraphExplorerProps, GraphHighlightedNodeData } from "./GraphExplorer"; describe("Check whether query result is vertex array", () => { it("should reject null as vertex array", () => { @@ -146,8 +146,8 @@ describe("GraphExplorer", () => { const gremlinRU = 789.12; const createMockProps = (): GraphExplorerProps => { - const graphConfig = GraphTab.createGraphConfig(); - const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig); + const igraphConfig = GraphTab.createIGraphConfig(); + const igraphConfigUi = GraphTab.createIGraphConfigUiData(igraphConfig); return { onGraphAccessorCreated: (instance: GraphAccessor): void => {}, @@ -170,8 +170,9 @@ describe("GraphExplorer", () => { resourceId: "resourceId", /* TODO Figure out how to make this Knockout-free */ - graphConfigUiData: graphConfigUi, - graphConfig: graphConfig, + igraphConfigUiData: igraphConfigUi, + igraphConfig: igraphConfig, + setIConfigUiData: (data: string[]): void => {}, }; }; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index bfd23f09c..5b2441e40 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -19,7 +19,7 @@ import { EditorReact } from "../../Controls/Editor/EditorReact"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; -import { GraphConfig } from "../../Tabs/GraphTab"; +import { IGraphConfig } from "../../Tabs/GraphTab"; import { ArraysByKeyCache } from "./ArraysByKeyCache"; import * as D3ForceGraph from "./D3ForceGraph"; import { EdgeInfoCache } from "./EdgeInfoCache"; @@ -31,10 +31,10 @@ import * as LeftPane from "./LeftPaneComponent"; import { MiddlePaneComponent } from "./MiddlePaneComponent"; import * as NodeProperties from "./NodePropertiesComponent"; import { QueryContainerComponent } from "./QueryContainerComponent"; - export interface GraphAccessor { applyFilter: () => void; addVertex: (v: ViewModels.NewVertexData) => Q.Promise; + shareIGraphConfig: (igraphConfig: IGraphConfig) => void; } export interface GraphExplorerProps { @@ -58,9 +58,10 @@ export interface GraphExplorerProps { onLoadStartKeyChange: (newKey: number) => void; resourceId: string; - /* TODO Figure out how to make this Knockout-free */ - graphConfigUiData: ViewModels.GraphConfigUiData; - graphConfig?: GraphConfig; + igraphConfigUiData: ViewModels.IGraphConfigUiData; + igraphConfig: IGraphConfig; + + setIConfigUiData?: (data: string[]) => void; } export interface GraphHighlightedNodeData { @@ -121,6 +122,10 @@ interface GraphExplorerState { filterQueryError: string; filterQueryWarning: string; filterQueryStatus: FilterQueryStatus; + change: string; + + igraphConfigUiData: ViewModels.IGraphConfigUiData; + igraphConfig: IGraphConfig; } export interface EditedProperties { @@ -218,6 +223,8 @@ export class GraphExplorer extends React.Component { - this.props.graphConfig.nodeCaption(key); - const selectedNode = this.state.highlightedNode; - if (selectedNode) { - this.updatePropertiesPane(selectedNode.id); - } - - this.render(); - }); - this.props.graphConfigUiData.nodeColorKeyChoice.subscribe((val) => { - this.props.graphConfig.nodeColorKey(val === GraphExplorer.NONE_CHOICE ? null : val); - this.render(); - }); - this.props.graphConfigUiData.showNeighborType.subscribe((val) => { - this.props.graphConfig.showNeighborType(val); - this.render(); - }); - - this.props.graphConfigUiData.nodeIconChoice.subscribe((val) => { - this.updateNodeIcons(val, this.props.graphConfigUiData.nodeIconSet()); - this.render(); - }); - this.props.graphConfigUiData.nodeIconSet.subscribe((val) => { - this.updateNodeIcons(this.props.graphConfigUiData.nodeIconChoice(), val); - this.render(); - }); - /* *************************************** */ + const selectedNode = this.state.highlightedNode; props.onGraphAccessorCreated({ applyFilter: this.submitQuery.bind(this), addVertex: this.addVertex.bind(this), + shareIGraphConfig: this.shareIGraphConfig.bind(this), }); } // constructor + public shareIGraphConfig(igraphConfig: IGraphConfig) { + this.setState({ + igraphConfig: { ...igraphConfig }, + }); + + const selectedNode = this.state.highlightedNode; + if (selectedNode) { + this.updatePropertiesPane(selectedNode.id); + this.setResultDisplay(GraphExplorer.TAB_INDEX_GRAPH); + } + } + /** * If pk is a string, return ["pk", "id"] * else return [pk, "id"] @@ -408,7 +404,7 @@ export class GraphExplorer extends React.Component { @@ -446,7 +442,7 @@ export class GraphExplorer extends React.Component { GraphExplorer.reportToConsole( @@ -809,7 +805,7 @@ export class GraphExplorer extends React.Component { let graphData = this.originalGraphData; graphData.removeEdge(edgeId, false); - this.updateGraphData(graphData); + this.updateGraphData(graphData, this.state.igraphConfig); }, (error: string) => { GraphExplorer.reportToConsole( @@ -858,7 +854,7 @@ export class GraphExplorer extends React.Component { GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`); @@ -1209,7 +1220,7 @@ export class GraphExplorer extends React.Component { @@ -1320,7 +1331,7 @@ export class GraphExplorer extends React.Component { @@ -1539,9 +1550,14 @@ export class GraphExplorer extends React.Component) { + private updateGraphData( + graphData: GraphData.GraphData, + igraphConfig?: IGraphConfig + ) { this.originalGraphData = graphData; let gd = JSON.parse(JSON.stringify(this.originalGraphData)); if (!this.d3ForceGraph) { console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); return; } - this.d3ForceGraph.updateGraph(gd); + this.d3ForceGraph.updateGraph(gd, igraphConfig); } public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void { @@ -1694,10 +1722,12 @@ export class GraphExplorer extends React.Component this.onMiddlePaneInitialized(instance), + onInitialized: (instance: D3ForceGraph.GraphRenderer): void => { + this.onMiddlePaneInitialized(instance); + }, onGraphUpdated: this.onGraphUpdated.bind(this), }; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx new file mode 100644 index 000000000..984419398 --- /dev/null +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx @@ -0,0 +1,74 @@ +import * as React from "react"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { IGraphConfig } from "../../Tabs/GraphTab"; +import { GraphAccessor, GraphExplorer } from "./GraphExplorer"; +interface Parameter { + onIsNewVertexDisabledChange: (isEnabled: boolean) => void; + onGraphAccessorCreated: (instance: GraphAccessor) => void; + onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void; + onIsValidQuery: (isValidQuery: boolean) => void; + onIsPropertyEditing: (isEditing: boolean) => void; + onIsGraphDisplayed: (isDisplayed: boolean) => void; + onResetDefaultGraphConfigValues: () => void; + + collectionPartitionKeyProperty: string; + graphBackendEndpoint: string; + databaseId: string; + collectionId: string; + masterKey: string; + + onLoadStartKey: number; + onLoadStartKeyChange: (newKey: number) => void; + resourceId: string; + + igraphConfigUiData: ViewModels.IGraphConfigUiData; + igraphConfig: IGraphConfig; + setIConfigUiData?: (data: string[]) => void; +} + +interface IGraphExplorerProps { + isChanged: boolean; +} + +interface IGraphExplorerStates { + isChangedState: boolean; +} + +export interface GraphExplorerAdapter + extends ReactAdapter, + React.Component {} +export class GraphExplorerAdapter implements ReactAdapter { + public params: Parameter; + public parameters = {}; + public isNewVertexDisabled: boolean; + + public constructor(params: Parameter, props?: IGraphExplorerProps) { + this.params = params; + } + + public renderComponent(): JSX.Element { + return ( + + ); + } +} diff --git a/src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts b/src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts deleted file mode 100644 index f88a37c81..000000000 --- a/src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as ko from "knockout"; -import { GraphStyleComponent, GraphStyleParams } from "./GraphStyleComponent"; -import * as ViewModels from "../../../Contracts/ViewModels"; - -function buildComponent(buttonOptions: any) { - document.body.innerHTML = GraphStyleComponent.template as any; - const vm = new GraphStyleComponent.viewModel(buttonOptions); - ko.applyBindings(vm); -} - -describe("Graph Style Component", () => { - let buildParams = (config: ViewModels.GraphConfigUiData): GraphStyleParams => { - return { - config: config, - }; - }; - - afterEach(() => { - ko.cleanNode(document); - }); - - describe("Rendering", () => { - it("should display proper list of choices passed in component parameters", () => { - const PROP2 = "prop2"; - const PROPC = "prop3"; - const params = buildParams({ - nodeCaptionChoice: ko.observable(null), - nodeIconChoice: ko.observable(null), - nodeColorKeyChoice: ko.observable(null), - nodeIconSet: ko.observable(null), - nodeProperties: ko.observableArray(["prop1", PROP2]), - nodePropertiesWithNone: ko.observableArray(["propa", "propb", PROPC]), - showNeighborType: ko.observable(null), - }); - - buildComponent(params); - - var e: any = document.querySelector(".graphStyle #nodeCaptionChoices"); - expect(e.options.length).toBe(2); - expect(e.options[1].value).toBe(PROP2); - - e = document.querySelector(".graphStyle #nodeColorKeyChoices"); - expect(e.options.length).toBe(3); - expect(e.options[2].value).toBe(PROPC); - - e = document.querySelector(".graphStyle #nodeIconChoices"); - expect(e.options.length).toBe(3); - expect(e.options[2].value).toBe(PROPC); - }); - }); -}); diff --git a/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.test.tsx b/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.test.tsx new file mode 100644 index 000000000..14dd36ca1 --- /dev/null +++ b/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { IGraphConfig } from "../../Tabs/GraphTab"; +import { GraphStyleComponent, GraphStyleProps } from "./GraphStyleComponent"; + +describe("Graph Style Component", () => { + let fakeGraphConfig: IGraphConfig; + let fakeGraphConfigUiData: ViewModels.IGraphConfigUiData; + let props: GraphStyleProps; + beforeEach(() => { + fakeGraphConfig = { + nodeColor: "orange", + nodeColorKey: "node2", + linkColor: "orange", + showNeighborType: 0, + nodeCaption: "node1", + nodeSize: 10, + linkWidth: 1, + nodeIconKey: undefined, + iconsMap: {}, + }; + fakeGraphConfigUiData = { + nodeCaptionChoice: "node1", + nodeIconChoice: undefined, + nodeColorKeyChoice: "node2", + nodeIconSet: undefined, + nodeProperties: ["node1", "node2", "node3"], + nodePropertiesWithNone: ["none", "node1", "node2", "node3"], + showNeighborType: undefined, + }; + props = { + igraphConfig: fakeGraphConfig, + igraphConfigUiData: fakeGraphConfigUiData, + getValues: (): void => undefined, + }; + + render(); + }); + + it("should render default property", () => { + const { asFragment } = render(); + expect(asFragment).toMatchSnapshot(); + }); + + it("should render node properties dropdown list ", () => { + const dropDownList = screen.getByText("Show vertex (node) as"); + expect(dropDownList).toBeDefined(); + }); + + it("should render Map this property to node color dropdown list", () => { + const nodeColorDropdownList = screen.getByText("Map this property to node color"); + expect(nodeColorDropdownList).toBeDefined(); + }); + + it("should render show neighbor options", () => { + const nodeShowNeighborOptions = screen.getByText("Show"); + expect(nodeShowNeighborOptions).toBeDefined(); + }); + + it("should call handleOnChange method", () => { + const handleOnChange = jest.fn(); + const nodeCaptionDropdownList = screen.getByText("Show vertex (node) as"); + nodeCaptionDropdownList.onchange = handleOnChange(); + expect(handleOnChange).toHaveBeenCalled(); + }); +}); diff --git a/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts b/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts deleted file mode 100644 index 9974e65da..000000000 --- a/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as Constants from "../../../Common/Constants"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; - -/** - * Parameters for this component - */ -export interface GraphStyleParams { - config: ViewModels.GraphConfigUiData; - firstFieldHasFocus?: ko.Observable; - - /** - * Callback triggered when the template is bound to the component (for testing purposes) - */ - onTemplateReady?: () => void; -} - -class GraphStyleViewModel extends WaitsForTemplateViewModel { - private params: GraphStyleParams; - - public constructor(params: GraphStyleParams) { - super(); - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady && params.onTemplateReady) { - params.onTemplateReady(); - } - }); - - this.params = params; - } - - public onAllNeighborsKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.params.config.showNeighborType(ViewModels.NeighborType.BOTH); - event.stopPropagation(); - return false; - } - - return true; - }; - - public onSourcesKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.params.config.showNeighborType(ViewModels.NeighborType.SOURCES_ONLY); - event.stopPropagation(); - return false; - } - - return true; - }; - - public onTargetsKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.params.config.showNeighborType(ViewModels.NeighborType.TARGETS_ONLY); - event.stopPropagation(); - return false; - } - - return true; - }; -} - -const template = ` -
-
-

Show vertex (node) as

- -
-
-

Map this property to node color

- -
-
-

Map this property to node icon

- - -
- -

Show

- -
-
- - -
-
- - -
-
- - -
-
-
`; - -export const GraphStyleComponent = { - viewModel: GraphStyleViewModel, - template, -}; diff --git a/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.tsx b/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.tsx new file mode 100644 index 000000000..5030ad730 --- /dev/null +++ b/src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.tsx @@ -0,0 +1,131 @@ +import { ChoiceGroup, Dropdown, IChoiceGroupOption, IDropdownOption, IDropdownStyles, Stack } from "@fluentui/react"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { IGraphConfigUiData, NeighborType } from "../../../Contracts/ViewModels"; +import { IGraphConfig } from "../../Tabs/GraphTab"; +const IGraphConfigType = { + NODE_CAPTION: "NODE_CAPTION", + NODE_COLOR: "NODE_COLOR", + NODE_ICON: "NODE_ICON", + SHOW_NEIGHBOR_TYPE: "SHOW_NEIGHBOR_TYPE", +}; +export interface GraphStyleProps { + igraphConfig: IGraphConfig; + igraphConfigUiData: IGraphConfigUiData; + getValues: (igraphConfig?: IGraphConfig) => void; +} + +export const GraphStyleComponent: FunctionComponent = ({ + igraphConfig, + igraphConfigUiData, + getValues, +}: GraphStyleProps): JSX.Element => { + const [igraphConfigState, setIGraphConfig] = useState(igraphConfig); + const [selected, setSelected] = useState(false); + + const nodePropertiesOptions = igraphConfigUiData.nodeProperties.map((nodeProperty) => ({ + key: nodeProperty, + text: nodeProperty, + })); + + const nodePropertiesWithNoneOptions = igraphConfigUiData.nodePropertiesWithNone.map((nodePropertyWithNone) => ({ + key: nodePropertyWithNone, + text: nodePropertyWithNone, + })); + + const showNeighborTypeOptions: IChoiceGroupOption[] = [ + { key: NeighborType.BOTH.toString(), text: "All neighbors" }, + { key: NeighborType.SOURCES_ONLY.toString(), text: "Sources" }, + { key: NeighborType.TARGETS_ONLY.toString(), text: "Targets" }, + ]; + + const dropdownStyles: Partial = { + dropdown: { height: 32, marginRight: 10 }, + }; + const choiceButtonStyles = { + flexContainer: [ + { + selectors: { + ".ms-ChoiceField-wrapper label": { + fontSize: 14, + paddingTop: 0, + }, + }, + }, + ], + }; + + useEffect(() => { + if (selected) { + getValues(igraphConfigState); + } + //eslint-disable-next-line + }, [igraphConfigState]); + + const handleOnChange = (val: string, igraphConfigType: string) => { + switch (igraphConfigType) { + case IGraphConfigType.NODE_CAPTION: + setSelected(true); + setIGraphConfig({ + ...igraphConfigState, + nodeCaption: val, + }); + break; + case IGraphConfigType.NODE_COLOR: + setSelected(true); + setIGraphConfig({ + ...igraphConfigState, + nodeColorKey: val, + }); + break; + case IGraphConfigType.SHOW_NEIGHBOR_TYPE: + setSelected(true); + setIGraphConfig({ + ...igraphConfigState, + showNeighborType: parseInt(val), + }); + break; + } + }; + return ( + +
+
+ + handleOnChange(options.key.toString(), IGraphConfigType.NODE_CAPTION) + } + /> +
+
+ + handleOnChange(options.key.toString(), IGraphConfigType.NODE_COLOR) + } + /> +
+ +
+ + handleOnChange(options.key.toString(), IGraphConfigType.SHOW_NEIGHBOR_TYPE) + } + /> +
+
+
+ ); +}; diff --git a/src/Explorer/Graph/GraphStyleComponent/__snapshots__/GraphStyleComponent.test.tsx.snap b/src/Explorer/Graph/GraphStyleComponent/__snapshots__/GraphStyleComponent.test.tsx.snap new file mode 100644 index 000000000..721d4cf50 --- /dev/null +++ b/src/Explorer/Graph/GraphStyleComponent/__snapshots__/GraphStyleComponent.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Graph Style Component should render default property 1`] = `[Function]`; diff --git a/src/Explorer/Graph/GraphStyleComponent/graph-style-component.html b/src/Explorer/Graph/GraphStyleComponent/graph-style-component.html deleted file mode 100644 index 3cc120590..000000000 --- a/src/Explorer/Graph/GraphStyleComponent/graph-style-component.html +++ /dev/null @@ -1,74 +0,0 @@ -
-
-

Show vertex (node) as

- -
-
-

Map this property to node color

- -
-
-

Map this property to node icon

- - -
- -

Show

- -
-
- - -
-
- - -
-
- - -
-
-
diff --git a/src/Explorer/OpenActions.test.ts b/src/Explorer/OpenActions.test.ts index e8113455f..0ea2057b7 100644 --- a/src/Explorer/OpenActions.test.ts +++ b/src/Explorer/OpenActions.test.ts @@ -17,7 +17,6 @@ describe("OpenActions", () => { explorer.onNewCollectionClicked = jest.fn(); explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane; explorer.cassandraAddCollectionPane.open = jest.fn(); - explorer.closeAllPanes = () => {}; database = { id: ko.observable("db"), diff --git a/src/Explorer/OpenActions.ts b/src/Explorer/OpenActions.ts index 8a7c87f53..afc3251c2 100644 --- a/src/Explorer/OpenActions.ts +++ b/src/Explorer/OpenActions.ts @@ -140,19 +140,16 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) { action.paneKind === ActionContracts.PaneKind.AddCollection || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] ) { - explorer.closeAllPanes(); explorer.onNewCollectionClicked(); } else if ( action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] ) { - explorer.closeAllPanes(); explorer.cassandraAddCollectionPane.open(); } else if ( action.paneKind === ActionContracts.PaneKind.GlobalSettings || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] ) { - explorer.closeAllPanes(); explorer.openSettingPane(); } } diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 5da784af4..9be73eb7e 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -21,115 +21,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -1015,26 +906,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], diff --git a/src/Explorer/Panes/GraphStylingPane.html b/src/Explorer/Panes/GraphStylingPane.html deleted file mode 100644 index b6667744c..000000000 --- a/src/Explorer/Panes/GraphStylingPane.html +++ /dev/null @@ -1,59 +0,0 @@ -
-
-
- -
- - -
- Graph Styling -
- Close -
-
- - - -
-
- Error - - - - More details - - -
-
- - - -
- -
-
-
-
- - -
- -
-
diff --git a/src/Explorer/Panes/GraphStylingPane.ts b/src/Explorer/Panes/GraphStylingPane.ts deleted file mode 100644 index 10188d7f1..000000000 --- a/src/Explorer/Panes/GraphStylingPane.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { ContextualPaneBase } from "./ContextualPaneBase"; - -export default class GraphStylingPane extends ContextualPaneBase { - public graphConfigUIData: ViewModels.GraphConfigUiData; - private remoteConfig: ViewModels.GraphConfigUiData; - - constructor(options: ViewModels.PaneOptions) { - super(options); - - this.graphConfigUIData = { - showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY), - nodeProperties: ko.observableArray([]), - nodePropertiesWithNone: ko.observableArray([]), - nodeCaptionChoice: ko.observable(null), - nodeColorKeyChoice: ko.observable(null), - nodeIconChoice: ko.observable(null), - nodeIconSet: ko.observable(null), - }; - - this.graphConfigUIData.nodeCaptionChoice.subscribe((val) => { - if (this.remoteConfig) { - this.remoteConfig.nodeCaptionChoice(val); - } - }); - this.graphConfigUIData.nodeColorKeyChoice.subscribe((val) => { - if (this.remoteConfig) { - this.remoteConfig.nodeColorKeyChoice(val); - } - }); - this.graphConfigUIData.nodeIconChoice.subscribe((val) => { - if (this.remoteConfig) { - this.remoteConfig.nodeIconChoice(val); - } - }); - this.graphConfigUIData.nodeIconSet.subscribe((val) => { - if (this.remoteConfig) { - this.remoteConfig.nodeIconSet(val); - } - }); - this.graphConfigUIData.showNeighborType.subscribe((val) => { - if (this.remoteConfig) { - this.remoteConfig.showNeighborType(val); - } - }); - } - - public setData(config: ViewModels.GraphConfigUiData): void { - // Update pane ko's with config's ko - this.graphConfigUIData.nodeIconChoice(config.nodeIconChoice()); - this.graphConfigUIData.nodeIconSet(config.nodeIconSet()); - this.graphConfigUIData.nodeProperties(config.nodeProperties()); - this.graphConfigUIData.nodePropertiesWithNone(config.nodePropertiesWithNone()); - this.graphConfigUIData.showNeighborType(config.showNeighborType()); - // Make sure these two happen *after* setting the options of the dropdown, - // otherwise, the ko will not get set if the choice is not part of the options - this.graphConfigUIData.nodeCaptionChoice(config.nodeCaptionChoice()); - this.graphConfigUIData.nodeColorKeyChoice(config.nodeColorKeyChoice()); - - this.remoteConfig = config; - } - - public close() { - this.remoteConfig = null; - super.close(); - } -} diff --git a/src/Explorer/Panes/GraphStylingPanel/GraphStylingPanel.tsx b/src/Explorer/Panes/GraphStylingPanel/GraphStylingPanel.tsx new file mode 100644 index 000000000..42e7dca60 --- /dev/null +++ b/src/Explorer/Panes/GraphStylingPanel/GraphStylingPanel.tsx @@ -0,0 +1,37 @@ +import React, { FunctionComponent } from "react"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent"; +import { IGraphConfig } from "../../Tabs/GraphTab"; +import { PanelFooterComponent } from "../PanelFooterComponent"; +interface GraphStylingProps { + closePanel: () => void; + igraphConfigUiData: ViewModels.IGraphConfigUiData; + igraphConfig: IGraphConfig; + getValues: (igraphConfig?: IGraphConfig) => void; +} + +export const GraphStylingPanel: FunctionComponent = ({ + closePanel, + igraphConfigUiData, + igraphConfig, + getValues, +}: GraphStylingProps): JSX.Element => { + const buttonLabel = "Ok"; + + const submit = () => { + closePanel(); + }; + + return ( +
+
+ +
+ + + ); +}; diff --git a/src/Explorer/Panes/PaneComponents.ts b/src/Explorer/Panes/PaneComponents.ts index 25aff2d54..655981948 100644 --- a/src/Explorer/Panes/PaneComponents.ts +++ b/src/Explorer/Panes/PaneComponents.ts @@ -1,7 +1,5 @@ import AddDatabasePaneTemplate from "./AddDatabasePane.html"; import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; -import GraphStylingPaneTemplate from "./GraphStylingPane.html"; - export class PaneComponent { constructor(data: any) { return data.data; @@ -17,15 +15,6 @@ export class AddDatabasePaneComponent { } } -export class GraphStylingPaneComponent { - constructor() { - return { - viewModel: PaneComponent, - template: GraphStylingPaneTemplate, - }; - } -} - export class CassandraAddCollectionPaneComponent { constructor() { return { diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index f779e8b74..3244fa044 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -11,115 +11,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -1005,26 +896,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index ace1945cb..ccce25b5b 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -9,115 +9,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "_closeSynapseLinkModalDialog": [Function], "_isAfecFeatureRegistered": [Function], "_isInitializingNotebooks": false, - "_panes": Array [ - AddDatabasePane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "databaseCreateNewShared": [Function], - "databaseId": [Function], - "databaseIdLabel": [Function], - "databaseIdPlaceHolder": [Function], - "databaseIdTooltipText": [Function], - "databaseLevelThroughputTooltipText": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "freeTierExceedThroughputTooltip": [Function], - "id": "adddatabasepane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isTemplateReady": [Function], - "maxAutoPilotThroughputSet": [Function], - "maxThroughputRU": [Function], - "maxThroughputRUText": [Function], - "minThroughputRU": [Function], - "onMoreDetailsKeyPress": [Function], - "requestUnitsUsageCost": [Function], - "ruToolTipText": [Function], - "showUpsellMessage": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "upsellAnchorText": [Function], - "upsellAnchorUrl": [Function], - "upsellMessage": [Function], - "upsellMessageAriaLabel": [Function], - "visible": [Function], - }, - GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, - CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, - ], "_refreshSparkEnabledStateForAccount": [Function], "_resetNotebookWorkspace": [Function], "addCollectionText": [Function], @@ -1003,26 +894,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database }, }, }, - "graphStylingPane": GraphStylingPane { - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "graphConfigUIData": Object { - "nodeCaptionChoice": [Function], - "nodeColorKeyChoice": [Function], - "nodeIconChoice": [Function], - "nodeIconSet": [Function], - "nodeProperties": [Function], - "nodePropertiesWithNone": [Function], - "showNeighborType": [Function], - }, - "id": "graphstylingpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "title": [Function], - "visible": [Function], - }, "hasStorageAnalyticsAfecFeature": [Function], "isAccountReady": [Function], "isAutoscaleDefaultEnabled": [Function], diff --git a/src/Explorer/Tabs/GraphTab.tsx b/src/Explorer/Tabs/GraphTab.tsx index 3402b7275..b621a1a3e 100644 --- a/src/Explorer/Tabs/GraphTab.tsx +++ b/src/Explorer/Tabs/GraphTab.tsx @@ -11,8 +11,10 @@ import { GraphExplorerError, GraphExplorerProps, } from "../Graph/GraphExplorerComponent/GraphExplorer"; +// import { GraphAccessor, GraphExplorer, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer"; +// import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; -import GraphStylingPane from "../Panes/GraphStylingPane"; +import { GraphStylingPanel } from "../Panes/GraphStylingPanel/GraphStylingPanel"; import { NewVertexPanel } from "../Panes/NewVertexPanel/NewVertexPanel"; import TabsBase from "./TabsBase"; export interface GraphIconMap { @@ -31,6 +33,18 @@ export interface GraphConfig { iconsMap: ko.Observable; } +export interface IGraphConfig { + nodeColor: string; + nodeColorKey: string; + linkColor: string; + showNeighborType: ViewModels.NeighborType; + nodeCaption: string; + nodeSize: number; + linkWidth: number; + nodeIconKey: string; + iconsMap: GraphIconMap; +} + interface GraphTabOptions extends ViewModels.TabOptions { account: DatabaseAccount; masterKey: string; @@ -39,6 +53,7 @@ interface GraphTabOptions extends ViewModels.TabOptions { collectionPartitionKeyProperty: string; } +// export default class GraphTab extends React.Component { export default class GraphTab extends TabsBase { // Graph default configuration public static readonly DEFAULT_NODE_CAPTION = "id"; @@ -51,27 +66,24 @@ export default class GraphTab extends TabsBase { private isPropertyEditing: ko.Observable; private isGraphDisplayed: ko.Observable; private graphAccessor: GraphAccessor; - private graphConfig: GraphConfig; - private graphConfigUiData: ViewModels.GraphConfigUiData; + private igraphConfig: IGraphConfig; + private igraphConfigUiData: ViewModels.IGraphConfigUiData; private isFilterQueryLoading: ko.Observable; private isValidQuery: ko.Observable; - private graphStylingPane: GraphStylingPane; private collectionPartitionKeyProperty: string; private contextualPane: ContextualPaneBase; - + public graphExplorer: GraphExplorer; + public options: GraphTabOptions; constructor(options: GraphTabOptions) { super(options); - this.graphStylingPane = options.collection && options.collection.container.graphStylingPane; this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty; - this.isNewVertexDisabled = ko.observable(false); this.isPropertyEditing = ko.observable(false); this.isGraphDisplayed = ko.observable(false); this.graphAccessor = undefined; - this.graphConfig = GraphTab.createGraphConfig(); - // TODO Merge this with this.graphConfig - this.graphConfigUiData = GraphTab.createGraphConfigUiData(this.graphConfig); + this.igraphConfig = GraphTab.createIGraphConfig(); + this.igraphConfigUiData = GraphTab.createIGraphConfigUiData(this.igraphConfig); this.graphExplorerProps = { onGraphAccessorCreated: (instance: GraphAccessor): void => { this.graphAccessor = instance; @@ -88,9 +100,9 @@ export default class GraphTab extends TabsBase { this.isGraphDisplayed(isDisplayed); this.updateNavbarWithTabsButtons(); }, - onResetDefaultGraphConfigValues: () => this.setDefaultGraphConfigValues(), - graphConfig: this.graphConfig, - graphConfigUiData: this.graphConfigUiData, + onResetDefaultGraphConfigValues: () => this.setDefaultIGraphConfigValues(), + igraphConfig: this.igraphConfig, + igraphConfigUiData: this.igraphConfigUiData, onIsFilterQueryLoadingChange: (isFilterQueryLoading: boolean): void => this.isFilterQueryLoading(isFilterQueryLoading), onIsValidQueryChange: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery), @@ -106,10 +118,12 @@ export default class GraphTab extends TabsBase { } }, resourceId: options.account.id, + setIConfigUiData: this.setIGraphConfigUiData, }; this.isFilterQueryLoading = ko.observable(false); this.isValidQuery = ko.observable(true); + // this.setCaption = this.setCaption.bind(this); } public static getGremlinEndpoint(account: DatabaseAccount): string { @@ -170,60 +184,76 @@ export default class GraphTab extends TabsBase { ); } public openStyling(): void { - this.setDefaultGraphConfigValues(); - // Update the styling pane with this instance - this.graphStylingPane.setData(this.graphConfigUiData); - this.graphStylingPane.open(); + this.collection.container.openSidePanel( + "Graph Style", + { + this.igraphConfig = igraphConfig; + this.graphAccessor.shareIGraphConfig(igraphConfig); + }} + /> + ); } - public static createGraphConfig(): GraphConfig { + setIGraphConfigUiData = (val: string[]): void => { + if (val.length > 0) { + this.igraphConfigUiData = { + showNeighborType: ViewModels.NeighborType.TARGETS_ONLY, + nodeProperties: val, + nodePropertiesWithNone: [GraphExplorer.NONE_CHOICE].concat(val), + nodeCaptionChoice: this.igraphConfig.nodeCaption, + nodeColorKeyChoice: "None", + nodeIconChoice: "Node", + nodeIconSet: "", + }; + } + }; + + public static createIGraphConfig(): IGraphConfig { return { - nodeColor: ko.observable(GraphTab.NODE_COLOR), - nodeColorKey: ko.observable(undefined), - linkColor: ko.observable(GraphTab.LINK_COLOR), - showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY), - nodeCaption: ko.observable(GraphTab.DEFAULT_NODE_CAPTION), - nodeSize: ko.observable(GraphTab.NODE_SIZE), - linkWidth: ko.observable(GraphTab.LINK_WIDTH), - nodeIconKey: ko.observable(undefined), - iconsMap: ko.observable({}), + nodeColor: GraphTab.NODE_COLOR, + nodeColorKey: "None", + linkColor: GraphTab.LINK_COLOR, + showNeighborType: ViewModels.NeighborType.TARGETS_ONLY, + nodeCaption: GraphTab.DEFAULT_NODE_CAPTION, + nodeSize: GraphTab.NODE_SIZE, + linkWidth: GraphTab.LINK_WIDTH, + nodeIconKey: undefined, + iconsMap: {}, }; } - public static createGraphConfigUiData(graphConfig: GraphConfig): ViewModels.GraphConfigUiData { + public static createIGraphConfigUiData(igraphConfig: IGraphConfig): ViewModels.IGraphConfigUiData { return { - showNeighborType: ko.observable(graphConfig.showNeighborType()), - nodeProperties: ko.observableArray([]), - nodePropertiesWithNone: ko.observableArray([]), - nodeCaptionChoice: ko.observable(graphConfig.nodeCaption()), - nodeColorKeyChoice: ko.observable(graphConfig.nodeColorKey()), - nodeIconChoice: ko.observable(graphConfig.nodeIconKey()), - nodeIconSet: ko.observable(undefined), + showNeighborType: igraphConfig.showNeighborType, + nodeProperties: [], + nodePropertiesWithNone: [], + nodeCaptionChoice: igraphConfig.nodeCaption, + nodeColorKeyChoice: igraphConfig.nodeIconKey, + nodeIconChoice: igraphConfig.nodeIconKey, + nodeIconSet: undefined, }; } - /** - * Make sure graph config values are not undefined - */ - private setDefaultGraphConfigValues() { + private setDefaultIGraphConfigValues() { // Assign default values if undefined - if ( - this.graphConfigUiData.nodeCaptionChoice() === undefined && - this.graphConfigUiData.nodeProperties().length > 1 - ) { - this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]); + if (this.igraphConfigUiData.nodeCaptionChoice === undefined && this.igraphConfigUiData.nodeProperties.length > 1) { + this.igraphConfigUiData.nodeCaptionChoice = this.igraphConfigUiData.nodeProperties[0]; } if ( - this.graphConfigUiData.nodeColorKeyChoice() === undefined && - this.graphConfigUiData.nodePropertiesWithNone().length > 1 + this.igraphConfigUiData.nodeColorKeyChoice === undefined && + this.igraphConfigUiData.nodePropertiesWithNone.length > 1 ) { - this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); + this.igraphConfigUiData.nodeColorKeyChoice = this.igraphConfigUiData.nodePropertiesWithNone[0]; } if ( - this.graphConfigUiData.nodeIconChoice() === undefined && - this.graphConfigUiData.nodePropertiesWithNone().length > 1 + this.igraphConfigUiData.nodeIconChoice === undefined && + this.igraphConfigUiData.nodePropertiesWithNone.length > 1 ) { - this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); + this.igraphConfigUiData.nodeIconChoice = this.igraphConfigUiData.nodePropertiesWithNone[0]; } } protected getTabsButtons(): CommandButtonComponentProps[] { diff --git a/src/Main.tsx b/src/Main.tsx index 2abf3d2f5..76bab54c2 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -155,7 +155,6 @@ const App: React.FunctionComponent = () => { isConsoleExpanded={isNotificationConsoleExpanded} />
-
{showDialog && }
From f9e8b5eaaae0af2b39b93ab999bcca5f0904a3a8 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Thu, 13 May 2021 18:03:29 -0500 Subject: [PATCH 12/12] Remove Unused Knockout Components (#783) --- src/Explorer/ComponentRegisterer.test.ts | 8 - src/Explorer/ComponentRegisterer.ts | 4 - .../ErrorDisplayComponent.ts | 27 --- .../error-display-component.html | 6 - .../Controls/InputTypeahead/InputTypeahead.ts | 186 ------------------ .../InputTypeahead/input-typeahead.html | 19 -- tsconfig.strict.json | 1 - 7 files changed, 251 deletions(-) delete mode 100644 src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts delete mode 100644 src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html delete mode 100644 src/Explorer/Controls/InputTypeahead/InputTypeahead.ts delete mode 100644 src/Explorer/Controls/InputTypeahead/input-typeahead.html diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts index 16244a16c..ea9164787 100644 --- a/src/Explorer/ComponentRegisterer.test.ts +++ b/src/Explorer/ComponentRegisterer.test.ts @@ -4,14 +4,6 @@ import * as ko from "knockout"; import "./ComponentRegisterer"; describe("Component Registerer", () => { - it("should register input-typeahead component", () => { - expect(ko.components.isRegistered("input-typeahead")).toBe(true); - }); - - it("should register error-display component", () => { - expect(ko.components.isRegistered("error-display")).toBe(true); - }); - it("should register json-editor component", () => { expect(ko.components.isRegistered("json-editor")).toBe(true); }); diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index ff4c4f96e..4dc4c8923 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -2,14 +2,10 @@ import * as ko from "knockout"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent"; -import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; -import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import * as PaneComponents from "./Panes/PaneComponents"; -ko.components.register("input-typeahead", new InputTypeaheadComponent()); -ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("editor", new EditorComponent()); ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent()); diff --git a/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts b/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts deleted file mode 100644 index 0d54838f6..000000000 --- a/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts +++ /dev/null @@ -1,27 +0,0 @@ -import template from "./error-display-component.html"; - -/** - * Helper class for ko component registration - * This component displays an error as designed in: - * https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29 - * TODO: support "More details" - */ -export class ErrorDisplayComponent { - constructor() { - return { - viewModel: ErrorDisplayViewModel, - template, - }; - } -} - -/** - * Parameters for this component - */ -interface ErrorDisplayParams { - errorMsg: ko.Observable; // Primary message -} - -class ErrorDisplayViewModel { - public constructor(public params: ErrorDisplayParams) {} -} diff --git a/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html b/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html deleted file mode 100644 index ca4b301ce..000000000 --- a/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
- Error - -
-
diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts b/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts deleted file mode 100644 index 03a3b0d87..000000000 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * How to use this component: - * - * In your html markup, use: - * - * The parameters are documented below. - * - * Notes: - * - dynamic:true by default, this allows choices to change after initialization. - * To turn it off, use: - * typeaheadOverrideOptions: { dynamic:false } - * - */ - -import "jquery-typeahead"; -import template from "./input-typeahead.html"; - -/** - * Helper class for ko component registration - */ -export class InputTypeaheadComponent { - constructor() { - return { - viewModel: InputTypeaheadViewModel, - template, - }; - } -} - -export interface Item { - caption: string; - value: any; -} - -/** - * Parameters for this component - */ -interface InputTypeaheadParams { - /** - * List of choices available in the dropdown. - */ - choices: ko.ObservableArray; - - /** - * Gets updated when user clicks on the choice in the dropdown - */ - selection?: ko.Observable; - - /** - * The current string value of - */ - inputValue?: ko.Observable; - - /** - * Define what text you want as the input placeholder - */ - placeholder: string; - - /** - * Override default jquery-typeahead options - * WARNING: do not override input, source or callback to avoid breaking the components behavior. - */ - typeaheadOverrideOptions?: any; - - /** - * This function gets called when pressing ENTER on the input box - */ - submitFct?: (inputValue: string | null, selection: Item | null) => void; - - /** - * Typehead comes with a Search button that we normally remove. - * If you want to use it, turn this on - */ - showSearchButton?: boolean; -} - -interface OnClickItem { - matchedKey: string; - value: any; - caption: string; - group: string; -} - -interface Cache { - inputValue: string | null; - selection: Item | null; -} - -class InputTypeaheadViewModel { - private static instanceCount = 0; // Generate unique id for each component's typeahead instance - private instanceNumber: number; - private params: InputTypeaheadParams; - - private cache: Cache; - - public constructor(params: InputTypeaheadParams) { - this.instanceNumber = InputTypeaheadViewModel.instanceCount++; - this.params = params; - - this.params.choices.subscribe(this.initializeTypeahead.bind(this)); - this.cache = { - inputValue: null, - selection: null, - }; - } - - /** - * Must execute once ko is rendered, so that it can find the input element by id - */ - private initializeTypeahead() { - let params = this.params; - let cache = this.cache; - let options: any = { - input: `#${this.getComponentId()}`, //'.input-typeahead', - order: "asc", - minLength: 0, - searchOnFocus: true, - source: { - display: "caption", - data: () => { - return this.params.choices(); - }, - }, - callback: { - onClick: (_node: unknown, _a: unknown, item: OnClickItem) => { - cache.selection = item; - - if (params.selection) { - params.selection(item); - } - }, - onResult(_node: unknown, query: any) { - cache.inputValue = query; - if (params.inputValue) { - params.inputValue(query); - } - }, - }, - template: (_query: string, item: any) => { - // Don't display id if caption *IS* the id - return item.caption === item.value - ? "{{caption}}" - : "
{{caption}}
{{value}}
"; - }, - dynamic: true, - }; - - // Override options - if (params.typeaheadOverrideOptions) { - for (let p in params.typeaheadOverrideOptions) { - options[p] = params.typeaheadOverrideOptions[p]; - } - } - - ($ as any).typeahead(options); - } - - /** - * Get this component id - * @return unique id per instance - */ - private getComponentId(): string { - return `input-typeahead${this.instanceNumber}`; - } - - /** - * Executed once ko is done rendering bindings - * Use ko's "template: afterRender" callback to do that without actually using any template. - * Another way is to call it within setTimeout() in constructor. - */ - public afterRender(): void { - this.initializeTypeahead(); - } - - public submit(): void { - if (this.params.submitFct) { - this.params.submitFct(this.cache.inputValue, this.cache.selection); - } - } -} diff --git a/src/Explorer/Controls/InputTypeahead/input-typeahead.html b/src/Explorer/Controls/InputTypeahead/input-typeahead.html deleted file mode 100644 index 171fef8bf..000000000 --- a/src/Explorer/Controls/InputTypeahead/input-typeahead.html +++ /dev/null @@ -1,19 +0,0 @@ - -
-
-
- - - - - - -
-
-
-
diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 3ff54e123..7a8971ce1 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -38,7 +38,6 @@ "./src/Contracts/Versions.ts", "./src/Explorer/Controls/Dialog.tsx", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", - "./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts",