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 1/2] 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 2/2] 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 +}