mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 18:01:39 +00:00
Merge branch 'master' into users/languy/save-documentstab-prefs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Icon, Label, Stack } from "@fluentui/react";
|
||||
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
@@ -8,6 +8,7 @@ export interface CollapsibleSectionProps {
|
||||
isExpandedByDefault: boolean;
|
||||
onExpand?: () => void;
|
||||
children: JSX.Element;
|
||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@@ -55,6 +56,19 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
>
|
||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||
<Label>{this.props.title}</Label>
|
||||
{this.props.tooltipContent && (
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.props.tooltipContent}
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: "0 !important",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
)}
|
||||
</Stack>
|
||||
{this.state.isExpanded && this.props.children}
|
||||
</>
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
|
||||
@@ -166,6 +166,7 @@ export class LegacyTreeNodeComponent extends React.Component<
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test={`Tree/TreeNode:${node.label}`}
|
||||
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||
@@ -174,9 +175,9 @@ export class LegacyTreeNodeComponent extends React.Component<
|
||||
>
|
||||
<div
|
||||
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
||||
data-test={`Tree/TreeNode/Header:${node.label}`}
|
||||
style={headerStyle}
|
||||
tabIndex={node.children ? -1 : 0}
|
||||
data-test={node.label}
|
||||
>
|
||||
{this.renderCollapseExpandIcon(node)}
|
||||
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
|
||||
@@ -264,7 +265,7 @@ export class LegacyTreeNodeComponent extends React.Component<
|
||||
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
|
||||
contextualMenuItemAs: (props: IContextualMenuItemProps) => (
|
||||
<div
|
||||
data-test={`treeComponentMenuItemContainer`}
|
||||
data-test={`Tree/TreeNode/MenuItem:${props.item.text}`}
|
||||
className="treeComponentMenuItemContainer"
|
||||
onContextMenu={(e) => e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())}
|
||||
>
|
||||
|
||||
@@ -124,6 +124,20 @@ describe("TreeNodeComponent", () => {
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders a node as expandable if it has empty, but defined, children array", () => {
|
||||
const node = generateTestNode("root", {
|
||||
isLoading: true,
|
||||
children: [
|
||||
generateTestNode("child1", {
|
||||
children: [],
|
||||
}),
|
||||
generateTestNode("child2"),
|
||||
],
|
||||
});
|
||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not render children if the node is loading", () => {
|
||||
const node = generateTestNode("root", {
|
||||
isLoading: true,
|
||||
|
||||
@@ -100,7 +100,8 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
return unsortedChildren;
|
||||
};
|
||||
|
||||
const isBranch = node.children?.length > 0;
|
||||
// A branch node is any node with a defined children array, even if the array is empty.
|
||||
const isBranch = !!node.children;
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
||||
@@ -146,9 +147,9 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
|
||||
const treeItem = (
|
||||
<TreeItem
|
||||
data-test={`TreeNodeContainer:${treeNodeId}`}
|
||||
value={treeNodeId}
|
||||
itemType={isBranch ? "branch" : "leaf"}
|
||||
style={{ height: "100%" }}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TreeItemLayout
|
||||
|
||||
@@ -36,13 +36,14 @@ exports[`LegacyTreeComponent renders a simple tree 1`] = `
|
||||
exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
data-test="Tree/TreeNode:label"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
data-test="Tree/TreeNode/Header:label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 9,
|
||||
@@ -138,6 +139,7 @@ exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
||||
exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
data-test="Tree/TreeNode:label"
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
@@ -145,7 +147,7 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
data-test="Tree/TreeNode/Header:label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 23,
|
||||
@@ -290,13 +292,14 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
||||
exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
data-test="Tree/TreeNode:label"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
data-test="Tree/TreeNode/Header:label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 9,
|
||||
@@ -363,6 +366,7 @@ exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
|
||||
exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
data-test="Tree/TreeNode:label"
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
@@ -370,7 +374,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
data-test="Tree/TreeNode/Header:label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 23,
|
||||
@@ -534,13 +538,14 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
||||
exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
data-test="Tree/TreeNode:label"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
data-test="Tree/TreeNode/Header:label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 9,
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
exports[`TreeNodeComponent does not render children if the node is loading 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -114,13 +110,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
treeNodeId="root"
|
||||
>
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<div
|
||||
@@ -129,15 +121,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
className="fui-TreeItem r1hiwysc"
|
||||
data-fui-tree-item-value="root"
|
||||
data-test="TreeNodeContainer:root"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="treeitem"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ContextSelector.Provider
|
||||
@@ -228,8 +216,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc"
|
||||
data-fui-tree-item-value="root"
|
||||
data-test="TreeNodeContainer:root"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
@@ -282,8 +270,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child1Label"
|
||||
data-test="TreeNodeContainer:root/child1Label"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
@@ -333,8 +321,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child2LoadingLabel"
|
||||
data-test="TreeNodeContainer:root/child2LoadingLabel"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
@@ -383,8 +371,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child3ExpandingLabel"
|
||||
data-test="TreeNodeContainer:root/child3ExpandingLabel"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
@@ -637,13 +625,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
treeNodeId="root/child1Label"
|
||||
>
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root/child1Label"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root/child1Label"
|
||||
>
|
||||
<div
|
||||
@@ -652,15 +636,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
className="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child1Label"
|
||||
data-test="TreeNodeContainer:root/child1Label"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="treeitem"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ContextSelector.Provider
|
||||
@@ -751,8 +731,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child1Label"
|
||||
data-test="TreeNodeContainer:root/child1Label"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
@@ -932,13 +912,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
treeNodeId="root/child2LoadingLabel"
|
||||
>
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root/child2LoadingLabel"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root/child2LoadingLabel"
|
||||
>
|
||||
<div
|
||||
@@ -947,15 +923,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
className="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child2LoadingLabel"
|
||||
data-test="TreeNodeContainer:root/child2LoadingLabel"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="treeitem"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ContextSelector.Provider
|
||||
@@ -1046,8 +1018,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child2LoadingLabel"
|
||||
data-test="TreeNodeContainer:root/child2LoadingLabel"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
@@ -1212,13 +1184,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
treeNodeId="root/child3ExpandingLabel"
|
||||
>
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root/child3ExpandingLabel"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root/child3ExpandingLabel"
|
||||
>
|
||||
<div
|
||||
@@ -1226,15 +1194,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
className="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child3ExpandingLabel"
|
||||
data-test="TreeNodeContainer:root/child3ExpandingLabel"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="treeitem"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ContextSelector.Provider
|
||||
@@ -1332,8 +1296,8 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r1hiwysc ___jer8zr0_6no3ah0 f10bgyvd"
|
||||
data-fui-tree-item-value="root/child3ExpandingLabel"
|
||||
data-test="TreeNodeContainer:root/child3ExpandingLabel"
|
||||
role="treeitem"
|
||||
style="height: 100%;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
@@ -1455,13 +1419,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1493,13 +1453,9 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
||||
|
||||
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1534,6 +1490,40 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
||||
</TreeItem>
|
||||
`;
|
||||
|
||||
exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
actions={false}
|
||||
className="rootClass"
|
||||
data-test="TreeNode:root"
|
||||
iconBefore={
|
||||
<img
|
||||
alt=""
|
||||
src="rootIcon"
|
||||
style={
|
||||
Object {
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
rootLabel
|
||||
</TreeItemLayout>
|
||||
</TreeItem>
|
||||
`;
|
||||
|
||||
exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
<Menu
|
||||
onOpenChange={[Function]}
|
||||
@@ -1544,13 +1534,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
disableButtonEnhancement={true}
|
||||
>
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1637,13 +1623,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders a single node 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1675,13 +1657,9 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1713,13 +1691,9 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1801,13 +1775,9 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
||||
|
||||
exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
@@ -1890,13 +1860,9 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
||||
|
||||
exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
||||
<TreeItem
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
onOpenChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
value="root"
|
||||
>
|
||||
<TreeItemLayout
|
||||
|
||||
@@ -60,21 +60,23 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName,
|
||||
},
|
||||
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
btn.onCommandClick(ev);
|
||||
let copilotEnabled = false;
|
||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||
}
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
|
||||
},
|
||||
onClick: btn.onCommandClick
|
||||
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
btn.onCommandClick(ev);
|
||||
let copilotEnabled = false;
|
||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||
}
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
|
||||
}
|
||||
: undefined,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: label,
|
||||
"data-test": label,
|
||||
title: btn.tooltipText,
|
||||
name: label,
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
"data-test": `CommandBar/Button:${label}`,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
@@ -82,22 +82,6 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
excludedPaths: [],
|
||||
};
|
||||
|
||||
const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: "/*",
|
||||
},
|
||||
],
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?',
|
||||
},
|
||||
],
|
||||
vectorIndexes: [],
|
||||
};
|
||||
|
||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||
vectorEmbeddings: [],
|
||||
};
|
||||
@@ -122,8 +106,9 @@ export interface AddCollectionPanelState {
|
||||
isExecuting: boolean;
|
||||
isThroughputCapExceeded: boolean;
|
||||
teachingBubbleStep: number;
|
||||
vectorIndexingPolicy: string;
|
||||
vectorEmbeddingPolicy: string;
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||
vectorPolicyValidated: boolean;
|
||||
}
|
||||
|
||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||
@@ -159,8 +144,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
isExecuting: false,
|
||||
isThroughputCapExceeded: false,
|
||||
teachingBubbleStep: 0,
|
||||
vectorIndexingPolicy: JSON.stringify(DefaultDatabaseVectorIndex, null, 2),
|
||||
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
|
||||
vectorEmbeddingPolicy: [],
|
||||
vectorIndexingPolicy: [],
|
||||
vectorPolicyValidated: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -896,60 +882,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
{this.shouldShowVectorSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Indexing Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={this.state.vectorIndexingPolicy}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing indexing policy"}
|
||||
lineNumbers={"on"}
|
||||
scrollBeyondLastLine={false}
|
||||
spinnerClassName="panelSectionSpinner"
|
||||
monacoContainerStyles={{
|
||||
minHeight: 200,
|
||||
}}
|
||||
onContentChanged={(newIndexingPolicy: string) => this.setVectorIndexingPolicy(newIndexingPolicy)}
|
||||
/>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Vector Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={this.state.vectorEmbeddingPolicy}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing container vector policy"}
|
||||
lineNumbers={"on"}
|
||||
scrollBeyondLastLine={false}
|
||||
spinnerClassName="panelSectionSpinner"
|
||||
monacoContainerStyles={{
|
||||
minHeight: 200,
|
||||
}}
|
||||
onContentChanged={(newVectorEmbeddingPolicy: string) =>
|
||||
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
|
||||
}
|
||||
/>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||
vectorIndex={this.state.vectorIndexingPolicy}
|
||||
onVectorEmbeddingChange={(
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||
vectorPolicyValidated: boolean,
|
||||
) => {
|
||||
this.setState({ vectorEmbeddingPolicy, vectorIndexingPolicy, vectorPolicyValidated });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
@@ -1159,13 +1113,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
}
|
||||
|
||||
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
|
||||
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: DataModels.VectorEmbedding[]): void {
|
||||
this.setState({
|
||||
vectorEmbeddingPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
|
||||
private setVectorIndexingPolicy(vectorIndexingPolicy: DataModels.VectorIndex[]): void {
|
||||
this.setState({
|
||||
vectorIndexingPolicy,
|
||||
});
|
||||
@@ -1251,6 +1205,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
);
|
||||
}
|
||||
|
||||
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||
queries.{" "}
|
||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
@@ -1370,20 +1336,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
try {
|
||||
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
|
||||
} catch (e) {
|
||||
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(this.state.vectorEmbeddingPolicy) as DataModels.VectorEmbeddingPolicy;
|
||||
} catch (e) {
|
||||
this.setState({ errorMessage: "Invalid JSON format for vectorEmbeddingPolicy" });
|
||||
return false;
|
||||
}
|
||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1461,15 +1416,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||
? AllPropertiesIndexed
|
||||
: SharedDatabaseDefault;
|
||||
|
||||
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
|
||||
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
|
||||
indexingPolicy.vectorIndexes = this.state.vectorIndexingPolicy;
|
||||
vectorEmbeddingPolicy = {
|
||||
vectorEmbeddings: this.state.vectorEmbeddingPolicy,
|
||||
};
|
||||
}
|
||||
|
||||
const telemetryData = {
|
||||
|
||||
@@ -379,6 +379,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -386,6 +387,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -666,6 +668,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -948,6 +951,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1231,6 +1235,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
<BaseButton
|
||||
ariaLabel="OK"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2105,6 +2110,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-122"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -37,11 +38,11 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
setFormError(
|
||||
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`,
|
||||
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
|
||||
);
|
||||
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
|
||||
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
|
||||
logConsoleError(
|
||||
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`,
|
||||
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -123,17 +124,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
};
|
||||
const confirmDatabase = "Confirm by typing the database id";
|
||||
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
|
||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the database id</Text>
|
||||
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
|
||||
<TextField
|
||||
id="confirmDatabaseId"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
@@ -149,7 +151,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this database?
|
||||
What is the reason why you are deleting this {getDatabaseName()}?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
|
||||
@@ -5312,6 +5312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Execute"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Execute"
|
||||
@@ -5319,6 +5320,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="Execute"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Execute"
|
||||
@@ -5599,6 +5601,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="Execute"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -5881,6 +5884,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="Execute"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -6164,6 +6168,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="Execute"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -7038,6 +7043,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-label="Execute"
|
||||
className="ms-Button ms-Button--primary root-148"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
id="sidePanelOkButton"
|
||||
data-test="Panel/OkButton"
|
||||
text={buttonLabel}
|
||||
ariaLabel={buttonLabel}
|
||||
disabled={!!isButtonDisabled}
|
||||
|
||||
@@ -21,6 +21,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Load"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Load"
|
||||
@@ -28,6 +29,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="Load"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Load"
|
||||
@@ -308,6 +310,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="Load"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -590,6 +593,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="Load"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -873,6 +877,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="Load"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1747,6 +1752,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||
aria-label="Load"
|
||||
className="ms-Button ms-Button--primary root-109"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
Toggle,
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { SplitterDirection } from "Common/Splitter";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { configContext } from "ConfigContext";
|
||||
import {
|
||||
DefaultRUThreshold,
|
||||
LocalStorageUtility,
|
||||
StorageKey,
|
||||
getDefaultQueryResultsView,
|
||||
getRUThreshold,
|
||||
ruThresholdEnabled as isRUThresholdEnabled,
|
||||
} from "Shared/StorageUtility";
|
||||
@@ -47,6 +49,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||
);
|
||||
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
|
||||
const [defaultQueryResultsView, setDefaultQueryResultsView] = useState<SplitterDirection>(
|
||||
getDefaultQueryResultsView(),
|
||||
);
|
||||
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
|
||||
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
|
||||
);
|
||||
@@ -121,6 +126,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
|
||||
|
||||
if (shouldShowGraphAutoVizOption) {
|
||||
LocalStorageUtility.setEntryBoolean(
|
||||
@@ -197,6 +203,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||
];
|
||||
|
||||
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
|
||||
{ key: SplitterDirection.Vertical, text: "Vertical" },
|
||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||
];
|
||||
|
||||
const handleOnPriorityLevelOptionChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>,
|
||||
option: IChoiceGroupOption,
|
||||
@@ -234,6 +245,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnDefaultQueryResultsViewChange = (
|
||||
ev: React.MouseEvent<HTMLElement>,
|
||||
option: IChoiceGroupOption,
|
||||
): void => {
|
||||
setDefaultQueryResultsView(option.key as SplitterDirection);
|
||||
};
|
||||
|
||||
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
||||
const retryAttempts = Number(newValue);
|
||||
if (!isNaN(retryAttempts)) {
|
||||
@@ -438,6 +456,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div>
|
||||
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
|
||||
Default Query Results View
|
||||
</legend>
|
||||
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="defaultQueryResultsView"
|
||||
selectedKey={defaultQueryResultsView}
|
||||
options={defaultQueryResultsViewOptionList}
|
||||
styles={choiceButtonStyles}
|
||||
onChange={handleOnDefaultQueryResultsViewChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="settingsSection">
|
||||
|
||||
@@ -205,6 +205,67 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<div>
|
||||
<legend
|
||||
className="settingsSectionLabel legendLabel"
|
||||
id="defaultQueryResultsView"
|
||||
>
|
||||
Default Query Results View
|
||||
</legend>
|
||||
<InfoTooltip>
|
||||
Select the default view to use when displaying query results.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<StyledChoiceGroup
|
||||
ariaLabelledBy="defaultQueryResultsView"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "vertical",
|
||||
"text": "Vertical",
|
||||
},
|
||||
Object {
|
||||
"key": "horizontal",
|
||||
"text": "Horizontal",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="vertical"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontSize": 12,
|
||||
"paddingTop": 0,
|
||||
},
|
||||
".ms-ChoiceFieldGroup root-133": Object {
|
||||
"clear": "both",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"root": Object {
|
||||
"clear": "both",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
|
||||
@@ -688,6 +688,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Create"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Create"
|
||||
@@ -695,6 +696,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="Create"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Create"
|
||||
@@ -975,6 +977,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="Create"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1257,6 +1260,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="Create"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1540,6 +1544,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="Create"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2414,6 +2419,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
aria-label="Create"
|
||||
className="ms-Button ms-Button--primary root-122"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -1258,6 +1258,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -1265,6 +1266,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -1545,6 +1547,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1827,6 +1830,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2110,6 +2114,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="OK"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2984,6 +2989,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-125"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -369,6 +369,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Add Entity"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Add Entity"
|
||||
@@ -376,6 +377,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="Add Entity"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Add Entity"
|
||||
@@ -656,6 +658,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="Add Entity"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -938,6 +941,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="Add Entity"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1221,6 +1225,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="Add Entity"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2095,6 +2100,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
aria-label="Add Entity"
|
||||
className="ms-Button ms-Button--primary root-113"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -375,6 +375,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Update"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Update"
|
||||
@@ -382,6 +383,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="Update"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="Update"
|
||||
@@ -662,6 +664,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="Update"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -944,6 +947,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="Update"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1227,6 +1231,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="Update"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2101,6 +2106,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
aria-label="Update"
|
||||
className="ms-Button ms-Button--primary root-113"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||
|
||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||
];
|
||||
|
||||
const mockVectorIndex: VectorIndex[] = [{ path: "/vector1", type: "flat" }];
|
||||
|
||||
const mockOnVectorEmbeddingChange = jest.fn();
|
||||
|
||||
describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
let component: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
component = render(
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={mockVectorEmbedding}
|
||||
vectorIndex={mockVectorIndex}
|
||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly", () => {
|
||||
expect(screen.getByText("Vector embedding 1")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("/vector1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onVectorEmbeddingChange on adding a new vector embedding", () => {
|
||||
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls onDelete when delete button is clicked", async () => {
|
||||
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||
fireEvent.click(deleteButton);
|
||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||
});
|
||||
|
||||
test("calls onVectorEmbeddingPathChange on input change", () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/newPath" } });
|
||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("validates input correctly", async () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||
).toBeInTheDocument(),
|
||||
{
|
||||
timeout: 1500,
|
||||
},
|
||||
);
|
||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
() => expect(screen.queryByText("Maximum allowed dimension for flat index is 505")).toBeInTheDocument(),
|
||||
{
|
||||
timeout: 1500,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("duplicate vector path is not allowed", async () => {
|
||||
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||
fireEvent.change(component.container.querySelector("#vector-policy-path-2"), { target: { value: "/vector1" } });
|
||||
await waitFor(() => expect(screen.queryByText("Vector embedding path is already defined")).toBeNull(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
IconButton,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
getDistanceFunctionOptions,
|
||||
getIndexTypeOptions,
|
||||
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
|
||||
export interface IAddVectorEmbeddingPolicyFormProps {
|
||||
vectorEmbedding: VectorEmbedding[];
|
||||
vectorIndex: VectorIndex[];
|
||||
onVectorEmbeddingChange: (
|
||||
vectorEmbeddings: VectorEmbedding[],
|
||||
vectorIndexingPolicies: VectorIndex[],
|
||||
validationPassed: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicyData {
|
||||
path: string;
|
||||
dataType: VectorEmbedding["dataType"];
|
||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||
dimensions: number;
|
||||
indexType: VectorIndex["type"] | "none";
|
||||
pathError: string;
|
||||
dimensionsError: string;
|
||||
}
|
||||
|
||||
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
|
||||
vectorEmbedding,
|
||||
vectorIndex,
|
||||
onVectorEmbeddingChange,
|
||||
}): JSX.Element => {
|
||||
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
if (!path) {
|
||||
error = "Vector embedding path should not be empty";
|
||||
}
|
||||
if (
|
||||
index >= 0 &&
|
||||
vectorEmbeddingPolicyData?.find(
|
||||
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||
dataIndex !== index && vectorEmbedding.path === path,
|
||||
)
|
||||
) {
|
||||
error = "Vector embedding path is already defined";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||
let error = "";
|
||||
if (dimension <= 0 || dimension > 4096) {
|
||||
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
|
||||
}
|
||||
if (indexType === "flat" && dimension > 505) {
|
||||
error = "Maximum allowed dimension for flat index is 505";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
|
||||
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||
vectorEmbedding.forEach((embedding) => {
|
||||
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
|
||||
mergedData.push({
|
||||
...embedding,
|
||||
indexType: matchingIndex?.type || "none",
|
||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||
});
|
||||
});
|
||||
return mergedData;
|
||||
};
|
||||
|
||||
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||
initializeData(vectorEmbedding, vectorIndex),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
propagateData();
|
||||
}, [vectorEmbeddingPolicyData]);
|
||||
|
||||
const propagateData = () => {
|
||||
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||
dataType: policy.dataType,
|
||||
dimensions: policy.dimensions,
|
||||
distanceFunction: policy.distanceFunction,
|
||||
path: policy.path,
|
||||
}));
|
||||
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
|
||||
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||
.map(
|
||||
(policy) =>
|
||||
({
|
||||
path: policy.path,
|
||||
type: policy.indexType,
|
||||
}) as VectorIndex,
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||
);
|
||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||
vectorEmbeddings[index].path = "/" + value;
|
||||
} else {
|
||||
vectorEmbeddings[index].path = value;
|
||||
}
|
||||
const error = onVectorEmbeddingPathError(value, index);
|
||||
vectorEmbeddings[index].pathError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value.trim()) || 0;
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].dimensions = value;
|
||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].indexType = option.key as never;
|
||||
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingPolicyChange = (
|
||||
index: number,
|
||||
option: IDropdownOption,
|
||||
property: VectorEmbeddingPolicyProperty,
|
||||
): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index][property] = option.key as never;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
setVectorEmbeddingPolicyData([
|
||||
...vectorEmbeddingPolicyData,
|
||||
{
|
||||
path: "",
|
||||
dataType: "float32",
|
||||
distanceFunction: "euclidean",
|
||||
dimensions: 0,
|
||||
indexType: "none",
|
||||
pathError: onVectorEmbeddingPathError(""),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
{vectorEmbeddingPolicyData.length > 0 &&
|
||||
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
styles={{
|
||||
root: {
|
||||
margin: "0 0 6px 20px !important",
|
||||
paddingLeft: 20,
|
||||
width: "80%",
|
||||
borderLeft: "1px solid",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
|
||||
<TextField
|
||||
id={`vector-policy-path-${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/vector1"
|
||||
styles={textFieldStyles}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||
value={vectorEmbeddingPolicy.path || ""}
|
||||
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDataTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDistanceFunctionOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
|
||||
<TextField
|
||||
id={`vector-policy-dimension-${index + 1}`}
|
||||
required={true}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onVectorEmbeddingDimensionsChange(index, event)
|
||||
}
|
||||
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getIndexTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingIndexTypeChange(index, option)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<IconButton
|
||||
id={`delete-vector-policy-${index + 1}`}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27, margin: "auto" }}
|
||||
onClick={() => onDelete(index)}
|
||||
/>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||
Add vector embedding
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IDropdownOption } from "@fluentui/react";
|
||||
|
||||
const dataTypes = ["float32", "uint8", "int8"];
|
||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||
|
||||
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
|
||||
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||
|
||||
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
|
||||
return literals.map((value) => ({
|
||||
key: value,
|
||||
text: value,
|
||||
}));
|
||||
}
|
||||
@@ -361,12 +361,15 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<span
|
||||
className="css-113"
|
||||
>
|
||||
Confirm by typing the database id
|
||||
Confirm by typing the
|
||||
Database
|
||||
id
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the database id"
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
id="confirmDatabaseId"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
@@ -379,8 +382,9 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the database id"
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
deferredValidationTime={200}
|
||||
id="confirmDatabaseId"
|
||||
onChange={[Function]}
|
||||
@@ -673,9 +677,10 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the database id"
|
||||
aria-label="Confirm by typing the Database id"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-117"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
id="confirmDatabaseId"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -711,11 +716,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<span
|
||||
className="css-126"
|
||||
>
|
||||
What is the reason why you are deleting this database?
|
||||
What is the reason why you are deleting this
|
||||
Database
|
||||
?
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
@@ -729,7 +736,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
|
||||
deferredValidationTime={200}
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
multiline={true}
|
||||
@@ -1023,7 +1030,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
|
||||
className="ms-TextField-field field-128"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
@@ -1049,6 +1056,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -1056,6 +1064,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<PrimaryButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
text="OK"
|
||||
@@ -1336,6 +1345,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1618,6 +1628,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<DefaultButton
|
||||
ariaLabel="OK"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -1901,6 +1912,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<BaseButton
|
||||
ariaLabel="OK"
|
||||
baseClassName="ms-Button"
|
||||
data-test="Panel/OkButton"
|
||||
disabled={false}
|
||||
id="sidePanelOkButton"
|
||||
onRenderDescription={[Function]}
|
||||
@@ -2775,6 +2787,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-130"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -91,6 +91,9 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Use this value to initialize the very time the component is rendered
|
||||
const RESET_INDEX = -1;
|
||||
|
||||
const filterButtonStyle: CSSProperties = {
|
||||
marginLeft: 8,
|
||||
};
|
||||
@@ -470,7 +473,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const [selectedDocumentContentBaseline, setSelectedDocumentContentBaseline] = useState<string>(undefined);
|
||||
|
||||
// Table user clicked on this row
|
||||
const [clickedRow, setClickedRow] = useState<TableRowId>(undefined);
|
||||
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
||||
// Table multiple selection
|
||||
const [selectedRows, setSelectedRows] = React.useState<Set<TableRowId>>(() => new Set<TableRowId>([0]));
|
||||
|
||||
@@ -498,6 +501,23 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}, [isFilterFocused]);
|
||||
|
||||
// Clicked row must be defined
|
||||
useEffect(() => {
|
||||
if (documentIds.length > 0) {
|
||||
let currentClickedRowIndex = clickedRowIndex;
|
||||
if (
|
||||
(currentClickedRowIndex === RESET_INDEX &&
|
||||
editorState === ViewModels.DocumentExplorerState.noDocumentSelected) ||
|
||||
currentClickedRowIndex > documentIds.length - 1
|
||||
) {
|
||||
// reset clicked row or the current clicked row is out of bounds
|
||||
currentClickedRowIndex = 0;
|
||||
setSelectedRows(new Set([0]));
|
||||
onDocumentClicked(currentClickedRowIndex, documentIds);
|
||||
}
|
||||
}
|
||||
}, [documentIds, clickedRowIndex, editorState]);
|
||||
|
||||
let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC'];
|
||||
|
||||
const applyFilterButton = {
|
||||
@@ -558,7 +578,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
if (!documentsIterator) {
|
||||
try {
|
||||
refreshDocumentsGrid();
|
||||
refreshDocumentsGrid(false);
|
||||
} catch (error) {
|
||||
if (onLoadStartKey !== null && onLoadStartKey !== undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -665,7 +685,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
setSelectedDocumentContent(defaultDocument);
|
||||
setSelectedDocumentContentBaseline(defaultDocument);
|
||||
setSelectedRows(new Set());
|
||||
setClickedRow(undefined);
|
||||
setClickedRowIndex(undefined);
|
||||
setEditorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
||||
};
|
||||
|
||||
@@ -681,8 +701,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
return createDocument(_collection, document)
|
||||
.then(
|
||||
(savedDocument: DataModels.DocumentId) => {
|
||||
// TODO: Reuse initDocumentEditor() to remove code duplication
|
||||
const value: string = renderObjectForEditor(savedDocument || {}, null, 4);
|
||||
setSelectedDocumentContentBaseline(value);
|
||||
setSelectedDocumentContent(value);
|
||||
setInitialDocumentContent(value);
|
||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
savedDocument,
|
||||
@@ -746,7 +768,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
partitionKey as PartitionKeyDefinition,
|
||||
);
|
||||
|
||||
const selectedDocumentId = documentIds[clickedRow as number];
|
||||
const selectedDocumentId = documentIds[clickedRowIndex as number];
|
||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||
|
||||
onExecutionErrorChange(false);
|
||||
@@ -794,7 +816,15 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
},
|
||||
)
|
||||
.finally(() => setIsExecuting(false));
|
||||
}, [onExecutionErrorChange, tabTitle, selectedDocumentContent, _collection, partitionKey, documentIds, clickedRow]);
|
||||
}, [
|
||||
onExecutionErrorChange,
|
||||
tabTitle,
|
||||
selectedDocumentContent,
|
||||
_collection,
|
||||
partitionKey,
|
||||
documentIds,
|
||||
clickedRowIndex,
|
||||
]);
|
||||
|
||||
const onRevertExistingDocumentClick = useCallback((): void => {
|
||||
setSelectedDocumentContentBaseline(initialDocumentContent);
|
||||
@@ -858,7 +888,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
setDocumentIds(newDocumentIds);
|
||||
|
||||
setSelectedDocumentContent(undefined);
|
||||
setClickedRow(undefined);
|
||||
setClickedRowIndex(undefined);
|
||||
setSelectedRows(new Set());
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
useDialog
|
||||
@@ -982,8 +1012,27 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
||||
setDocumentIds(newDocumentsIds);
|
||||
|
||||
if (onLoadStartKey !== null && onLoadStartKey !== undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: _collection.databaseId,
|
||||
collectionName: _collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle,
|
||||
},
|
||||
onLoadStartKey,
|
||||
);
|
||||
setOnLoadStartKey(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
let loadNextPage = useCallback(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>, applyFilterButtonClicked?: boolean): Promise<unknown> => {
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>, applyFilterButtonClicked: boolean): Promise<unknown> => {
|
||||
setIsExecuting(true);
|
||||
onExecutionErrorChange(false);
|
||||
let automaticallyCancelQueryAfterTimeout: boolean;
|
||||
@@ -1036,21 +1085,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
});
|
||||
|
||||
const merged = currentDocuments.concat(nextDocumentIds);
|
||||
setDocumentIds(merged);
|
||||
if (onLoadStartKey !== null && onLoadStartKey !== undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: _collection.databaseId,
|
||||
collectionName: _collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle,
|
||||
},
|
||||
onLoadStartKey,
|
||||
);
|
||||
setOnLoadStartKey(undefined);
|
||||
}
|
||||
updateDocumentIds(merged);
|
||||
},
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
@@ -1120,7 +1155,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const onLoadMoreKeyInput: KeyboardEventHandler<HTMLAnchorElement> = (event) => {
|
||||
if (event.key === " " || event.key === "Enter") {
|
||||
const focusElement = event.target as HTMLElement;
|
||||
loadNextPage(documentsIterator.iterator);
|
||||
loadNextPage(documentsIterator.iterator, false);
|
||||
focusElement && focusElement.focus();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -1166,10 +1201,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
* Document has been clicked on in table
|
||||
* @param tabRowId
|
||||
*/
|
||||
const onDocumentClicked = (tabRowId: TableRowId) => {
|
||||
const onDocumentClicked = (tabRowId: TableRowId, currentDocumentIds: DocumentId[]) => {
|
||||
const index = tabRowId as number;
|
||||
setClickedRow(index);
|
||||
loadDocument(documentIds[index]);
|
||||
setClickedRowIndex(index);
|
||||
loadDocument(currentDocumentIds[index]);
|
||||
};
|
||||
|
||||
let loadDocument = (documentId: DocumentId) =>
|
||||
@@ -1286,13 +1321,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
confirmDiscardingChange(() => {
|
||||
if (selectedRows.size === 0) {
|
||||
setSelectedDocumentContent(undefined);
|
||||
setClickedRow(undefined);
|
||||
setClickedRowIndex(undefined);
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
}
|
||||
|
||||
// Find if clickedRow is in selectedRows.If not, clear clickedRow and content
|
||||
if (clickedRow !== undefined && !selectedRows.has(clickedRow)) {
|
||||
setClickedRow(undefined);
|
||||
if (clickedRowIndex !== undefined && !selectedRows.has(clickedRowIndex)) {
|
||||
setClickedRowIndex(undefined);
|
||||
setSelectedDocumentContent(undefined);
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
}
|
||||
@@ -1300,6 +1335,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// If only one selection, we consider as a click
|
||||
if (selectedRows.size === 1) {
|
||||
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
||||
onDocumentClicked(selectedRows.values().next().value, documentIds);
|
||||
}
|
||||
|
||||
setSelectedRows(selectedRows);
|
||||
@@ -1460,6 +1496,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
const value: string = renderObjectForEditor(savedDocument || {}, null, 4);
|
||||
setSelectedDocumentContentBaseline(value);
|
||||
setSelectedDocumentContent(value);
|
||||
setInitialDocumentContent(value);
|
||||
|
||||
setDocumentIds(ids);
|
||||
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
||||
@@ -1510,7 +1548,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
tabTitle,
|
||||
});
|
||||
|
||||
const selectedDocumentId = documentIds[clickedRow as number];
|
||||
const selectedDocumentId = documentIds[clickedRowIndex as number];
|
||||
return MongoProxyClient.updateDocument(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
@@ -1578,8 +1616,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
.then(
|
||||
({ continuationToken: newContinuationToken, documents }) => {
|
||||
setContinuationToken(newContinuationToken);
|
||||
let currentDocuments = documentIds;
|
||||
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
|
||||
const currentDocumentsRids = documentIds.map((currentDocument) => currentDocument.rid);
|
||||
const nextDocumentIds = documents
|
||||
.filter((d: { _rid: string }) => {
|
||||
return currentDocumentsRids.indexOf(d._rid) < 0;
|
||||
@@ -1588,34 +1625,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
.map((rawDocument: any) => {
|
||||
const partitionKeyValue = rawDocument._partitionKeyValue;
|
||||
return newDocumentId(rawDocument, partitionKeyProperties, [partitionKeyValue]);
|
||||
// return new DocumentId(this, rawDocument, [partitionKeyValue]);
|
||||
});
|
||||
|
||||
const merged = currentDocuments.concat(nextDocumentIds);
|
||||
|
||||
setDocumentIds(merged);
|
||||
currentDocuments = merged;
|
||||
|
||||
if (filterContent.length > 0 && currentDocuments.length > 0) {
|
||||
currentDocuments[0].click();
|
||||
} else {
|
||||
setSelectedDocumentContent("");
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
}
|
||||
if (_onLoadStartKey !== null && _onLoadStartKey !== undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: _collection.databaseId,
|
||||
collectionName: _collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle,
|
||||
},
|
||||
_onLoadStartKey,
|
||||
);
|
||||
setOnLoadStartKey(undefined);
|
||||
}
|
||||
const merged = documentIds.concat(nextDocumentIds);
|
||||
updateDocumentIds(merged);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(error: any) => {
|
||||
@@ -1643,7 +1656,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// ***************** Mongo ***************************
|
||||
|
||||
const refreshDocumentsGrid = useCallback(
|
||||
async (applyFilterButtonPressed?: boolean): Promise<void> => {
|
||||
(applyFilterButtonPressed: boolean): void => {
|
||||
// clear documents grid
|
||||
setDocumentIds([]);
|
||||
setContinuationToken(undefined); // For mongo
|
||||
@@ -1657,6 +1670,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// collapse filter
|
||||
setAppliedFilter(filterContent);
|
||||
setIsFilterExpanded(false);
|
||||
|
||||
// If apply filter is pressed, reset current selected document
|
||||
if (applyFilterButtonPressed) {
|
||||
setClickedRowIndex(RESET_INDEX);
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
setSelectedDocumentContent(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
|
||||
@@ -1804,7 +1824,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
>
|
||||
<DocumentsTableComponent
|
||||
items={tableItems}
|
||||
onItemClicked={onDocumentClicked}
|
||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
selectedRows={selectedRows}
|
||||
size={tableContainerSizePx}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { NormalizedEventKey } from "Common/Constants";
|
||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||
|
||||
export type DocumentsTableComponentItem = {
|
||||
@@ -62,7 +62,6 @@ const MIN_COLUMN_WIDTH_PX = 50;
|
||||
|
||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||
items,
|
||||
onItemClicked,
|
||||
onSelectedRowsChange,
|
||||
selectedRows,
|
||||
style,
|
||||
@@ -70,8 +69,6 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
columnsDefinition,
|
||||
isSelectionDisabled,
|
||||
}: IDocumentsTableComponentProps) => {
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(undefined);
|
||||
|
||||
const initialSizingOptions: TableColumnSizingOptions = {};
|
||||
columnsDefinition.forEach((column) => {
|
||||
initialSizingOptions[column.id] = {
|
||||
@@ -232,18 +229,6 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
[toggleAllRows],
|
||||
);
|
||||
|
||||
// Load document depending on selection
|
||||
useEffect(() => {
|
||||
if (selectedRows.size === 1 && items.length > 0) {
|
||||
const newActiveItemIndex = selectedRows.values().next().value;
|
||||
if (newActiveItemIndex !== activeItemIndex) {
|
||||
onItemClicked(newActiveItemIndex);
|
||||
setActiveItemIndex(newActiveItemIndex);
|
||||
setSelectionStartIndex(newActiveItemIndex);
|
||||
}
|
||||
}
|
||||
}, [selectedRows, items, activeItemIndex, onItemClicked]);
|
||||
|
||||
// Cell keyboard navigation
|
||||
const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-console */
|
||||
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
||||
import { SplitterDirection } from "Common/Splitter";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||
@@ -12,7 +13,13 @@ import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
import {
|
||||
LocalStorageUtility,
|
||||
StorageKey,
|
||||
getDefaultQueryResultsView,
|
||||
getRUThreshold,
|
||||
ruThresholdEnabled,
|
||||
} from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { TabsState, useTabs } from "hooks/useTabs";
|
||||
@@ -25,6 +32,7 @@ import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import CheckIcon from "../../../../images/check-1.svg";
|
||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
@@ -103,6 +111,7 @@ interface IQueryTabStates {
|
||||
cancelQueryTimeoutID: NodeJS.Timeout;
|
||||
copilotActive: boolean;
|
||||
currentTabActive: boolean;
|
||||
queryResultsView: SplitterDirection;
|
||||
}
|
||||
|
||||
export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => {
|
||||
@@ -147,6 +156,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
cancelQueryTimeoutID: undefined,
|
||||
copilotActive: this._queryCopilotActive(),
|
||||
currentTabActive: true,
|
||||
queryResultsView: getDefaultQueryResultsView(),
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
@@ -508,9 +518,45 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push(this.createViewButtons());
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private createViewButtons(): CommandButtonComponentProps {
|
||||
const verticalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Vertical,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Vertical ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Vertical",
|
||||
ariaLabel: "Vertical",
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Vertical),
|
||||
hasPopup: false,
|
||||
};
|
||||
const horizontalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Horizontal,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Horizontal ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Horizontal",
|
||||
ariaLabel: "Horizontal",
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal),
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
return {
|
||||
commandButtonLabel: "View",
|
||||
ariaLabel: "View",
|
||||
hasPopup: true,
|
||||
children: [verticalButton, horizontalButton],
|
||||
};
|
||||
}
|
||||
private _setViewLayout(direction: SplitterDirection): void {
|
||||
this.setState({ queryResultsView: direction });
|
||||
|
||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||
setTimeout(() => {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private _toggleCopilot = (active: boolean) => {
|
||||
this.setState({ copilotActive: active });
|
||||
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||
@@ -634,7 +680,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
<div className="tabPaneContentContainer">
|
||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||
<SplitterLayout
|
||||
vertical={this.state.queryResultsView === SplitterDirection.Vertical}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={100}
|
||||
secondaryMinSize={200}
|
||||
>
|
||||
<Fragment>
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
|
||||
return (
|
||||
<>
|
||||
<FluentProvider theme={lightTheme} style={{ overflow: "hidden" }}>
|
||||
<FluentProvider theme={lightTheme} style={{ overflow: "auto" }}>
|
||||
<Tree
|
||||
aria-label="CosmosDB resources"
|
||||
openItems={openItems}
|
||||
|
||||
Reference in New Issue
Block a user