Compare commits

..

4 Commits

Author SHA1 Message Date
sunilyadav840
c4bc0ad5f9 remove commented code 2021-09-07 16:22:30 +05:30
sunilyadav840
a0a77002d2 Fixed eslint issue of InputTypeaheadComponent.tsx NotebookTerminalComponent.tsx NotebookViewerComponent.tsx LeftPaneComponent.tsx NodePropertiesComponent.tsx files 2021-09-07 16:17:48 +05:30
kcheekuri
95c9b7ee31 Users/kcheekuri/aciconatinerpooling (#1008)
* initial changes for CP

* Added container unprovisioning

* Added postgreSQL terminal

* changed postgres terminal -> shell

* Initialize Container Request payload change

* added postgres button

* Added notebookServerInfo

* Feature flag enabling and integration with phoenix

* Remove postgre implementations

* fix issues

* fix format issues

* fix format issues-1

* fix format issues-2

* fix format issues-3

* fix format issues-4

* fix format issues-5

* connection status component

* connection status component-1

* connection status component-2

* connection status component-3

* address issues

* removal of ms

* removal of ms

* removal of ms-1

* removal of time after connected

* removal of time after connected

* removing unnecessary code

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
Co-authored-by: Bala Lakshmi Narayanasami <balalakshmin@microsoft.com>
2021-09-03 23:04:26 -07:00
Zachary Foster
39dd293fc1 Fetch aad token against tenant's authority (#1004) 2021-08-30 14:21:32 -05:00
39 changed files with 507 additions and 184 deletions

View File

@@ -137,18 +137,10 @@ src/Terminal/NotebookAppContracts.d.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts
src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx

View File

@@ -230,4 +230,4 @@
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
} }
} }

View File

@@ -96,6 +96,7 @@ export class Flights {
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -337,6 +338,13 @@ export enum ConflictOperationType {
Delete = "delete", Delete = "delete",
} }
export enum ConnectionStatusType {
Connecting = "Connecting",
Allocating = "Allocating",
Connected = "Connected",
Failed = "Connection Failed",
}
export const EmulatorMasterKey = export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

View File

@@ -36,7 +36,7 @@ export interface TableEntityProps {
onDeleteEntity?: () => void; onDeleteEntity?: () => void;
onEditEntity?: () => void; onEditEntity?: () => void;
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption | undefined) => void; onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void; onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;

View File

@@ -1,3 +1,5 @@
import { ConnectionStatusType } from "../Common/Constants";
export interface DatabaseAccount { export interface DatabaseAccount {
id: string; id: string;
name: string; name: string;
@@ -496,3 +498,8 @@ export interface MemoryUsageInfo {
freeKB: number; freeKB: number;
totalKB: number; totalKB: number;
} }
export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}

View File

