From afacde404165576ef206b95b31327bdf0511a4e6 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Mon, 19 Jul 2021 22:00:33 -0700 Subject: [PATCH 01/20] Fix AddTableEntityPanel (#945) * Fix AddTableEntityPanel * Add CSS * Fix snapshot --- src/Explorer/Notebook/NotebookManager.tsx | 1 + .../Panes/PanelContainerComponent.tsx | 4 +- .../Panes/Tables/AddTableEntityPanel.tsx | 214 +- .../Tables/Validators/EntityTableHelper.tsx | 13 +- .../AddTableEntityPanel.test.tsx.snap | 4555 +++++++---------- src/Explorer/Tabs/QueryTablesTab.tsx | 5 +- src/hooks/useSidePanel.ts | 7 +- 7 files changed, 1904 insertions(+), 2895 deletions(-) diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 989cd39d1..d6d13b6a1 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -141,6 +141,7 @@ export default class NotebookManager { notebookContentRef={notebookContentRef} onTakeSnapshot={onTakeSnapshot} />, + "440px", onClosePanel ); } diff --git a/src/Explorer/Panes/PanelContainerComponent.tsx b/src/Explorer/Panes/PanelContainerComponent.tsx index e60f07230..2ef918b7a 100644 --- a/src/Explorer/Panes/PanelContainerComponent.tsx +++ b/src/Explorer/Panes/PanelContainerComponent.tsx @@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component { const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded); - const { isOpen, panelContent, headerText } = useSidePanel((state) => { + const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => { return { isOpen: state.isOpen, panelContent: state.panelContent, headerText: state.headerText, + panelWidth: state.panelWidth, }; }); // TODO Refactor PanelContainerComponent into a functional component and remove this wrapper @@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => { panelContent={panelContent} headerText={headerText} isConsoleExpanded={isConsoleExpanded} + panelWidth={panelWidth} /> ); }; diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 5107bc6da..402e87da6 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -1,11 +1,11 @@ -import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react"; +import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as _ from "underscore"; import AddPropertyIcon from "../../../../images/Add-property.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg"; +import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { TableEntity } from "../../../Common/TableEntity"; -import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "../../../UserContext"; import * as TableConstants from "../../Tables/Constants"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; @@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../. import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as Utilities from "../../Tables/Utilities"; import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import { PanelContainerComponent } from "../PanelContainerComponent"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { attributeNameLabel, attributeValueLabel, @@ -30,9 +30,7 @@ import { getCassandraDefaultEntities, getDefaultEntities, getEntityValuePlaceholder, - getPanelTitle, imageProps, - isValidEntities, options, } from "./Validators/EntityTableHelper"; @@ -61,7 +59,6 @@ export const AddTableEntityPanel: FunctionComponent = tableEntityListViewModel, cassandraApiClient, }: AddTableEntityPanelProps): JSX.Element => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const [entities, setEntities] = useState([]); const [selectedRow, setSelectedRow] = useState(0); const [entityAttributeValue, setEntityAttributeValue] = useState(""); @@ -70,6 +67,8 @@ export const AddTableEntityPanel: FunctionComponent = isEntityValuePanelOpen, { setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse }, ] = useBoolean(false); + const [formError, setFormError] = useState(""); + const [isExecuting, setIsExecuting] = useState(false); /* Get default and previous saved entity headers */ useEffect(() => { @@ -98,19 +97,36 @@ export const AddTableEntityPanel: FunctionComponent = }; /* Add new entity attribute */ - const submit = async (event: React.FormEvent): Promise => { - if (!isValidEntities(entities)) { - return undefined; - } - event.preventDefault(); + const onSubmit = async (): Promise => { + for (let i = 0; i < entities.length; i++) { + const { property, type } = entities[i]; + if (property === "" || property === undefined) { + setFormError(`Property name cannot be empty. Please enter a property name`); + return; + } + if (!type) { + setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); + return; + } + } + + setIsExecuting(true); const entity: Entities.ITableEntity = entityFromAttributes(entities); const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); - await tableEntityListViewModel.addEntityToCache(newEntity); - if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { - tableEntityListViewModel.redrawTableThrottled(); + try { + await tableEntityListViewModel.addEntityToCache(newEntity); + if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { + tableEntityListViewModel.redrawTableThrottled(); + } + } catch (error) { + const errorMessage = getErrorMessage(error); + setFormError(errorMessage); + handleError(errorMessage, "AddTableRow"); + throw error; + } finally { + setIsExecuting(false); } - closeSidePanel(); }; const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { @@ -200,110 +216,80 @@ export const AddTableEntityPanel: FunctionComponent = setIsEntityValuePanelTrue(); }; - const renderPanelContent = (): JSX.Element => { - return ( -
-
-
- {entities.map((entity, index) => { - return ( - editEntity(index)} - onSelectDate={(date: Date) => { - entityChange(date, index, "value"); - }} - onDeleteEntity={() => deleteEntityAtIndex(index)} - onEntityPropertyChange={(event, newInput?: string) => { - entityChange(newInput, index, "property"); - }} - onEntityTypeChange={(event: React.FormEvent, selectedParam: IDropdownOption) => { - entityTypeChange(event, selectedParam, index); - }} - onEntityValueChange={(event, newInput?: string) => { - entityChange(newInput, index, "value"); - }} - onEntityTimeValueChange={(event, newInput?: string) => { - entityChange(newInput, index, "time"); - }} - /> - ); - })} - {userContext.apiType !== "Cassandra" && ( - - Add Entity - {getAddButtonLabel(userContext.apiType)} - - )} -
-
-
- -
-
-
-
- ); - }; - - const onRenderNavigationContent: IRenderFunction = () => { - return ( - - back setIsEntityValuePanelFalse()} /> - - - ); - }; - if (isEntityValuePanelOpen) { return ( - { - entityChange(newInput, selectedRow, "value"); - setEntityAttributeValue(newInput); - }} - /> - } - isConsoleExpanded={false} - /> + + + back setIsEntityValuePanelFalse()} /> + + + { + entityChange(newInput, selectedRow, "value"); + setEntityAttributeValue(newInput); + }} + /> + ); } + const props: RightPaneFormProps = { + formError, + isExecuting, + submitButtonText: getButtonLabel(userContext.apiType), + onSubmit, + }; + return ( - + +
+ {entities.map((entity, index) => { + return ( + editEntity(index)} + onSelectDate={(date: Date) => { + entityChange(date, index, "value"); + }} + onDeleteEntity={() => deleteEntityAtIndex(index)} + onEntityPropertyChange={(event, newInput?: string) => { + entityChange(newInput, index, "property"); + }} + onEntityTypeChange={(event: React.FormEvent, selectedParam: IDropdownOption) => { + entityTypeChange(event, selectedParam, index); + }} + onEntityValueChange={(event, newInput?: string) => { + entityChange(newInput, index, "value"); + }} + onEntityTimeValueChange={(event, newInput?: string) => { + entityChange(newInput, index, "time"); + }} + /> + ); + })} + {userContext.apiType !== "Cassandra" && ( + + Add Entity + {getAddButtonLabel(userContext.apiType)} + + )} +
+
); }; diff --git a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx index 5011659dd..b5542e86e 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx +++ b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx @@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2 export const columnProps: Partial = { tokens: { childrenGap: 10 }, - styles: { root: { width: 680 } }, + styles: { root: { width: 680, marginBottom: 8 } }, }; // helper functions @@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string = export const isValidEntities = (entities: EntityRowType[]): boolean => { for (let i = 0; i < entities.length; i++) { - const { property } = entities[i]; - if (property === "" || property === undefined) { + const { property, type } = entities[i]; + if (property === "" || property === undefined || !type) { return false; } } @@ -170,13 +170,6 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType): return defaultEntities; }; -export const getPanelTitle = (apiType: string): string => { - if (apiType === "Cassandra") { - return "Add Table Row"; - } - return "Add Table Row"; -}; - export const getAddButtonLabel = (apiType: string): string => { if (apiType === "Cassandra") { return "Add Row"; diff --git a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap index cae12c746..c78e43d5c 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap @@ -12,879 +12,1780 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` } } > - +
+
-
- - - +
+ Add Entity +
+ + + + Add Property - -
+ +
-
-
- -
-
-
- - } - panelWidth="700px" - > - - +
+ - - + - - -
-
-
-
-
- -
-
-
+ + - - *": Object { + "left": 0, + "position": "relative", + "top": 0, + }, + }, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + }, + Object { + "height": "32px", + "minWidth": "80px", + }, + Object { + "backgroundColor": "#0078d4", + "border": "1px solid #0078d4", + "color": "#ffffff", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus": Object { + "selectors": Object { + ":after": Object { + "border": "none", + "outlineColor": "#ffffff", + }, + }, + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "borderColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + ], + "rootChecked": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootCheckedHovered": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootDisabled": Array [ + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid transparent", + "bottom": 2, + "content": "\\"\\"", + "left": 2, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 2, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "bottom": -2, + "left": -2, + "outlineColor": "ButtonText", + "right": -2, + "top": -2, + }, + }, + "top": 2, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "backgroundColor": "#f3f2f1", + "borderColor": "#f3f2f1", + "color": "#a19f9d", + "cursor": "default", + "selectors": Object { + ":focus": Object { + "outline": 0, + }, + ":hover": Object { + "outline": 0, + }, + }, + }, + Object { + "backgroundColor": "#f3f2f1", + "color": "#d2d0ce", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + ], + "rootExpanded": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootHovered": Object { + "backgroundColor": "#106ebe", + "border": "1px solid #106ebe", + "color": "#ffffff", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Highlight", + "borderColor": "Highlight", + "color": "Window", + }, + }, + }, + "rootPressed": Object { + "backgroundColor": "#005a9e", + "border": "1px solid #005a9e", + "color": "#ffffff", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "borderColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + "screenReaderText": Object { + "border": 0, + "height": 1, + "margin": -1, + "overflow": "hidden", + "padding": 0, + "position": "absolute", + "width": 1, + }, + "splitButtonContainer": Array [ + Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + }, + }, + }, + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid #ffffff", + "bottom": 3, + "content": "\\"\\"", + "left": 3, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 3, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + "bottom": -2, + "left": -2, + "right": -2, + "top": -2, + }, + }, + "top": 3, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "display": "inline-flex", + "selectors": Object { + ".ms-Button--default": Object { + "borderBottomRightRadius": "0", + "borderRight": "none", + "borderTopRightRadius": "0", + }, + ".ms-Button--primary": Object { + "border": "none", + "borderBottomRightRadius": "0", + "borderTopRightRadius": "0", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "border": "1px solid WindowText", + "borderRightWidth": "0", + "color": "WindowText", + "forcedColorAdjust": "none", + }, + }, + }, + ".ms-Button--primary + .ms-Button": Object { + "border": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "1px solid WindowText", + "borderLeftWidth": "0", + }, + }, + }, + }, + }, + ], + "splitButtonContainerChecked": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerCheckedHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerDisabled": Object { + "border": "none", + "outline": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + "forcedColorAdjust": "none", + }, + }, + }, + "splitButtonContainerFocused": Object { + "outline": "none!important", + }, + "splitButtonContainerHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Highlight", + "color": "Window", + }, + }, + }, + ".ms-Button.is-disabled": Object { + "color": "#a19f9d", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + }, + }, + "splitButtonDivider": Array [ + Object { + "backgroundColor": "#ffffff", + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + }, + }, + "top": 8, + "width": 1, + }, + Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "WindowText", + }, + }, + "top": 8, + "width": 1, + }, + ], + "splitButtonDividerDisabled": Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "GrayText", + }, + }, + "top": 8, + "width": 1, + }, + "splitButtonFlexContainer": Object { + "alignItems": "center", + "display": "flex", + "flexWrap": "nowrap", + "height": "100%", + "justifyContent": "center", + }, + "splitButtonMenuButton": Array [ + Object { + "backgroundColor": "#0078d4", + "color": "#ffffff", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#106ebe", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "Highlight", + }, + }, + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "WindowText", + }, + }, + }, + Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + ".ms-Button-menuIcon": Object { + "color": "WindowText", + }, + }, + "border": "1px solid #8a8886", + "borderBottomRightRadius": "2px", + "borderLeft": "none", + "borderRadius": 0, + "borderTopRightRadius": "2px", + "boxSizing": "border-box", + "cursor": "pointer", + "display": "inline-block", + "height": "auto", + "marginBottom": 0, + "marginLeft": -1, + "marginRight": 0, + "marginTop": 0, + "outline": "transparent", + "padding": 6, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + "verticalAlign": "top", + "width": 32, + }, + ], + "splitButtonMenuButtonChecked": Object { + "backgroundColor": "#005a9e", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#005a9e", + }, + }, + }, + "splitButtonMenuButtonDisabled": Array [ + Object { + "backgroundColor": "#f3f2f1", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#f3f2f1", + }, + }, + }, + Object { + "border": "none", + "pointerEvents": "none", + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + ".ms-Button-menuIcon": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "GrayText", + }, + }, + }, + ":hover": Object { + "cursor": "default", + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "border": "1px solid GrayText", + "color": "GrayText", + }, + }, + }, + ], + "splitButtonMenuButtonExpanded": Object { + "backgroundColor": "#005a9e", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#005a9e", + }, + }, + }, + "splitButtonMenuFocused": Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid #ffffff", + "bottom": 3, + "content": "\\"\\"", + "left": 3, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 3, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + "bottom": -2, + "left": -2, + "right": -2, + "top": -2, + }, + }, + "top": 3, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + "splitButtonMenuIcon": Object { + "color": "#ffffff", + }, + "splitButtonMenuIconDisabled": Object { + "color": "#a19f9d", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "GrayText", + }, + }, + }, + "textContainer": Object { + "display": "block", + "flexGrow": 1, + }, + } + } + text="Add Entity" theme={ Object { "disableGlobalClassNames": false, @@ -1158,1926 +2059,48 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` }, } } + type="submit" + variantClassName="ms-Button--primary" > -
- -
-
- - -
- - - -
-
-
-
-
-
- Add Table Row -
-
- - - *": Object { - "left": 0, - "position": "relative", - "top": 0, - }, - }, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - }, - Object { - "backgroundColor": "transparent", - "border": "none", - "color": "#0078d4", - "height": "32px", - "padding": "0 4px", - "width": "32px", - }, - "ms-Panel-closeButton ms-PanelAction-close", - Object { - "color": "#605e5c", - "fontSize": "20px", - "marginRight": 14, - }, - false, - ], - "rootChecked": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootCheckedHovered": Object { - "backgroundColor": "#e1dfdd", - "color": "#005a9e", - }, - "rootDisabled": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid transparent", - "bottom": 2, - "content": "\\"\\"", - "left": 2, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 2, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "bottom": -2, - "left": -2, - "outlineColor": "ButtonText", - "right": -2, - "top": -2, - }, - }, - "top": 2, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "backgroundColor": "#f3f2f1", - "borderColor": "#f3f2f1", - "color": "#a19f9d", - "cursor": "default", - "selectors": Object { - ":focus": Object { - "outline": 0, - }, - ":hover": Object { - "outline": 0, - }, - }, - }, - Object { - "color": "#c8c6c4", - }, - ], - "rootExpanded": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootHasMenu": Object { - "width": "auto", - }, - "rootHovered": Array [ - Object { - "backgroundColor": "#f3f2f1", - "color": "#106ebe", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "borderColor": "Highlight", - "color": "Highlight", - }, - }, - }, - Object { - "color": "#323130", - }, - ], - "rootPressed": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "screenReaderText": Object { - "border": 0, - "height": 1, - "margin": -1, - "overflow": "hidden", - "padding": 0, - "position": "absolute", - "width": 1, - }, - "splitButtonContainer": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "display": "inline-flex", - "selectors": Object { - ".ms-Button--default": Object { - "borderBottomRightRadius": "0", - "borderRight": "none", - "borderTopRightRadius": "0", - }, - ".ms-Button--primary": Object { - "border": "none", - "borderBottomRightRadius": "0", - "borderTopRightRadius": "0", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "border": "1px solid WindowText", - "borderRightWidth": "0", - "color": "WindowText", - "forcedColorAdjust": "none", - }, - }, - }, - ".ms-Button--primary + .ms-Button": Object { - "border": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "1px solid WindowText", - "borderLeftWidth": "0", - }, - }, - }, - }, - }, - ], - "splitButtonContainerChecked": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerCheckedHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerDisabled": Object { - "border": "none", - "outline": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - "forcedColorAdjust": "none", - }, - }, - }, - "splitButtonContainerFocused": Object { - "outline": "none!important", - }, - "splitButtonContainerHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Highlight", - "color": "Window", - }, - }, - }, - ".ms-Button.is-disabled": Object { - "color": "#a19f9d", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - }, - }, - "splitButtonDivider": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "WindowText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonDividerDisabled": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "GrayText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonFlexContainer": Object { - "alignItems": "center", - "display": "flex", - "flexWrap": "nowrap", - "height": "100%", - "justifyContent": "center", - }, - "splitButtonMenuButton": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - ".ms-Button-menuIcon": Object { - "color": "WindowText", - }, - }, - "border": "1px solid #8a8886", - "borderBottomRightRadius": "2px", - "borderLeft": "none", - "borderRadius": 0, - "borderTopRightRadius": "2px", - "boxSizing": "border-box", - "cursor": "pointer", - "display": "inline-block", - "height": "auto", - "marginBottom": 0, - "marginLeft": -1, - "marginRight": 0, - "marginTop": 0, - "outline": "transparent", - "padding": 6, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - "verticalAlign": "top", - "width": 32, - }, - "splitButtonMenuButtonDisabled": Object { - "border": "none", - "pointerEvents": "none", - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - ".ms-Button-menuIcon": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "color": "GrayText", - }, - }, - }, - ":hover": Object { - "cursor": "default", - }, - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "border": "1px solid GrayText", - "color": "GrayText", - }, - }, - }, - "splitButtonMenuFocused": Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - "textContainer": Object { - "display": "block", - "flexGrow": 1, - }, - } - } - theme={ - Object { - "disableGlobalClassNames": false, - "effects": Object { - "elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)", - "elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)", - "elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "roundedCorner2": "2px", - "roundedCorner4": "4px", - "roundedCorner6": "6px", - }, - "fonts": Object { - "large": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "18px", - "fontWeight": 400, - }, - "medium": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "14px", - "fontWeight": 400, - }, - "mediumPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "16px", - "fontWeight": 400, - }, - "mega": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "68px", - "fontWeight": 600, - }, - "small": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "smallPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "superLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "42px", - "fontWeight": 600, - }, - "tiny": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "20px", - "fontWeight": 600, - }, - "xLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "24px", - "fontWeight": 600, - }, - "xSmall": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xxLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "28px", - "fontWeight": 600, - }, - "xxLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "32px", - "fontWeight": 600, - }, - }, - "isInverted": false, - "palette": Object { - "accent": "#0078d4", - "black": "#000000", - "blackTranslucent40": "rgba(0,0,0,.4)", - "blue": "#0078d4", - "blueDark": "#002050", - "blueLight": "#00bcf2", - "blueMid": "#00188f", - "green": "#107c10", - "greenDark": "#004b1c", - "greenLight": "#bad80a", - "magenta": "#b4009e", - "magentaDark": "#5c005c", - "magentaLight": "#e3008c", - "neutralDark": "#201f1e", - "neutralLight": "#edebe9", - "neutralLighter": "#f3f2f1", - "neutralLighterAlt": "#faf9f8", - "neutralPrimary": "#323130", - "neutralPrimaryAlt": "#3b3a39", - "neutralQuaternary": "#d2d0ce", - "neutralQuaternaryAlt": "#e1dfdd", - "neutralSecondary": "#605e5c", - "neutralSecondaryAlt": "#8a8886", - "neutralTertiary": "#a19f9d", - "neutralTertiaryAlt": "#c8c6c4", - "orange": "#d83b01", - "orangeLight": "#ea4300", - "orangeLighter": "#ff8c00", - "purple": "#5c2d91", - "purpleDark": "#32145a", - "purpleLight": "#b4a0ff", - "red": "#e81123", - "redDark": "#a4262c", - "teal": "#008272", - "tealDark": "#004b50", - "tealLight": "#00b294", - "themeDark": "#005a9e", - "themeDarkAlt": "#106ebe", - "themeDarker": "#004578", - "themeLight": "#c7e0f4", - "themeLighter": "#deecf9", - "themeLighterAlt": "#eff6fc", - "themePrimary": "#0078d4", - "themeSecondary": "#2b88d8", - "themeTertiary": "#71afe5", - "white": "#ffffff", - "whiteTranslucent40": "rgba(255,255,255,.4)", - "yellow": "#ffb900", - "yellowDark": "#d29200", - "yellowLight": "#fff100", - }, - "rtl": undefined, - "semanticColors": Object { - "accentButtonBackground": "#0078d4", - "accentButtonText": "#ffffff", - "actionLink": "#323130", - "actionLinkHovered": "#201f1e", - "blockingBackground": "#FDE7E9", - "blockingIcon": "#FDE7E9", - "bodyBackground": "#ffffff", - "bodyBackgroundChecked": "#edebe9", - "bodyBackgroundHovered": "#f3f2f1", - "bodyDivider": "#edebe9", - "bodyFrameBackground": "#ffffff", - "bodyFrameDivider": "#edebe9", - "bodyStandoutBackground": "#faf9f8", - "bodySubtext": "#605e5c", - "bodyText": "#323130", - "bodyTextChecked": "#000000", - "buttonBackground": "#ffffff", - "buttonBackgroundChecked": "#c8c6c4", - "buttonBackgroundCheckedHovered": "#edebe9", - "buttonBackgroundDisabled": "#f3f2f1", - "buttonBackgroundHovered": "#f3f2f1", - "buttonBackgroundPressed": "#edebe9", - "buttonBorder": "#8a8886", - "buttonBorderDisabled": "#f3f2f1", - "buttonText": "#323130", - "buttonTextChecked": "#201f1e", - "buttonTextCheckedHovered": "#000000", - "buttonTextDisabled": "#a19f9d", - "buttonTextHovered": "#201f1e", - "buttonTextPressed": "#201f1e", - "cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "cardStandoutBackground": "#ffffff", - "defaultStateBackground": "#faf9f8", - "disabledBackground": "#f3f2f1", - "disabledBodySubtext": "#c8c6c4", - "disabledBodyText": "#a19f9d", - "disabledBorder": "#c8c6c4", - "disabledSubtext": "#d2d0ce", - "disabledText": "#a19f9d", - "errorBackground": "#FDE7E9", - "errorIcon": "#A80000", - "errorText": "#a4262c", - "focusBorder": "#605e5c", - "infoBackground": "#f3f2f1", - "infoIcon": "#605e5c", - "inputBackground": "#ffffff", - "inputBackgroundChecked": "#0078d4", - "inputBackgroundCheckedHovered": "#005a9e", - "inputBorder": "#605e5c", - "inputBorderHovered": "#323130", - "inputFocusBorderAlt": "#0078d4", - "inputForegroundChecked": "#ffffff", - "inputIcon": "#0078d4", - "inputIconDisabled": "#a19f9d", - "inputIconHovered": "#005a9e", - "inputPlaceholderBackgroundChecked": "#deecf9", - "inputPlaceholderText": "#605e5c", - "inputText": "#323130", - "inputTextHovered": "#201f1e", - "link": "#0078d4", - "linkHovered": "#004578", - "listBackground": "#ffffff", - "listHeaderBackgroundHovered": "#f3f2f1", - "listHeaderBackgroundPressed": "#edebe9", - "listItemBackgroundChecked": "#edebe9", - "listItemBackgroundCheckedHovered": "#e1dfdd", - "listItemBackgroundHovered": "#f3f2f1", - "listText": "#323130", - "listTextColor": "#323130", - "menuBackground": "#ffffff", - "menuDivider": "#c8c6c4", - "menuHeader": "#0078d4", - "menuIcon": "#0078d4", - "menuItemBackgroundChecked": "#edebe9", - "menuItemBackgroundHovered": "#f3f2f1", - "menuItemBackgroundPressed": "#edebe9", - "menuItemText": "#323130", - "menuItemTextHovered": "#201f1e", - "messageLink": "#005A9E", - "messageLinkHovered": "#004578", - "messageText": "#323130", - "primaryButtonBackground": "#0078d4", - "primaryButtonBackgroundDisabled": "#f3f2f1", - "primaryButtonBackgroundHovered": "#106ebe", - "primaryButtonBackgroundPressed": "#005a9e", - "primaryButtonBorder": "transparent", - "primaryButtonText": "#ffffff", - "primaryButtonTextDisabled": "#d2d0ce", - "primaryButtonTextHovered": "#ffffff", - "primaryButtonTextPressed": "#ffffff", - "severeWarningBackground": "#FED9CC", - "severeWarningIcon": "#D83B01", - "smallInputBorder": "#605e5c", - "successBackground": "#DFF6DD", - "successIcon": "#107C10", - "successText": "#107C10", - "variantBorder": "#edebe9", - "variantBorderHovered": "#a19f9d", - "warningBackground": "#FFF4CE", - "warningHighlight": "#ffb900", - "warningIcon": "#797775", - "warningText": "#323130", - }, - "spacing": Object { - "l1": "20px", - "l2": "32px", - "m": "16px", - "s1": "8px", - "s2": "4px", - }, - } - } - title="Close" - variantClassName="ms-Button--icon" - > - - - - - -
-
-
-
-
-
-
-
- -
- - -
- Add Entity -
-
-
- - - Add Property - - -
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
- -
- - - - - - - - - + Add Entity + + + + + + + + + + +
+ + + `; diff --git a/src/Explorer/Tabs/QueryTablesTab.tsx b/src/Explorer/Tabs/QueryTablesTab.tsx index b48a6a10e..aa208fcfd 100644 --- a/src/Explorer/Tabs/QueryTablesTab.tsx +++ b/src/Explorer/Tabs/QueryTablesTab.tsx @@ -137,13 +137,14 @@ export default class QueryTablesTab extends TabsBase { useSidePanel .getState() .openSidePanel( - "Add Table Entity", + "Add Table Row", + />, + "700px" ); }; diff --git a/src/hooks/useSidePanel.ts b/src/hooks/useSidePanel.ts index 26fce5954..e1558b910 100644 --- a/src/hooks/useSidePanel.ts +++ b/src/hooks/useSidePanel.ts @@ -2,14 +2,17 @@ import create, { UseStore } from "zustand"; export interface SidePanelState { isOpen: boolean; + panelWidth: string; panelContent?: JSX.Element; headerText?: string; - openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void; + openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void; closeSidePanel: () => void; } export const useSidePanel: UseStore = create((set) => ({ isOpen: false, - openSidePanel: (headerText, panelContent) => set((state) => ({ ...state, headerText, panelContent, isOpen: true })), + panelWidth: "440px", + openSidePanel: (headerText, panelContent, panelWidth = "440px") => + set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })), closeSidePanel: () => set((state) => ({ ...state, isOpen: false })), })); From 6d46e484903f3b39faa5e233fc4e5d486930365f Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:40:04 -0700 Subject: [PATCH 02/20] Migrate resource tree to react (#941) --- .env.example | 15 - .eslintignore | 3 +- package-lock.json | 5 + package.json | 1 + ...urceTree.tsx => ResourceTreeContainer.tsx} | 14 +- .../SettingsComponent.test.tsx.snap | 18 - src/Explorer/Explorer.tsx | 10 +- .../Notebook/NotebookContentClient.ts | 37 +- src/Explorer/Notebook/useNotebook.ts | 105 ++- .../GitHubReposPanel.test.tsx.snap | 9 - .../StringInputPane.test.tsx.snap | 9 - src/Explorer/Tree/Collection.ts | 2 +- src/Explorer/Tree/Database.tsx | 4 +- src/Explorer/Tree/ResourceTree.tsx | 722 ++++++++++++++++++ src/Explorer/Tree/ResourceTreeAdapter.tsx | 9 +- src/Main.tsx | 8 +- src/Platform/Hosted/extractFeatures.ts | 2 + test/cassandra/container.spec.ts | 7 +- test/graph/container.spec.ts | 9 +- test/mongo/container.spec.ts | 11 +- test/mongo/container32.spec.ts | 9 +- test/sql/container.spec.ts | 7 +- test/tables/container.spec.ts | 7 +- test/utils/safeClick.ts | 11 - tsconfig.strict.json | 5 +- 25 files changed, 915 insertions(+), 124 deletions(-) rename src/Common/{ResourceTree.tsx => ResourceTreeContainer.tsx} (83%) create mode 100644 src/Explorer/Tree/ResourceTree.tsx delete mode 100644 test/utils/safeClick.ts diff --git a/.env.example b/.env.example index 62538cbc0..ea79c9a84 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1 @@ -PORTAL_RUNNER_USERNAME= -PORTAL_RUNNER_PASSWORD= -PORTAL_RUNNER_SUBSCRIPTION= -PORTAL_RUNNER_RESOURCE_GROUP= -PORTAL_RUNNER_DATABASE_ACCOUNT= -PORTAL_RUNNER_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_CONNECTION_STRING= -NOTEBOOKS_TEST_RUNNER_TENANT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET= -CASSANDRA_CONNECTION_STRING= -MONGO_CONNECTION_STRING= -TABLES_CONNECTION_STRING= DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 15d8762b6..8c86e0e52 100644 --- a/.eslintignore +++ b/.eslintignore @@ -191,4 +191,5 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx __mocks__/monaco-editor.ts -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx \ No newline at end of file +src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx +src/Explorer/Tree/ResourceTree.tsx \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 997269344..319b7ee6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5583,6 +5583,11 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, + "@types/lodash": { + "version": "4.14.171", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", + "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", diff --git a/package.json b/package.json index 886b54c8f..0135c1797 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@octokit/rest": "17.9.2", "@phosphor/widgets": "1.9.3", "@testing-library/jest-dom": "5.11.9", + "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "applicationinsights": "1.8.0", diff --git a/src/Common/ResourceTree.tsx b/src/Common/ResourceTreeContainer.tsx similarity index 83% rename from src/Common/ResourceTree.tsx rename to src/Common/ResourceTreeContainer.tsx index da9c1bad4..129a38115 100644 --- a/src/Common/ResourceTree.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -2,17 +2,21 @@ import React, { FunctionComponent } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import refreshImg from "../../images/refresh-cosmos.svg"; import { AuthType } from "../AuthType"; +import Explorer from "../Explorer/Explorer"; +import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; -export interface ResourceTreeProps { +export interface ResourceTreeContainerProps { toggleLeftPaneExpanded: () => void; isLeftPaneExpanded: boolean; + container: Explorer; } -export const ResourceTree: FunctionComponent = ({ +export const ResourceTreeContainer: FunctionComponent = ({ toggleLeftPaneExpanded, isLeftPaneExpanded, -}: ResourceTreeProps): JSX.Element => { + container, +}: ResourceTreeContainerProps): JSX.Element => { return (
{/* Collections Window - - Start */} @@ -49,8 +53,10 @@ export const ResourceTree: FunctionComponent = ({
{userContext.authType === AuthType.ResourceToken ? (
- ) : ( + ) : userContext.features.enableKOResourceTree ? (
+ ) : ( + )}
{/* 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 edb2e91db..cb8ecbfaf 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -42,15 +42,6 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "gitHubOAuthService": GitHubOAuthService { - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, - "token": [Function], - }, - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -122,15 +113,6 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "gitHubOAuthService": GitHubOAuthService { - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, - "token": [Function], - }, - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 7a8d2465b..ac1e077d6 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -362,6 +362,9 @@ export default class Explorer { notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, authToken: userContext.features.notebookServerToken || connectionInfo.authToken, }); + + useNotebook.getState().initializeNotebooksTree(this.notebookManager); + this.refreshNotebookList(); this._isInitializingNotebooks = false; @@ -842,6 +845,8 @@ export default class Explorer { } await this.resourceTree.initialize(); + await useNotebook.getState().initializeNotebooksTree(this.notebookManager); + this.notebookManager?.refreshPinnedRepos(); if (this.notebookToImport) { this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); @@ -932,14 +937,15 @@ export default class Explorer { .finally(clearInProgressMessage); } - public refreshContentItem(item: NotebookContentItem): Promise { + // TODO: Delete this function when ResourceTreeAdapter is removed. + public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to refresh notebook list, but notebook is not enabled"; handleError(error, "Explorer/refreshContentItem"); return Promise.reject(new Error(error)); } - return this.notebookManager?.notebookContentClient.updateItemChildren(item); + await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); } public openNotebookTerminal(kind: ViewModels.TerminalKind) { diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index a4a9958d0..3599c009c 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -1,5 +1,6 @@ import { stringifyNotebook } from "@nteract/commutable"; import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core"; +import { cloneDeep } from "lodash"; import { AjaxResponse } from "rxjs/ajax"; import * as StringUtils from "../../Utils/StringUtils"; import * as FileSystemUtil from "./FileSystemUtil"; @@ -14,7 +15,17 @@ export class NotebookContentClient { * This updates the item and points all the children's parent to this item * @param item */ - public updateItemChildren(item: NotebookContentItem): Promise { + public async updateItemChildren(item: NotebookContentItem): Promise { + const subItems = await this.fetchNotebookFiles(item.path); + const clonedItem = cloneDeep(item); + subItems.forEach((subItem) => (subItem.parent = clonedItem)); + clonedItem.children = subItems; + + return clonedItem; + } + + // TODO: Delete this function when ResourceTreeAdapter is removed. + public async updateItemChildrenInPlace(item: NotebookContentItem): Promise { return this.fetchNotebookFiles(item.path).then((subItems) => { item.children = subItems; subItems.forEach((subItem) => (subItem.parent = item)); @@ -55,18 +66,20 @@ export class NotebookContentClient { }); } - public deleteContentItem(item: NotebookContentItem): Promise { - return this.deleteNotebookFile(item.path).then((path: string) => { - if (!path || path !== item.path) { - throw new Error("No path provided"); - } + public async deleteContentItem(item: NotebookContentItem): Promise { + const path = await this.deleteNotebookFile(item.path); + useNotebook.getState().deleteNotebookItem(item); - if (item.parent && item.parent.children) { - // Remove deleted child - const newChildren = item.parent.children.filter((child) => child.path !== path); - item.parent.children = newChildren; - } - }); + // TODO: Delete once old resource tree is removed + if (!path || path !== item.path) { + throw new Error("No path provided"); + } + + if (item.parent && item.parent.children) { + // Remove deleted child + const newChildren = item.parent.children.filter((child) => child.path !== path); + item.parent.children = newChildren; + } } /** diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index ecf927565..75b368fc4 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from "lodash"; import create, { UseStore } from "zustand"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; @@ -5,8 +6,12 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; +import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; +import NotebookManager from "./NotebookManager"; interface NotebookState { isNotebookEnabled: boolean; @@ -18,6 +23,9 @@ interface NotebookState { isShellEnabled: boolean; notebookBasePath: string; isInitializingNotebooks: boolean; + myNotebooksContentRoot: NotebookContentItem; + gitHubNotebooksContentRoot: NotebookContentItem; + galleryContentRoot: NotebookContentItem; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -27,9 +35,13 @@ interface NotebookState { setIsShellEnabled: (isShellEnabled: boolean) => void; setNotebookBasePath: (notebookBasePath: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; + findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; + updateNotebookItem: (item: NotebookContentItem) => void; + deleteNotebookItem: (item: NotebookContentItem) => void; + initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; } -export const useNotebook: UseStore = create((set) => ({ +export const useNotebook: UseStore = create((set, get) => ({ isNotebookEnabled: false, isNotebooksEnabledForAccount: false, notebookServerInfo: { @@ -46,6 +58,9 @@ export const useNotebook: UseStore = create((set) => ({ isShellEnabled: false, notebookBasePath: Constants.Notebook.defaultBasePath, isInitializingNotebooks: false, + myNotebooksContentRoot: undefined, + gitHubNotebooksContentRoot: undefined, + galleryContentRoot: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -103,4 +118,92 @@ export const useNotebook: UseStore = create((set) => ({ set({ isNotebooksEnabledForAccount: false }); } }, + findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => { + const currentItem = root || get().myNotebooksContentRoot; + + if (currentItem) { + if (currentItem.path === item.path && currentItem.name === item.name) { + return currentItem; + } + + if (currentItem.children) { + for (const childItem of currentItem.children) { + const result = get().findItem(childItem, item); + if (result) { + return result; + } + } + } + } + + return undefined; + }, + updateNotebookItem: (item: NotebookContentItem): void => { + const root = cloneDeep(get().myNotebooksContentRoot); + const parentItem = get().findItem(root, item.parent); + parentItem.children = parentItem.children.filter((child) => child.path !== item.path); + parentItem.children.push(item); + item.parent = parentItem; + set({ myNotebooksContentRoot: root }); + }, + deleteNotebookItem: (item: NotebookContentItem): void => { + const root = cloneDeep(get().myNotebooksContentRoot); + const parentItem = get().findItem(root, item.parent); + parentItem.children = parentItem.children.filter((child) => child.path !== item.path); + set({ myNotebooksContentRoot: root }); + }, + initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { + set({ + myNotebooksContentRoot: { + name: "My Notebooks", + path: get().notebookBasePath, + type: NotebookContentItemType.Directory, + }, + galleryContentRoot: { + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }, + }); + + if (notebookManager?.gitHubOAuthService?.isLoggedIn()) { + set({ + gitHubNotebooksContentRoot: { + name: "GitHub repos", + path: "PsuedoDir", + type: NotebookContentItemType.Directory, + }, + }); + } + + if (get().notebookServerInfo?.notebookServerEndpoint) { + const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren({ + name: "My Notebooks", + path: get().notebookBasePath, + type: NotebookContentItemType.Directory, + }); + set({ myNotebooksContentRoot: updatedRoot }); + + if (updatedRoot?.children) { + // Count 1st generation children (tree is lazy-loaded) + const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; + updatedRoot.children.forEach((notebookItem) => { + switch (notebookItem.type) { + case NotebookContentItemType.File: + nodeCounts.files++; + break; + case NotebookContentItemType.Directory: + nodeCounts.directories++; + break; + case NotebookContentItemType.Notebook: + nodeCounts.notebooks++; + break; + default: + break; + } + }); + TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); + } + } + }, })); diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index f1c03499b..9d1b8cc0c 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -31,15 +31,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "gitHubOAuthService": GitHubOAuthService { - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, - "token": [Function], - }, - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index e159e0052..27f1f941d 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -21,15 +21,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "gitHubOAuthService": GitHubOAuthService { - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, - "token": [Function], - }, - "junoClient": JunoClient { - "cachedPinnedRepos": [Function], - }, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index c486da03e..cefa6bbeb 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection { }; public onSettingsClick = async (): Promise => { - await this.loadOffer(); useSelectedNode.getState().setSelectedNode(this); + await this.loadOffer(); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Settings node", diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx index 5d4f83fc7..5dc01343f 100644 --- a/src/Explorer/Tree/Database.tsx +++ b/src/Explorer/Tree/Database.tsx @@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database { this.isOfferRead = false; } - public onSettingsClick = () => { + public onSettingsClick = (): void => { useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { @@ -193,6 +193,8 @@ export default class Database implements ViewModels.Database { //merge collections this.addCollectionsToList(collectionVMs); this.deleteCollectionsFromList(deltaCollections.toDelete); + + useDatabases.getState().updateDatabase(this); } public async openAddCollection(database: Database): Promise { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx new file mode 100644 index 000000000..b5b6dfe92 --- /dev/null +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -0,0 +1,722 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; +import * as React from "react"; +import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; +import CollectionIcon from "../../../images/tree-collection.svg"; +import { Areas } from "../../Common/Constants"; +import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; +import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; +import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import Explorer from "../Explorer"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; +import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; +import TabsBase from "../Tabs/TabsBase"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; +import StoredProcedure from "./StoredProcedure"; +import Trigger from "./Trigger"; +import UserDefinedFunction from "./UserDefinedFunction"; + +export const MyNotebooksTitle = "My Notebooks"; +export const GitHubReposTitle = "GitHub repos"; + +interface ResourceTreeProps { + container: Explorer; +} + +export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { + const databases = useDatabases((state) => state.databases); + const { + isNotebookEnabled, + myNotebooksContentRoot, + galleryContentRoot, + gitHubNotebooksContentRoot, + updateNotebookItem, + } = useNotebook(); + const { activeTab, refreshActiveTab } = useTabs(); + const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; + const pseudoDirPath = "PsuedoDir"; + + const buildGalleryCallout = (): JSX.Element => { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + container.openGallery(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + }; + + const buildNotebooksTree = (): TreeNode => { + const notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (galleryContentRoot) { + notebooksTree.children.push(buildGalleryNotebooksTree()); + } + + if (myNotebooksContentRoot) { + notebooksTree.children.push(buildMyNotebooksTree()); + } + + if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(buildGitHubNotebooksTree()); + } + + return notebooksTree; + }; + + const buildGalleryNotebooksTree = (): TreeNode => { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => container.openGallery(), + isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, + }; + }; + + const buildMyNotebooksTree = (): TreeNode => { + const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( + myNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + } + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + }; + + const buildGitHubNotebooksTree = (): TreeNode => { + const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( + gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + } + ); + + gitHubNotebooksTree.contextMenu = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + }; + + const buildChildNodes = ( + container: Explorer, + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode[] => { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? buildNotebookDirectoryNode(item, onFileClick) + : buildNotebookFileNode(item, onFileClick); + result.timestamp = item.timestamp; + return result; + }); + } + }; + + const buildNotebookFileNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode => { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu(container, item), + data: item, + }; + }; + + const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + container.showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => copyNotebook(container, item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await container.readFile(item); + if (content) { + await container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + }; + + const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { + const content = await container.readFile(item); + if (content) { + container.copyNotebook(item.name, content); + } + }; + + const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => loadSubitems(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + container.showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => container.onCreateDirectory(item), + }, + { + label: "New Notebook", + iconSrc: NewNotebookIcon, + onClick: () => container.onNewNotebookClicked(item), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => container.openUploadFilePanel(item), + }, + ]; + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File" + ); + } + + return items; + }; + + const buildNotebookDirectoryNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode => { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + loadSubitems(item); + } + }, + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, + data: item, + children: buildChildNodes(container, item, onFileClick), + }; + }; + + const buildDataTree = (): TreeNode => { + const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { + const databaseNode: TreeNode = { + label: database.id(), + iconSrc: CosmosDBIcon, + isExpanded: false, + className: "databaseHeader", + children: [], + isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), + contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), + onClick: async (isExpanded) => { + useSelectedNode.getState().setSelectedNode(database); + // Rewritten version of expandCollapseDatabase(): + if (isExpanded) { + database.collapseDatabase(); + } else { + if (databaseNode.children?.length === 0) { + databaseNode.isLoading = true; + } + await database.expandDatabase(); + } + databaseNode.isLoading = false; + useCommandBar.getState().setContextButtons([]); + refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); + }, + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), + }; + + if (database.isDatabaseShared()) { + databaseNode.children.push({ + label: "Scale", + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]), + onClick: database.onSettingsClick.bind(database), + }); + } + + // Find collections + database + .collections() + .forEach((collection: ViewModels.Collection) => + databaseNode.children.push(buildCollectionNode(database, collection)) + ); + + database.collections.subscribe((collections: ViewModels.Collection[]) => { + collections.forEach((collection: ViewModels.Collection) => + databaseNode.children.push(buildCollectionNode(database, collection)) + ); + }); + + return databaseNode; + }); + + return { + label: undefined, + isExpanded: true, + children: databaseTreeNodes, + }; + }; + + const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => { + const children: TreeNode[] = []; + children.push({ + label: collection.getLabel(), + onClick: () => { + collection.openTab(); + // push to most recent + mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + }, + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.Documents, + ViewModels.CollectionTabKind.Graph, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), + }); + + if (isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) { + children.push({ + label: "Schema (Preview)", + onClick: collection.onSchemaAnalyzerClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), + }); + } + + if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { + children.push({ + label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", + onClick: collection.onSettingsClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.CollectionSettingsV2, + ]), + }); + } + + const schemaNode: TreeNode = buildSchemaNode(collection); + if (schemaNode) { + children.push(schemaNode); + } + + if (showScriptNodes) { + children.push(buildStoredProcedureNode(collection)); + children.push(buildUserDefinedFunctionsNode(collection)); + children.push(buildTriggerNode(collection)); + } + + // This is a rewrite of showConflicts + const showConflicts = + userContext?.databaseAccount?.properties.enableMultipleWriteLocations && + collection.rawDataModel && + !!collection.rawDataModel.conflictResolutionPolicy; + + if (showConflicts) { + children.push({ + label: "Conflicts", + onClick: collection.onConflictsClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), + }); + } + + return { + label: collection.id(), + iconSrc: CollectionIcon, + isExpanded: false, + children: children, + className: "collectionHeader", + contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), + onClick: () => { + // Rewritten version of expandCollapseCollection + useSelectedNode.getState().setSelectedNode(collection); + useCommandBar.getState().setContextButtons([]); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + onExpanded: () => { + if (showScriptNodes) { + collection.loadStoredProcedures(); + collection.loadUserDefinedFunctions(); + collection.loadTriggers(); + } + }, + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), + }; + }; + + const buildStoredProcedureNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "Stored Procedures", + children: collection.storedProcedures().map((sp: StoredProcedure) => ({ + label: sp.id(), + onClick: sp.open.bind(sp), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.StoredProcedures, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildUserDefinedFunctionsNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "User Defined Functions", + children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({ + label: udf.id(), + onClick: udf.open.bind(udf), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.UserDefinedFunctions, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildTriggerNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "Triggers", + children: collection.triggers().map((trigger: Trigger) => ({ + label: trigger.id(), + onClick: trigger.open.bind(trigger), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), + contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildSchemaNode = (collection: ViewModels.Collection): TreeNode => { + if (collection.analyticalStorageTtl() === undefined) { + return undefined; + } + + if (!collection.schema || !collection.schema.fields) { + return undefined; + } + + return { + label: "Schema", + children: getSchemaNodes(collection.schema.fields), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); + refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid); + }, + }; + }; + + const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode[] => { + const schema: any = {}; + + //unflatten + fields.forEach((field: DataModels.IDataField) => { + const path: string[] = field.path.split("."); + const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; + let current: any = {}; + path.forEach((name: string, pathIndex: number) => { + if (pathIndex === 0) { + if (schema[name] === undefined) { + if (pathIndex === path.length - 1) { + schema[name] = fieldProperties; + } else { + schema[name] = {}; + } + } + current = schema[name]; + } else { + if (current[name] === undefined) { + if (pathIndex === path.length - 1) { + current[name] = fieldProperties; + } else { + current[name] = {}; + } + } + current = current[name]; + } + }); + }); + + const traverse = (obj: any): TreeNode[] => { + const children: TreeNode[] = []; + + if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") { + Object.entries(obj).forEach(([key, value]) => { + children.push({ label: key, children: traverse(value) }); + }); + } else if (Array.isArray(obj)) { + return [{ label: obj[0] }, { label: obj[1] }]; + } + + return children; + }; + + return traverse(schema); + }; + + const loadSubitems = async (item: NotebookContentItem): Promise => { + const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); + updateNotebookItem(updatedItem); + }; + + const dataRootNode = buildDataTree(); + + if (isNotebookEnabled) { + return ( + <> + + + + + + + + + + {buildGalleryCallout()} + + ); + } + + return ; +}; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 85b6f4a53..6a00e92e9 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -16,10 +16,9 @@ import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; -import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; +import { IPinnedRepo } from "../../Juno/JunoClient"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -56,8 +55,6 @@ export class ResourceTreeAdapter implements ReactAdapter { public galleryContentRoot: NotebookContentItem; public myNotebooksContentRoot: NotebookContentItem; public gitHubNotebooksContentRoot: NotebookContentItem; - public junoClient: JunoClient; - public gitHubOAuthService: GitHubOAuthService; public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -74,8 +71,6 @@ export class ResourceTreeAdapter implements ReactAdapter { useDatabases.subscribe(() => this.triggerRender()); this.triggerRender(); - this.junoClient = new JunoClient(); - this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); } private traceMyNotebookTreeInfo() { @@ -639,7 +634,7 @@ export class ResourceTreeAdapter implements ReactAdapter { ), }, diff --git a/src/Main.tsx b/src/Main.tsx index c85a39f10..80d008b17 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -26,7 +26,7 @@ import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/queryBuilder.less"; import "../less/tree.less"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; -import { ResourceTree } from "./Common/ResourceTree"; +import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import { Dialog } from "./Explorer/Controls/Dialog"; @@ -84,7 +84,11 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree Expanded - Start */} - + {/* Collections Tree Expanded - End */} {/* Collections Tree Collapsed - Start */} { await explorer.click('[aria-label="addCollection-tableId"]'); await explorer.fill('[aria-label="addCollection-tableId"]', tableId); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click(`.nodeItem >> text=${keyspaceId}`, { timeout: 50000 }); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index cf52b5bb4..8f7015ed3 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,6 +1,5 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; jest.setTimeout(240000); @@ -20,11 +19,11 @@ test("Graph CRUD", async () => { await explorer.fill('[aria-label="Graph id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")'); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Graph")'); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 64ccbba42..01d5a64f5 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,6 +1,5 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; jest.setTimeout(240000); @@ -20,10 +19,10 @@ test("Mongo CRUD", async () => { 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}`); + await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${containerId}`); // Create indexing policy - await safeClick(explorer, ".nodeItem >> text=Settings"); + await explorer.click(".nodeItem >> text=Settings"); await explorer.click('button[role="tab"]:has-text("Indexing Policy")'); await explorer.click('[aria-label="Index Field Name 0"]'); await explorer.fill('[aria-label="Index Field Name 0"]', "foo"); @@ -34,8 +33,8 @@ test("Mongo CRUD", async () => { await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[data-test="Save"]'); // 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.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('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="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 30fdfd5fe..239ef31c2 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,6 +1,5 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; jest.setTimeout(240000); @@ -20,11 +19,11 @@ test("Mongo CRUD", async () => { 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}`); + explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + explorer.click(`.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")'); + explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + explorer.click('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="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index e68930083..91af5ce9b 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,6 +1,5 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; jest.setTimeout(120000); @@ -19,9 +18,9 @@ test("SQL CRUD", async () => { await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")'); + await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index b2337baa9..92ac6e41e 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,6 +1,5 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; jest.setTimeout(120000); @@ -17,9 +16,9 @@ test("Tables CRUD", async () => { await explorer.click('[data-test="New Table"]'); await explorer.fill('[aria-label="Table id"]', tableId); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `[data-test="TablesDB"]`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click(`[data-test="TablesDB"]`, { timeout: 50000 }); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); await expect(explorer).not.toHaveText(".dataResourceTree", tableId); diff --git a/test/utils/safeClick.ts b/test/utils/safeClick.ts deleted file mode 100644 index d0c307bd0..000000000 --- a/test/utils/safeClick.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Frame } from "playwright"; - -export async function safeClick(page: Frame, selector: string): Promise { - // TODO: Remove. Playwright does this for you... mostly. - // But our knockout+react setup sometimes leaves dom nodes detached and even playwright can't recover. - // Resource tree is particually bad. - // Ideally this should only be added as a last resort - await page.waitForSelector(selector); - await page.waitForTimeout(5000); - await page.click(selector); -} diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 6a9a6e78a..6b80784b5 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -26,7 +26,6 @@ "./src/Common/ObjectCache.ts", "./src/Common/OfferUtility.test.ts", "./src/Common/OfferUtility.ts", - "./src/Common/ResourceTree.tsx", "./src/Common/Splitter.ts", "./src/Common/ThemeUtility.ts", "./src/Common/UrlUtility.ts", @@ -142,7 +141,7 @@ "./src/userContext.test.ts", "src/Common/EntityValue.tsx", "./src/Platform/Hosted/Components/SwitchAccount.tsx", - "./src/Platform/Hosted/Components/SwitchSubscription.tsx", + "./src/Platform/Hosted/Components/SwitchSubscription.tsx" ], "include": [ "src/CellOutputViewer/transforms/**/*", @@ -168,4 +167,4 @@ "src/Terminal/**/*", "src/Utils/arm/**/*" ] -} \ No newline at end of file +} From 913fec4e69d00ab154d1633d8f4c5466d5fa1cfb Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 21 Jul 2021 16:22:31 -0700 Subject: [PATCH 03/20] Improve e2e stability (#949) --- .../dataAccess/createCollection.test.ts | 12 ++ src/Common/dataAccess/createCollection.ts | 112 +++--------------- src/Common/dataAccess/createDatabase.ts | 88 ++------------ src/Explorer/useDatabases.ts | 10 ++ test/cassandra/container.spec.ts | 8 +- test/graph/container.spec.ts | 9 +- test/mongo/container.spec.ts | 9 +- test/mongo/container32.spec.ts | 9 +- test/notebooks/upload.spec.ts | 6 +- test/sql/container.spec.ts | 10 +- test/tables/container.spec.ts | 10 +- test/utils/waitForExplorer.ts | 9 ++ 12 files changed, 86 insertions(+), 206 deletions(-) create mode 100644 test/utils/waitForExplorer.ts diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index ce04404a6..2f6bf63e4 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -1,7 +1,10 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); +import ko from "knockout"; import { AuthType } from "../../AuthType"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; +import { Database } from "../../Contracts/ViewModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { updateUserContext } from "../../UserContext"; import { armRequest } from "../../Utils/arm/request"; import { client } from "../CosmosClient"; @@ -23,6 +26,15 @@ describe("createCollection", () => { } as DatabaseAccount, apiType: "SQL", }); + useDatabases.setState({ + databases: [ + { + id: ko.observable("testDatabase"), + loadCollections: () => undefined, + collections: ko.observableArray([]), + } as Database, + ], + }); }); it("should call ARM if logged in with AAD", async () => { diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 6d7798dd8..791b29fcc 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -4,20 +4,16 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraTable, - getCassandraTable, -} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; -import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; -import { - createUpdateMongoDBCollection, - getMongoDBCollection, -} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; -import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; -import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; +import { getCollectionName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; @@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams }; const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise => { + if (!params.createNewDatabase) { + const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); + if (!isValid) { + const collectionName = getCollectionName().toLocaleLowerCase(); + throw new Error( + `Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists` + ); + } + } + const { apiType } = userContext; switch (apiType) { case "SQL": @@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams }; const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getSqlContainer( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create container failed: container with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.SqlContainerResource = { id: params.collectionId, @@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise => { const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; - try { - const getResponse = await getMongoDBCollection( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.MongoDBCollectionResource = { id: params.collectionId, @@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): }; const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getCassandraTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.CassandraTableResource = { id: params.collectionId, @@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams): }; const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getGremlinGraph( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.GremlinGraphResource = { id: params.collectionId, @@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.TableResource = { id: params.collectionId, diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index 7d13871f6..2467b7975 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -2,20 +2,13 @@ import { DatabaseResponse } from "@azure/cosmos"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraKeyspace, - getCassandraKeyspace, -} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; -import { - createUpdateGremlinDatabase, - getGremlinDatabase, -} from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; -import { - createUpdateMongoDBDatabase, - getMongoDBDatabase, -} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; -import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { getDatabaseName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { CassandraKeyspaceCreateUpdateParameters, CreateUpdateOptions, @@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P } async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { + if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { + const databaseName = getDatabaseName().toLocaleLowerCase(); + throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); + } + const { apiType } = userContext; switch (apiType) { @@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P } async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getSqlDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: SqlDatabaseCreateUpdateParameters = { properties: { @@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi } async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getMongoDBDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: MongoDBDatabaseCreateUpdateParameters = { properties: { @@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro } async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getCassandraKeyspace( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: CassandraKeyspaceCreateUpdateParameters = { properties: { @@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): } async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getGremlinDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: GremlinDatabaseCreateUpdateParameters = { properties: { diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 0a1f3ae45..3808f42c5 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -19,6 +19,8 @@ interface DatabasesState { loadDatabaseOffers: () => Promise; isFirstResourceCreated: () => boolean; findSelectedDatabase: () => ViewModels.Database; + validateDatabaseId: (id: string) => boolean; + validateCollectionId: (databaseId: string, collectionId: string) => Promise; } export const useDatabases: UseStore = create((set, get) => ({ @@ -129,4 +131,12 @@ export const useDatabases: UseStore = create((set, get) => ({ return selectedNode.collection?.database; }, + validateDatabaseId: (id: string): boolean => { + return !get().databases.some((database) => database.id() === id); + }, + validateCollectionId: async (databaseId: string, collectionId: string): Promise => { + const database = get().databases.find((db) => db.id() === databaseId); + await database.loadCollections(); + return !database.collections().some((collection) => collection.id() === collectionId); + }, })); diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 68078b6ba..af68a47dc 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,17 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Table"]'); await explorer.click('[aria-label="Keyspace id"]'); @@ -19,7 +19,7 @@ test("Cassandra keyspace and table CRUD", async () => { await explorer.click('[aria-label="addCollection-tableId"]'); await explorer.fill('[aria-label="addCollection-tableId"]', tableId); await explorer.click("#sidePanelOkButton"); - await explorer.click(`.nodeItem >> text=${keyspaceId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${keyspaceId}`); await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 8f7015ed3..0b2bf5090 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,17 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and graph await explorer.click('[data-test="New Graph"]'); @@ -19,7 +18,7 @@ test("Graph CRUD", async () => { await explorer.fill('[aria-label="Graph id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 01d5a64f5..53d343882 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,17 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -19,7 +18,7 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${containerId}`); // Create indexing policy await explorer.click(".nodeItem >> text=Settings"); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 239ef31c2..5b3845a14 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,17 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -19,7 +18,7 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "pk"); await explorer.click("#sidePanelOkButton"); - explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + explorer.click(`.nodeItem >> text=${databaseId}`); explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and collection explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); diff --git a/test/notebooks/upload.spec.ts b/test/notebooks/upload.spec.ts index 36b4658bf..5f2483531 100644 --- a/test/notebooks/upload.spec.ts +++ b/test/notebooks/upload.spec.ts @@ -2,6 +2,7 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import fs from "fs"; import path from "path"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); const filename = "GettingStarted.ipynb"; @@ -11,10 +12,7 @@ fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUploa test("Notebooks", async () => { await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Upload and Delete Notebook await explorer.click('[data-test="My Notebooks"] [aria-label="More"]'); await explorer.click('button[role="menuitem"]:has-text("Upload File")'); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 91af5ce9b..4deb3e30e 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,24 +1,22 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); - + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await explorer.click(`.nodeItem >> text=${databaseId}`, { timeout: 50000 }); + await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index 92ac6e41e..5dbfc9cfa 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,22 +1,22 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); + await page.waitForSelector('text="Querying databases"', { state: "detached" }); await explorer.click('[data-test="New Table"]'); await explorer.fill('[aria-label="Table id"]', tableId); await explorer.click("#sidePanelOkButton"); - await explorer.click(`[data-test="TablesDB"]`, { timeout: 50000 }); + await explorer.click(`[data-test="TablesDB"]`); await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); diff --git a/test/utils/waitForExplorer.ts b/test/utils/waitForExplorer.ts new file mode 100644 index 000000000..30675a0cc --- /dev/null +++ b/test/utils/waitForExplorer.ts @@ -0,0 +1,9 @@ +import { Frame } from "playwright"; + +export const waitForExplorer = async (): Promise => { + await page.waitForSelector("iframe"); + await page.waitForTimeout(5000); + return page.frame({ + name: "explorer", + }); +}; From c5e4ee9c2b434f4424ec3a348559f8b7f144bacb Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Wed, 21 Jul 2021 19:32:53 -0500 Subject: [PATCH 04/20] Upgrade Playwright (#939) --- package-lock.json | 24 ++++++++++++++++++++---- package.json | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 319b7ee6d..79d3475da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20623,9 +20623,9 @@ } }, "playwright": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz", - "integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz", + "integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==", "dev": true, "requires": { "commander": "^6.1.0", @@ -20640,7 +20640,8 @@ "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", "stack-utils": "^2.0.3", - "ws": "^7.3.1" + "ws": "^7.4.6", + "yazl": "^2.5.1" }, "dependencies": { "commander": { @@ -20672,6 +20673,12 @@ "requires": { "escape-string-regexp": "^2.0.0" } + }, + "ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "dev": true } } }, @@ -26162,6 +26169,15 @@ "fd-slicer": "~1.1.0" } }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 0135c1797..153244680 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "mini-css-extract-plugin": "0.4.3", "monaco-editor-webpack-plugin": "1.7.0", "node-fetch": "2.6.1", - "playwright": "1.10.0", + "playwright": "1.13.0", "prettier": "2.2.1", "raw-loader": "0.5.1", "react-dev-utils": "11.0.4", From 401660ae15a40a69870737ea583477ccb89c4939 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 21 Jul 2021 17:47:55 -0700 Subject: [PATCH 05/20] Fix connect to github (#951) --- src/Explorer/Notebook/useNotebook.ts | 44 +++++++++++++--------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 75b368fc4..c4967a228 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -153,35 +153,31 @@ export const useNotebook: UseStore = create((set, get) => ({ set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { - set({ - myNotebooksContentRoot: { - name: "My Notebooks", - path: get().notebookBasePath, - type: NotebookContentItemType.Directory, - }, - galleryContentRoot: { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }, - }); - - if (notebookManager?.gitHubOAuthService?.isLoggedIn()) { - set({ - gitHubNotebooksContentRoot: { + const myNotebooksContentRoot = { + name: "My Notebooks", + path: get().notebookBasePath, + type: NotebookContentItemType.Directory, + }; + const galleryContentRoot = { + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }; + const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() + ? { name: "GitHub repos", path: "PsuedoDir", type: NotebookContentItemType.Directory, - }, - }); - } + } + : undefined; + set({ + myNotebooksContentRoot, + galleryContentRoot, + gitHubNotebooksContentRoot, + }); if (get().notebookServerInfo?.notebookServerEndpoint) { - const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren({ - name: "My Notebooks", - path: get().notebookBasePath, - type: NotebookContentItemType.Directory, - }); + const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot); set({ myNotebooksContentRoot: updatedRoot }); if (updatedRoot?.children) { From e443d17b2ed1263f6a38a0893a00ddae0a19e99e Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Thu, 22 Jul 2021 20:49:17 +0530 Subject: [PATCH 06/20] Migrate Index page to React (#952) --- src/Index.ts | 23 --------------- src/Index.tsx | 66 ++++++++++++++++++++++++++++++++++++++++++++ src/index.html | 49 +------------------------------- tsconfig.strict.json | 1 - webpack.config.js | 2 +- 5 files changed, 68 insertions(+), 73 deletions(-) delete mode 100644 src/Index.ts create mode 100644 src/Index.tsx diff --git a/src/Index.ts b/src/Index.ts deleted file mode 100644 index 9eb33943c..000000000 --- a/src/Index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "../less/index.less"; -import "./Libs/jquery"; - -import * as ko from "knockout"; - -class Index { - public navigationSelection: ko.Observable; - - constructor() { - this.navigationSelection = ko.observable("quickstart"); - } - - public quickstart_click() { - this.navigationSelection("quickstart"); - } - - public explorer_click() { - this.navigationSelection("explorer"); - } -} - -var index = new Index(); -ko.applyBindings(index); diff --git a/src/Index.tsx b/src/Index.tsx new file mode 100644 index 000000000..d660eaed0 --- /dev/null +++ b/src/Index.tsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import ReactDOM from "react-dom"; +import Arrow from "../images/Arrow.svg"; +import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg"; +import Explorer from "../images/Explorer.svg"; +import Feedback from "../images/Feedback.svg"; +import Quickstart from "../images/Quickstart.svg"; +import "../less/index.less"; + +const Index = (): JSX.Element => { + const [navigationSelection, setNavigationSelection] = useState("quickstart"); + + const quickstart_click = () => { + setNavigationSelection("quickstart"); + }; + + const explorer_click = () => { + setNavigationSelection("explorer"); + }; + + return ( + +
+
+ Azure Cosmos DB + + Create an Azure Cosmos DB account + + Azure Cosmos DB Emulator +
+
+ + + {navigationSelection === "quickstart" && ( + + )} + + {navigationSelection === "explorer" && ( + + )} +
+ ); +}; + +ReactDOM.render(, document.getElementById("root")); diff --git a/src/index.html b/src/index.html index e9672edd7..5332ae673 100644 --- a/src/index.html +++ b/src/index.html @@ -8,54 +8,7 @@ - -
-
- Azure Cosmos DB - - Create an Azure Cosmos DB account - - Azure Cosmos DB Emulator -
-
- - - - - - +
diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 6b80784b5..d48357cda 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -83,7 +83,6 @@ "./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/GitHub/GitHubConnector.ts", "./src/HostedExplorerChildFrame.ts", - "./src/Index.ts", "./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.tsx", diff --git a/webpack.config.js b/webpack.config.js index 12c980224..054b06984 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -198,7 +198,7 @@ module.exports = function (_env = {}, argv = {}) { mode: mode, entry: { main: "./src/Main.tsx", - index: "./src/Index.ts", + index: "./src/Index.tsx", quickstart: "./src/quickstart.ts", hostedExplorer: "./src/HostedExplorer.tsx", testExplorer: "./test/testExplorer/TestExplorer.ts", From ed9cf01b50c82dbe6c2552360ecfae9564691467 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 22 Jul 2021 21:42:35 +0530 Subject: [PATCH 07/20] Fixed resourse tree collapse issue (#953) --- less/resourceTree.less | 1 + less/tree.less | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/less/resourceTree.less b/less/resourceTree.less index cac3f049f..39bced9da 100644 --- a/less/resourceTree.less +++ b/less/resourceTree.less @@ -2,6 +2,7 @@ .dataResourceTree { margin-left: @MediumSpace; + overflow: auto; .databaseHeader { font-size: 14px; diff --git a/less/tree.less b/less/tree.less index e60bcf69c..56a5ee38e 100644 --- a/less/tree.less +++ b/less/tree.less @@ -3,7 +3,6 @@ .resourceTree { height: 100%; - width: 20%; flex: 0 0 auto; .main { height: 100%; From 39a67dbc983410a9d991dcd5b1f985886899653b Mon Sep 17 00:00:00 2001 From: siddjoshi-ms <86025894+siddjoshi-ms@users.noreply.github.com> Date: Thu, 22 Jul 2021 10:48:19 -0700 Subject: [PATCH 08/20] Sqlx estimated cost calculation (#925) Adds cost estimate for the SqlX services in the Dedicated Gateway blade (queries the new FetchPrices API to retrieve price data) --- src/Localization/en/SqlX.json | 2 +- src/SelfServe/SqlX/SqlX.rp.ts | 71 ++++++++++++++++++++++++++++++++- src/SelfServe/SqlX/SqlX.tsx | 64 ++++++++++++++++++++++++++--- src/SelfServe/SqlX/SqlxTypes.ts | 20 ++++++++++ 4 files changed, 150 insertions(+), 7 deletions(-) diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json index 58f6e89a6..9c0ac667e 100644 --- a/src/Localization/en/SqlX.json +++ b/src/Localization/en/SqlX.json @@ -40,7 +40,7 @@ "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", - "Cost": "Cost", + "ApproximateCost": "Approximate Cost Per Hour", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "ConnectionString": "Connection String", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 365422c57..61080763e 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { RefreshResult } from "../SelfServeTypes"; import SqlX from "./SqlX"; -import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; +import { + FetchPricesResponse, + RegionsResponse, + SqlxServiceResource, + UpdateDedicatedGatewayRequestParameters, +} from "./SqlxTypes"; const apiVersion = "2021-04-01-preview"; @@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index c6a431ede..ca1177fa3 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils"; import { deleteDedicatedGatewayResource, getCurrentProvisioningState, + getPriceMap, + getReadRegions, refreshDedicatedGatewayProvisioning, updateDedicatedGatewayResource, } from "./SqlX.rp"; -const costPerHourValue: Description = { +const costPerHourDefaultValue: Description = { textTKey: "CostText", type: DescriptionType.Text, link: { @@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s"; const onSKUChange = (newValue: InputType, currentValues: Map): Map => { currentValues.set("sku", { value: newValue }); - currentValues.set("costPerHour", { value: costPerHourValue }); + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), + }); + return currentValues; }; @@ -79,6 +84,11 @@ const onNumberOfInstancesChange = ( } else { currentValues.set("warningBanner", undefined); } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), + }); + return currentValues; }; @@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = ( } as Description, hidden: false, }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); } else { currentValues.set("warningBanner", { value: { @@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = ( } as Description, hidden: false, }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); } const sku = currentValues.get("sku"); const instances = currentValues.get("instances"); @@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = ( disabled: dedicatedGatewayOriginallyEnabled, }); - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("connectionString", { value: connectionStringValue, hidden: !newValue || !dedicatedGatewayOriginallyEnabled, @@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = { }, }; +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "DedicatedGatewayPricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + @IsDisplayable() @RefreshOptions({ retryIntervalInMs: 20000 }) export default class SqlX extends SelfServeBaseClass { @@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass { hidden: true, }); + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); + const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { defaults.set("enableDedicatedGateway", { value: true }); defaults.set("sku", { value: response.sku, disabled: true }); defaults.set("instances", { value: response.instances, disabled: false }); - defaults.set("costPerHour", { value: costPerHourValue }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); defaults.set("connectionString", { value: connectionStringValue, hidden: false, @@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass { }) instances: number; + @PropertyInfo(ApproximateCostDropDownInfo) @Values({ - labelTKey: "Cost", + labelTKey: "ApproximateCost", isDynamicDescription: true, }) costPerHour: string; diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts index 70557f4f4..a150ccbb1 100644 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ b/src/SelfServe/SqlX/SqlxTypes.ts @@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = { instanceCount: number; serviceType: string; }; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; From dc21032d6970fee661a55d6a68b557dff09680db Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Thu, 22 Jul 2021 16:18:48 -0700 Subject: [PATCH 09/20] Clean up EditTableEntityPanel (#955) --- src/Explorer/Panes/PanelComponent.less | 3 - .../Panes/Tables/AddTableEntityPanel.tsx | 2 +- .../Panes/Tables/EditTableEntityPanel.tsx | 226 +- .../Tables/Validators/EntityTableHelper.tsx | 2 +- .../EditTableEntityPanel.test.tsx.snap | 4555 +++++++---------- src/Explorer/Tabs/QueryTablesTab.tsx | 3 +- 6 files changed, 1901 insertions(+), 2890 deletions(-) diff --git a/src/Explorer/Panes/PanelComponent.less b/src/Explorer/Panes/PanelComponent.less index 2421fe68b..acf5d3d2c 100644 --- a/src/Explorer/Panes/PanelComponent.less +++ b/src/Explorer/Panes/PanelComponent.less @@ -150,9 +150,6 @@ .backImageIcon { margin-top: 8px; } -.entityValueTextField { - margin: 24px; -} .addEntityDatePicker { max-width: 145px; } diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 402e87da6..eafa4ae1b 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -218,7 +218,7 @@ export const AddTableEntityPanel: FunctionComponent = if (isEntityValuePanelOpen) { return ( - + back setIsEntityValuePanelFalse()} /> diff --git a/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx b/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx index f71a1a829..914aaf724 100644 --- a/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx @@ -1,11 +1,11 @@ -import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react"; +import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as _ from "underscore"; import AddPropertyIcon from "../../../../images/Add-property.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg"; +import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { TableEntity } from "../../../Common/TableEntity"; -import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "../../../UserContext"; import * as TableConstants from "../../Tables/Constants"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; @@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities"; import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import { PanelContainerComponent } from "../PanelContainerComponent"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { attributeNameLabel, attributeValueLabel, @@ -29,7 +29,6 @@ import { getEntityValuePlaceholder, getFormattedTime, imageProps, - isValidEntities, options, } from "./Validators/EntityTableHelper"; @@ -59,12 +58,13 @@ export const EditTableEntityPanel: FunctionComponent tableEntityListViewModel, cassandraApiClient, }: EditTableEntityPanelProps): JSX.Element => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const [entities, setEntities] = useState([]); const [selectedRow, setSelectedRow] = useState(0); const [entityAttributeValue, setEntityAttributeValue] = useState(""); const [originalDocument, setOriginalDocument] = useState({}); const [entityAttributeProperty, setEntityAttributeProperty] = useState(""); + const [formError, setFormError] = useState(""); + const [isExecuting, setIsExecuting] = useState(false); const [ isEntityValuePanelOpen, @@ -190,26 +190,44 @@ export const EditTableEntityPanel: FunctionComponent return displayValue; }; - const submit = async (event: React.FormEvent): Promise => { - if (!isValidEntities(entities)) { - return undefined; + const onSubmit = async (): Promise => { + for (let i = 0; i < entities.length; i++) { + const { property, type } = entities[i]; + if (property === "" || property === undefined) { + setFormError(`Property name cannot be empty. Please enter a property name`); + return; + } + + if (!type) { + setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); + return; + } } - event.preventDefault(); + + setIsExecuting(true); const entity: Entities.ITableEntity = entityFromAttributes(entities); const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient; const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument; - const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument( - queryTablesTab.collection, - originalDocumentData, - entity - ); - await tableEntityListViewModel.updateCachedEntity(newEntity); - if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { - tableEntityListViewModel.redrawTableThrottled(); + + try { + const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument( + queryTablesTab.collection, + originalDocumentData, + entity + ); + await tableEntityListViewModel.updateCachedEntity(newEntity); + if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { + tableEntityListViewModel.redrawTableThrottled(); + } + tableEntityListViewModel.selected.removeAll(); + tableEntityListViewModel.selected.push(newEntity); + } catch (error) { + const errorMessage = getErrorMessage(error); + handleError(errorMessage, "EditTableRow"); + throw error; + } finally { + setIsExecuting(false); } - tableEntityListViewModel.selected.removeAll(); - tableEntityListViewModel.selected.push(newEntity); - closeSidePanel(); }; const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { @@ -299,109 +317,81 @@ export const EditTableEntityPanel: FunctionComponent setIsEntityValuePanelTrue(); }; - const renderPanelContent = (): JSX.Element => { - return ( -
-
-
- {entities.map((entity, index) => { - return ( - editEntity(index)} - onSelectDate={(date: Date) => { - entityChange(date, index, "value"); - }} - onDeleteEntity={() => deleteEntityAtIndex(index)} - onEntityPropertyChange={(event, newInput?: string) => { - entityChange(newInput, index, "property"); - }} - onEntityTypeChange={(event: React.FormEvent, selectedParam: IDropdownOption) => { - entityTypeChange(event, selectedParam, index); - }} - onEntityValueChange={(event, newInput?: string) => { - entityChange(newInput, index, "value"); - }} - onEntityTimeValueChange={(event, newInput?: string) => { - entityChange(newInput, index, "time"); - }} - /> - ); - })} - {userContext.apiType !== "Cassandra" && ( - - Add Entity - {getAddButtonLabel(userContext.apiType)} - - )} -
- {renderPanelFooter()} -
-
- ); - }; - - const renderPanelFooter = (): JSX.Element => { - return ( -
-
- -
-
- ); - }; - - const onRenderNavigationContent: IRenderFunction = () => ( - - back setIsEntityValuePanelFalse()} /> - - - ); if (isEntityValuePanelOpen) { return ( - { - setEntityAttributeValue(newInput); - entityChange(newInput, selectedRow, "value"); - }} - /> - } - isConsoleExpanded={false} - /> + + + back setIsEntityValuePanelFalse()} /> + + + { + setEntityAttributeValue(newInput); + entityChange(newInput, selectedRow, "value"); + }} + /> + ); } + const props: RightPaneFormProps = { + formError, + isExecuting, + submitButtonText: "Update", + onSubmit, + }; + return ( - + +
+ {entities.map((entity, index) => { + return ( + editEntity(index)} + onSelectDate={(date: Date) => { + entityChange(date, index, "value"); + }} + onDeleteEntity={() => deleteEntityAtIndex(index)} + onEntityPropertyChange={(event, newInput?: string) => { + entityChange(newInput, index, "property"); + }} + onEntityTypeChange={(event: React.FormEvent, selectedParam: IDropdownOption) => { + entityTypeChange(event, selectedParam, index); + }} + onEntityValueChange={(event, newInput?: string) => { + entityChange(newInput, index, "value"); + }} + onEntityTimeValueChange={(event, newInput?: string) => { + entityChange(newInput, index, "time"); + }} + /> + ); + })} + {userContext.apiType !== "Cassandra" && ( + + Add Entity + {getAddButtonLabel(userContext.apiType)} + + )} +
+
); }; diff --git a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx index b5542e86e..b38f569e0 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx +++ b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx @@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2 export const columnProps: Partial = { tokens: { childrenGap: 10 }, - styles: { root: { width: 680, marginBottom: 8 } }, + styles: { root: { marginBottom: 8 } }, }; // helper functions diff --git a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap index 89e86e6f0..018fe7118 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap @@ -13,879 +13,1780 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` } } > - +
+
-
- - - +
+ Add Entity +
+ + + + Add Property - -
+ +
-
-
- -
-
-
- - } - panelWidth="700px" - > - - +
+ - - + - - -
-
-
-
-
- -
-
-
+ + - - *": Object { + "left": 0, + "position": "relative", + "top": 0, + }, + }, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + }, + Object { + "height": "32px", + "minWidth": "80px", + }, + Object { + "backgroundColor": "#0078d4", + "border": "1px solid #0078d4", + "color": "#ffffff", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus": Object { + "selectors": Object { + ":after": Object { + "border": "none", + "outlineColor": "#ffffff", + }, + }, + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "borderColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + ], + "rootChecked": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootCheckedHovered": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootDisabled": Array [ + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid transparent", + "bottom": 2, + "content": "\\"\\"", + "left": 2, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 2, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "bottom": -2, + "left": -2, + "outlineColor": "ButtonText", + "right": -2, + "top": -2, + }, + }, + "top": 2, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "backgroundColor": "#f3f2f1", + "borderColor": "#f3f2f1", + "color": "#a19f9d", + "cursor": "default", + "selectors": Object { + ":focus": Object { + "outline": 0, + }, + ":hover": Object { + "outline": 0, + }, + }, + }, + Object { + "backgroundColor": "#f3f2f1", + "color": "#d2d0ce", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + ], + "rootExpanded": Object { + "backgroundColor": "#005a9e", + "color": "#ffffff", + }, + "rootHovered": Object { + "backgroundColor": "#106ebe", + "border": "1px solid #106ebe", + "color": "#ffffff", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Highlight", + "borderColor": "Highlight", + "color": "Window", + }, + }, + }, + "rootPressed": Object { + "backgroundColor": "#005a9e", + "border": "1px solid #005a9e", + "color": "#ffffff", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "borderColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + "screenReaderText": Object { + "border": 0, + "height": 1, + "margin": -1, + "overflow": "hidden", + "padding": 0, + "position": "absolute", + "width": 1, + }, + "splitButtonContainer": Array [ + Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + }, + }, + }, + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid #ffffff", + "bottom": 3, + "content": "\\"\\"", + "left": 3, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 3, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + "bottom": -2, + "left": -2, + "right": -2, + "top": -2, + }, + }, + "top": 3, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "display": "inline-flex", + "selectors": Object { + ".ms-Button--default": Object { + "borderBottomRightRadius": "0", + "borderRight": "none", + "borderTopRightRadius": "0", + }, + ".ms-Button--primary": Object { + "border": "none", + "borderBottomRightRadius": "0", + "borderTopRightRadius": "0", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "border": "1px solid WindowText", + "borderRightWidth": "0", + "color": "WindowText", + "forcedColorAdjust": "none", + }, + }, + }, + ".ms-Button--primary + .ms-Button": Object { + "border": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "1px solid WindowText", + "borderLeftWidth": "0", + }, + }, + }, + }, + }, + ], + "splitButtonContainerChecked": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerCheckedHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerDisabled": Object { + "border": "none", + "outline": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + "forcedColorAdjust": "none", + }, + }, + }, + "splitButtonContainerFocused": Object { + "outline": "none!important", + }, + "splitButtonContainerHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Highlight", + "color": "Window", + }, + }, + }, + ".ms-Button.is-disabled": Object { + "color": "#a19f9d", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + }, + }, + "splitButtonDivider": Array [ + Object { + "backgroundColor": "#ffffff", + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + }, + }, + "top": 8, + "width": 1, + }, + Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "WindowText", + }, + }, + "top": 8, + "width": 1, + }, + ], + "splitButtonDividerDisabled": Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "GrayText", + }, + }, + "top": 8, + "width": 1, + }, + "splitButtonFlexContainer": Object { + "alignItems": "center", + "display": "flex", + "flexWrap": "nowrap", + "height": "100%", + "justifyContent": "center", + }, + "splitButtonMenuButton": Array [ + Object { + "backgroundColor": "#0078d4", + "color": "#ffffff", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#106ebe", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "Highlight", + }, + }, + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "WindowText", + }, + }, + }, + Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + ".ms-Button-menuIcon": Object { + "color": "WindowText", + }, + }, + "border": "1px solid #8a8886", + "borderBottomRightRadius": "2px", + "borderLeft": "none", + "borderRadius": 0, + "borderTopRightRadius": "2px", + "boxSizing": "border-box", + "cursor": "pointer", + "display": "inline-block", + "height": "auto", + "marginBottom": 0, + "marginLeft": -1, + "marginRight": 0, + "marginTop": 0, + "outline": "transparent", + "padding": 6, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + "verticalAlign": "top", + "width": 32, + }, + ], + "splitButtonMenuButtonChecked": Object { + "backgroundColor": "#005a9e", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#005a9e", + }, + }, + }, + "splitButtonMenuButtonDisabled": Array [ + Object { + "backgroundColor": "#f3f2f1", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#f3f2f1", + }, + }, + }, + Object { + "border": "none", + "pointerEvents": "none", + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + ".ms-Button-menuIcon": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "GrayText", + }, + }, + }, + ":hover": Object { + "cursor": "default", + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "border": "1px solid GrayText", + "color": "GrayText", + }, + }, + }, + ], + "splitButtonMenuButtonExpanded": Object { + "backgroundColor": "#005a9e", + "selectors": Object { + ":hover": Object { + "backgroundColor": "#005a9e", + }, + }, + }, + "splitButtonMenuFocused": Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid #ffffff", + "bottom": 3, + "content": "\\"\\"", + "left": 3, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 3, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + "bottom": -2, + "left": -2, + "right": -2, + "top": -2, + }, + }, + "top": 3, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + "splitButtonMenuIcon": Object { + "color": "#ffffff", + }, + "splitButtonMenuIconDisabled": Object { + "color": "#a19f9d", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "GrayText", + }, + }, + }, + "textContainer": Object { + "display": "block", + "flexGrow": 1, + }, + } + } + text="Update" theme={ Object { "disableGlobalClassNames": false, @@ -1159,1926 +2060,48 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` }, } } + type="submit" + variantClassName="ms-Button--primary" > -
- -
-
- - -
- - - -
-
-
-
-
-
- Edit Table Entity -
-
- - - *": Object { - "left": 0, - "position": "relative", - "top": 0, - }, - }, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - }, - Object { - "backgroundColor": "transparent", - "border": "none", - "color": "#0078d4", - "height": "32px", - "padding": "0 4px", - "width": "32px", - }, - "ms-Panel-closeButton ms-PanelAction-close", - Object { - "color": "#605e5c", - "fontSize": "20px", - "marginRight": 14, - }, - false, - ], - "rootChecked": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootCheckedHovered": Object { - "backgroundColor": "#e1dfdd", - "color": "#005a9e", - }, - "rootDisabled": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid transparent", - "bottom": 2, - "content": "\\"\\"", - "left": 2, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 2, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "bottom": -2, - "left": -2, - "outlineColor": "ButtonText", - "right": -2, - "top": -2, - }, - }, - "top": 2, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "backgroundColor": "#f3f2f1", - "borderColor": "#f3f2f1", - "color": "#a19f9d", - "cursor": "default", - "selectors": Object { - ":focus": Object { - "outline": 0, - }, - ":hover": Object { - "outline": 0, - }, - }, - }, - Object { - "color": "#c8c6c4", - }, - ], - "rootExpanded": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootHasMenu": Object { - "width": "auto", - }, - "rootHovered": Array [ - Object { - "backgroundColor": "#f3f2f1", - "color": "#106ebe", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "borderColor": "Highlight", - "color": "Highlight", - }, - }, - }, - Object { - "color": "#323130", - }, - ], - "rootPressed": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "screenReaderText": Object { - "border": 0, - "height": 1, - "margin": -1, - "overflow": "hidden", - "padding": 0, - "position": "absolute", - "width": 1, - }, - "splitButtonContainer": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "display": "inline-flex", - "selectors": Object { - ".ms-Button--default": Object { - "borderBottomRightRadius": "0", - "borderRight": "none", - "borderTopRightRadius": "0", - }, - ".ms-Button--primary": Object { - "border": "none", - "borderBottomRightRadius": "0", - "borderTopRightRadius": "0", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "border": "1px solid WindowText", - "borderRightWidth": "0", - "color": "WindowText", - "forcedColorAdjust": "none", - }, - }, - }, - ".ms-Button--primary + .ms-Button": Object { - "border": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "1px solid WindowText", - "borderLeftWidth": "0", - }, - }, - }, - }, - }, - ], - "splitButtonContainerChecked": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerCheckedHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerDisabled": Object { - "border": "none", - "outline": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - "forcedColorAdjust": "none", - }, - }, - }, - "splitButtonContainerFocused": Object { - "outline": "none!important", - }, - "splitButtonContainerHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Highlight", - "color": "Window", - }, - }, - }, - ".ms-Button.is-disabled": Object { - "color": "#a19f9d", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - }, - }, - "splitButtonDivider": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "WindowText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonDividerDisabled": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "GrayText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonFlexContainer": Object { - "alignItems": "center", - "display": "flex", - "flexWrap": "nowrap", - "height": "100%", - "justifyContent": "center", - }, - "splitButtonMenuButton": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - ".ms-Button-menuIcon": Object { - "color": "WindowText", - }, - }, - "border": "1px solid #8a8886", - "borderBottomRightRadius": "2px", - "borderLeft": "none", - "borderRadius": 0, - "borderTopRightRadius": "2px", - "boxSizing": "border-box", - "cursor": "pointer", - "display": "inline-block", - "height": "auto", - "marginBottom": 0, - "marginLeft": -1, - "marginRight": 0, - "marginTop": 0, - "outline": "transparent", - "padding": 6, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - "verticalAlign": "top", - "width": 32, - }, - "splitButtonMenuButtonDisabled": Object { - "border": "none", - "pointerEvents": "none", - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - ".ms-Button-menuIcon": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "color": "GrayText", - }, - }, - }, - ":hover": Object { - "cursor": "default", - }, - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "border": "1px solid GrayText", - "color": "GrayText", - }, - }, - }, - "splitButtonMenuFocused": Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - "textContainer": Object { - "display": "block", - "flexGrow": 1, - }, - } - } - theme={ - Object { - "disableGlobalClassNames": false, - "effects": Object { - "elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)", - "elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)", - "elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "roundedCorner2": "2px", - "roundedCorner4": "4px", - "roundedCorner6": "6px", - }, - "fonts": Object { - "large": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "18px", - "fontWeight": 400, - }, - "medium": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "14px", - "fontWeight": 400, - }, - "mediumPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "16px", - "fontWeight": 400, - }, - "mega": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "68px", - "fontWeight": 600, - }, - "small": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "smallPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "superLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "42px", - "fontWeight": 600, - }, - "tiny": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "20px", - "fontWeight": 600, - }, - "xLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "24px", - "fontWeight": 600, - }, - "xSmall": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xxLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "28px", - "fontWeight": 600, - }, - "xxLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "32px", - "fontWeight": 600, - }, - }, - "isInverted": false, - "palette": Object { - "accent": "#0078d4", - "black": "#000000", - "blackTranslucent40": "rgba(0,0,0,.4)", - "blue": "#0078d4", - "blueDark": "#002050", - "blueLight": "#00bcf2", - "blueMid": "#00188f", - "green": "#107c10", - "greenDark": "#004b1c", - "greenLight": "#bad80a", - "magenta": "#b4009e", - "magentaDark": "#5c005c", - "magentaLight": "#e3008c", - "neutralDark": "#201f1e", - "neutralLight": "#edebe9", - "neutralLighter": "#f3f2f1", - "neutralLighterAlt": "#faf9f8", - "neutralPrimary": "#323130", - "neutralPrimaryAlt": "#3b3a39", - "neutralQuaternary": "#d2d0ce", - "neutralQuaternaryAlt": "#e1dfdd", - "neutralSecondary": "#605e5c", - "neutralSecondaryAlt": "#8a8886", - "neutralTertiary": "#a19f9d", - "neutralTertiaryAlt": "#c8c6c4", - "orange": "#d83b01", - "orangeLight": "#ea4300", - "orangeLighter": "#ff8c00", - "purple": "#5c2d91", - "purpleDark": "#32145a", - "purpleLight": "#b4a0ff", - "red": "#e81123", - "redDark": "#a4262c", - "teal": "#008272", - "tealDark": "#004b50", - "tealLight": "#00b294", - "themeDark": "#005a9e", - "themeDarkAlt": "#106ebe", - "themeDarker": "#004578", - "themeLight": "#c7e0f4", - "themeLighter": "#deecf9", - "themeLighterAlt": "#eff6fc", - "themePrimary": "#0078d4", - "themeSecondary": "#2b88d8", - "themeTertiary": "#71afe5", - "white": "#ffffff", - "whiteTranslucent40": "rgba(255,255,255,.4)", - "yellow": "#ffb900", - "yellowDark": "#d29200", - "yellowLight": "#fff100", - }, - "rtl": undefined, - "semanticColors": Object { - "accentButtonBackground": "#0078d4", - "accentButtonText": "#ffffff", - "actionLink": "#323130", - "actionLinkHovered": "#201f1e", - "blockingBackground": "#FDE7E9", - "blockingIcon": "#FDE7E9", - "bodyBackground": "#ffffff", - "bodyBackgroundChecked": "#edebe9", - "bodyBackgroundHovered": "#f3f2f1", - "bodyDivider": "#edebe9", - "bodyFrameBackground": "#ffffff", - "bodyFrameDivider": "#edebe9", - "bodyStandoutBackground": "#faf9f8", - "bodySubtext": "#605e5c", - "bodyText": "#323130", - "bodyTextChecked": "#000000", - "buttonBackground": "#ffffff", - "buttonBackgroundChecked": "#c8c6c4", - "buttonBackgroundCheckedHovered": "#edebe9", - "buttonBackgroundDisabled": "#f3f2f1", - "buttonBackgroundHovered": "#f3f2f1", - "buttonBackgroundPressed": "#edebe9", - "buttonBorder": "#8a8886", - "buttonBorderDisabled": "#f3f2f1", - "buttonText": "#323130", - "buttonTextChecked": "#201f1e", - "buttonTextCheckedHovered": "#000000", - "buttonTextDisabled": "#a19f9d", - "buttonTextHovered": "#201f1e", - "buttonTextPressed": "#201f1e", - "cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "cardStandoutBackground": "#ffffff", - "defaultStateBackground": "#faf9f8", - "disabledBackground": "#f3f2f1", - "disabledBodySubtext": "#c8c6c4", - "disabledBodyText": "#a19f9d", - "disabledBorder": "#c8c6c4", - "disabledSubtext": "#d2d0ce", - "disabledText": "#a19f9d", - "errorBackground": "#FDE7E9", - "errorIcon": "#A80000", - "errorText": "#a4262c", - "focusBorder": "#605e5c", - "infoBackground": "#f3f2f1", - "infoIcon": "#605e5c", - "inputBackground": "#ffffff", - "inputBackgroundChecked": "#0078d4", - "inputBackgroundCheckedHovered": "#005a9e", - "inputBorder": "#605e5c", - "inputBorderHovered": "#323130", - "inputFocusBorderAlt": "#0078d4", - "inputForegroundChecked": "#ffffff", - "inputIcon": "#0078d4", - "inputIconDisabled": "#a19f9d", - "inputIconHovered": "#005a9e", - "inputPlaceholderBackgroundChecked": "#deecf9", - "inputPlaceholderText": "#605e5c", - "inputText": "#323130", - "inputTextHovered": "#201f1e", - "link": "#0078d4", - "linkHovered": "#004578", - "listBackground": "#ffffff", - "listHeaderBackgroundHovered": "#f3f2f1", - "listHeaderBackgroundPressed": "#edebe9", - "listItemBackgroundChecked": "#edebe9", - "listItemBackgroundCheckedHovered": "#e1dfdd", - "listItemBackgroundHovered": "#f3f2f1", - "listText": "#323130", - "listTextColor": "#323130", - "menuBackground": "#ffffff", - "menuDivider": "#c8c6c4", - "menuHeader": "#0078d4", - "menuIcon": "#0078d4", - "menuItemBackgroundChecked": "#edebe9", - "menuItemBackgroundHovered": "#f3f2f1", - "menuItemBackgroundPressed": "#edebe9", - "menuItemText": "#323130", - "menuItemTextHovered": "#201f1e", - "messageLink": "#005A9E", - "messageLinkHovered": "#004578", - "messageText": "#323130", - "primaryButtonBackground": "#0078d4", - "primaryButtonBackgroundDisabled": "#f3f2f1", - "primaryButtonBackgroundHovered": "#106ebe", - "primaryButtonBackgroundPressed": "#005a9e", - "primaryButtonBorder": "transparent", - "primaryButtonText": "#ffffff", - "primaryButtonTextDisabled": "#d2d0ce", - "primaryButtonTextHovered": "#ffffff", - "primaryButtonTextPressed": "#ffffff", - "severeWarningBackground": "#FED9CC", - "severeWarningIcon": "#D83B01", - "smallInputBorder": "#605e5c", - "successBackground": "#DFF6DD", - "successIcon": "#107C10", - "successText": "#107C10", - "variantBorder": "#edebe9", - "variantBorderHovered": "#a19f9d", - "warningBackground": "#FFF4CE", - "warningHighlight": "#ffb900", - "warningIcon": "#797775", - "warningText": "#323130", - }, - "spacing": Object { - "l1": "20px", - "l2": "32px", - "m": "16px", - "s1": "8px", - "s2": "4px", - }, - } - } - title="Close" - variantClassName="ms-Button--icon" - > - - - - - -
-
-
-
-
-
-
-
- -
- - -
- Add Entity -
-
-
- - - Add Property - - -
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
- -
- - - - - - - - - + Update + + + + + + + + + + +
+ + + `; diff --git a/src/Explorer/Tabs/QueryTablesTab.tsx b/src/Explorer/Tabs/QueryTablesTab.tsx index aa208fcfd..fb096e1a9 100644 --- a/src/Explorer/Tabs/QueryTablesTab.tsx +++ b/src/Explorer/Tabs/QueryTablesTab.tsx @@ -158,7 +158,8 @@ export default class QueryTablesTab extends TabsBase { queryTablesTab={this} tableEntityListViewModel={this.tableEntityListViewModel()} cassandraApiClient={new CassandraAPIDataClient()} - /> + />, + "700px" ); }; From fecac5625a9749a854735426d394b30a634cfe2b Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Thu, 22 Jul 2021 17:11:19 -0700 Subject: [PATCH 10/20] Migrate resource tree for resource token to react (#956) --- .eslintignore | 1 - less/tree.less | 270 +++++++++--------- src/Common/ResourceTreeContainer.tsx | 3 +- .../SettingsComponent.test.tsx.snap | 8 - src/Explorer/Explorer.tsx | 5 - .../GitHubReposPanel.test.tsx.snap | 4 - .../StringInputPane.test.tsx.snap | 4 - ...esourceToken.tsx => ResourceTokenTree.tsx} | 43 +-- ...sourceTreeAdapterForResourceToken.test.tsx | 35 --- ...eTreeAdapterForResourceToken.test.tsx.snap | 36 --- 10 files changed, 143 insertions(+), 266 deletions(-) rename src/Explorer/Tree/{ResourceTreeAdapterForResourceToken.tsx => ResourceTokenTree.tsx} (61%) delete mode 100644 src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx delete mode 100644 src/Explorer/Tree/__snapshots__/ResourceTreeAdapterForResourceToken.test.tsx.snap diff --git a/.eslintignore b/.eslintignore index 8c86e0e52..7a5d06bbf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -191,5 +191,4 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx __mocks__/monaco-editor.ts -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx src/Explorer/Tree/ResourceTree.tsx \ No newline at end of file diff --git a/less/tree.less b/less/tree.less index 56a5ee38e..ed0fbf71f 100644 --- a/less/tree.less +++ b/less/tree.less @@ -1,272 +1,270 @@ @import "./Common/Constants"; - .resourceTree { + height: 100%; + flex: 0 0 auto; + .main { height: 100%; - flex: 0 0 auto; - .main { - height: 100%; - } + } } .resourceTreeScroll { - height: 100%; - display: flex; - overflow-y: auto; - overflow-x: hidden; - padding-right: 10px; + height: 100%; + display: flex; + overflow-y: auto; + overflow-x: hidden; + padding-right: 10px; } .userSelectNone { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } .treeHovermargin { - margin-left: 16px; + margin-left: 16px; } .highlight { - padding: @SmallSpace 2px; - outline: 0; + padding: @SmallSpace 2px; + outline: 0; - &:hover { - .hover(); - } + &:hover { + .hover(); + } - &:active { - .active(); - } + &:active { + .active(); + } - &:focus { - .focus(); - } + &:focus { + .focus(); + } } .contextmenushowing { - background-color: #EEE; + background-color: #eee; } .collectionstree { - width: 100%; - margin-top: @DefaultSpace; + width: 100%; + margin-top: @DefaultSpace; + .databaseList { + list-style-type: none; + padding-left: 0px; - .databaseList { - list-style-type: none; - padding-left: 0px; - - .collectionList { - padding-left:(2 * @MediumSpace); - } - - .collectionChildList { - padding-left: @LargeSpace; - } - - .databaseDocuments { - padding-left: (5 * @MediumSpace); - } + .collectionList { + padding-left: (2 * @MediumSpace); } + + .collectionChildList { + padding-left: @LargeSpace; + } + + .databaseDocuments { + padding-left: (5 * @MediumSpace); + } + } } .pointerCursor { - cursor: pointer; + cursor: pointer; } .menuEllipsis { - padding-right: 6px; - font-weight: bold; - font-size: 18px; - position: relative; - top: -5px; - left: 0px; - float: right; - display: none; - padding-left: 6px!important; - line-height: @TreeLineHeight; + padding-right: 6px; + font-weight: bold; + font-size: 18px; + position: relative; + top: -5px; + left: 0px; + float: right; + display: none; + padding-left: 6px !important; + line-height: @TreeLineHeight; } .databaseMenu { - .flex-display(); + .flex-display(); } .databaseMenu:hover .menuEllipsis, .databaseMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseCollChildTextOverflow { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex: 1; } .collectionMenu { - .flex-display(); + .flex-display(); } .collectionMenu:hover .menuEllipsis, .collectionMenu:focus .menuEllipsis { - display: block; + display: block; } .documentsMenu:hover .menuEllipsis, .documentsMenu:focus .menuEllipsis { - display: block; + display: block; } .treeChildMenu { - display: flex; + display: flex; } .storedProcedureMenu:hover .menuEllipsis, .storedProcedureMenu:focus .menuEllipsis { - display: block; + display: block; } .childMenu { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-left: (6 * @MediumSpace); - width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: (6 * @MediumSpace); + width: 100%; } .storedChildMenu:hover .menuEllipsis, .storedChildMenu:focus .menuEllipsis { - display: block; + display: block; } .contextmenu6 { - top: -29px; + top: -29px; } .userDefinedMenu:hover .contextmenu6 { - display: block; + display: block; } .userDefinedchildMenu:hover .menuEllipsis, .userDefinedchildMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersMenu:hover .menuEllipsis, .triggersMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersChildMenu:hover .menuEllipsis, .triggersChildMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseId { - font-size: 14px; + font-size: 14px; } .storedUdfTriggerMenu { - padding-left: 0px; + padding-left: 0px; } .collectionstree img { - width: 16px; - height: 16px; - vertical-align: text-top; + width: 16px; + height: 16px; + vertical-align: text-top; } img.collectionsTreeCollapseExpand { - width: 10px; - height: 10px; - vertical-align: middle; - margin-bottom: 5px; + width: 10px; + height: 10px; + vertical-align: middle; + margin-bottom: 5px; } .collapsed::before { - content: "\23F5"; - margin-left: 0px; - font-size: 15px; + content: "\23F5"; + margin-left: 0px; + font-size: 15px; } .expanded::before { - content: '\23F7'; - margin-left: 0px; - font-size: 15px; + content: "\23F7"; + margin-left: 0px; + font-size: 15px; } .collectionMenuChildren { - padding-left: 42px; + padding-left: 42px; } .main-nav { - width: 100vh; - height: 40px; - background: white; - transform-origin: left top; - -webkit-transform-origin: left top; - -ms-transform-origin: left top; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); - border-bottom: 1px solid #CCC; + width: 100vh; + height: 40px; + background: white; + transform-origin: left top; + -webkit-transform-origin: left top; + -ms-transform-origin: left top; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); + border-bottom: 1px solid #ccc; } .main-nav-img { - width: 16px; - height: 16px; - margin: -32px 0 0 0; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); + width: 16px; + height: 16px; + margin: -32px 0 0 0; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); } .main-nav-img.main-nav-sub-img { - width: 16px; - height: 16px; - margin: 0px 0px 0 0; - transform: rotate(180deg) translateX(0%); - -webkit-transform: rotate(180deg) translateX(0%); - -ms-transform: rotate(180deg) translateX(0%); - position: absolute; - right: -8px; - top: 16px; + width: 16px; + height: 16px; + margin: 0px 0px 0 0; + transform: rotate(180deg) translateX(0%); + -webkit-transform: rotate(180deg) translateX(0%); + -ms-transform: rotate(180deg) translateX(0%); + position: absolute; + right: -8px; + top: 16px; } ul.nav { - margin: 0 auto; - margin-top: 0px; - margin-left: 0px; + margin: 0 auto; + margin-top: 0px; + margin-left: 0px; } .mini ul.nav li { - float: right; - line-height: 25px; - height: auto; - margin-top: 3px; + float: right; + line-height: 25px; + height: auto; + margin-top: 3px; } .spancolchildstyle { - padding: 4px; + padding: 4px; } .contextmenubutton { - float: right; - display: none; + float: right; + display: none; } -.highlight:hover>.contextmenubutton { - display: unset; +.highlight:hover > .contextmenubutton { + display: unset; } -.highlight:hover>.contextmenubutton::after { - content: "\2026"; - font-size: 12px; +.highlight:hover > .contextmenubutton::after { + content: "\2026"; + font-size: 12px; } .showEllipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} \ No newline at end of file + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index 129a38115..fe04f9e04 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -3,6 +3,7 @@ import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import refreshImg from "../../images/refresh-cosmos.svg"; import { AuthType } from "../AuthType"; import Explorer from "../Explorer/Explorer"; +import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; @@ -52,7 +53,7 @@ export const ResourceTreeContainer: FunctionComponent
{userContext.authType === AuthType.ResourceToken ? ( -
+ ) : userContext.features.enableKOResourceTree ? (
) : ( diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index cb8ecbfaf..b1164fe05 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -44,10 +44,6 @@ exports[`SettingsComponent renders 1`] = ` "copyNotebook": [Function], "parameters": [Function], }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [Function], - }, }, "databaseId": "test", "defaultTtl": [Function], @@ -115,10 +111,6 @@ exports[`SettingsComponent renders 1`] = ` "copyNotebook": [Function], "parameters": [Function], }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [Function], - }, }, "databaseId": "test", "defaultTtl": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index ac1e077d6..79f6fec11 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -59,7 +59,6 @@ import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; -import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; import { useDatabases } from "./useDatabases"; import { useSelectedNode } from "./useSelectedNode"; @@ -74,9 +73,6 @@ export default class Explorer { // Resource Tree private resourceTree: ResourceTreeAdapter; - // Resource Token - public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; - // Tabs public isTabsContentExpanded: ko.Observable; @@ -186,7 +182,6 @@ export default class Explorer { ); this.resourceTree = new ResourceTreeAdapter(this); - this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); // Override notebook server parameters from URL parameters if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 9d1b8cc0c..7406a14ee 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -33,10 +33,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "copyNotebook": [Function], "parameters": [Function], }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [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 27f1f941d..80bb050de 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -23,10 +23,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "copyNotebook": [Function], "parameters": [Function], }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [Function], - }, } } inProgressMessage="Creating directory " diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx b/src/Explorer/Tree/ResourceTokenTree.tsx similarity index 61% rename from src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx rename to src/Explorer/Tree/ResourceTokenTree.tsx index 49529f0d7..b405e2559 100644 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx +++ b/src/Explorer/Tree/ResourceTokenTree.tsx @@ -1,45 +1,18 @@ -import * as ko from "knockout"; -import * as React from "react"; +import React from "react"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { userContext } from "../../UserContext"; import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; -import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; -export class ResourceTreeAdapterForResourceToken implements ReactAdapter { - public parameters: ko.Observable; - public myNotebooksContentRoot: NotebookContentItem; +export const ResourceTokenTree: React.FC = (): JSX.Element => { + const collection = useDatabases((state) => state.resourceTokenCollection); - public constructor(private container: Explorer) { - this.parameters = ko.observable(Date.now()); - - useDatabases.subscribe( - () => this.triggerRender(), - (state) => state.resourceTokenCollection - ); - useSelectedNode.subscribe(() => this.triggerRender()); - useTabs.subscribe( - () => this.triggerRender(), - (state) => state.activeTab - ); - - this.triggerRender(); - } - - public renderComponent(): JSX.Element { - const dataRootNode = this.buildCollectionNode(); - return ; - } - - public buildCollectionNode(): TreeNode { - const collection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection; + const buildCollectionNode = (): TreeNode => { if (!collection) { return { label: undefined, @@ -86,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { isExpanded: true, children: [collectionNode], }; - } + }; - public triggerRender() { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} + return ; +}; diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx deleted file mode 100644 index 47133e42a..000000000 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { shallow } from "enzyme"; -import React from "react"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { TreeComponent, TreeComponentProps, TreeNode } from "../Controls/TreeComponent/TreeComponent"; -import Explorer from "../Explorer"; -import { useDatabases } from "../useDatabases"; -import ResourceTokenCollection from "./ResourceTokenCollection"; -import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForResourceToken"; - -describe("Resource tree for resource token", () => { - const mockContainer = {} as Explorer; - const resourceTree = new ResourceTreeAdapterForResourceToken(mockContainer); - const mockCollection = { - _rid: "fakeRid", - _self: "fakeSelf", - id: "fakeId", - } as DataModels.Collection; - const mockResourceTokenCollection: ViewModels.CollectionBase = new ResourceTokenCollection( - mockContainer, - "fakeDatabaseId", - mockCollection - ); - useDatabases.setState({ resourceTokenCollection: mockResourceTokenCollection }); - - it("should render", () => { - const rootNode: TreeNode = resourceTree.buildCollectionNode(); - const props: TreeComponentProps = { - rootNode, - className: "dataResourceTree", - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapterForResourceToken.test.tsx.snap b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapterForResourceToken.test.tsx.snap deleted file mode 100644 index cb88eaa00..000000000 --- a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapterForResourceToken.test.tsx.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Resource tree for resource token should render 1`] = ` -
- -
-`; From 1394aae944e3af135c9ac94b901e2d8559b1b4a7 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 23 Jul 2021 18:44:16 -0700 Subject: [PATCH 11/20] Fix validateCollectionId for new tables account (#958) --- src/Explorer/useDatabases.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 3808f42c5..1f9886a7d 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -2,6 +2,7 @@ import _ from "underscore"; import create, { UseStore } from "zustand"; import * as Constants from "../Common/Constants"; import * as ViewModels from "../Contracts/ViewModels"; +import { userContext } from "../UserContext"; import { useSelectedNode } from "./useSelectedNode"; interface DatabasesState { @@ -136,6 +137,11 @@ export const useDatabases: UseStore = create((set, get) => ({ }, validateCollectionId: async (databaseId: string, collectionId: string): Promise => { const database = get().databases.find((db) => db.id() === databaseId); + // For a new tables account, database is undefined when creating the first table + if (!database && userContext.apiType === "Tables") { + return true; + } + await database.loadCollections(); return !database.collections().some((collection) => collection.id() === collectionId); }, From a8bc821dec0ea572d1d07a01a561499b38afc639 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 23 Jul 2021 18:44:24 -0700 Subject: [PATCH 12/20] Add initializeGitHubRepos function in useNotebooks store (#957) --- src/Explorer/Notebook/NotebookManager.tsx | 2 ++ src/Explorer/Notebook/useNotebook.ts | 30 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index d6d13b6a1..4d14abf6a 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -29,6 +29,7 @@ import { SnapshotRequest } from "./NotebookComponent/types"; import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContentClient } from "./NotebookContentClient"; import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils"; +import { useNotebook } from "./useNotebook"; type NotebookPaneContent = string | ImmutableNotebook; @@ -110,6 +111,7 @@ export default class NotebookManager { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); + useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); this.refreshPinnedRepos(); } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index c4967a228..af1b47477 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -6,10 +6,12 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; +import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import NotebookManager from "./NotebookManager"; @@ -39,6 +41,7 @@ interface NotebookState { updateNotebookItem: (item: NotebookContentItem) => void; deleteNotebookItem: (item: NotebookContentItem) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; + initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -202,4 +205,31 @@ export const useNotebook: UseStore = create((set, get) => ({ } } }, + initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => { + const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot); + if (gitHubNotebooksContentRoot) { + gitHubNotebooksContentRoot.children = []; + pinnedRepos?.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + const repoTreeItem: NotebookContentItem = { + name: repoFullName, + path: "PsuedoDir", + type: NotebookContentItemType.Directory, + children: [], + }; + + pinnedRepo.branches.forEach((branch) => { + repoTreeItem.children.push({ + name: branch.name, + path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), + type: NotebookContentItemType.Directory, + }); + }); + + gitHubNotebooksContentRoot.children.push(repoTreeItem); + }); + + set({ gitHubNotebooksContentRoot }); + } + }, })); From a66fc06dad737468facb24d8b7a9beb3dd584555 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 28 Jul 2021 21:29:45 -0700 Subject: [PATCH 13/20] Turn off react resource tree (#963) --- src/Common/ResourceTreeContainer.tsx | 6 +++--- src/Platform/Hosted/extractFeatures.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index fe04f9e04..ca41610da 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -54,10 +54,10 @@ export const ResourceTreeContainer: FunctionComponent {userContext.authType === AuthType.ResourceToken ? ( - ) : userContext.features.enableKOResourceTree ? ( -
- ) : ( + ) : userContext.features.enableReactResourceTree ? ( + ) : ( +
)}
{/* Collections Window - End */} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 673d0c255..f1f0115a5 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -15,7 +15,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableKOResourceTree: boolean; + readonly enableReactResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -57,7 +57,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), - enableKOResourceTree: "true" === get("enablekoresourcetree"), + enableReactResourceTree: "true" === get("enablereactresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), From 7e0c4b72909110c9862b6ee76fec526f08247d83 Mon Sep 17 00:00:00 2001 From: t-tarabhatia <84812001+t-tarabhatia@users.noreply.github.com> Date: Thu, 29 Jul 2021 06:48:03 -0700 Subject: [PATCH 14/20] Add changes to Partition Key A/B Test (#954) --- src/Common/Constants.ts | 1 + src/Explorer/Panes/AddCollectionPanel.tsx | 19 ++++++++++++++----- src/Platform/Hosted/extractFeatures.ts | 2 ++ src/hooks/useKnockoutExplorer.ts | 6 ++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index fc97d559d..cbdd2da8e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -95,6 +95,7 @@ export class Flights { public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; + public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; } export class AfecFeatures { diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index f0ce4f4d1..39d7ad2c8 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -113,11 +113,7 @@ export class AddCollectionPanel extends React.Component Date: Fri, 30 Jul 2021 10:27:27 -0700 Subject: [PATCH 15/20] Replace window.confirm and window.alert with modal dialog (#965) --- src/Explorer/Controls/Dialog.tsx | 55 +++++++++++++++- .../QueriesGridComponent.tsx | 66 +++++++++++-------- .../DataSamples/DataSamplesUtil.test.ts | 1 - src/Explorer/DataSamples/DataSamplesUtil.ts | 5 +- src/Explorer/Explorer.tsx | 58 +++------------- .../Notebook/NotebookComponent/epics.ts | 35 +++++----- src/Explorer/Notebook/NotebookManager.tsx | 41 ++++++------ .../Tables/DataTable/TableCommands.ts | 34 ++++++---- src/Explorer/Tabs/ConflictsTab.ts | 29 +++++--- src/Explorer/Tabs/DocumentsTab.ts | 48 +++++++++----- src/Explorer/Tabs/MongoDocumentsTab.ts | 5 +- src/Explorer/Tabs/NotebookV2Tab.ts | 19 +++--- src/Explorer/Tree/ConflictId.ts | 28 ++++---- src/Explorer/Tree/DocumentId.ts | 15 ++++- src/Explorer/Tree/ResourceTree.tsx | 37 ++++++----- src/Explorer/Tree/ResourceTreeAdapter.tsx | 37 ++++++----- src/Explorer/Tree/StoredProcedure.ts | 22 ++++--- src/Explorer/Tree/Trigger.ts | 22 ++++--- src/Explorer/Tree/UserDefinedFunction.ts | 26 +++++--- src/Utils/GalleryUtils.test.ts | 17 ++--- src/Utils/GalleryUtils.ts | 6 +- tsconfig.strict.json | 1 - 22 files changed, 353 insertions(+), 254 deletions(-) diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index ad9805348..d5e308d69 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -23,13 +23,66 @@ export interface DialogState { dialogProps?: DialogProps; openDialog: (props: DialogProps) => void; closeDialog: () => void; + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ) => void; + showOkModalDialog: (title: string, subText: string) => void; } -export const useDialog: UseStore = create((set) => ({ +export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + get().closeDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + get().closeDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled, + }), + showOkModalDialog: (title: string, subText: string): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => { + get().closeDialog(); + }, + onSecondaryButtonClick: undefined, + }), })); export interface TextFieldProps extends ITextFieldProps { diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 3c3ef24b7..236dea5c3 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -29,6 +29,7 @@ import { QueriesClient } from "../../../Common/QueriesClient"; import * as DataModels from "../../../Contracts/DataModels"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Dialog"; const title = "Open Saved Queries"; @@ -222,35 +223,42 @@ export class QueriesGridComponent extends React.Component { - if (window.confirm("Are you sure you want to delete this query?")) { - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }); - try { - await this.props.queriesClient.deleteQuery(query); - TelemetryProcessor.traceSuccess( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }, - startKey - ); - } catch (error) { - TelemetryProcessor.traceFailure( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - } - await this.fetchSavedQueries(); // get latest state - } + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete this query?", + "Delete", + async () => { + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }); + try { + await this.props.queriesClient.deleteQuery(query); + TelemetryProcessor.traceSuccess( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }, + startKey + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + await this.fetchSavedQueries(); // get latest state + }, + "Cancel", + undefined + ); }, }, ], diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index f8ff6f8e5..c8ae2e66a 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -17,7 +17,6 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.showOkModalDialog = () => {}; useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 4007608c0..d28ef0426 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -1,6 +1,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; @@ -20,7 +21,7 @@ export class DataSamplesUtil { const containerName = generator.getCollectionId(); if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); return; } @@ -29,7 +30,7 @@ export class DataSamplesUtil { .createSampleContainerAsync() .catch((error) => logConsoleError(`Error creating sample container: ${error}`)); const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleInfo(msg); } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 79f6fec11..a4cd403f0 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { IChoiceGroupProps } from "@fluentui/react"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; @@ -35,7 +34,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import "./ComponentRegisterer"; -import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; +import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; @@ -548,7 +547,7 @@ export default class Explorer { const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => this.showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); return promise; } @@ -614,51 +613,6 @@ export default class Explorer { this.notebookManager?.openCopyNotebookPane(name, content); } - public showOkModalDialog(title: string, msg: string): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - }, - onSecondaryButtonClick: undefined, - }); - } - - public showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps, - isPrimaryButtonDisabled?: boolean - ): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: okLabel, - secondaryButtonText: cancelLabel, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - onOk && onOk(); - }, - onSecondaryButtonClick: () => { - useDialog.getState().closeDialog(); - onCancel && onCancel(); - }, - choiceGroupProps, - textFieldProps, - primaryButtonDisabled: isPrimaryButtonDisabled, - }); - } - /** * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. @@ -732,7 +686,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); } else { useSidePanel.getState().openSidePanel( "Rename Notebook", @@ -862,7 +818,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); return Promise.reject(); } diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 718028dab..9e6616a06 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -38,6 +38,7 @@ import { useTabs } from "../../../hooks/useTabs"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../../Controls/Dialog"; import * as FileSystemUtil from "../FileSystemUtil"; import * as cdbActions from "../NotebookComponent/actions"; import { NotebookUtil } from "../NotebookUtil"; @@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = ( logConsoleError(msg); logFailureToTelemetry(state, "Kernel restart error", msg); - const explorer = window.dataExplorer; - if (explorer) { - explorer.showOkModalDialog("kernel restarts", msg); - } + useDialog.getState().showOkModalDialog("kernel restarts", msg); + return of(EMPTY); } @@ -773,8 +772,7 @@ const closeUnsupportedMimetypesEpic = ( ofType(actions.FETCH_CONTENT_FULFILLED), mergeMap((action) => { const mimetype = action.payload.model.mimetype; - const explorer = window.dataExplorer; - if (explorer && !TextFile.handles(mimetype)) { + if (!TextFile.handles(mimetype)) { const filepath = action.payload.filepath; // Close tab and show error message useTabs @@ -783,7 +781,7 @@ const closeUnsupportedMimetypesEpic = ( (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) ); const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; - explorer.showOkModalDialog("File cannot be rendered", msg); + useDialog.getState().showOkModalDialog("File cannot be rendered", msg); logConsoleError(msg); } return EMPTY; @@ -803,19 +801,16 @@ const closeContentFailedToFetchEpic = ( return action$.pipe( ofType(actions.FETCH_CONTENT_FAILED), mergeMap((action) => { - const explorer = window.dataExplorer; - if (explorer) { - const filepath = action.payload.filepath; - // Close tab and show error message - useTabs - .getState() - .closeTabsByComparator( - (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) - ); - const msg = `Failed to load file: ${filepath}.`; - explorer.showOkModalDialog("Failure to load", msg); - logConsoleError(msg); - } + const filepath = action.payload.filepath; + // Close tab and show error message + useTabs + .getState() + .closeTabsByComparator( + (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) + ); + const msg = `Failed to load file: ${filepath}.`; + useDialog.getState().showOkModalDialog("Failure to load", msg); + logConsoleError(msg); return EMPTY; }) ); diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 4d14abf6a..ed66caab1 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -18,6 +18,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; @@ -172,31 +173,33 @@ export default class NotebookManager { if (error.status === HttpStatusCodes.Unauthorized) { this.gitHubOAuthService.resetToken(); - this.params.container.showOkCancelModalDialog( - undefined, - "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", - "Connect to GitHub", - () => - useSidePanel - .getState() - .openSidePanel( - "Connect to GitHub", - - ), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + undefined, + "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", + "Connect to GitHub", + () => + useSidePanel + .getState() + .openSidePanel( + "Connect to GitHub", + + ), + "Cancel", + undefined + ); } }; private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { return new Promise((resolve, reject) => { let commitMsg = "Committed from Azure Cosmos DB Notebooks"; - this.params.container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( title || "Commit", undefined, primaryButtonLabel || "Commit", diff --git a/src/Explorer/Tables/DataTable/TableCommands.ts b/src/Explorer/Tables/DataTable/TableCommands.ts index 9da9746ee..c5a80f65e 100644 --- a/src/Explorer/Tables/DataTable/TableCommands.ts +++ b/src/Explorer/Tables/DataTable/TableCommands.ts @@ -1,5 +1,6 @@ import Q from "q"; import { userContext } from "../../../UserContext"; +import { useDialog } from "../../Controls/Dialog"; import Explorer from "../../Explorer"; import * as Entities from "../Entities"; import * as DataTableUtilities from "./DataTableUtilities"; @@ -69,19 +70,28 @@ export default class TableCommands { return null; // Error } var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); - let deleteMessage: string = "Are you sure you want to delete the selected entities?"; - if (userContext.apiType === "Cassandra") { - deleteMessage = "Are you sure you want to delete the selected rows?"; - } - if (window.confirm(deleteMessage)) { - viewModel.queryTablesTab.container.tableDataClient - .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) - .then((results: any) => { - return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { - viewModel.redrawTableThrottled(); + const deleteMessage: string = + userContext.apiType === "Cassandra" + ? "Are you sure you want to delete the selected rows?" + : "Are you sure you want to delete the selected entities?"; + + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + deleteMessage, + "Delete", + () => { + viewModel.queryTablesTab.container.tableDataClient + .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) + .then((results: any) => { + return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { + viewModel.redrawTableThrottled(); + }); }); - }); - } + }, + "Cancel", + undefined + ); + return null; } diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index 6d95b01eb..fb51394e2 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -21,6 +21,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import ConflictId from "../Tree/ConflictId"; @@ -228,7 +229,7 @@ export default class ConflictsTab extends TabsBase { this._documentsIterator = this.createIterator(); await this.loadNextPage(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -252,10 +253,23 @@ export default class ConflictsTab extends TabsBase { } public onAcceptChangesClick = async (): Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return; + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + async () => await this.resolveConflict(), + "Cancel", + undefined + ); + } else { + await this.resolveConflict(); } + }; + private resolveConflict = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -318,7 +332,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.ResolveConflict, { @@ -372,7 +386,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.DeleteConflict, { @@ -662,11 +676,6 @@ export default class ConflictsTab extends TabsBase { return jsonObject; } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - private _getPartitionKeyPropertyHeader(): string { return ( (this.partitionKey && diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 2968a3511..504797a68 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -25,6 +25,7 @@ import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import * as QueryUtils from "../../Utils/QueryUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import DocumentId from "../Tree/DocumentId"; @@ -378,7 +379,7 @@ export default class DocumentsTab extends TabsBase { this.isFilterExpanded(false); document.getElementById("errorStatusIcon")?.focus(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -401,18 +402,29 @@ export default class DocumentsTab extends TabsBase { return Q(); } - public onNewDocumentClick = (): Q.Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); + public onNewDocumentClick = (): void => { + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + () => this.initializeNewDocument(), + "Cancel", + undefined + ); + } else { + this.initializeNewDocument(); } - this.selectedDocumentId(null); + }; + private initializeNewDocument = (): void => { + this.selectedDocumentId(null); const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - - return Q(); }; public onSaveNewDocumentClick = (): Promise => { @@ -453,7 +465,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -516,7 +528,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { @@ -546,9 +558,16 @@ export default class DocumentsTab extends TabsBase { ? "Are you sure you want to delete the selected item ?" : "Are you sure you want to delete the selected document ?"; - if (window.confirm(msg)) { - await this._deleteDocument(selectedDocumentId); - } + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + msg, + "Delete", + async () => await this._deleteDocument(selectedDocumentId), + "Cancel", + undefined + ); }; public onValidDocumentEdit(): Q.Promise { @@ -617,11 +636,6 @@ export default class DocumentsTab extends TabsBase { } } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - protected __deleteDocument(documentId: DocumentId): Promise { return deleteDocument(this.collection, documentId); } diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 3b3866b9e..fff7cb042 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -16,6 +16,7 @@ import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import DocumentId from "../Tree/DocumentId"; import ObjectId from "../Tree/ObjectId"; import DocumentsTab from "./DocumentsTab"; @@ -111,7 +112,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -169,7 +170,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 012406760..01aab3c6a 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -17,6 +17,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import * as CdbActions from "../Notebook/NotebookComponent/actions"; @@ -59,14 +60,16 @@ export default class NotebookTabV2 extends NotebookTabBase { }; if (this.notebookComponentAdapter.isContentDirty()) { - this.container.showOkCancelModalDialog( - "Close without saving?", - `File has unsaved changes, close without saving?`, - "Close", - cleanup, - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Close without saving?", + `File has unsaved changes, close without saving?`, + "Close", + cleanup, + "Cancel", + undefined + ); return Q.resolve(null); } else { cleanup(); diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index c9cf1a98a..89185b80a 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -1,12 +1,11 @@ -import Q from "q"; +import { extractPartitionKey } from "@azure/cosmos"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; -import DocumentId from "./DocumentId"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { extractPartitionKey } from "@azure/cosmos"; -import ConflictsTab from "../Tabs/ConflictsTab"; import { readDocument } from "../../Common/dataAccess/readDocument"; +import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; +import ConflictsTab from "../Tabs/ConflictsTab"; +import DocumentId from "./DocumentId"; export default class ConflictId { public container: ConflictsTab; @@ -50,13 +49,20 @@ export default class ConflictId { } public click() { - if ( - !this.container.isEditorDirty() || - window.confirm("Your unsaved changes will be lost. Do you want to continue?") - ) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadConflict(), + "Cancel", + undefined + ); + } else { this.loadConflict(); } - return; } public async loadConflict(): Promise { diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index a30c0b534..af2d0250c 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -1,5 +1,6 @@ import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; import DocumentsTab from "../Tabs/DocumentsTab"; export default class DocumentId { @@ -28,10 +29,20 @@ export default class DocumentId { } public click() { - if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadDocument(), + "Cancel", + undefined + ); + } else { this.loadDocument(); } - return; } public partitionKeyHeader(): Object { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5b6dfe92..2130c01bb 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -24,6 +24,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -254,14 +255,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { @@ -319,14 +322,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6a00e92e9..6a9352db2 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -27,6 +27,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -712,14 +713,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { @@ -777,14 +780,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 8e6f77ece..215035ffe 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -8,6 +8,7 @@ import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { getErrorMessage } from "../Tables/Utilities"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; @@ -138,16 +139,21 @@ export default class StoredProcedure { } }; public delete() { - if (!window.confirm("Are you sure you want to delete the stored procedure?")) { - return; - } - - deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the stored procedure?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index d90428b65..59d05de60 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import TriggerTab from "../Tabs/TriggerTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -99,16 +100,21 @@ export default class Trigger { }; public delete() { - if (!window.confirm("Are you sure you want to delete the trigger?")) { - return; - } - - deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the trigger?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } } diff --git a/src/Explorer/Tree/UserDefinedFunction.ts b/src/Explorer/Tree/UserDefinedFunction.ts index 4264c0954..555f547bb 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -95,18 +96,23 @@ export default class UserDefinedFunction { } public delete() { - if (!window.confirm("Are you sure you want to delete the user defined function?")) { - return; - } - - deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the user defined function?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + () => { + /**/ + } + ); }, - () => { - /**/ - } + "Cancel", + undefined ); } } diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index b911fa352..4df3651cd 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -1,8 +1,9 @@ -import * as GalleryUtils from "./GalleryUtils"; -import { JunoClient, IGalleryItem } from "../Juno/JunoClient"; import { HttpStatusCodes } from "../Common/Constants"; +import { useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; +import * as GalleryUtils from "./GalleryUtils"; const galleryItem: IGalleryItem = { id: "id", @@ -29,11 +30,11 @@ describe("GalleryUtils", () => { it("downloadItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks"); }); it("favoriteItem favorites item", async () => { @@ -66,11 +67,11 @@ describe("GalleryUtils", () => { it("deleteItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook"); }); it("getGalleryViewerProps gets gallery viewer props correctly", () => { diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index d9a52d348..16a41c6d3 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -3,7 +3,7 @@ import { Notebook } from "@nteract/commutable"; import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { HttpStatusCodes } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; -import { TextFieldProps } from "../Explorer/Controls/Dialog"; +import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, GalleryViewerComponent, @@ -222,7 +222,7 @@ export function downloadItem( }); const name = data.name; - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Download to My Notebooks", `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", @@ -388,7 +388,7 @@ export function deleteItem( if (container) { trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Remove published notebook", `Would you like to remove ${data.name} from the gallery?`, "Remove", diff --git a/tsconfig.strict.json b/tsconfig.strict.json index d48357cda..ee037db87 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -37,7 +37,6 @@ "./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/SmartUi/InputUtils.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", From 56699ccb1b13d88d66cc0832afa2404754e94055 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 30 Jul 2021 16:23:36 -0700 Subject: [PATCH 16/20] Fix new resource tree (#962) --- src/Common/ResourceTreeContainer.tsx | 6 +- src/Explorer/Explorer.tsx | 27 +++++--- .../Notebook/NotebookContentClient.ts | 31 +++++++-- src/Explorer/Notebook/useNotebook.ts | 30 ++++++--- .../CopyNotebookPane/CopyNotebookPane.tsx | 7 +- src/Explorer/Tree/ResourceTree.tsx | 67 ++++++++++++------- src/Platform/Hosted/extractFeatures.ts | 4 +- 7 files changed, 117 insertions(+), 55 deletions(-) diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index ca41610da..18a769b12 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -54,10 +54,10 @@ export const ResourceTreeContainer: FunctionComponent {userContext.authType === AuthType.ResourceToken ? ( - ) : userContext.features.enableReactResourceTree ? ( - - ) : ( + ) : userContext.features.enableKoResourceTree ? (
+ ) : ( + )}
{/* Collections Window - End */} diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index a4cd403f0..d854278c2 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -537,17 +537,22 @@ export default class Explorer { } } - public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { + public uploadFile( + name: string, + content: string, + parent: NotebookContentItem, + isGithubTree?: boolean + ): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to upload notebook, but notebook is not enabled"; handleError(error, "Explorer/uploadFile"); throw new Error(error); } - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); return promise; } @@ -672,7 +677,7 @@ export default class Explorer { return true; } - public renameNotebook(notebookFile: NotebookContentItem): void { + public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to rename notebook, but notebook is not enabled"; handleError(error, "Explorer/renameNotebook"); @@ -705,7 +710,7 @@ export default class Explorer { paneTitle="Rename Notebook" defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) + this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) } notebookFile={notebookFile} /> @@ -713,7 +718,7 @@ export default class Explorer { } } - public onCreateDirectory(parent: NotebookContentItem): void { + public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create notebook directory, but notebook is not enabled"; handleError(error, "Explorer/onCreateDirectory"); @@ -735,7 +740,7 @@ export default class Explorer { submitButtonLabel="Create" defaultInput="" onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input) + this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) } notebookFile={parent} /> @@ -804,7 +809,7 @@ export default class Explorer { } }; - public deleteNotebookFile(item: NotebookContentItem): Promise { + public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to delete notebook file, but notebook is not enabled"; handleError(error, "Explorer/deleteNotebookFile"); @@ -837,7 +842,7 @@ export default class Explorer { return Promise.reject(); } - return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( + return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( () => logConsoleInfo(`Successfully deleted: ${item.path}`), (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) ); @@ -846,7 +851,7 @@ export default class Explorer { /** * This creates a new notebook file, then opens the notebook */ - public onNewNotebookClicked(parent?: NotebookContentItem): void { + public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; handleError(error, "Explorer/onNewNotebookClicked"); @@ -861,7 +866,7 @@ export default class Explorer { }); this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent) + .createNewNotebookFile(parent, isGithubTree) .then((newFile: NotebookContentItem) => { logConsoleInfo(`Successfully created: ${newFile.name}`); TelemetryProcessor.traceSuccess( diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 3599c009c..5ca408c2a 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -36,7 +36,7 @@ export class NotebookContentClient { * * @param parent parent folder */ - public createNewNotebookFile(parent: NotebookContentItem): Promise { + public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } @@ -57,6 +57,8 @@ export class NotebookContentClient { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -66,9 +68,9 @@ export class NotebookContentClient { }); } - public async deleteContentItem(item: NotebookContentItem): Promise { + public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise { const path = await this.deleteNotebookFile(item.path); - useNotebook.getState().deleteNotebookItem(item); + useNotebook.getState().deleteNotebookItem(item, isGithubTree); // TODO: Delete once old resource tree is removed if (!path || path !== item.path) { @@ -91,7 +93,8 @@ export class NotebookContentClient { public async uploadFileAsync( name: string, content: string, - parent: NotebookContentItem + parent: NotebookContentItem, + isGithubTree?: boolean ): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); @@ -115,6 +118,8 @@ export class NotebookContentClient { .then((xhr: AjaxResponse) => { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -137,7 +142,11 @@ export class NotebookContentClient { * @param sourcePath * @param targetName is not prefixed with path */ - public renameNotebook(item: NotebookContentItem, targetName: string): Promise { + public renameNotebook( + item: NotebookContentItem, + targetName: string, + isGithubTree?: boolean + ): Promise { const sourcePath = item.path; // Match extension if (sourcePath.indexOf(".") !== -1) { @@ -163,6 +172,9 @@ export class NotebookContentClient { item.name = notebookFile.name; item.path = notebookFile.path; item.timestamp = NotebookUtil.getCurrentTimestamp(); + + useNotebook.getState().updateNotebookItem(item, isGithubTree); + return item; }); } @@ -172,7 +184,11 @@ export class NotebookContentClient { * @param parent * @param newDirectoryName basename of the new directory */ - public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise { + public async createDirectory( + parent: NotebookContentItem, + newDirectoryName: string, + isGithubTree?: boolean + ): Promise { if (parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent is not a directory: ${parent.path}`); } @@ -199,8 +215,11 @@ export class NotebookContentClient { const dir = xhr.response; const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed item.parent = parent; parent.children?.push(item); + return item; }); } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index af1b47477..5a2c97b3e 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -38,8 +38,9 @@ interface NotebookState { setNotebookBasePath: (notebookBasePath: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; - updateNotebookItem: (item: NotebookContentItem) => void; - deleteNotebookItem: (item: NotebookContentItem) => void; + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; } @@ -141,19 +142,30 @@ export const useNotebook: UseStore = create((set, get) => ({ return undefined; }, - updateNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + const parentItem = get().findItem(root, parent); + item.parent = parentItem; + if (parentItem.children) { + parentItem.children.push(item); + } else { + parentItem.children = [item]; + } + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + }, + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children.push(item); item.parent = parentItem; - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, - deleteNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const myNotebooksContentRoot = { @@ -216,6 +228,7 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "PsuedoDir", type: NotebookContentItemType.Directory, children: [], + parent: gitHubNotebooksContentRoot, }; pinnedRepo.branches.forEach((branch) => { @@ -223,6 +236,7 @@ export const useNotebook: UseStore = create((set, get) => ({ name: branch.name, path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), type: NotebookContentItemType.Directory, + parent: repoTreeItem, }); }); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 60cb731b8..3c63c00ae 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -98,6 +98,7 @@ export const CopyNotebookPane: FunctionComponent = ({ const copyNotebook = async (location: Location): Promise => { let parent: NotebookContentItem; + let isGithubTree: boolean; switch (location.type) { case "MyNotebooks": parent = { @@ -105,21 +106,23 @@ export const CopyNotebookPane: FunctionComponent = ({ path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; + isGithubTree = false; break; case "GitHub": parent = { - name: ResourceTreeAdapter.GitHubReposTitle, + name: selectedLocation.branch, path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), type: NotebookContentItemType.Directory, }; + isGithubTree = true; break; default: throw new Error(`Unsupported location type ${location.type}`); } - return container.uploadFile(name, content, parent); + return container.uploadFile(name, content, parent, isGithubTree); }; const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 2130c01bb..b8dae8259 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,5 +1,6 @@ import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import * as React from "react"; +import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import DeleteIcon from "../../../images/delete.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg"; @@ -55,7 +56,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc galleryContentRoot, gitHubNotebooksContentRoot, updateNotebookItem, - } = useNotebook(); + } = useNotebook( + (state) => ({ + isNotebookEnabled: state.isNotebookEnabled, + myNotebooksContentRoot: state.myNotebooksContentRoot, + galleryContentRoot: state.galleryContentRoot, + gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, + updateNotebookItem: state.updateNotebookItem, + }), + shallow + ); const { activeTab, refreshActiveTab } = useTabs(); const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const pseudoDirPath = "PsuedoDir"; @@ -166,7 +176,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); - } + }, + true ); gitHubNotebooksTree.contextMenu = [ @@ -202,9 +213,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc }; const buildChildNodes = ( - container: Explorer, item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode[] => { if (!item || !item.children) { return []; @@ -212,8 +223,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc return item.children.map((item) => { const result = item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick) - : buildNotebookFileNode(item, onFileClick); + ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) + : buildNotebookFileNode(item, onFileClick, isGithubTree); result.timestamp = item.timestamp; return result; }); @@ -222,7 +233,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookFileNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -239,17 +251,21 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: createFileContextMenu(container, item), + contextMenu: createFileContextMenu(container, item, isGithubTree), data: item, }; }; - const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createFileContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "Delete", @@ -261,7 +277,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -311,12 +327,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc } }; - const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createDirectoryContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Refresh", iconSrc: RefreshIcon, - onClick: () => loadSubitems(item), + onClick: () => loadSubitems(item, isGithubTree), }, { label: "Delete", @@ -328,7 +348,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}?"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -337,17 +357,17 @@ export const ResourceTree: React.FC = ({ container }: Resourc { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "New Directory", iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item), + onClick: () => container.onCreateDirectory(item, isGithubTree), }, { label: "New Notebook", iconSrc: NewNotebookIcon, - onClick: () => container.onNewNotebookClicked(item), + onClick: () => container.onNewNotebookClicked(item, isGithubTree), }, { label: "Upload File", @@ -372,7 +392,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookDirectoryNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -382,7 +403,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc isLeavesParentsSeparate: true, onClick: () => { if (!item.children) { - loadSubitems(item); + loadSubitems(item, isGithubTree); } }, isSelected: () => { @@ -395,9 +416,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, data: item, - children: buildChildNodes(container, item, onFileClick), + children: buildChildNodes(item, onFileClick, isGithubTree), }; }; @@ -699,9 +720,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem): Promise => { + const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem); + updateNotebookItem(updatedItem, isGithubTree); }; const dataRootNode = buildDataTree(); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 4a85dca8d..c7ea359ab 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -16,7 +16,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableReactResourceTree: boolean; + readonly enableKoResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -58,7 +58,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), - enableReactResourceTree: "true" === get("enablereactresourcetree"), + enableKoResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), From b65456f754da964581ee96e13cb1f967245d7a84 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 2 Aug 2021 18:54:30 -0700 Subject: [PATCH 17/20] Fix bug in Dialog State store (#969) --- src/Explorer/Controls/Dialog.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index d5e308d69..449627d13 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -41,7 +41,16 @@ export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => - set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + set( + (state) => ({ + visible: false, + openDialog: state.openDialog, + closeDialog: state.closeDialog, + showOkCancelModalDialog: state.showOkCancelModalDialog, + showOkModalDialog: state.showOkModalDialog, + }), + true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above + ), showOkCancelModalDialog: ( title: string, subText: string, From ee4404c439efa3e5d97e0c3e61675d5017959f6c Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:11:42 -0700 Subject: [PATCH 18/20] Fix enable synapse link error (#918) * Fix enable synapse error * Default all ARM requests to JSON Co-authored-by: Steve Faulkner --- src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts | 8 +++++++- src/Utils/arm/request.ts | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts index 87b00b24b..5c52c7d61 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts @@ -29,7 +29,13 @@ export async function update( body: Types.DatabaseAccountUpdateParameters ): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; - return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); + return armRequest({ + host: configContext.ARM_ENDPOINT, + path, + method: "PATCH", + apiVersion, + body, + }); } /* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */ diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index e593633ac..69391201b 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -6,6 +6,7 @@ Instead, generate ARM clients that consume this function with stricter typing. */ import promiseRetry, { AbortError } from "p-retry"; +import { HttpHeaders } from "../../Common/Constants"; import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; @@ -45,6 +46,7 @@ interface Options { method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; body?: unknown; queryParams?: ARMQueryParams; + contentType?: string; } export async function armRequestWithoutPolling({ @@ -54,6 +56,7 @@ export async function armRequestWithoutPolling({ method, body: requestBody, queryParams, + contentType, }: Options): Promise<{ result: T; operationStatusUrl: string }> { const url = new URL(path, host); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); @@ -70,6 +73,7 @@ export async function armRequestWithoutPolling({ method, headers: { Authorization: userContext.authorizationToken, + [HttpHeaders.contentType]: contentType || "application/json", }, body: requestBody ? JSON.stringify(requestBody) : undefined, }); @@ -104,6 +108,7 @@ export async function armRequest({ method, body: requestBody, queryParams, + contentType, }: Options): Promise { const armRequestResult = await armRequestWithoutPolling({ host, @@ -112,6 +117,7 @@ export async function armRequest({ method, body: requestBody, queryParams, + contentType, }); const operationStatusUrl = armRequestResult.operationStatusUrl; if (operationStatusUrl) { From 51f3f9a718f39cdb2f17053ff201351dbb204c0b Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Wed, 4 Aug 2021 20:16:18 +0530 Subject: [PATCH 19/20] Move inputTypeahead to react (#946) Co-authored-by: Steve Faulkner --- .../InputTypeahead/InputTypeahead.less | 12 +- .../InputTypeaheadComponent.tsx | 242 +++++++----------- .../InputTypeaheadComponent.test.tsx.snap | 78 +++--- 3 files changed, 139 insertions(+), 193 deletions(-) diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less index e56bfaa51..1d68e3b7e 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less @@ -5,6 +5,9 @@ display: inline-block; width: 100%; + .input-type-head-text-field { + width: 100%; + } textarea { width: 100%; line-height: 1; @@ -21,4 +24,11 @@ } } } - +.input-typeahead-chocies-container { + border: 1px solid lightgrey; + padding: 5px 10px 5px 10px; + cursor: pointer; + .choice-caption{ + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 69f01a745..2e9a2d104 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -6,14 +6,13 @@ * typeaheadOverrideOptions: { dynamic:false } * */ -import "jquery-typeahead"; +import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react"; import * as React from "react"; -import { KeyCodes } from "../../../Common/Constants"; import "./InputTypeahead.less"; export interface Item { caption: string; - value: any; + value: string; } /** @@ -75,170 +74,125 @@ export interface InputTypeaheadComponentProps { useTextarea?: boolean; } -interface OnClickItem { - matchedKey: string; - value: any; - caption: string; - group: string; +interface InputTypeaheadComponentState { + isSuggestionVisible: boolean; + selectedChoice: Item; + filteredChoices: Item[]; } -interface Cache { - inputValue: string; - selection: Item; -} - -interface InputTypeaheadComponentState {} - export class InputTypeaheadComponent extends React.Component< InputTypeaheadComponentProps, InputTypeaheadComponentState > { - private inputElt: HTMLElement; - private containerElt: HTMLElement; - - private cache: Cache; - private inputValue: string; - private selection: Item; - - public constructor(props: InputTypeaheadComponentProps) { + constructor(props: InputTypeaheadComponentProps) { super(props); - this.cache = { - inputValue: null, - selection: null, + this.state = { + isSuggestionVisible: false, + filteredChoices: [], + selectedChoice: { + caption: "", + value: "", + }, }; } - /** - * Props have changed - * @param prevProps - * @param prevState - * @param snapshot - */ - public componentDidUpdate( - prevProps: InputTypeaheadComponentProps, - prevState: InputTypeaheadComponentState, - snapshot: any - ): void { - if (prevProps.defaultValue !== this.props.defaultValue) { - $(this.inputElt).val(this.props.defaultValue); - this.initializeTypeahead(); - } - } - - /** - * Executed once react is done building the DOM for this component - */ - public componentDidMount(): void { - this.initializeTypeahead(); - } - - public render(): JSX.Element { + private onRenderCell = (item: Item): JSX.Element => { return ( - -
) => this.onKeyDown(event)} - > -
(this.containerElt = input)}> -
- - {this.props.useTextarea ? ( -