Enable column selection and sorting in DocumentsTab (with persistence) (#1881)
* Initial implementation of saving split value to local storage
* Make table columns generic (no more id and partition keys)
* Save column width
* Add column selection from right-click
* Implement new menu for column selection with search.
* Switch icons and search compare with lowercase.
* Search uses string includes instead of startsWith
* Only allow data fields that can be rendered (string and numbers) in column selection
* Accumulate properties rather than replace for column definitions
* Do not allow deselecting all columns
* Move table values under its own property
* Update choices of column when creating new or updating document
* Rework column selection UI
* Fix table size issue with some heuristics
* Fix heuristic for size update
* Don't allow unselecting last column
* Implement column sorting
* Fix format
* Fix format, update snapshots
* Add reset button to column selection and fix naming of openUploadItemsPanePane()
* Fix unit tests
* Fix unit test
* Persist column selection
* Persist column sorting
* Save columns definition (schema) along with selected columns.
* Merge branch 'master' into users/languy/save-documentstab-prefs
* Revert "Merge branch 'master' into users/languy/save-documentstab-prefs"
This reverts commit e5a82fd356
.
* Disable column selection for Mongo. Remove extra refresh button
* Update test snapshots
* Remove unused function
* Fix table width
* Add background color to "..." button for column selection
* Label to indicate which field is a partition key in Column Selection Pane
* Update unit test snapshot
* Move column selection and sorting behind feature flag enableDocumentsTableColumnSelection
* Cleanup checkbox styles
This commit is contained in:
parent
1be221e106
commit
7e95f5d8c8
|
@ -2527,13 +2527,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
|
||||||
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
|
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
"@babel/helper-define-polyfill-provider": "^0.6.2",
|
||||||
"core-js-compat": "^3.36.1"
|
"core-js-compat": "^3.38.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
|
@ -2932,10 +2932,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.3"
|
"@floating-ui/utils": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/devtools": {
|
"node_modules/@floating-ui/devtools": {
|
||||||
|
@ -2945,15 +2945,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/dom": {
|
"node_modules/@floating-ui/dom": {
|
||||||
"version": "1.6.6",
|
"version": "1.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.0.0",
|
"@floating-ui/core": "^1.0.0",
|
||||||
"@floating-ui/utils": "^0.2.3"
|
"@floating-ui/utils": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/date-time-utilities": {
|
"node_modules/@fluentui/date-time-utilities": {
|
||||||
|
@ -3501,7 +3501,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
||||||
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-window-provider": "^2.2.27",
|
"@fluentui/react-window-provider": "^2.2.28",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"@fluentui/utilities": "^8.15.13",
|
"@fluentui/utilities": "^8.15.13",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
|
@ -4426,9 +4426,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/react-window-provider": {
|
"node_modules/@fluentui/react-window-provider": {
|
||||||
"version": "2.2.27",
|
"version": "2.2.28",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.27.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz",
|
||||||
"integrity": "sha512-Dg0G9bizjryV0Q/r0CPtCVTPa2II/EsT9E6JT3jPSALjQADDLlW4/+ZXbcEC7geZ/40+KpZDmhplvk/AJSFBKg==",
|
"integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
|
@ -4512,7 +4512,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/dom-utilities": "^2.3.7",
|
"@fluentui/dom-utilities": "^2.3.7",
|
||||||
"@fluentui/merge-styles": "^8.6.12",
|
"@fluentui/merge-styles": "^8.6.12",
|
||||||
"@fluentui/react-window-provider": "^2.2.27",
|
"@fluentui/react-window-provider": "^2.2.28",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
@ -14966,9 +14966,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.23.2",
|
"version": "4.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||||
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
|
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -14984,9 +14984,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001640",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"electron-to-chromium": "^1.4.820",
|
"electron-to-chromium": "^1.5.4",
|
||||||
"node-releases": "^2.0.14",
|
"node-releases": "^2.0.18",
|
||||||
"update-browserslist-db": "^1.1.0"
|
"update-browserslist-db": "^1.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -15142,9 +15142,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001645",
|
"version": "1.0.30001651",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
|
||||||
"integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==",
|
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -16063,12 +16063,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.37.1",
|
"version": "3.38.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
|
||||||
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
"integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.23.0"
|
"browserslist": "^4.23.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
|
@ -1119,7 +1119,7 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUploadItemsPanePane(): void {
|
public openUploadItemsPane(): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||||
}
|
}
|
||||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxOnChangeData,
|
||||||
|
InputOnChangeData,
|
||||||
|
makeStyles,
|
||||||
|
SearchBox,
|
||||||
|
SearchBoxChangeEvent,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
|
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import React from "react";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
|
||||||
|
const useColumnSelectionStyles = makeStyles({
|
||||||
|
paneContainer: {
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
searchBox: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
checkboxContainer: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
checkboxLabel: {
|
||||||
|
padding: "4px 8px",
|
||||||
|
marginBottom: "0px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export interface TableColumnSelectionPaneProps {
|
||||||
|
columnDefinitions: ColumnDefinition[];
|
||||||
|
selectedColumnIds: string[];
|
||||||
|
onSelectionChange: (newSelectedColumnIds: string[]) => void;
|
||||||
|
defaultSelection: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> = ({
|
||||||
|
columnDefinitions,
|
||||||
|
selectedColumnIds,
|
||||||
|
onSelectionChange,
|
||||||
|
defaultSelection,
|
||||||
|
}: TableColumnSelectionPaneProps): JSX.Element => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
const originalSelectedColumnIds = React.useMemo(() => selectedColumnIds, []);
|
||||||
|
const [columnSearchText, setColumnSearchText] = React.useState<string>("");
|
||||||
|
const [newSelectedColumnIds, setNewSelectedColumnIds] = React.useState<string[]>(originalSelectedColumnIds);
|
||||||
|
const styles = useColumnSelectionStyles();
|
||||||
|
|
||||||
|
const selectedColumnIdsSet = new Set(newSelectedColumnIds);
|
||||||
|
const onCheckedValueChange = (id: string, checkedData?: CheckboxOnChangeData): void => {
|
||||||
|
const checked = checkedData?.checked;
|
||||||
|
if (checked === "mixed" || checked === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
selectedColumnIdsSet.add(id);
|
||||||
|
} else {
|
||||||
|
if (selectedColumnIdsSet.size === 1 && selectedColumnIdsSet.has(id)) {
|
||||||
|
// Don't allow unchecking the last column
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedColumnIdsSet.delete(id);
|
||||||
|
}
|
||||||
|
setNewSelectedColumnIds([...selectedColumnIdsSet]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSave = (): void => {
|
||||||
|
onSelectionChange(newSelectedColumnIds);
|
||||||
|
closeSidePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChange: (event: SearchBoxChangeEvent, data: InputOnChangeData) => void = (_, data) =>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
setColumnSearchText(data.value);
|
||||||
|
|
||||||
|
const theme = getPlatformTheme(configContext.platform);
|
||||||
|
|
||||||
|
// Filter and move partition keys to the top
|
||||||
|
const columnDefinitionList = columnDefinitions
|
||||||
|
.filter((def) => !columnSearchText || def.label.toLowerCase().includes(columnSearchText.toLowerCase()))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ID = "id";
|
||||||
|
// "id" always at the top, then partition keys, then everything else sorted
|
||||||
|
if (a.id === ID) {
|
||||||
|
return b.id === ID ? 0 : -1;
|
||||||
|
} else if (b.id === ID) {
|
||||||
|
return a.id === ID ? 0 : 1;
|
||||||
|
} else if (a.isPartitionKey && !b.isPartitionKey) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.isPartitionKey && !a.isPartitionKey) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return a.label.localeCompare(b.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.paneContainer}>
|
||||||
|
<CosmosFluentProvider>
|
||||||
|
<div className="panelFormWrapper">
|
||||||
|
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<Text>Select which columns to display in your view of items in your container.</Text>
|
||||||
|
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
|
||||||
|
<SearchBox
|
||||||
|
className={styles.searchBox}
|
||||||
|
value={columnSearchText}
|
||||||
|
onChange={onSearchChange}
|
||||||
|
placeholder="Search fields"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.checkboxContainer}>
|
||||||
|
{columnDefinitionList.map((columnDefinition) => (
|
||||||
|
<Checkbox
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
key={columnDefinition.id}
|
||||||
|
label={{
|
||||||
|
className: styles.checkboxLabel,
|
||||||
|
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
|
||||||
|
}}
|
||||||
|
checked={selectedColumnIdsSet.has(columnDefinition.id)}
|
||||||
|
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
|
||||||
|
<Button appearance="primary" onClick={onSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button appearance="secondary" onClick={closeSidePanel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CosmosFluentProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
// Definitions of State data
|
// Definitions of State data
|
||||||
|
|
||||||
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
@ -11,11 +12,16 @@ export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
FilterHistory = "FilterHistory",
|
FilterHistory = "FilterHistory",
|
||||||
MainTabDivider = "MainTabDivider",
|
MainTabDivider = "MainTabDivider",
|
||||||
|
ColumnsSelection = "ColumnsSelection",
|
||||||
|
ColumnSort = "ColumnSort",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
|
export type FilterHistory = string[];
|
||||||
export type WidthDefinition = { widthPx: number };
|
export type WidthDefinition = { widthPx: number };
|
||||||
export type TabDivider = { leftPaneWidthPercent: number };
|
export type TabDivider = { leftPaneWidthPercent: number };
|
||||||
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
|
@ -92,7 +92,13 @@ async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | S
|
||||||
describe("Documents tab (noSql API)", () => {
|
describe("Documents tab (noSql API)", () => {
|
||||||
describe("buildQuery", () => {
|
describe("buildQuery", () => {
|
||||||
it("should generate the right select query for SQL API", () => {
|
it("should generate the right select query for SQL API", () => {
|
||||||
expect(buildQuery(false, "")).toContain("select");
|
expect(
|
||||||
|
buildQuery(false, "", ["pk"], {
|
||||||
|
paths: ["pk"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
}),
|
||||||
|
).toContain("select");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
||||||
import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons";
|
import { Dismiss16Filled } from "@fluentui/react-icons";
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { StyleConstants } from "Common/StyleConstants";
|
|
||||||
import { createDocument } from "Common/dataAccess/createDocument";
|
import { createDocument } from "Common/dataAccess/createDocument";
|
||||||
import {
|
import {
|
||||||
deleteDocument as deleteNoSqlDocument,
|
deleteDocument as deleteNoSqlDocument,
|
||||||
|
@ -21,11 +20,14 @@ import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
|
ColumnsSelection,
|
||||||
|
FilterHistory,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
saveSubComponentState,
|
saveSubComponentState,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
@ -51,11 +53,11 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
||||||
import DocumentId from "../../Tree/DocumentId";
|
import DocumentId from "../../Tree/DocumentId";
|
||||||
import ObjectId from "../../Tree/ObjectId";
|
import ObjectId from "../../Tree/ObjectId";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import { DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
||||||
|
|
||||||
|
@ -101,17 +103,6 @@ export const useDocumentsTabStyles = makeStyles({
|
||||||
...shorthands.outline("1px", "dotted"),
|
...shorthands.outline("1px", "dotted"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
floatingControlsContainer: {
|
|
||||||
position: "relative",
|
|
||||||
},
|
|
||||||
floatingControls: {
|
|
||||||
position: "absolute",
|
|
||||||
top: "6px",
|
|
||||||
right: 0,
|
|
||||||
float: "right",
|
|
||||||
backgroundColor: "white",
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
|
@ -281,7 +272,7 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps =>
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && container.openUploadItemsPanePane();
|
selectedCollection && container.openUploadItemsPane();
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -469,17 +460,33 @@ export const showPartitionKey = (collection: ViewModels.CollectionBase, isPrefer
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
|
/**
|
||||||
|
* Build default query
|
||||||
|
* @param isMongo true if mongo api
|
||||||
|
* @param filter
|
||||||
|
* @param partitionKeyProperties optional for mongo
|
||||||
|
* @param partitionKey optional for mongo
|
||||||
|
* @param additionalField
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const buildQuery = (
|
export const buildQuery = (
|
||||||
isMongo: boolean,
|
isMongo: boolean,
|
||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties?: string[],
|
partitionKeyProperties?: string[],
|
||||||
partitionKey?: DataModels.PartitionKey,
|
partitionKey?: DataModels.PartitionKey,
|
||||||
|
additionalField?: string[],
|
||||||
): string => {
|
): string => {
|
||||||
if (isMongo) {
|
if (isMongo) {
|
||||||
return filter || "{}";
|
return filter || "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey);
|
// Filter out fields starting with "/" (partition keys)
|
||||||
|
return QueryUtils.buildDocumentsQuery(
|
||||||
|
filter,
|
||||||
|
partitionKeyProperties,
|
||||||
|
partitionKey,
|
||||||
|
additionalField?.filter((f) => !f.startsWith("/")) || [],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -522,6 +529,12 @@ const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
||||||
);
|
);
|
||||||
const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
||||||
|
|
||||||
|
// Extend DocumentId to include fields displayed in the table
|
||||||
|
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
||||||
|
|
||||||
|
// This is based on some heuristics
|
||||||
|
const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 29;
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
|
@ -540,7 +553,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
||||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
const [filterContent, setFilterContent] = useState<string>("");
|
||||||
const [documentIds, setDocumentIds] = useState<DocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
const filterInput = useRef<HTMLInputElement>(null);
|
const filterInput = useRef<HTMLInputElement>(null);
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
@ -571,7 +584,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readSubComponentState(SubComponentName.MainTabDivider, _collection, {
|
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -585,8 +598,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<string[]>(() =>
|
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||||
readSubComponentState(SubComponentName.FilterHistory, _collection, []),
|
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
@ -635,10 +648,37 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
[partitionKeyPropertyHeaders],
|
[partitionKeyPropertyHeaders],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getInitialColumnSelection = () => {
|
||||||
|
const defaultColumnsIds = ["id"];
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
defaultColumnsIds.push(...partitionKeyPropertyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultColumnsIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
|
SubComponentName.ColumnsSelection,
|
||||||
|
_collection,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!persistedColumnsSelection) {
|
||||||
|
return getInitialColumnSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedColumnsSelection.selectedColumnIds;
|
||||||
|
});
|
||||||
|
|
||||||
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
||||||
const newDocumentId = useCallback(
|
const newDocumentId = useCallback(
|
||||||
(rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) =>
|
(
|
||||||
new DocumentId(
|
rawDocument: DataModels.DocumentId,
|
||||||
|
partitionKeyProperties: string[],
|
||||||
|
partitionKeyValue: string[],
|
||||||
|
): ExtendedDocumentId => {
|
||||||
|
const extendedDocumentId = new DocumentId(
|
||||||
{
|
{
|
||||||
partitionKey,
|
partitionKey,
|
||||||
partitionKeyProperties,
|
partitionKeyProperties,
|
||||||
|
@ -648,7 +688,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
},
|
},
|
||||||
rawDocument,
|
rawDocument,
|
||||||
partitionKeyValue,
|
partitionKeyValue,
|
||||||
),
|
) as ExtendedDocumentId;
|
||||||
|
extendedDocumentId.tableFields = { ...rawDocument };
|
||||||
|
return extendedDocumentId;
|
||||||
|
},
|
||||||
[partitionKey],
|
[partitionKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -810,6 +853,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
|
|
||||||
setDocumentIds(ids);
|
setDocumentIds(ids);
|
||||||
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
||||||
|
|
||||||
|
// Update column choices
|
||||||
|
setColumnDefinitionsFromDocument(savedDocument);
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.CreateDocument,
|
Action.CreateDocument,
|
||||||
{
|
{
|
||||||
|
@ -892,6 +939,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
},
|
},
|
||||||
startKey,
|
startKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update column choices
|
||||||
|
selectedDocumentId.tableFields = { ...documentContent };
|
||||||
|
setColumnDefinitionsFromDocument(documentContent);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
onExecutionErrorChange(true);
|
onExecutionErrorChange(true);
|
||||||
|
@ -1093,7 +1144,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
const _queryAbortController = new AbortController();
|
const _queryAbortController = new AbortController();
|
||||||
setQueryAbortController(_queryAbortController);
|
setQueryAbortController(_queryAbortController);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter, partitionKeyProperties, partitionKey);
|
const query: string = buildQuery(
|
||||||
|
isPreferredApiMongoDB,
|
||||||
|
filter,
|
||||||
|
partitionKeyProperties,
|
||||||
|
partitionKey,
|
||||||
|
selectedColumnIds,
|
||||||
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const options: any = {};
|
const options: any = {};
|
||||||
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
||||||
|
@ -1116,6 +1173,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
resourceTokenPartitionKey,
|
resourceTokenPartitionKey,
|
||||||
isQueryCopilotSampleContainer,
|
isQueryCopilotSampleContainer,
|
||||||
_collection,
|
_collection,
|
||||||
|
selectedColumnIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onHideFilterClick = (): void => {
|
const onHideFilterClick = (): void => {
|
||||||
|
@ -1261,16 +1319,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
documentsIterator, // loadNextPage: disabled as it will trigger a circular dependency and infinite loop
|
documentsIterator, // loadNextPage: disabled as it will trigger a circular dependency and infinite loop
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onRefreshKeyInput: KeyboardEventHandler<HTMLButtonElement> = (event) => {
|
|
||||||
if (event.key === " " || event.key === "Enter") {
|
|
||||||
const focusElement = event.target as HTMLElement;
|
|
||||||
refreshDocumentsGrid(false);
|
|
||||||
focusElement && focusElement.focus();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoadMoreKeyInput: KeyboardEventHandler<HTMLAnchorElement> = (event) => {
|
const onLoadMoreKeyInput: KeyboardEventHandler<HTMLAnchorElement> = (event) => {
|
||||||
if (event.key === " " || event.key === "Enter") {
|
if (event.key === " " || event.key === "Enter") {
|
||||||
const focusElement = event.target as HTMLElement;
|
const focusElement = event.target as HTMLElement;
|
||||||
|
@ -1302,9 +1350,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
|
|
||||||
// Table config here
|
// Table config here
|
||||||
const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => {
|
const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => {
|
||||||
const item: Record<string, string> & { id: string } = {
|
const item: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() };
|
||||||
id: documentId.id(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) {
|
if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) {
|
||||||
for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) {
|
for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) {
|
||||||
|
@ -1315,6 +1361,44 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extractColumnDefinitionsFromDocument = (document: unknown): ColumnDefinition[] => {
|
||||||
|
let columnDefinitions: ColumnDefinition[] = Object.keys(document)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
.filter((key) => typeof (document as any)[key] === "string" || typeof (document as any)[key] === "number") // Only allow safe types for displayable React children
|
||||||
|
.map((key) =>
|
||||||
|
key === "id"
|
||||||
|
? { id: key, label: isPreferredApiMongoDB ? "_id" : "id", isPartitionKey: false }
|
||||||
|
: { id: key, label: key, isPartitionKey: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
columnDefinitions.push(
|
||||||
|
...partitionKeyPropertyHeaders.map((key) => ({ id: key, label: key, isPartitionKey: true })),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove properties that are the partition keys, since they are already included
|
||||||
|
columnDefinitions = columnDefinitions.filter(
|
||||||
|
(columnDefinition) => !partitionKeyProperties.includes(columnDefinition.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnDefinitions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract column definitions from document and add to the definitions
|
||||||
|
* @param document
|
||||||
|
*/
|
||||||
|
const setColumnDefinitionsFromDocument = (document: unknown): void => {
|
||||||
|
const currentIds = new Set(columnDefinitions.map((columnDefinition) => columnDefinition.id));
|
||||||
|
extractColumnDefinitionsFromDocument(document).forEach((columnDefinition) => {
|
||||||
|
if (!currentIds.has(columnDefinition.id)) {
|
||||||
|
columnDefinitions.push(columnDefinition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setColumnDefinitions([...columnDefinitions]);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* replicate logic of selectedDocument.click();
|
* replicate logic of selectedDocument.click();
|
||||||
* Document has been clicked on in table
|
* Document has been clicked on in table
|
||||||
|
@ -1330,6 +1414,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
||||||
(content) => {
|
(content) => {
|
||||||
initDocumentEditor(documentId, content);
|
initDocumentEditor(documentId, content);
|
||||||
|
|
||||||
|
// Update columns
|
||||||
|
setColumnDefinitionsFromDocument(content);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1420,10 +1507,22 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
return () => resizeObserver.disconnect(); // clean up
|
return () => resizeObserver.disconnect(); // clean up
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columnHeaders = {
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
idHeader: isPreferredApiMongoDB ? "_id" : "id",
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [],
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
};
|
SubComponentName.ColumnsSelection,
|
||||||
|
_collection,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!persistedColumnsSelection) {
|
||||||
|
return extractColumnDefinitionsFromDocument({
|
||||||
|
id: "id",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedColumnsSelection.columnDefinitions;
|
||||||
|
});
|
||||||
|
|
||||||
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
||||||
confirmDiscardingChange(() => {
|
confirmDiscardingChange(() => {
|
||||||
|
@ -1655,7 +1754,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter);
|
const query: string = buildQuery(isPreferredApiMongoDB, filter, selectedColumnIds);
|
||||||
|
|
||||||
return MongoProxyClient.queryDocuments(
|
return MongoProxyClient.queryDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
|
@ -1721,7 +1820,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveSubComponentState(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
|
@ -1754,6 +1853,41 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
[createIterator, filterContent],
|
[createIterator, filterContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||||
|
// Do not allow to unselecting all columns
|
||||||
|
if (newSelectedColumnIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
|
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
|
columnDefinitions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevSelectedColumnIds = usePrevious({ selectedColumnIds, setSelectedColumnIds });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If we are adding a field, let's refresh to include the field in the query
|
||||||
|
let addedField = false;
|
||||||
|
for (const field of selectedColumnIds) {
|
||||||
|
if (
|
||||||
|
!defaultQueryFields.includes(field) &&
|
||||||
|
prevSelectedColumnIds &&
|
||||||
|
!prevSelectedColumnIds.selectedColumnIds.includes(field)
|
||||||
|
) {
|
||||||
|
addedField = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedField) {
|
||||||
|
refreshDocumentsGrid(false);
|
||||||
|
}
|
||||||
|
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||||
|
@ -1848,42 +1982,40 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={(sizes: number[]) => {
|
onDragEnd={(sizes: number[]) => {
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData);
|
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
setTabStateData(tabStateData);
|
setTabStateData(tabStateData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||||
<div className={styles.floatingControlsContainer}>
|
|
||||||
<div className={styles.floatingControls}>
|
|
||||||
<Button
|
|
||||||
appearance="transparent"
|
|
||||||
aria-label="Refresh"
|
|
||||||
size="small"
|
|
||||||
icon={<ArrowClockwise16Filled />}
|
|
||||||
style={{
|
|
||||||
color: StyleConstants.AccentMedium,
|
|
||||||
}}
|
|
||||||
onClick={() => refreshDocumentsGrid(false)}
|
|
||||||
onKeyDown={onRefreshKeyInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||||
|
} /* Fix to make table not resize beyond parent's width */
|
||||||
|
}
|
||||||
|
>
|
||||||
<DocumentsTableComponent
|
<DocumentsTableComponent
|
||||||
|
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||||
items={tableItems}
|
items={tableItems}
|
||||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
selectedRows={selectedRows}
|
selectedRows={selectedRows}
|
||||||
size={tableContainerSizePx}
|
size={tableContainerSizePx}
|
||||||
columnHeaders={columnHeaders}
|
selectedColumnIds={selectedColumnIds}
|
||||||
isSelectionDisabled={
|
columnDefinitions={columnDefinitions}
|
||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) ||
|
isRowSelectionDisabled={
|
||||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
}
|
}
|
||||||
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
collection={_collection}
|
collection={_collection}
|
||||||
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
className={styles.loadMore}
|
className={styles.loadMore}
|
||||||
|
|
|
@ -21,15 +21,19 @@ describe("DocumentsTableComponent", () => {
|
||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
},
|
},
|
||||||
columnHeaders: {
|
columnDefinitions: [
|
||||||
idHeader: ID_HEADER,
|
{ id: ID_HEADER, label: "ID", isPartitionKey: false },
|
||||||
partitionKeyHeaders: [PARTITION_KEY_HEADER],
|
{ id: PARTITION_KEY_HEADER, label: "Partition Key", isPartitionKey: true },
|
||||||
},
|
],
|
||||||
isSelectionDisabled: false,
|
isRowSelectionDisabled: false,
|
||||||
collection: {
|
collection: {
|
||||||
databaseId: "db",
|
databaseId: "db",
|
||||||
id: ((): string => "coll") as ko.Observable<string>,
|
id: ((): string => "coll") as ko.Observable<string>,
|
||||||
} as ViewModels.CollectionBase,
|
} as ViewModels.CollectionBase,
|
||||||
|
onRefreshTable: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
selectedColumnIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render documents and partition keys in header", () => {
|
it("should render documents and partition keys in header", () => {
|
||||||
|
@ -40,7 +44,7 @@ describe("DocumentsTableComponent", () => {
|
||||||
|
|
||||||
it("should not render selection column when isSelectionDisabled is true", () => {
|
it("should not render selection column when isSelectionDisabled is true", () => {
|
||||||
const props: IDocumentsTableComponentProps = createMockProps();
|
const props: IDocumentsTableComponentProps = createMockProps();
|
||||||
props.isSelectionDisabled = true;
|
props.isRowSelectionDisabled = true;
|
||||||
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,48 @@
|
||||||
import {
|
import {
|
||||||
createTableColumn,
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
|
MenuDivider,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
TableRowData as RowStateBase,
|
TableRowData as RowStateBase,
|
||||||
|
SortDirection,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCellLayout,
|
TableCellLayout,
|
||||||
TableColumnDefinition,
|
TableColumnDefinition,
|
||||||
|
TableColumnId,
|
||||||
TableColumnSizingOptions,
|
TableColumnSizingOptions,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableRowId,
|
TableRowId,
|
||||||
TableSelectionCell,
|
TableSelectionCell,
|
||||||
|
tokens,
|
||||||
useArrowNavigationGroup,
|
useArrowNavigationGroup,
|
||||||
useTableColumnSizing_unstable,
|
useTableColumnSizing_unstable,
|
||||||
useTableFeatures,
|
useTableFeatures,
|
||||||
useTableSelection,
|
useTableSelection,
|
||||||
|
useTableSort,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import {
|
||||||
|
ArrowClockwise16Regular,
|
||||||
|
ArrowResetRegular,
|
||||||
|
DeleteRegular,
|
||||||
|
EditRegular,
|
||||||
|
MoreHorizontalRegular,
|
||||||
|
TableResizeColumnRegular,
|
||||||
|
TextSortAscendingRegular,
|
||||||
|
TextSortDescendingRegular,
|
||||||
|
} from "@fluentui/react-icons";
|
||||||
import { NormalizedEventKey } from "Common/Constants";
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
|
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
|
ColumnSort,
|
||||||
|
deleteSubComponentState,
|
||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
saveSubComponentState,
|
saveSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
|
@ -32,29 +50,37 @@ import {
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
|
||||||
export type DocumentsTableComponentItem = {
|
export type DocumentsTableComponentItem = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Record<string, string>;
|
} & Record<string, string | number>;
|
||||||
|
|
||||||
export type ColumnHeaders = {
|
export type ColumnDefinition = {
|
||||||
idHeader: string;
|
id: string;
|
||||||
partitionKeyHeaders: string[];
|
label: string;
|
||||||
|
isPartitionKey: boolean;
|
||||||
};
|
};
|
||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
|
onRefreshTable: () => void;
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
onItemClicked: (index: number) => void;
|
onItemClicked: (index: number) => void;
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
columnHeaders: ColumnHeaders;
|
selectedColumnIds: string[];
|
||||||
|
columnDefinitions: ColumnDefinition[];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
isSelectionDisabled?: boolean;
|
isRowSelectionDisabled?: boolean;
|
||||||
collection: ViewModels.CollectionBase;
|
collection: ViewModels.CollectionBase;
|
||||||
|
onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void;
|
||||||
|
defaultColumnSelection?: string[];
|
||||||
|
isColumnSelectionDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
|
@ -67,25 +93,33 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
||||||
data: TableRowData[];
|
data: TableRowData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COLUMNS_MENU_NAME = "columnsMenu";
|
||||||
|
|
||||||
const defaultSize = {
|
const defaultSize = {
|
||||||
idealWidth: 200,
|
idealWidth: 200,
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
};
|
};
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
|
onRefreshTable,
|
||||||
items,
|
items,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
columnHeaders,
|
selectedColumnIds,
|
||||||
isSelectionDisabled,
|
columnDefinitions,
|
||||||
|
isRowSelectionDisabled: isSelectionDisabled,
|
||||||
collection,
|
collection,
|
||||||
|
onColumnSelectionChange,
|
||||||
|
defaultColumnSelection,
|
||||||
|
isColumnSelectionDisabled,
|
||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
const columnIds = ["id"].concat(columnHeaders.partitionKeyHeaders);
|
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
columnIds.forEach((columnId) => {
|
selectedColumnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
!columnSizesMap ||
|
!columnSizesMap ||
|
||||||
!columnSizesMap[columnId] ||
|
!columnSizesMap[columnId] ||
|
||||||
|
@ -103,7 +137,24 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
return columnSizesPx;
|
return columnSizesPx;
|
||||||
});
|
});
|
||||||
|
|
||||||
const styles = useDocumentsTabStyles();
|
const [sortState, setSortState] = React.useState<{
|
||||||
|
sortDirection: "ascending" | "descending";
|
||||||
|
sortColumn: TableColumnId | undefined;
|
||||||
|
}>(() => {
|
||||||
|
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
|
if (!sort) {
|
||||||
|
return {
|
||||||
|
sortDirection: undefined,
|
||||||
|
sortColumn: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortDirection: sort.direction,
|
||||||
|
sortColumn: sort.columnId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
||||||
setColumnSizingOptions((state) => {
|
setColumnSizingOptions((state) => {
|
||||||
|
@ -122,42 +173,123 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveSubComponentState(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||||
|
|
||||||
return newSizingOptions;
|
return newSizingOptions;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// const restoreFocusTargetAttribute = useRestoreFocusTarget();
|
||||||
|
|
||||||
|
const onSortClick = (event: React.SyntheticEvent, columnId: string, direction: SortDirection) => {
|
||||||
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
|
if (columnId === undefined || direction === undefined) {
|
||||||
|
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
||||||
|
};
|
||||||
|
|
||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
columnDefinitions
|
||||||
createTableColumn<DocumentsTableComponentItem>({
|
.filter((column) => selectedColumnIds.includes(column.id))
|
||||||
columnId: "id",
|
.map((column) => ({
|
||||||
compare: (a, b) => a.id.localeCompare(b.id),
|
columnId: column.id,
|
||||||
renderHeaderCell: () => columnHeaders.idHeader,
|
compare: (a, b) => {
|
||||||
|
if (typeof a[column.id] === "string") {
|
||||||
|
return (a[column.id] as string).localeCompare(b[column.id] as string);
|
||||||
|
} else if (typeof a[column.id] === "number") {
|
||||||
|
return (a[column.id] as number) - (b[column.id] as number);
|
||||||
|
} else {
|
||||||
|
// Should not happen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderHeaderCell: () => (
|
||||||
|
<>
|
||||||
|
<span title={column.label}>{column.label}</span>
|
||||||
|
<Menu>
|
||||||
|
<MenuTrigger disableButtonEnhancement>
|
||||||
|
<Button
|
||||||
|
// {...restoreFocusTargetAttribute}
|
||||||
|
appearance="transparent"
|
||||||
|
aria-label="Select column"
|
||||||
|
size="small"
|
||||||
|
icon={<MoreHorizontalRegular />}
|
||||||
|
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||||
|
/>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||||
|
Refresh
|
||||||
|
</MenuItem>
|
||||||
|
{userContext.features.enableDocumentsTableColumnSelection && (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
icon={<TextSortAscendingRegular />}
|
||||||
|
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||||
|
>
|
||||||
|
Sort ascending
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<TextSortDescendingRegular />}
|
||||||
|
onClick={(e) => onSortClick(e, column.id, "descending")}
|
||||||
|
>
|
||||||
|
Sort descending
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
||||||
|
Reset sorting
|
||||||
|
</MenuItem>
|
||||||
|
{!isColumnSelectionDisabled && (
|
||||||
|
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
||||||
|
Edit columns
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
key="keyboardresize"
|
||||||
|
icon={<TableResizeColumnRegular />}
|
||||||
|
onClick={columnSizing.enableKeyboardMode(column.id)}
|
||||||
|
>
|
||||||
|
Resize with left/right arrow keys
|
||||||
|
</MenuItem>
|
||||||
|
{!isColumnSelectionDisabled && (
|
||||||
|
<MenuItem
|
||||||
|
key="remove"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
// Remove column id from selectedColumnIds
|
||||||
|
const index = selectedColumnIds.indexOf(column.id);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSelectedColumnIds = [...selectedColumnIds];
|
||||||
|
newSelectedColumnIds.splice(index, 1);
|
||||||
|
onColumnSelectionChange(newSelectedColumnIds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove column
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
),
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<TableCellLayout truncate title={item.id}>
|
<TableCellLayout truncate title={`${item[column.id]}`}>
|
||||||
{item.id}
|
{item[column.id]}
|
||||||
</TableCellLayout>
|
</TableCellLayout>
|
||||||
),
|
),
|
||||||
}),
|
})),
|
||||||
].concat(
|
[columnDefinitions, onColumnSelectionChange, selectedColumnIds],
|
||||||
columnHeaders.partitionKeyHeaders.map((pkHeader) =>
|
|
||||||
createTableColumn<DocumentsTableComponentItem>({
|
|
||||||
columnId: pkHeader,
|
|
||||||
compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]),
|
|
||||||
// Show Refresh button on last column
|
|
||||||
renderHeaderCell: () => <span title={pkHeader}>{pkHeader}</span>,
|
|
||||||
renderCell: (item) => (
|
|
||||||
<TableCellLayout truncate title={item[pkHeader]}>
|
|
||||||
{item[pkHeader]}
|
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[columnHeaders],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||||
|
@ -247,6 +379,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
columnSizing_unstable: columnSizing,
|
columnSizing_unstable: columnSizing,
|
||||||
tableRef,
|
tableRef,
|
||||||
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||||
|
sort: { getSortDirection, setColumnSort, sort },
|
||||||
} = useTableFeatures(
|
} = useTableFeatures(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
|
@ -260,10 +393,20 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
||||||
}),
|
}),
|
||||||
|
useTableSort({
|
||||||
|
sortState,
|
||||||
|
onSortChange: (e, nextSortState) => setSortState(nextSortState),
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows: TableRowData[] = getRows((row) => {
|
const headerSortProps = (columnId: TableColumnId) => ({
|
||||||
|
// onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId),
|
||||||
|
sortDirection: getSortDirection(columnId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows: TableRowData[] = sort(
|
||||||
|
getRows((row) => {
|
||||||
const selected = isRowSelected(row.rowId);
|
const selected = isRowSelected(row.rowId);
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
|
@ -277,7 +420,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
selected,
|
selected,
|
||||||
appearance: selected ? ("brand" as const) : ("none" as const),
|
appearance: selected ? ("brand" as const) : ("none" as const),
|
||||||
};
|
};
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const toggleAllKeydown = React.useCallback(
|
const toggleAllKeydown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
@ -304,37 +448,50 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkedValues: { [COLUMNS_MENU_NAME]: string[] } = {
|
||||||
|
[COLUMNS_MENU_NAME]: [],
|
||||||
|
};
|
||||||
|
columnDefinitions.forEach(
|
||||||
|
(columnDefinition) =>
|
||||||
|
selectedColumnIds.includes(columnDefinition.id) && checkedValues[COLUMNS_MENU_NAME].push(columnDefinition.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const openColumnSelectionPane = (): void => {
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Select columns",
|
||||||
|
<TableColumnSelectionPane
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
|
onSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultSelection={defaultColumnSelection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table noNativeElements {...tableProps}>
|
<Table noNativeElements sortable {...tableProps}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
||||||
{!isSelectionDisabled && (
|
{!isSelectionDisabled && (
|
||||||
<TableSelectionCell
|
<TableSelectionCell
|
||||||
|
key="selectcell"
|
||||||
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
||||||
onClick={toggleAllRows}
|
onClick={toggleAllRows}
|
||||||
onKeyDown={toggleAllKeydown}
|
onKeyDown={toggleAllKeydown}
|
||||||
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{columns.map((column /* index */) => (
|
{columns.map((column) => (
|
||||||
<Menu openOnContext key={column.columnId}>
|
|
||||||
<MenuTrigger>
|
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
className={styles.tableCell}
|
className={styles.tableCell}
|
||||||
key={column.columnId}
|
key={column.columnId}
|
||||||
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
||||||
|
{...headerSortProps(column.columnId)}
|
||||||
>
|
>
|
||||||
{column.renderHeaderCell()}
|
{column.renderHeaderCell()}
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
|
||||||
Keyboard Column Resizing
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to help with selection.
|
* Utility class to help with selection.
|
||||||
* This emulates File Explorer selection behavior.
|
* This emulates File Explorer selection behavior.
|
||||||
|
@ -90,3 +92,12 @@ export const selectionHelper = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// To get previous values of a state in useEffect
|
||||||
|
export const usePrevious = <T>(value: T): T | undefined => {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
};
|
||||||
|
|
|
@ -55,28 +55,15 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___77lcry0_0000000 f10pi13n"
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___1rwkz4r_0000000 f1euv43f f1l8gmrm f1e31b4d f150nix6 fy6ml6n f19g0ac"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
appearance="transparent"
|
|
||||||
aria-label="Refresh"
|
|
||||||
icon={<ArrowClockwise16Filled />}
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
size="small"
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"color": undefined,
|
"height": "100%",
|
||||||
|
"width": "calc(100% + -13px)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
|
||||||
>
|
>
|
||||||
<DocumentsTableComponent
|
<DocumentsTableComponent
|
||||||
collection={
|
collection={
|
||||||
|
@ -85,16 +72,32 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
columnHeaders={
|
columnDefinitions={
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"idHeader": "id",
|
"id": "id",
|
||||||
"partitionKeyHeaders": [],
|
"isPartitionKey": false,
|
||||||
|
"label": "id",
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
defaultColumnSelection={
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
isSelectionDisabled={true}
|
isColumnSelectionDisabled={false}
|
||||||
|
isRowSelectionDisabled={true}
|
||||||
items={[]}
|
items={[]}
|
||||||
|
onColumnSelectionChange={[Function]}
|
||||||
onItemClicked={[Function]}
|
onItemClicked={[Function]}
|
||||||
|
onRefreshTable={[Function]}
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
|
selectedColumnIds={
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
]
|
||||||
|
}
|
||||||
selectedRows={
|
selectedRows={
|
||||||
Set {
|
Set {
|
||||||
0,
|
0,
|
||||||
|
@ -103,6 +106,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
<Allotment.Pane
|
<Allotment.Pane
|
||||||
minSize={30}
|
minSize={30}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -38,6 +38,7 @@ export type Features = {
|
||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly enableDocumentsTableColumnSelection: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
|
@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
enableDocumentsTableColumnSelection: "true" === get("enabledocumentstablecolumnselection"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export enum StorageKey {
|
||||||
GalleryCalloutDismissed,
|
GalleryCalloutDismissed,
|
||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
PriorityLevel,
|
PriorityLevel,
|
||||||
|
DocumentsTabPrefs,
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
AppState,
|
AppState,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,28 @@ import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export const defaultQueryFields = ["id", "_self", "_rid", "_ts"];
|
||||||
|
|
||||||
export function buildDocumentsQuery(
|
export function buildDocumentsQuery(
|
||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties: string[],
|
partitionKeyProperties: string[],
|
||||||
partitionKey: DataModels.PartitionKey,
|
partitionKey: DataModels.PartitionKey,
|
||||||
|
additionalField: string[] = [],
|
||||||
): string {
|
): string {
|
||||||
|
const fieldSet = new Set<string>(defaultQueryFields);
|
||||||
|
additionalField.forEach((prop) => fieldSet.add(prop));
|
||||||
|
|
||||||
|
const objectListSpec = [...fieldSet]
|
||||||
|
.filter((f) => !partitionKeyProperties.includes(f))
|
||||||
|
.map((prop) => `c.${prop}`)
|
||||||
|
.join(",");
|
||||||
let query =
|
let query =
|
||||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||||
? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections(
|
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
||||||
"c",
|
"c",
|
||||||
partitionKey,
|
partitionKey,
|
||||||
)}] as _partitionKeyValue from c`
|
)}] as _partitionKeyValue from c`
|
||||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
: `select ${objectListSpec} from c`;
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
query += " " + filter;
|
query += " " + filter;
|
||||||
|
|
Loading…
Reference in New Issue