@@ -181,8 +181,7 @@ export const Dialog: FC = () => {
text: secondaryButtonText, text: secondaryButtonText,
onClick: onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: {}; : undefined;
return visible ? ( return visible ? (
<FluentDialog {...dialogProps}> <FluentDialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}

View File

@@ -50,7 +50,7 @@ export interface InputTypeaheadComponentProps {
* Override default jquery-typeahead options * Override default jquery-typeahead options
* WARNING: do not override input, source or callback to avoid breaking the components behavior. * WARNING: do not override input, source or callback to avoid breaking the components behavior.
*/ */
typeaheadOverrideOptions?: any; typeaheadOverrideOptions?: { dynamic: boolean };
/** /**
* This function gets called when pressing ENTER on the input box * This function gets called when pressing ENTER on the input box
@@ -132,6 +132,7 @@ export class InputTypeaheadComponent extends React.Component<
private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => { private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => {
return choices.filter((choice) => return choices.filter((choice) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase())) Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase()))
); );

View File

@@ -264,6 +264,6 @@ export class NotebookViewerComponent
}; };
private reportAbuse = (): void => { private reportAbuse = (): void => {
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {}); GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => ({}));
}; };
} }

View File

@@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -43,7 +43,7 @@ export interface TreeNode {
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean; isLoading?: boolean;
isSelected?: () => boolean; isSelected?: () => boolean;
onClick?: (isExpanded?: boolean) => void; // Only if a leaf, other click will expand/collapse onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void; onExpanded?: () => void;
onCollapsed?: () => void; onCollapsed?: () => void;
onContextMenuOpen?: () => void; onContextMenuOpen?: () => void;
@@ -73,7 +73,7 @@ interface TreeNodeComponentProps {
} }
interface TreeNodeComponentState { interface TreeNodeComponentState {
isExpanded?: boolean; isExpanded: boolean;
isMenuShowing: boolean; isMenuShowing: boolean;
} }
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> { export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
@@ -82,7 +82,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
private static readonly transitionDurationMS = 200; private static readonly transitionDurationMS = 200;
private static readonly callbackDelayMS = 100; // avoid calling at the same time as transition to make it smoother private static readonly callbackDelayMS = 100; // avoid calling at the same time as transition to make it smoother
private contextMenuRef = React.createRef<HTMLDivElement>(); private contextMenuRef = React.createRef<HTMLDivElement>();
private isExpanded?: boolean; private isExpanded: boolean;
constructor(props: TreeNodeComponentProps) { constructor(props: TreeNodeComponentProps) {
super(props); super(props);
@@ -93,7 +93,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}; };
} }
componentDidUpdate(_prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) { componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
// Only call when expand has actually changed // Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) { if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) { if (this.state.isExpanded) {
@@ -103,7 +103,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
} }
} }
if (this.props.node.isExpanded !== this.isExpanded) { if (this.props.node.isExpanded !== this.isExpanded) {
this.isExpanded = this.props.node && this.props.node.isExpanded; this.isExpanded = this.props.node.isExpanded;
this.setState({ this.setState({
isExpanded: this.props.node.isExpanded, isExpanded: this.props.node.isExpanded,
}); });
@@ -114,7 +114,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return this.renderNode(this.props.node, this.props.generation); return this.renderNode(this.props.node, this.props.generation);
} }
private static getSortedChildren(treeNode: TreeNode): TreeNode[] | undefined { private static getSortedChildren(treeNode: TreeNode): TreeNode[] {
if (!treeNode || !treeNode.children) { if (!treeNode || !treeNode.children) {
return undefined; return undefined;
} }
@@ -195,7 +195,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
{node.children && ( {node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}> <AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label}> <div className="nodeChildren" data-test={node.label}>
{TreeNodeComponent?.getSortedChildren(node)?.map((childNode: TreeNode) => ( {TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent <TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`} key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
node={childNode} node={childNode}
@@ -214,15 +214,15 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
* Recursive: is the node or any descendant selected * Recursive: is the node or any descendant selected
* @param node * @param node
*/ */
private static isAnyDescendantSelected(node: TreeNode): boolean { private static isAnyDescendantSelected(node: TreeNode): boolean {
return node.children return (
? node.children.reduce( node.children &&
(previous: boolean, child: TreeNode) => node.children.reduce(
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child), (previous: boolean, child: TreeNode) =>
false previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
) false
: false; )
);
} }
private static createClickEvent(): MouseEvent { private static createClickEvent(): MouseEvent {
@@ -230,7 +230,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
} }
private onRightClick = (): void => { private onRightClick = (): void => {
this.contextMenuRef?.current?.firstChild?.dispatchEvent(TreeNodeComponent.createClickEvent()); this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
}; };
private renderContextMenuButton(node: TreeNode): JSX.Element { private renderContextMenuButton(node: TreeNode): JSX.Element {
@@ -253,18 +253,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
coverTarget: true, coverTarget: true,
isBeakVisible: false, isBeakVisible: false,
directionalHint: DirectionalHint.topAutoEdge, directionalHint: DirectionalHint.topAutoEdge,
onMenuOpened: (_contextualMenu?: IContextualMenuProps) => { onMenuOpened: (contextualMenu?: IContextualMenuProps) => {
this.setState({ isMenuShowing: true }); this.setState({ isMenuShowing: true });
node.onContextMenuOpen && node.onContextMenuOpen(); node.onContextMenuOpen && node.onContextMenuOpen();
}, },
onMenuDismissed: (_contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }), onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
contextualMenuItemAs: (props: IContextualMenuItemProps) => ( contextualMenuItemAs: (props: IContextualMenuItemProps) => (
<div <div
data-test={`treeComponentMenuItemContainer`} data-test={`treeComponentMenuItemContainer`}
className="treeComponentMenuItemContainer" className="treeComponentMenuItemContainer"
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
> >
{props.item.onRenderIcon && props.item.onRenderIcon()} {props.item.onRenderIcon()}
<span <span
className={ className={
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "") "treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
@@ -274,21 +274,19 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
</span> </span>
</div> </div>
), ),
items: node.contextMenu items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
? node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({ key: menuItem.label,
key: menuItem.label, text: menuItem.label,
text: menuItem.label, disabled: menuItem.isDisabled,
disabled: menuItem.isDisabled, className: menuItem.styleClass,
className: menuItem.styleClass, onClick: () => {
onClick: () => { menuItem.onClick();
menuItem.onClick(); TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, { label: menuItem.label,
label: menuItem.label, });
}); },
}, onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />,
onRenderIcon: (_props: any) => <img src={menuItem.iconSrc} alt="" />, })),
}))
: [],
}} }}
styles={buttonStyles} styles={buttonStyles}
/> />
@@ -326,7 +324,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded); this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
}; };
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, _node: TreeNode): void => { private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation(); event.stopPropagation();
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded); this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);

View File

