mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-10 10:15:13 +00:00
added pin(fav) and sorting in local in sidebar tree of DE
This commit is contained in:
3
images/Pin.svg
Normal file
3
images/Pin.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.25 1.5C9.25 1.22386 9.47386 1 9.75 1H10.25C10.5261 1 10.75 1.22386 10.75 1.5V5.5L13 7.5V9H8.75V14L8 15L7.25 14V9H3V7.5L5.25 5.5V1.5C5.25 1.22386 5.47386 1 5.75 1H6.25C6.52614 1 6.75 1.22386 6.75 1.5V5.25H9.25V1.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
@@ -23,7 +23,9 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg";
|
||||
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import PinIcon from "../../images/Pin.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
@@ -35,7 +37,6 @@ import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
|
||||
export interface CollectionContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
@@ -52,8 +53,14 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
const isPinned = useDatabases.getState().isPinned(databaseId);
|
||||
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: PinIcon,
|
||||
onClick: () => useDatabases.getState().togglePinDatabase(databaseId),
|
||||
label: isPinned ? "Unpin from top" : "Pin to top",
|
||||
},
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked({ databaseId }),
|
||||
@@ -76,13 +83,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
useSidePanel.getState().getRef = lastFocusedElement;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
@@ -175,13 +182,13 @@ export const createCollectionContextMenuButton = (
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
useSidePanel.getState().getRef = lastFocusedElement;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
|
||||
@@ -167,7 +167,7 @@ export function createContextCommandBarButtons(
|
||||
|
||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [
|
||||
ThemeToggleButton(),
|
||||
...(configContext.platform !== Platform.Portal ? [ThemeToggleButton()] : []),
|
||||
{
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: "Settings",
|
||||
|
||||
@@ -17,6 +17,24 @@ import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
const themedTextFieldStyles = {
|
||||
fieldGroup: {
|
||||
width: 300,
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
borderColor: "var(--colorNeutralStroke1)",
|
||||
selectors: {
|
||||
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
|
||||
},
|
||||
},
|
||||
field: {
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
},
|
||||
subComponentStyles: {
|
||||
label: { root: { color: "var(--colorNeutralForeground1)" } },
|
||||
},
|
||||
};
|
||||
|
||||
export interface DeleteCollectionConfirmationPaneProps {
|
||||
refreshDatabases: () => Promise<void>;
|
||||
}
|
||||
@@ -111,18 +129,21 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
};
|
||||
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Confirm by typing the {collectionName.toLowerCase()} id
|
||||
</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
value={inputCollectionName}
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
styles={themedTextFieldStyles}
|
||||
onChange={(event, newInput?: string) => {
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
@@ -132,15 +153,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
What is the reason why you are deleting this {collectionName}?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
styles={themedTextFieldStyles}
|
||||
multiline
|
||||
value={deleteCollectionFeedback}
|
||||
rows={3}
|
||||
|
||||
@@ -29,10 +29,20 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-109"
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
>
|
||||
Confirm by typing the
|
||||
container
|
||||
@@ -47,9 +57,27 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
required={true}
|
||||
styles={
|
||||
{
|
||||
"field": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
"fieldGroup": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"borderColor": "var(--colorNeutralStroke1)",
|
||||
"selectors": {
|
||||
":hover": {
|
||||
"borderColor": "var(--colorNeutralStroke1Hover)",
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
"subComponentStyles": {
|
||||
"label": {
|
||||
"root": {
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
|
||||
@@ -19,6 +19,24 @@ import { useSelectedNode } from "../useSelectedNode";
|
||||
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
||||
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
|
||||
|
||||
const themedTextFieldStyles = {
|
||||
fieldGroup: {
|
||||
width: 300,
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
borderColor: "var(--colorNeutralStroke1)",
|
||||
selectors: {
|
||||
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
|
||||
},
|
||||
},
|
||||
field: {
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
},
|
||||
subComponentStyles: {
|
||||
label: { root: { color: "var(--colorNeutralForeground1)" } },
|
||||
},
|
||||
};
|
||||
|
||||
interface DeleteDatabaseConfirmationPanelProps {
|
||||
refreshDatabases: () => Promise<void>;
|
||||
}
|
||||
@@ -132,12 +150,12 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">{confirmDatabase}</Text>
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>{confirmDatabase}</Text>
|
||||
<TextField
|
||||
id="confirmDatabaseId"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
styles={themedTextFieldStyles}
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
@@ -147,15 +165,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
<div className="deleteDatabaseFeedback">
|
||||
<Text variant="small" block>
|
||||
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
What is the reason why you are deleting this {getDatabaseName()}?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
styles={themedTextFieldStyles}
|
||||
multiline
|
||||
rows={3}
|
||||
onChange={(event, newInput?: string) => {
|
||||
|
||||
@@ -356,10 +356,20 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-113"
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
>
|
||||
Confirm by typing the Database id (name)
|
||||
</span>
|
||||
@@ -373,9 +383,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
required={true}
|
||||
styles={
|
||||
{
|
||||
"field": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
"fieldGroup": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"borderColor": "var(--colorNeutralStroke1)",
|
||||
"selectors": {
|
||||
":hover": {
|
||||
"borderColor": "var(--colorNeutralStroke1Hover)",
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
"subComponentStyles": {
|
||||
"label": {
|
||||
"root": {
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -699,20 +727,40 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<Text
|
||||
block={true}
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-126"
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
block={true}
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-126"
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
>
|
||||
What is the reason why you are deleting this
|
||||
Database
|
||||
@@ -727,9 +775,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
rows={3}
|
||||
styles={
|
||||
{
|
||||
"field": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
"fieldGroup": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"borderColor": "var(--colorNeutralStroke1)",
|
||||
"selectors": {
|
||||
":hover": {
|
||||
"borderColor": "var(--colorNeutralStroke1Hover)",
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
"subComponentStyles": {
|
||||
"label": {
|
||||
"root": {
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Input, Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||
import { Home16Regular, Search20Regular } from "@fluentui/react-icons";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Tree,
|
||||
TreeItemValue,
|
||||
TreeOpenChangeData,
|
||||
TreeOpenChangeEvent,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowSortDown20Regular,
|
||||
ArrowSortUp20Regular,
|
||||
Home16Regular,
|
||||
Search20Regular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { AuthType } from "AuthType";
|
||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
@@ -57,6 +69,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
const databasesFetchedSuccessfully = useDatabases((state) => state.databasesFetchedSuccessfully);
|
||||
const searchText = useDatabases((state) => state.searchText);
|
||||
const setSearchText = useDatabases((state) => state.setSearchText);
|
||||
const sortOrder = useDatabases((state) => state.sortOrder);
|
||||
const setSortOrder = useDatabases((state) => state.setSortOrder);
|
||||
const pinnedDatabaseIds = useDatabases((state) => state.pinnedDatabaseIds);
|
||||
const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({
|
||||
isCopilotEnabled: state.copilotEnabled,
|
||||
isCopilotSampleDBEnabled: state.copilotSampleDBEnabled,
|
||||
@@ -65,8 +80,24 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
const databaseTreeNodes = useMemo(() => {
|
||||
return userContext.authType === AuthType.ResourceToken
|
||||
? createResourceTokenTreeNodes(resourceTokenCollection)
|
||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab, searchText);
|
||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab, searchText]);
|
||||
: createDatabaseTreeNodes(
|
||||
explorer,
|
||||
isNotebookEnabled,
|
||||
databases,
|
||||
refreshActiveTab,
|
||||
searchText,
|
||||
sortOrder,
|
||||
pinnedDatabaseIds,
|
||||
);
|
||||
}, [
|
||||
resourceTokenCollection,
|
||||
databases,
|
||||
isNotebookEnabled,
|
||||
refreshActiveTab,
|
||||
searchText,
|
||||
sortOrder,
|
||||
pinnedDatabaseIds,
|
||||
]);
|
||||
|
||||
const isSampleDataEnabled =
|
||||
isCopilotEnabled &&
|
||||
@@ -80,22 +111,26 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
: [];
|
||||
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
||||
|
||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "home",
|
||||
iconSrc: <Home16Regular />,
|
||||
label: "Home",
|
||||
isSelected: () =>
|
||||
useSelectedNode.getState().selectedNode === undefined &&
|
||||
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
||||
},
|
||||
},
|
||||
];
|
||||
const headerNodes: TreeNode[] = useMemo(
|
||||
() =>
|
||||
isFabricMirrored()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "home",
|
||||
iconSrc: <Home16Regular />,
|
||||
label: "Home",
|
||||
isSelected: () =>
|
||||
useSelectedNode.getState().selectedNode === undefined &&
|
||||
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const rootNodes: TreeNode[] = useMemo(() => {
|
||||
if (sampleDataNodes.length > 0) {
|
||||
@@ -116,54 +151,60 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
} else {
|
||||
return [...headerNodes, ...databaseTreeNodes];
|
||||
}
|
||||
}, [databaseTreeNodes, sampleDataNodes]);
|
||||
}, [headerNodes, databaseTreeNodes, sampleDataNodes]);
|
||||
|
||||
// Track complete DatabaseLoad scenario (start, tree rendered, interactive)
|
||||
useDatabaseLoadScenario(databaseTreeNodes, databasesFetchedSuccessfully);
|
||||
|
||||
useEffect(() => {
|
||||
// Compute open items based on node.isExpanded
|
||||
const updateOpenItems = (node: TreeNode, parentNodeId: string): void => {
|
||||
// This will look for ANY expanded node, event if its parent node isn't expanded
|
||||
// and add it to the openItems list
|
||||
const expandedIds: TreeItemValue[] = [];
|
||||
const collectExpandedIds = (node: TreeNode, parentNodeId: string | undefined): void => {
|
||||
const globalId = parentNodeId === undefined ? node.label : `${parentNodeId}/${node.label}`;
|
||||
|
||||
if (node.isExpanded) {
|
||||
let found = false;
|
||||
for (const id of openItems) {
|
||||
if (id === globalId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
setOpenItems((prevOpenItems) => [...prevOpenItems, globalId]);
|
||||
}
|
||||
expandedIds.push(globalId);
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
updateOpenItems(child, globalId);
|
||||
collectExpandedIds(child, globalId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
rootNodes.forEach((n) => updateOpenItems(n, undefined));
|
||||
}, [rootNodes, openItems, setOpenItems]);
|
||||
rootNodes.forEach((n) => collectExpandedIds(n, undefined));
|
||||
|
||||
if (expandedIds.length > 0) {
|
||||
setOpenItems((prevOpenItems) => {
|
||||
const prevSet = new Set(prevOpenItems);
|
||||
const newIds = expandedIds.filter((id) => !prevSet.has(id));
|
||||
return newIds.length > 0 ? [...prevOpenItems, ...newIds] : prevOpenItems;
|
||||
});
|
||||
}
|
||||
}, [rootNodes]);
|
||||
|
||||
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) =>
|
||||
setOpenItems(Array.from(data.openItems));
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
setSortOrder(sortOrder === "az" ? "za" : "az");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={treeStyles.treeContainer}>
|
||||
{userContext.authType !== AuthType.ResourceToken && databases.length > 0 && (
|
||||
<div style={{ padding: "8px" }}>
|
||||
<div style={{ padding: "8px", display: "flex", gap: "4px", alignItems: "center" }}>
|
||||
<Input
|
||||
placeholder="Search databases only"
|
||||
value={searchText}
|
||||
onChange={(_, data) => setSearchText(data?.value || "")}
|
||||
size="small"
|
||||
contentBefore={<Search20Regular />}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
size="small"
|
||||
icon={sortOrder === "az" ? <ArrowSortDown20Regular /> : <ArrowSortUp20Regular />}
|
||||
onClick={toggleSortOrder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, Pin16Filled, SettingsRegular } from "@fluentui/react-icons";
|
||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { DatabaseSortOrder, useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
@@ -27,7 +27,10 @@ export const shouldShowScriptNodes = (): boolean => {
|
||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||
const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
||||
const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />;
|
||||
|
||||
const pinnedIconStyle: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: "2px" };
|
||||
const pinnedBadgeStyle: React.CSSProperties = { color: "#0078D4" };
|
||||
|
||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||
const updatedSampleTree: TreeNode = {
|
||||
@@ -132,13 +135,28 @@ export const createDatabaseTreeNodes = (
|
||||
databases: ViewModels.Database[],
|
||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
||||
searchText = "",
|
||||
sortOrder: DatabaseSortOrder = "az",
|
||||
pinnedDatabaseIds: Set<string> = new Set(),
|
||||
): TreeNode[] => {
|
||||
// Filter databases based on search text
|
||||
// Filter databases based on search text (cache lowercase to avoid repeated conversion)
|
||||
const lowerSearch = searchText.toLowerCase();
|
||||
const filteredDatabases = searchText
|
||||
? databases.filter((db) => db.id().toLowerCase().includes(searchText.toLowerCase()))
|
||||
? databases.filter((db) => db.id().toLowerCase().includes(lowerSearch))
|
||||
: databases;
|
||||
|
||||
const databaseTreeNodes: TreeNode[] = filteredDatabases.map((database: ViewModels.Database) => {
|
||||
// Sort: pinned first, then by name (A-Z or Z-A) within each group
|
||||
const orderedDatabases = [...filteredDatabases].sort((first, second) => {
|
||||
const isFirstPinned = pinnedDatabaseIds.has(first.id());
|
||||
const isSecondPinned = pinnedDatabaseIds.has(second.id());
|
||||
if (isFirstPinned !== isSecondPinned) return isFirstPinned ? -1 : 1;
|
||||
const firstName = first.id();
|
||||
const secondName = second.id();
|
||||
return sortOrder === "az"
|
||||
? firstName.localeCompare(secondName, undefined, { sensitivity: "base" })
|
||||
: secondName.localeCompare(firstName, undefined, { sensitivity: "base" });
|
||||
});
|
||||
|
||||
const databaseTreeNodes: TreeNode[] = orderedDatabases.map((database: ViewModels.Database) => {
|
||||
const buildDatabaseChildNodes = (databaseNode: TreeNode) => {
|
||||
databaseNode.children = [];
|
||||
if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) {
|
||||
@@ -176,13 +194,24 @@ export const createDatabaseTreeNodes = (
|
||||
}
|
||||
};
|
||||
|
||||
const isPinned = pinnedDatabaseIds.has(database.id());
|
||||
|
||||
const databaseIcon = isPinned ? (
|
||||
<span style={pinnedIconStyle}>
|
||||
<DatabaseRegular fontSize={16} />
|
||||
<Pin16Filled fontSize={10} style={pinnedBadgeStyle} />
|
||||
</span>
|
||||
) : (
|
||||
TreeDatabaseIcon
|
||||
);
|
||||
|
||||
const databaseNode: TreeNode = {
|
||||
label: database.id(),
|
||||
className: "databaseNode",
|
||||
children: [],
|
||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||
iconSrc: TreeDatabaseIcon,
|
||||
iconSrc: databaseIcon,
|
||||
onExpanded: async () => {
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||
@@ -198,7 +227,6 @@ export const createDatabaseTreeNodes = (
|
||||
isExpanded: database.isDatabaseExpanded(),
|
||||
onCollapsed: () => {
|
||||
database.collapseDatabase();
|
||||
// useCommandBar.getState().setContextButtons([]);
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
},
|
||||
};
|
||||
@@ -248,13 +276,13 @@ export const buildCollectionNode = (
|
||||
(tab: TabsBase) =>
|
||||
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId,
|
||||
);
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
|
||||
// If we're showing script nodes, start loading them.
|
||||
// If we're showing script nodes, start loading them in parallel.
|
||||
if (shouldShowScriptNodes()) {
|
||||
await collection.loadStoredProcedures();
|
||||
await collection.loadUserDefinedFunctions();
|
||||
await collection.loadTriggers();
|
||||
await Promise.all([
|
||||
collection.loadStoredProcedures(),
|
||||
collection.loadUserDefinedFunctions(),
|
||||
collection.loadTriggers(),
|
||||
]);
|
||||
}
|
||||
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
@@ -263,7 +291,6 @@ export const buildCollectionNode = (
|
||||
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
|
||||
onCollapsed: () => {
|
||||
collection.collapseCollection();
|
||||
// useCommandBar.getState().setContextButtons([]);
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
},
|
||||
isExpanded: collection.isCollectionExpanded(),
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import _ from "underscore";
|
||||
import create, { UseStore } from "zustand";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import * as LocalStorageUtility from "../Shared/LocalStorageUtility";
|
||||
import { StorageKey } from "../Shared/StorageUtility";
|
||||
import { userContext } from "../UserContext";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
|
||||
export type DatabaseSortOrder = "az" | "za";
|
||||
|
||||
interface DatabasesState {
|
||||
databases: ViewModels.Database[];
|
||||
resourceTokenCollection: ViewModels.CollectionBase;
|
||||
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
||||
databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful
|
||||
searchText: string;
|
||||
sortOrder: DatabaseSortOrder;
|
||||
pinnedDatabaseIds: Set<string>;
|
||||
setSearchText: (searchText: string) => void;
|
||||
setSortOrder: (sortOrder: DatabaseSortOrder) => void;
|
||||
togglePinDatabase: (databaseId: string) => void;
|
||||
isPinned: (databaseId: string) => boolean;
|
||||
updateDatabase: (database: ViewModels.Database) => void;
|
||||
addDatabases: (databases: ViewModels.Database[]) => void;
|
||||
deleteDatabase: (database: ViewModels.Database) => void;
|
||||
@@ -29,13 +37,41 @@ interface DatabasesState {
|
||||
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const loadPinnedDatabases = (): Set<string> => {
|
||||
const stored = LocalStorageUtility.getEntryObject<string[]>(StorageKey.PinnedDatabases);
|
||||
return new Set(Array.isArray(stored) ? stored : []);
|
||||
};
|
||||
|
||||
const loadSortOrder = (): DatabaseSortOrder => {
|
||||
const stored = LocalStorageUtility.getEntryString(StorageKey.DatabaseSortOrder);
|
||||
return stored === "az" || stored === "za" ? stored : "az";
|
||||
};
|
||||
|
||||
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
databases: [],
|
||||
resourceTokenCollection: undefined,
|
||||
sampleDataResourceTokenCollection: undefined,
|
||||
databasesFetchedSuccessfully: false,
|
||||
searchText: "",
|
||||
sortOrder: loadSortOrder(),
|
||||
pinnedDatabaseIds: loadPinnedDatabases(),
|
||||
setSearchText: (searchText: string) => set({ searchText }),
|
||||
setSortOrder: (sortOrder: DatabaseSortOrder) => {
|
||||
LocalStorageUtility.setEntryString(StorageKey.DatabaseSortOrder, sortOrder);
|
||||
set({ sortOrder });
|
||||
},
|
||||
togglePinDatabase: (databaseId: string) => {
|
||||
const current = get().pinnedDatabaseIds;
|
||||
const updated = new Set(current);
|
||||
if (updated.has(databaseId)) {
|
||||
updated.delete(databaseId);
|
||||
} else {
|
||||
updated.add(databaseId);
|
||||
}
|
||||
LocalStorageUtility.setEntryObject(StorageKey.PinnedDatabases, [...updated]);
|
||||
set({ pinnedDatabaseIds: updated });
|
||||
},
|
||||
isPinned: (databaseId: string) => get().pinnedDatabaseIds.has(databaseId),
|
||||
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
||||
set((state) => {
|
||||
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
||||
@@ -49,29 +85,27 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
}),
|
||||
addDatabases: (databases: ViewModels.Database[]) =>
|
||||
set((state) => ({
|
||||
databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())),
|
||||
databases: [...state.databases, ...databases],
|
||||
})),
|
||||
deleteDatabase: (database: ViewModels.Database) =>
|
||||
set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })),
|
||||
set((state) => {
|
||||
const updated = new Set(state.pinnedDatabaseIds);
|
||||
if (updated.delete(database.id())) {
|
||||
LocalStorageUtility.setEntryObject(StorageKey.PinnedDatabases, [...updated]);
|
||||
}
|
||||
return {
|
||||
databases: state.databases.filter((db) => database.id() !== db.id()),
|
||||
pinnedDatabaseIds: updated,
|
||||
};
|
||||
}),
|
||||
clearDatabases: () => set(() => ({ databases: [] })),
|
||||
isSaveQueryEnabled: () => {
|
||||
const savedQueriesDatabase: ViewModels.Database = _.find(
|
||||
get().databases,
|
||||
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName,
|
||||
const savedQueriesDatabase = get().databases.find(
|
||||
(database) => database.id() === Constants.SavedQueries.DatabaseName,
|
||||
);
|
||||
if (!savedQueriesDatabase) {
|
||||
return false;
|
||||
}
|
||||
const savedQueriesCollection: ViewModels.Collection =
|
||||
savedQueriesDatabase &&
|
||||
_.find(
|
||||
savedQueriesDatabase.collections(),
|
||||
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName,
|
||||
);
|
||||
if (!savedQueriesCollection) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !!savedQueriesDatabase
|
||||
?.collections()
|
||||
?.find((collection) => collection.id() === Constants.SavedQueries.CollectionName);
|
||||
},
|
||||
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
||||
return isSampleDatabase === undefined
|
||||
@@ -105,43 +139,27 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
},
|
||||
loadDatabaseOffers: async () => {
|
||||
await Promise.all(
|
||||
get().databases?.map(async (database: ViewModels.Database) => {
|
||||
await database.loadOffer();
|
||||
}),
|
||||
get().databases.map((database: ViewModels.Database) => database.loadOffer()),
|
||||
);
|
||||
},
|
||||
loadAllOffers: async () => {
|
||||
await Promise.all(
|
||||
get().databases?.map(async (database: ViewModels.Database) => {
|
||||
await database.loadOffer();
|
||||
await database.loadCollections();
|
||||
get().databases.map(async (database: ViewModels.Database) => {
|
||||
await Promise.all([database.loadOffer(), database.loadCollections()]);
|
||||
await Promise.all(
|
||||
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
|
||||
await collection.loadOffer();
|
||||
}),
|
||||
(database.collections() || []).map((collection: ViewModels.Collection) => collection.loadOffer()),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
isFirstResourceCreated: () => {
|
||||
const databases = get().databases;
|
||||
|
||||
if (!databases || databases.length === 0) {
|
||||
if (databases.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return databases.some((database) => {
|
||||
// user has created at least one collection
|
||||
if (database.collections()?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// user has created a database with shared throughput
|
||||
if (database.offer()) {
|
||||
return true;
|
||||
}
|
||||
// use has created an empty database without shared throughput
|
||||
return false;
|
||||
});
|
||||
return databases.some(
|
||||
(database) => database.collections()?.length > 0 || !!database.offer(),
|
||||
);
|
||||
},
|
||||
findSelectedDatabase: (): ViewModels.Database => {
|
||||
const selectedNode = useSelectedNode.getState().selectedNode;
|
||||
@@ -149,7 +167,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
return undefined;
|
||||
}
|
||||
if (selectedNode.nodeKind === "Database") {
|
||||
return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id());
|
||||
return get().databases.find((database) => database.id() === selectedNode.id());
|
||||
}
|
||||
|
||||
if (selectedNode.nodeKind === "Collection") {
|
||||
|
||||
34
src/Main.tsx
34
src/Main.tsx
@@ -82,6 +82,32 @@ const useStyles = makeStyles({
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
splashContainer: {
|
||||
zIndex: 5,
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
opacity: "0.7",
|
||||
},
|
||||
splashContent: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
textAlign: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
splashTitle: {
|
||||
fontSize: "13px",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
margin: "6px 6px 12px 6px",
|
||||
},
|
||||
splashText: {
|
||||
marginTop: "12px",
|
||||
color: "var(--colorNeutralForeground2)",
|
||||
},
|
||||
});
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
@@ -234,15 +260,15 @@ function LoadingExplorer(): JSX.Element {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="splashLoaderContainer">
|
||||
<div className="splashLoaderContentContainer">
|
||||
<div className={styles.splashContainer}>
|
||||
<div className={styles.splashContent}>
|
||||
<p className="connectExplorerContent">
|
||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||
<p className={styles.splashTitle} id="explorerLoadingStatusTitle">
|
||||
Welcome to Azure Cosmos DB
|
||||
</p>
|
||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||
<p className={styles.splashText} id="explorerLoadingStatusText" role="alert">
|
||||
Connecting...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,8 @@ export enum StorageKey {
|
||||
AppState,
|
||||
MongoGuidRepresentation,
|
||||
IgnorePartitionKeyOnDocumentUpdate,
|
||||
PinnedDatabases,
|
||||
DatabaseSortOrder,
|
||||
}
|
||||
|
||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user