mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
5 Commits
fixed-ts-s
...
create_dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be9e179383 | ||
|
|
98640785fc | ||
|
|
4db4d8494e | ||
|
|
b783ebc834 | ||
|
|
b4d23ac913 |
@@ -21,8 +21,16 @@ src/Common/MongoUtility.ts
|
|||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/QueriesClient.ts
|
src/Common/QueriesClient.ts
|
||||||
src/Common/Splitter.ts
|
src/Common/Splitter.ts
|
||||||
|
src/Config.ts
|
||||||
|
src/Contracts/ActionContracts.ts
|
||||||
|
src/Contracts/DataModels.ts
|
||||||
|
src/Contracts/Diagnostics.ts
|
||||||
|
src/Contracts/ExplorerContracts.ts
|
||||||
|
src/Contracts/Versions.ts
|
||||||
|
src/Contracts/ViewModels.ts
|
||||||
src/Controls/Heatmap/Heatmap.test.ts
|
src/Controls/Heatmap/Heatmap.test.ts
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
src/Controls/Heatmap/Heatmap.ts
|
||||||
|
src/Controls/Heatmap/HeatmapDatatypes.ts
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.d.ts
|
src/Definitions/globals.d.ts
|
||||||
@@ -36,10 +44,29 @@ src/Definitions/png.d.ts
|
|||||||
src/Definitions/svg.d.ts
|
src/Definitions/svg.d.ts
|
||||||
src/Explorer/ComponentRegisterer.test.ts
|
src/Explorer/ComponentRegisterer.test.ts
|
||||||
src/Explorer/ComponentRegisterer.ts
|
src/Explorer/ComponentRegisterer.ts
|
||||||
|
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
|
||||||
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
|
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
|
||||||
|
src/Explorer/Controls/DynamicList/DynamicList.test.ts
|
||||||
|
src/Explorer/Controls/DynamicList/DynamicListComponent.ts
|
||||||
src/Explorer/Controls/Editor/EditorComponent.ts
|
src/Explorer/Controls/Editor/EditorComponent.ts
|
||||||
|
src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
|
||||||
|
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
|
||||||
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
|
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
|
||||||
|
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
|
||||||
|
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
|
||||||
|
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarAction.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarDropDown.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarItem.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarSeperator.ts
|
||||||
|
src/Explorer/Controls/Toolbar/IToolbarToggle.ts
|
||||||
|
src/Explorer/Controls/Toolbar/KeyCodes.ts
|
||||||
|
src/Explorer/Controls/Toolbar/Toolbar.ts
|
||||||
|
src/Explorer/Controls/Toolbar/ToolbarAction.ts
|
||||||
|
src/Explorer/Controls/Toolbar/ToolbarDropDown.ts
|
||||||
|
src/Explorer/Controls/Toolbar/ToolbarToggle.ts
|
||||||
|
src/Explorer/Controls/Toolbar/Utilities.ts
|
||||||
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
|
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
|
||||||
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
||||||
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
||||||
@@ -165,3 +192,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
|||||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTree.tsx
|
src/Explorer/Tree/ResourceTree.tsx
|
||||||
|
src/Explorer/Tree/DatabasesResourceTree.tsx
|
||||||
|
src/Explorer/Tree/NotebooksResourceTree.tsx
|
||||||
@@ -54,8 +54,6 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
</div>
|
</div>
|
||||||
{userContext.authType === AuthType.ResourceToken ? (
|
{userContext.authType === AuthType.ResourceToken ? (
|
||||||
<ResourceTokenTree />
|
<ResourceTokenTree />
|
||||||
) : userContext.features.enableKoResourceTree ? (
|
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
|
||||||
) : (
|
) : (
|
||||||
<ResourceTree container={container} />
|
<ResourceTree container={container} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let heatmap: Heatmap;
|
let heatmap: Heatmap;
|
||||||
const theme: PortalTheme = 1;
|
let theme: PortalTheme = 1;
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||||
|
|
||||||
describe("drawHeatmap rendering", () => {
|
describe("drawHeatmap rendering", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should show a no data message with a dark theme", () => {
|
it("should show a no data message with a dark theme", () => {
|
||||||
const data = {
|
let data = {
|
||||||
data: {
|
data: {
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
data: {
|
data: {
|
||||||
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||||
document.body.innerHTML = divElement;
|
document.body.innerHTML = divElement;
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
handleMessage(data as MessageEvent);
|
||||||
@@ -120,7 +120,7 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should show a no data message with a white theme", () => {
|
it("should show a no data message with a white theme", () => {
|
||||||
const data = {
|
let data = {
|
||||||
data: {
|
data: {
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
data: {
|
data: {
|
||||||
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||||
document.body.innerHTML = divElement;
|
document.body.innerHTML = divElement;
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
handleMessage(data as MessageEvent);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class Heatmap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
|
||||||
return {
|
return {
|
||||||
family: StyleConstants.DataExplorerFont,
|
family: StyleConstants.DataExplorerFont,
|
||||||
size,
|
size,
|
||||||
@@ -78,9 +78,9 @@ export class Heatmap {
|
|||||||
// go thru all rows and create 2d matrix for heatmap...
|
// go thru all rows and create 2d matrix for heatmap...
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
output.yAxisPoints.push(rows[i]);
|
output.yAxisPoints.push(rows[i]);
|
||||||
const dataPoints: number[] = [];
|
let dataPoints: number[] = [];
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||||||
const row: PartitionTimeStampToData = data[rows[i]];
|
let row: PartitionTimeStampToData = data[rows[i]];
|
||||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
||||||
}
|
}
|
||||||
output.dataPoints.push(dataPoints);
|
output.dataPoints.push(dataPoints);
|
||||||
@@ -193,7 +193,7 @@ export class Heatmap {
|
|||||||
this._getLayoutSettings(),
|
this._getLayoutSettings(),
|
||||||
this._getChartDisplaySettings()
|
this._getChartDisplaySettings()
|
||||||
);
|
);
|
||||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
let plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||||
plotDiv.on("plotly_click", (data: any) => {
|
plotDiv.on("plotly_click", (data: any) => {
|
||||||
let timeSelected: string = data.points[0].x;
|
let timeSelected: string = data.points[0].x;
|
||||||
timeSelected = timeSelected.replace(" ", "T");
|
timeSelected = timeSelected.replace(" ", "T");
|
||||||
@@ -205,7 +205,7 @@ export class Heatmap {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const output = [];
|
let output = [];
|
||||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
node.children.reduce(
|
||||||
(previous: boolean, child: TreeNode) =>
|
(previous: boolean, child: TreeNode) =>
|
||||||
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
|
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
|
||||||
false
|
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,8 +274,7 @@ 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,
|
||||||
@@ -286,9 +285,8 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
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);
|
||||||
|
|||||||
@@ -550,6 +550,61 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{userContext.apiType !== "Tables" && (
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Advanced"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
|
this.scrollToAdvancedSection();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||||
|
{isCapabilityEnabled("EnableMongo") && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Indexing
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Create a Wildcard Index on all fields"
|
||||||
|
checked={this.state.createMongoWildCardIndex}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center" },
|
||||||
|
}}
|
||||||
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
|
this.setState({ createMongoWildCardIndex: isChecked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
|
<Checkbox
|
||||||
|
label="My partition key is larger than 100 bytes"
|
||||||
|
checked={this.state.useHashV2}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center" },
|
||||||
|
}}
|
||||||
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
|
this.setState({ useHashV2: isChecked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{this.shouldShowAnalyticalStoreOptions() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -615,61 +670,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType !== "Tables" && (
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Advanced"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
|
||||||
this.scrollToAdvancedSection();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
|
||||||
{isCapabilityEnabled("EnableMongo") && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Indexing
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
|
||||||
>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Create a Wildcard Index on all fields"
|
|
||||||
checked={this.state.createMongoWildCardIndex}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center" },
|
|
||||||
}}
|
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
|
||||||
this.setState({ createMongoWildCardIndex: isChecked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
|
||||||
<Checkbox
|
|
||||||
label="My partition key is larger than 100 bytes"
|
|
||||||
checked={this.state.useHashV2}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center" },
|
|
||||||
}}
|
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
|
||||||
this.setState({ useHashV2: isChecked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
)}
|
)}
|
||||||
|
|||||||
341
src/Explorer/Tree/DatabasesResourceTree.tsx
Normal file
341
src/Explorer/Tree/DatabasesResourceTree.tsx
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import React from "react";
|
||||||
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
|
import { AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
|
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface DatabasesResourceTreeProps {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatabasesResourceTree: React.FC<DatabasesResourceTreeProps> = ({
|
||||||
|
container,
|
||||||
|
}: DatabasesResourceTreeProps): JSX.Element => {
|
||||||
|
const databases = useDatabases((state) => state.databases);
|
||||||
|
const isNotebookEnabled = useNotebook((state) => state.isNotebookEnabled);
|
||||||
|
const gitHubNotebooksContentRoot = useNotebook((state) => state.gitHubNotebooksContentRoot);
|
||||||
|
|
||||||
|
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
const refreshActiveTab = useTabs.getState().refreshActiveTab;
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={buildDataTree()} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
436
src/Explorer/Tree/NotebooksResourceTree.tsx
Normal file
436
src/Explorer/Tree/NotebooksResourceTree.tsx
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import shallow from "zustand/shallow";
|
||||||
|
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 { Areas, Notebook } from "../../Common/Constants";
|
||||||
|
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 * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
|
import { useDialog } from "../Controls/Dialog";
|
||||||
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const MyNotebooksTitle = "My Notebooks";
|
||||||
|
export const GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
|
interface NotebooksResourceTreeProps {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotebooksResourceTree: React.FC<NotebooksResourceTreeProps> = ({
|
||||||
|
container,
|
||||||
|
}: NotebooksResourceTreeProps): JSX.Element => {
|
||||||
|
const {
|
||||||
|
isNotebookEnabled,
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
updateNotebookItem,
|
||||||
|
} = useNotebook(
|
||||||
|
(state) => ({
|
||||||
|
isNotebookEnabled: state.isNotebookEnabled,
|
||||||
|
myNotebooksContentRoot: state.myNotebooksContentRoot,
|
||||||
|
galleryContentRoot: state.galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
|
||||||
|
updateNotebookItem: state.updateNotebookItem,
|
||||||
|
}),
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
const activeTab = useTabs((state) => state.activeTab);
|
||||||
|
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 (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebooksTree = (): TreeNode => {
|
||||||
|
const notebooksTree: TreeNode = {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userContext.features.notebooksTemporarilyDown) {
|
||||||
|
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
||||||
|
} else {
|
||||||
|
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 buildNotebooksTemporarilyDownTree = (): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: Notebook.temporarilyDownMsg,
|
||||||
|
className: "clickDisabled",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
gitHubNotebooksTree.contextMenu = [
|
||||||
|
{
|
||||||
|
label: "Manage GitHub settings",
|
||||||
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={container}
|
||||||
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={container.notebookManager.junoClient}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 = (
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): TreeNode[] => {
|
||||||
|
if (!item || !item.children) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return item.children.map((item) => {
|
||||||
|
const result =
|
||||||
|
item.type === NotebookContentItemType.Directory
|
||||||
|
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
|
||||||
|
: buildNotebookFileNode(item, onFileClick, isGithubTree);
|
||||||
|
result.timestamp = item.timestamp;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebookFileNode = (
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): 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, isGithubTree),
|
||||||
|
data: item,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFileContextMenu = (
|
||||||
|
container: Explorer,
|
||||||
|
item: NotebookContentItem,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item, isGithubTree),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item, isGithubTree),
|
||||||
|
"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,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Refresh",
|
||||||
|
iconSrc: RefreshIcon,
|
||||||
|
onClick: () => loadSubitems(item, isGithubTree),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}?"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item, isGithubTree),
|
||||||
|
"Cancel",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item, isGithubTree),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Directory",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Notebook",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
iconSrc: undefined,
|
||||||
|
className: "notebookHeader",
|
||||||
|
isAlphaSorted: true,
|
||||||
|
isLeavesParentsSeparate: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (!item.children) {
|
||||||
|
loadSubitems(item, isGithubTree);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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, isGithubTree) : undefined,
|
||||||
|
data: item,
|
||||||
|
children: buildChildNodes(item, onFileClick, isGithubTree),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
|
||||||
|
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
|
||||||
|
updateNotebookItem(updatedItem, isGithubTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
return isNotebookEnabled ? (
|
||||||
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||||
|
{buildGalleryCallout()}
|
||||||
|
</AccordionItemComponent>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,764 +1,18 @@
|
|||||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import shallow from "zustand/shallow";
|
import { AccordionComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
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, Notebook } 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 { useDialog } from "../Controls/Dialog";
|
|
||||||
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { DatabasesResourceTree } from "./DatabasesResourceTree";
|
||||||
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
import { NotebooksResourceTree } from "./NotebooksResourceTree";
|
||||||
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 {
|
interface ResourceTreeProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
|
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
|
||||||
const databases = useDatabases((state) => state.databases);
|
|
||||||
const {
|
|
||||||
isNotebookEnabled,
|
|
||||||
myNotebooksContentRoot,
|
|
||||||
galleryContentRoot,
|
|
||||||
gitHubNotebooksContentRoot,
|
|
||||||
updateNotebookItem,
|
|
||||||
} = useNotebook(
|
|
||||||
(state) => ({
|
|
||||||
isNotebookEnabled: state.isNotebookEnabled,
|
|
||||||
myNotebooksContentRoot: state.myNotebooksContentRoot,
|
|
||||||
galleryContentRoot: state.galleryContentRoot,
|
|
||||||
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
|
|
||||||
updateNotebookItem: state.updateNotebookItem,
|
|
||||||
}),
|
|
||||||
shallow
|
|
||||||
);
|
|
||||||
const { activeTab, refreshActiveTab } = useTabs();
|
|
||||||
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
|
||||||
const pseudoDirPath = "PsuedoDir";
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Callout {...calloutProps}>
|
|
||||||
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
|
||||||
<Text variant="xLarge" block>
|
|
||||||
New gallery
|
|
||||||
</Text>
|
|
||||||
<Text block>
|
|
||||||
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
|
||||||
contributors.
|
|
||||||
</Text>
|
|
||||||
<Link {...openGalleryProps}>Open gallery</Link>
|
|
||||||
</Stack>
|
|
||||||
</Callout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildNotebooksTree = (): TreeNode => {
|
|
||||||
const notebooksTree: TreeNode = {
|
|
||||||
label: undefined,
|
|
||||||
isExpanded: true,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userContext.features.notebooksTemporarilyDown) {
|
|
||||||
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
|
||||||
} else {
|
|
||||||
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 buildNotebooksTemporarilyDownTree = (): TreeNode => {
|
|
||||||
return {
|
|
||||||
label: Notebook.temporarilyDownMsg,
|
|
||||||
className: "clickDisabled",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
gitHubNotebooksTree.contextMenu = [
|
|
||||||
{
|
|
||||||
label: "Manage GitHub settings",
|
|
||||||
onClick: () =>
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Manage GitHub settings",
|
|
||||||
<GitHubReposPanel
|
|
||||||
explorer={container}
|
|
||||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
|
||||||
junoClientProp={container.notebookManager.junoClient}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 = (
|
|
||||||
item: NotebookContentItem,
|
|
||||||
onFileClick: (item: NotebookContentItem) => void,
|
|
||||||
isGithubTree?: boolean
|
|
||||||
): TreeNode[] => {
|
|
||||||
if (!item || !item.children) {
|
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
return item.children.map((item) => {
|
|
||||||
const result =
|
|
||||||
item.type === NotebookContentItemType.Directory
|
|
||||||
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
|
|
||||||
: buildNotebookFileNode(item, onFileClick, isGithubTree);
|
|
||||||
result.timestamp = item.timestamp;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildNotebookFileNode = (
|
|
||||||
item: NotebookContentItem,
|
|
||||||
onFileClick: (item: NotebookContentItem) => void,
|
|
||||||
isGithubTree?: boolean
|
|
||||||
): 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, isGithubTree),
|
|
||||||
data: item,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFileContextMenu = (
|
|
||||||
container: Explorer,
|
|
||||||
item: NotebookContentItem,
|
|
||||||
isGithubTree?: boolean
|
|
||||||
): TreeNodeMenuItem[] => {
|
|
||||||
let items: TreeNodeMenuItem[] = [
|
|
||||||
{
|
|
||||||
label: "Rename",
|
|
||||||
iconSrc: NotebookIcon,
|
|
||||||
onClick: () => container.renameNotebook(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
iconSrc: DeleteIcon,
|
|
||||||
onClick: () => {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkCancelModalDialog(
|
|
||||||
"Confirm delete",
|
|
||||||
`Are you sure you want to delete "${item.name}"`,
|
|
||||||
"Delete",
|
|
||||||
() => container.deleteNotebookFile(item, isGithubTree),
|
|
||||||
"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,
|
|
||||||
isGithubTree?: boolean
|
|
||||||
): TreeNodeMenuItem[] => {
|
|
||||||
let items: TreeNodeMenuItem[] = [
|
|
||||||
{
|
|
||||||
label: "Refresh",
|
|
||||||
iconSrc: RefreshIcon,
|
|
||||||
onClick: () => loadSubitems(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
iconSrc: DeleteIcon,
|
|
||||||
onClick: () => {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkCancelModalDialog(
|
|
||||||
"Confirm delete",
|
|
||||||
`Are you sure you want to delete "${item.name}?"`,
|
|
||||||
"Delete",
|
|
||||||
() => container.deleteNotebookFile(item, isGithubTree),
|
|
||||||
"Cancel",
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Rename",
|
|
||||||
iconSrc: NotebookIcon,
|
|
||||||
onClick: () => container.renameNotebook(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "New Directory",
|
|
||||||
iconSrc: NewNotebookIcon,
|
|
||||||
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "New Notebook",
|
|
||||||
iconSrc: NewNotebookIcon,
|
|
||||||
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
isGithubTree?: boolean
|
|
||||||
): TreeNode => {
|
|
||||||
return {
|
|
||||||
label: item.name,
|
|
||||||
iconSrc: undefined,
|
|
||||||
className: "notebookHeader",
|
|
||||||
isAlphaSorted: true,
|
|
||||||
isLeavesParentsSeparate: true,
|
|
||||||
onClick: () => {
|
|
||||||
if (!item.children) {
|
|
||||||
loadSubitems(item, isGithubTree);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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, isGithubTree) : undefined,
|
|
||||||
data: item,
|
|
||||||
children: buildChildNodes(item, onFileClick, isGithubTree),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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() &&
|
|
||||||
!userContext.features.notebooksTemporarilyDown
|
|
||||||
) {
|
|
||||||
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, isGithubTree?: boolean): Promise<void> => {
|
|
||||||
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
|
|
||||||
updateNotebookItem(updatedItem, isGithubTree);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataRootNode = buildDataTree();
|
|
||||||
|
|
||||||
if (isNotebookEnabled) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
<DatabasesResourceTree container={container} />
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<NotebooksResourceTree container={container} />
|
||||||
</AccordionItemComponent>
|
|
||||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
|
||||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
|
||||||
</AccordionItemComponent>
|
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
{buildGalleryCallout()}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export type Features = {
|
|||||||
readonly enableTtl: boolean;
|
readonly enableTtl: boolean;
|
||||||
readonly executeSproc: boolean;
|
readonly executeSproc: boolean;
|
||||||
readonly enableAadDataPlane: boolean;
|
readonly enableAadDataPlane: boolean;
|
||||||
readonly enableKoResourceTree: boolean;
|
|
||||||
readonly hostedDataExplorer: boolean;
|
readonly hostedDataExplorer: boolean;
|
||||||
readonly junoEndpoint?: string;
|
readonly junoEndpoint?: string;
|
||||||
readonly livyEndpoint?: string;
|
readonly livyEndpoint?: string;
|
||||||
@@ -59,7 +58,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableSDKoperations: "true" === get("enablesdkoperations"),
|
enableSDKoperations: "true" === get("enablesdkoperations"),
|
||||||
enableSpark: "true" === get("enablespark"),
|
enableSpark: "true" === get("enablespark"),
|
||||||
enableTtl: "true" === get("enablettl"),
|
enableTtl: "true" === get("enablettl"),
|
||||||
enableKoResourceTree: "true" === get("enablekoresourcetree"),
|
|
||||||
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
||||||
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
||||||
junoEndpoint: get("junoendpoint"),
|
junoEndpoint: get("junoendpoint"),
|
||||||
@@ -75,6 +73,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||||
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"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +161,7 @@
|
|||||||
"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/**/*"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
process: "process/browser",
|
process: "process/browser",
|
||||||
Buffer: ["buffer", "Buffer"],
|
|
||||||
}),
|
}),
|
||||||
new CreateFileWebpack({
|
new CreateFileWebpack({
|
||||||
path: "./dist",
|
path: "./dist",
|
||||||
@@ -230,7 +229,6 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
alias: {
|
alias: {
|
||||||
process: "process/browser",
|
process: "process/browser",
|
||||||
},
|
},
|
||||||
|
|
||||||
fallback: {
|
fallback: {
|
||||||
crypto: false,
|
crypto: false,
|
||||||
fs: false,
|
fs: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user