@@ -4,6 +4,7 @@ import _ from "underscore";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { ConnectionStatusType } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
@@ -16,6 +17,7 @@ import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -87,12 +89,13 @@ export default class Explorer {
}; };
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
private phoenixClient: PhoenixClient;
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient();
useNotebook.subscribe( useNotebook.subscribe(
() => this.refreshCommandBarButtons(), () => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount (state) => state.isNotebooksEnabledForAccount
@@ -343,19 +346,43 @@ export default class Explorer {
return; return;
} }
this._isInitializingNotebooks = true; this._isInitializingNotebooks = true;
if (userContext.features.phoenix) {
const connectionStatus: DataModels.ContainerConnectionInfo = {
status: ConnectionStatusType.Allocating,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const provisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
resourceId: userContext.databaseAccount.id,
dbAccountName: userContext.databaseAccount.name,
aadToken: userContext.authorizationToken,
resourceGroup: userContext.resourceGroup,
subscriptionId: userContext.subscriptionId,
};
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
await this.ensureNotebookWorkspaceRunning(); useNotebook.getState().setNotebookServerInfo({
const connectionInfo = await listConnectionInfo( notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
userContext.subscriptionId, authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
userContext.resourceGroup, });
databaseAccount.name, }
"default" } else {
); await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({ useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken, authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
}); });
}
useNotebook.getState().initializeNotebooksTree(this.notebookManager); useNotebook.getState().initializeNotebooksTree(this.notebookManager);
@@ -364,7 +391,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public resetNotebookWorkspace() { public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError( handleError(
"Attempt to reset notebook workspace, but notebook is not enabled", "Attempt to reset notebook workspace, but notebook is not enabled",
@@ -389,7 +416,6 @@ export default class Explorer {
if (!databaseAccount) { if (!databaseAccount) {
return false; return false;
} }
try { try {
const { value: workspaces } = await listByDatabaseAccount( const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId, userContext.subscriptionId,
@@ -906,7 +932,7 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind) { public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
let title: string; let title: string;
switch (kind) { switch (kind) {
@@ -1026,7 +1052,10 @@ export default class Explorer {
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { if (
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1072,10 +1101,13 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(); : this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
const isNotebookEnabled: boolean = let isNotebookEnabled = true;
userContext.authType !== AuthType.ResourceToken && if (!userContext.features.phoenix) {
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || isNotebookEnabled =
userContext.features.enableNotebooks); userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
}
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());

View File

@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const partitionKeyProperty = this.props.collectionPartitionKeyProperty; const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
// aggregate all the properties, remove dropped ones // aggregate all the properties, remove dropped ones
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties); const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
// Compose the query // Compose the query
let pkId = editedProperties.pkId; const pkId = editedProperties.pkId;
let updateQueryFragment = ""; let updateQueryFragment = "";
finalProperties.forEach((p) => { finalProperties.forEach((p) => {
@@ -473,7 +473,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false; return false;
} }
let pairs: any[] = data; const pairs: any[] = data;
for (let i = 0; i < pairs.length; i++) { for (let i = 0; i < pairs.length; i++) {
const item = pairs[i]; const item = pairs[i];
if ( if (
@@ -772,8 +772,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let edge = edges[0]; const edge = edges[0];
let graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addEdge(edge); graphData.addEdge(edge);
// Allow loadNeighbors to load list new edge // Allow loadNeighbors to load list new edge
@@ -803,7 +803,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public removeEdge(edgeId: string): Q.Promise<any> { public removeEdge(edgeId: string): Q.Promise<any> {
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then( return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
() => { () => {
let graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false); graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
@@ -826,9 +826,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false; return false;
} }
let vertices: any[] = data; const vertices: any[] = data;
if (vertices.length > 0) { if (vertices.length > 0) {
let v0 = vertices[0]; const v0 = vertices[0];
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") { if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
return false; return false;
} }
@@ -933,7 +933,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
throw { title: err }; throw { title: err };
} }
let vertex = vertices[0]; const vertex = vertices[0];
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addVertex(vertex); graphData.addVertex(vertex);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
@@ -1082,7 +1082,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr: string = ""; let errorDataStr = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + JSON.stringify(errorData);
@@ -1224,7 +1224,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return $.map( return $.map(
this.state.rootMap, this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => { (value: any, index: number): LeftPane.CaptionId => {
let result = GraphData.GraphData.getNodePropValue(value, key); const result = GraphData.GraphData.getNodePropValue(value, key);
return { return {
caption: result !== undefined ? result : value.id, caption: result !== undefined ? result : value.id,
id: value.id, id: value.id,
@@ -1388,7 +1388,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return id * @return id
*/ */
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string { public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
let { id } = d; const { id } = d;
if (typeof id !== "string") { if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`; const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
logConsoleError(error); logConsoleError(error);
@@ -1425,7 +1425,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then( return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
let possibleVertices = [] as PossibleVertex[]; const possibleVertices = [] as PossibleVertex[];
$.each(documents, (index: number, item: any) => { $.each(documents, (index: number, item: any) => {
if (highlightedNodeId && item.id === highlightedNodeId) { if (highlightedNodeId && item.id === highlightedNodeId) {
// Exclude highlighed node in the list // Exclude highlighed node in the list
@@ -1463,16 +1463,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return promise when done * @return promise when done
*/ */
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> { private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
let promises = []; const promises = [];
// Drop edges // Drop edges
for (let i = 0; i < editedEdges.droppedIds.length; i++) { for (let i = 0; i < editedEdges.droppedIds.length; i++) {
let id = editedEdges.droppedIds[i]; const id = editedEdges.droppedIds[i];
promises.push(this.removeEdge(id)); promises.push(this.removeEdge(id));
} }
// Add edges // Add edges
for (let i = 0; i < editedEdges.addedEdges.length; i++) { for (let i = 0; i < editedEdges.addedEdges.length; i++) {
let e = editedEdges.addedEdges[i]; const e = editedEdges.addedEdges[i];
promises.push( promises.push(
this.createNewEdge(e).then(() => { this.createNewEdge(e).then(() => {
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph // Reload neighbors in case we linked to a vertex that isn't loaded in the graph
@@ -1533,7 +1533,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) { private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
const props = {} as any; // Hashset const props = {} as any; // Hashset
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => { $.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
for (var p in item) { for (const p in item) {
// DocDB: Exclude type because it's always 'vertex' // DocDB: Exclude type because it's always 'vertex'
if (p !== "type" && typeof (item as any)[p] === "string") { if (p !== "type" && typeof (item as any)[p] === "string") {
props[p] = true; props[p] = true;
@@ -1543,7 +1543,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (item.hasOwnProperty("properties")) { if (item.hasOwnProperty("properties")) {
// TODO This is DocDB-graph specific // TODO This is DocDB-graph specific
// Assume each property value is [{value:... }] // Assume each property value is [{value:... }]
for (var f in item.properties) { for (const f in item.properties) {
props[f] = true; props[f] = true;
} }
} }
@@ -1570,21 +1570,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let data = this.originalGraphData.getVertexById(id); const data = this.originalGraphData.getVertexById(id);
// A bit of translation to make it easier to display // A bit of translation to make it easier to display
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {}; const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
for (let p in data.properties) { for (const p in data.properties) {
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value); props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
} }
// update neighbors // update neighbors
let sources: NeighborVertexBasicInfo[] = []; const sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = []; const targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues(); this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice; const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets); this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = { const sData: GraphHighlightedNodeData = {
id: data.id, id: data.id,
label: data.label, label: data.label,
properties: props, properties: props,
@@ -1611,16 +1611,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
targets: NeighborVertexBasicInfo[] targets: NeighborVertexBasicInfo[]
): void { ): void {
// update neighbors // update neighbors
let gd = this.originalGraphData; const gd = this.originalGraphData;
let v = gd.getVertexById(id); const v = gd.getVertexById(id);
// Clear the array while keeping the references // Clear the array while keeping the references
sources.length = 0; sources.length = 0;
targets.length = 0; targets.length = 0;
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
for (let p in v.inE) { for (const p in v.inE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.inE[p]; const edges = v.inE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
@@ -1629,7 +1629,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({ sources.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1639,7 +1639,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
} }
for (let p in v.outE) { for (const p in v.outE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.outE[p]; const edges = v.outE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
@@ -1648,7 +1648,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({ targets.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1681,20 +1681,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let updatedVertex = vertices[0]; const updatedVertex = vertices[0];
if (this.originalGraphData.hasVertexId(updatedVertex.id)) { if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id); const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
// Copy updated properties // Copy updated properties
if (currentVertex.hasOwnProperty("properties")) { if (currentVertex.hasOwnProperty("properties")) {
delete currentVertex["properties"]; delete currentVertex["properties"];
} }
for (var p in updatedVertex) { for (const p in updatedVertex) {
(currentVertex as any)[p] = updatedVertex[p]; (currentVertex as any)[p] = updatedVertex[p];
} }
} }
// TODO This kind of assumes saveVertexProperty is done from property panes. // TODO This kind of assumes saveVertexProperty is done from property panes.
let hn = this.state.highlightedNode; const hn = this.state.highlightedNode;
if (hn && hn.id === updatedVertex.id) { if (hn && hn.id === updatedVertex.id) {
this.updatePropertiesPane(hn.id); this.updatePropertiesPane(hn.id);
} }
@@ -1708,7 +1708,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
igraphConfig?: IGraphConfig igraphConfig?: IGraphConfig
) { ) {
this.originalGraphData = graphData; this.originalGraphData = graphData;
let gd = JSON.parse(JSON.stringify(this.originalGraphData)); const gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return; return;

View File

@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
className={className} className={className}
as="tr" as="tr"
aria-label={node.caption} aria-label={node.caption}
onActivated={(e) => this.props.onRootNodeSelected(node.id)} onActivated={() => this.props.onRootNodeSelected(node.id)}
key={node.id} key={node.id}
> >
<td className="resultItem"> <td className="resultItem">

View File

@@ -1,8 +1,8 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import * as Q from "q"; import * as Q from "q";
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent"; import React from "react";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer"; import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer";
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
describe("Property pane", () => { describe("Property pane", () => {
const title = "My Title"; const title = "My Title";
@@ -37,17 +37,25 @@ describe("Property pane", () => {
return { return {
expandedTitle: title, expandedTitle: title,
isCollapsed: false, isCollapsed: false,
onCollapsedChanged: (newValue: boolean): void => {}, onCollapsedChanged: (): void => {
("");
},
node: highlightedNode, node: highlightedNode,
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null, getPkIdFromNodeData: (): string => undefined,
collectionPartitionKeyProperty: null, collectionPartitionKeyProperty: undefined,
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(), updateVertexProperties: (): Q.Promise<void> => Q.resolve(),
selectNode: (id: string): void => {}, selectNode: (): void => {
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null), ("");
possibleEdgeLabels: null, },
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(), updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined),
deleteHighlightedNode: (): void => {}, possibleEdgeLabels: undefined,
onModeChanged: (newMode: Mode): void => {}, editGraphEdges: (): Q.Promise<unknown> => Q.resolve(),
deleteHighlightedNode: (): void => {
("");
},
onModeChanged: (): void => {
("");
},
viewMode: Mode.READONLY_PROP, viewMode: Mode.READONLY_PROP,
}; };
}; };

View File

@@ -45,7 +45,7 @@ export interface NodePropertiesComponentProps {
selectNode: (id: string) => void; selectNode: (id: string) => void;
updatePossibleVertices: () => Q.Promise<PossibleVertex[]>; updatePossibleVertices: () => Q.Promise<PossibleVertex[]>;
possibleEdgeLabels: Item[]; possibleEdgeLabels: Item[];
editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<any>; editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<unknown>;
deleteHighlightedNode: () => void; deleteHighlightedNode: () => void;
onModeChanged: (newMode: Mode) => void; onModeChanged: (newMode: Mode) => void;
viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it
@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
super(props); super(props);
this.state = { this.state = {
editedProperties: { editedProperties: {
pkId: null, pkId: undefined,
readOnlyProperties: [], readOnlyProperties: [],
existingProperties: [], existingProperties: [],
addedProperties: [], addedProperties: [],
@@ -98,15 +98,12 @@ export class NodePropertiesComponent extends React.Component<
}; };
} }
public static getDerivedStateFromProps( public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
props: NodePropertiesComponentProps,
state: NodePropertiesComponentState
): Partial<NodePropertiesComponentState> {
if (props.viewMode !== Mode.READONLY_PROP) { if (props.viewMode !== Mode.READONLY_PROP) {
return { isDeleteConfirm: false }; return { isDeleteConfirm: false };
} }
return null; return undefined;
} }
public render(): JSX.Element { public render(): JSX.Element {
@@ -137,11 +134,14 @@ export class NodePropertiesComponent extends React.Component<
* Get type option. Limit to string, number or boolean * Get type option. Limit to string, number or boolean
* @param value * @param value
*/ */
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString { private static getTypeOption(
if (value == null) { value: null | string | number | undefined | boolean
): ViewModels.InputPropertyValueTypeString {
// eslint-disable-next-line no-null/no-null
if (value === null) {
return "null"; return "null";
} }
let type = typeof value; const type = typeof value;
switch (type) { switch (type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -173,9 +173,10 @@ export class NodePropertiesComponent extends React.Component<
const existingProps: ViewModels.InputProperty[] = []; const existingProps: ViewModels.InputProperty[] = [];
// eslint-disable-next-line no-prototype-builtins
if (this.props.node.hasOwnProperty("properties")) { if (this.props.node.hasOwnProperty("properties")) {
const hProps = this.props.node["properties"]; const hProps = this.props.node["properties"];
for (let p in hProps) { for (const p in hProps) {
const propValues = hProps[p]; const propValues = hProps[p];
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({ (p === partitionKeyProperty ? readOnlyProps : existingProps).push({
key: p, key: p,
@@ -437,7 +438,7 @@ export class NodePropertiesComponent extends React.Component<
</div> </div>
); );
} else { } else {
return null; return undefined;
} }
} }

View File

@@ -54,6 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }

View File

@@ -80,8 +80,9 @@ export function createStaticCommandBarButtons(
} }
notebookButtons.push(createOpenTerminalButton(container)); notebookButtons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
notebookButtons.push(createNotebookWorkspaceResetButton(container)); notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if ( if (
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&

View File

@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
/** /**
@@ -201,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
onRender: () => <MemoryTracker />, onRender: () => <MemoryTracker />,
}; };
}; };
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus />,
};
};

View File

@@ -0,0 +1,79 @@
@import "../../../../less/Common/Constants";
.connectionStatusContainer {
cursor: default;
align-items: center;
margin: 0 9px;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 13px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
}
.connectionStatusFailed{
color: #bd1919;
}
.ring-container {
position: relative;
}
.ringringGreen {
border: 3px solid green;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringYellow{
border: 3px solid #ffbf00;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringRed{
border: 3px solid #bd1919;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
@keyframes pulsate {
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
15% {opacity: 0.8;}
25% {opacity: 0.6;}
45% {opacity: 0.4;}
70% {opacity: 0.3;}
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
}
.locationGreenDot{
font-size: 20px;
margin-right: 0.07em;
color: green;
}
.locationYellowDot{
font-size: 20px;
margin-right: 0.07em;
color: #ffbf00;
}
.locationRedDot{
font-size: 20px;
margin-right: 0.07em;
color: #bd1919;
}

View File

@@ -0,0 +1,77 @@
import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react";
import { ConnectionStatusType } from "../../../Common/Constants";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
export const ConnectionStatus: React.FC = (): JSX.Element => {
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
const toolTipContent = "Hosted runtime status.";
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isActive) {
intervalId = setInterval(() => {
const secondCounter = counter % 60;
const minuteCounter = Math.floor(counter / 60);
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
setSecond(computedSecond);
setMinute(computedMinute);
setCounter((counter) => counter + 1);
}, 1000);
}
return () => clearInterval(intervalId);
}, [isActive, counter]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
setSecond("00");
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
if (!connectionInfo) {
return (
<Stack className="connectionStatusContainer" horizontal>
<span>Connecting</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) {
setIsActive(true);
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("locationGreenDot");
setStatusColorAnimation("ringringGreen");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("locationRedDot");
setStatusColorAnimation("ringringRed");
}
return (
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<div className="ring-container">
<div className={statusColorAnimation}></div>
<Icon iconName="LocationDot" className={statusColor} />
</div>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Allocating && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
</Stack>
</TooltipHost>
);
};

View File

@@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
const q = params.toString(); const q = params.toString();
const suffix = q !== "" ? `?${q}` : ""; const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`; const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1"); return url.replace(/^http(s)?/, "ws$1");
}; };

View File

@@ -56,7 +56,7 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try { try {
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, { const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: authToken, Authorization: authToken,

View File

@@ -36,7 +36,10 @@ export class NotebookContentClient {
* *
* @param parent parent folder * @param parent parent folder
*/ */
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> { public async createNewNotebookFile(
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
@@ -99,7 +102,6 @@ export class NotebookContentClient {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
const filepath = NotebookUtil.getFilePath(parent.path, name); const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) { if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`); throw new Error(`File already exists: ${filepath}`);

View File

@@ -28,6 +28,8 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem;
connectionInfo: DataModels.ContainerConnectionInfo;
NotebookFolderName: string;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -43,6 +45,7 @@ interface NotebookState {
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -65,6 +68,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined, galleryContentRoot: undefined,
connectionInfo: undefined,
NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks",
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -169,7 +174,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: "My Notebooks", name: get().NotebookFolderName,
path: get().notebookBasePath, path: get().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -178,13 +183,11 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
path: "Gallery", path: "Gallery",
type: NotebookContentItemType.File, type: NotebookContentItemType.File,
}; };
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() const gitHubNotebooksContentRoot = {
? { name: "GitHub repos",
name: "GitHub repos", path: "PsuedoDir",
path: "PsuedoDir", type: NotebookContentItemType.Directory,
type: NotebookContentItemType.Directory, };
}
: undefined;
set({ set({
myNotebooksContentRoot, myNotebooksContentRoot,
galleryContentRoot, galleryContentRoot,
@@ -246,4 +249,5 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot }); set({ gitHubNotebooksContentRoot });
} }
}, },
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
})); }));

View File

@@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner, selectedLocation.owner,
selectedLocation.repo selectedLocation.repo
)} - ${selectedLocation.branch}`; )} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = "My Notebooks Scratch";
} }
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);

View File

@@ -12,6 +12,7 @@ import {
import React, { FormEvent, FunctionComponent } from "react"; import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient"; import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location { interface Location {
@@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
const getDropDownOptions = (): IDropdownOption[] => { const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = []; const options: IDropdownOption[] = [];
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle, text: useNotebook.getState().NotebookFolderName,
title: ResourceTreeAdapter.MyNotebooksTitle, title: useNotebook.getState().NotebookFolderName,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,

View File

@@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -19,7 +19,7 @@ export function createDataTable($dataTableElem: JQuery, settings: any): DataTabl
* @return The given settings with all columns having a rendering function * @return The given settings with all columns having a rendering function
*/ */
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy { function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
var tableColumns: DataTables.ColumnLegacy[] = null; let tableColumns: DataTables.ColumnLegacy[] = null;
if (settings.aoColumns) { if (settings.aoColumns) {
tableColumns = settings.aoColumns; tableColumns = settings.aoColumns;
@@ -34,7 +34,7 @@ function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
return settings; return settings;
} }
for (var i = 0; i < tableColumns.length; i++) { for (let i = 0; i < tableColumns.length; i++) {
// the column does not have a render function // the column does not have a render function
if (!tableColumns[i].mRender) { if (!tableColumns[i].mRender) {
tableColumns[i].mRender = defaultDataRender; tableColumns[i].mRender = defaultDataRender;

View File

@@ -131,14 +131,14 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (myNotebooksContentRoot) { if (myNotebooksContentRoot) {
notebooksTree.children.push(buildMyNotebooksTree()); notebooksTree.children.push(buildMyNotebooksTree());
} }
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes // collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree()); notebooksTree.children.push(buildGitHubNotebooksTree(true));
} else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) {
notebooksTree.children.push(buildGitHubNotebooksTree(false));
} }
} }
return notebooksTree; return notebooksTree;
}; };
@@ -178,7 +178,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return myNotebooksTree; return myNotebooksTree;
}; };
const buildGitHubNotebooksTree = (): TreeNode => { const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
@@ -190,8 +190,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
true true
); );
const manageGitContextMenu: TreeNodeMenuItem[] = [
gitHubNotebooksTree.contextMenu = [
{ {
label: "Manage GitHub settings", label: "Manage GitHub settings",
onClick: () => onClick: () =>
@@ -216,7 +215,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
}, },
]; ];
const connectGitContextMenu: TreeNodeMenuItem[] = [
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true; gitHubNotebooksTree.isAlphaSorted = true;

View File

@@ -45,6 +45,7 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks"; public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos"; public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";
@@ -130,9 +131,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
path: "Gallery", path: "Gallery",
type: NotebookContentItemType.File, type: NotebookContentItemType.File,
}; };
this.myNotebooksContentRoot = { this.myNotebooksContentRoot = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: useNotebook.getState().NotebookFolderName,
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -146,16 +146,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
}) })
); );
} }
this.gitHubNotebooksContentRoot = {
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { name: ResourceTreeAdapter.GitHubReposTitle,
this.gitHubNotebooksContentRoot = { path: ResourceTreeAdapter.PseudoDirPath,
name: ResourceTreeAdapter.GitHubReposTitle, type: NotebookContentItemType.Directory,
path: ResourceTreeAdapter.PseudoDirPath, };
type: NotebookContentItemType.Directory,
};
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks); return Promise.all(refreshTasks);
} }

View File

@@ -37,6 +37,7 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";

View File

@@ -0,0 +1,70 @@
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export interface IPhoenixResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
}
export interface IProvosionData {
cosmosEndpoint: string;
resourceId: string;
dbAccountName: string;
aadToken: string;
resourceGroup: string;
subscriptionId: string;
}
export class PhoenixClient {
public async containerConnectionInfo(
provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
method: "POST",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
} else {
const connectionStatus: DataModels.ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
return {
status: response.status,
data,
};
}
public static getPhoenixEndpoint(): string {
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);
}
return phoenixEndpoint;
}
public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}

View File

@@ -11,6 +11,7 @@ export type Features = {
autoscaleDefault: boolean; autoscaleDefault: boolean;
partitionKeyDefault: boolean; partitionKeyDefault: boolean;
partitionKeyDefault2: boolean; partitionKeyDefault2: boolean;
phoenix: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
@@ -76,5 +77,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
phoenix: "true" === get("phoenix"),
}; };
} }

View File

@@ -2,9 +2,7 @@ import * as DataModels from "../Contracts/DataModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export class DefaultExperienceUtility { export class DefaultExperienceUtility {
public static getApiKindFromDefaultExperience( public static getApiKindFromDefaultExperience(defaultExperience: typeof userContext.apiType): DataModels.ApiKind {
defaultExperience: typeof userContext.apiType | null
): DataModels.ApiKind {
if (!defaultExperience) { if (!defaultExperience) {
return DataModels.ApiKind.SQL; return DataModels.ApiKind.SQL;
} }

View File

@@ -10,6 +10,7 @@ import {
SortBy, SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
@@ -223,11 +224,13 @@ export function downloadItem(
const name = data.name; const name = data.name;
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
"Download to My Notebooks", `Download to ${useNotebook.getState().NotebookFolderName}`,
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
"Download", "Download",
async () => { async () => {
const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`); const clearInProgressMessage = logConsoleProgress(
`Downloading ${name} to ${useNotebook.getState().NotebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, { const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id, notebookId: data.id,
downloadCount: data.downloads, downloadCount: data.downloads,

View File

@@ -56,6 +56,7 @@ export function useAADAuth(): ReturnType {
}); });
setTenantId(response.tenantId); setTenantId(response.tenantId);
setAccount(response.account); setAccount(response.account);
localStorage.setItem("cachedTenantId", response.tenantId);
}, },
[account, tenantId] [account, tenantId]
); );

View File

@@ -98,9 +98,11 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
const msalInstance = getMsalInstance(); const msalInstance = getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount); msalInstance.setActiveAccount(cachedAccount);
const cachedTenantId = localStorage.getItem("cachedTenantId");
const aadTokenResponse = await msalInstance.acquireTokenSilent({ const aadTokenResponse = await msalInstance.acquireTokenSilent({
forceRefresh: true, forceRefresh: true,
scopes: [hrefEndpoint], scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
}); });
aadToken = aadTokenResponse.accessToken; aadToken = aadTokenResponse.accessToken;
} }
@@ -337,6 +339,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true; userContext.features.partitionKeyDefault2 = true;
} }
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
userContext.features.phoenix = true;
}
} }
} }

View File

@@ -8,7 +8,6 @@
"noUnusedParameters": true "noUnusedParameters": true
}, },
"files": [ "files": [
"./src/Explorer/Controls/TreeComponent/TreeComponent.tsx",
"./src/AuthType.ts", "./src/AuthType.ts",
"./src/Bindings/ReactBindingHandler.ts", "./src/Bindings/ReactBindingHandler.ts",
"./src/Common/ArrayHashMap.ts", "./src/Common/ArrayHashMap.ts",
@@ -56,7 +55,6 @@
"./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx",
"./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx",
"./src/Explorer/Notebook/NotebookRenderer/PromptContent.test.tsx",
"./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx", "./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx",
"./src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx", "./src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx",
"./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx",
@@ -84,13 +82,10 @@
"./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/Explorer/Tree/AccessibleVerticalList.ts",
"./src/GitHub/GitHubConnector.ts", "./src/GitHub/GitHubConnector.ts",
"./src/HostedExplorerChildFrame.ts", "./src/HostedExplorerChildFrame.ts",
"./src/Index.tsx",
"./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.test.tsx",
"./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/MeControl.tsx",
"./src/Platform/Hosted/Components/SignInButton.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx",
"./src/Platform/Hosted/Components/SwitchAccount.tsx",
"./src/Platform/Hosted/Components/SwitchSubscription.tsx",
"./src/Platform/Hosted/HostedUtils.test.ts", "./src/Platform/Hosted/HostedUtils.test.ts",
"./src/Platform/Hosted/HostedUtils.ts", "./src/Platform/Hosted/HostedUtils.ts",
"./src/Platform/Hosted/extractFeatures.test.ts", "./src/Platform/Hosted/extractFeatures.test.ts",
@@ -99,6 +94,17 @@
"./src/SelfServe/Example/SelfServeExample.types.ts", "./src/SelfServe/Example/SelfServeExample.types.ts",
"./src/SelfServe/SelfServeStyles.tsx", "./src/SelfServe/SelfServeStyles.tsx",
"./src/SelfServe/SqlX/SqlxTypes.ts", "./src/SelfServe/SqlX/SqlxTypes.ts",
"./src/Shared/Constants.ts",
"./src/Shared/DefaultExperienceUtility.ts",
"./src/Shared/ExplorerSettings.ts",
"./src/Shared/LocalStorageUtility.ts",
"./src/Shared/PriceEstimateCalculator.ts",
"./src/Shared/SessionStorageUtility.ts",
"./src/Shared/StorageUtility.test.ts",
"./src/Shared/StorageUtility.ts",
"./src/Shared/StringUtility.test.ts",
"./src/Shared/StringUtility.ts",
"./src/Shared/appInsights.ts",
"./src/UserContext.ts", "./src/UserContext.ts",
"./src/Utils/APITypeUtils.ts", "./src/Utils/APITypeUtils.ts",
"./src/Utils/AutoPilotUtils.ts", "./src/Utils/AutoPilotUtils.ts",
@@ -123,16 +129,17 @@
"./src/hooks/useFullScreenURLs.tsx", "./src/hooks/useFullScreenURLs.tsx",
"./src/hooks/useGraphPhoto.tsx", "./src/hooks/useGraphPhoto.tsx",
"./src/hooks/useNotebookSnapshotStore.ts", "./src/hooks/useNotebookSnapshotStore.ts",
"./src/hooks/usePortalAccessToken.tsx",
"./src/hooks/useNotificationConsole.ts", "./src/hooks/useNotificationConsole.ts",
"./src/hooks/useObservable.ts", "./src/hooks/useObservable.ts",
"./src/hooks/usePortalAccessToken.tsx",
"./src/hooks/useSidePanel.ts", "./src/hooks/useSidePanel.ts",
"./src/i18n.ts", "./src/i18n.ts",
"./src/quickstart.ts", "./src/quickstart.ts",
"./src/setupTests.ts", "./src/setupTests.ts",
"./src/userContext.test.ts", "./src/userContext.test.ts",
"src/Common/EntityValue.tsx", "src/Common/EntityValue.tsx",
"src/Common/TableEntity.tsx" "./src/Platform/Hosted/Components/SwitchAccount.tsx",
"./src/Platform/Hosted/Components/SwitchSubscription.tsx"
], ],
"include": [ "include": [
"src/CellOutputViewer/transforms/**/*", "src/CellOutputViewer/transforms/**/*",
@@ -154,8 +161,8 @@
"src/Localization/**/*", "src/Localization/**/*",
"src/Platform/Emulator/**/*", "src/Platform/Emulator/**/*",
"src/SelfServe/Documentation/**/*", "src/SelfServe/Documentation/**/*",
"src/Shared/**/*", "src/Shared/Telemetry/**/*",
"src/Terminal/**/*", "src/Terminal/**/*",
"src/Utils/arm/**/*" "src/Utils/arm/**/*"
] ]
} }