Compare commits

...

12 Commits

Author SHA1 Message Date
Laurent Nguyen
88e47403d8 Replace datalist with fluentui menu 2024-07-31 16:17:49 +02:00
Laurent Nguyen
582d581edf Fix unit tests 2024-07-23 12:09:56 +02:00
Laurent Nguyen
f65e7ab1d6 Disable saving middle split position for now 2024-07-23 11:05:04 +02:00
Laurent Nguyen
bf1474e101 Make datalist for filter unique per database/container combination 2024-07-23 10:09:01 +02:00
Laurent Nguyen
4fda518a3f Replace read/save methods with more generic ones 2024-07-22 17:16:05 +02:00
Laurent Nguyen
9aa55aa4e5 Save filters 2024-07-22 16:21:26 +02:00
Laurent Nguyen
96b2ef728a Infrastructure to save app state 2024-07-22 12:17:56 +02:00
sindhuba
5871c1e2d0 Add more logs for RBAC (#1906)
* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Remove unnecessary code

* Remove unnecessary code

* Add code to fix VCoreMongo/PG bug

* Address feedback

* Add more logs for RBAC feature

* Add more logs for RBAC features

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-07-10 10:16:05 -07:00
sindhuba
81dccbe5be Fix vCoreMongo/PostGres quickstart bug (#1903)
* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Remove unnecessary code

* Remove unnecessary code

* Add code to fix VCoreMongo/PG bug

* Address feedback

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-07-09 10:58:13 -07:00
vchske
49c3d0f0cb Reorder Asier's commits in order to deploy CRI fixes (#1905)
* Set AllowPartialScopes flag to true (#1900)

* add partial scopes flag

* add partial scopes flag

* add partial scopes flag

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* Adding CRI fixes to pre RBAC commit (#1902)

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* Add Data Plane RBAC functionality (#1878)

* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Address errors and checks

* Cleanup DP RBAC code

* Run format

* Fix unit tests

* Remove unnecessary code

* Run npm format

* Fix enableAadDataPlane feature flag behavior

* Fix  enable AAD dataplane feature flag behavior

* Address feedback comments

* Minor fix

* Add new fixes

* Fix Tables test

* Run npm format

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* Fix LMS regression when using old backend (#1890)

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* Address RBAC local storage default setting issue (#1892)

* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Address errors and checks

* Cleanup DP RBAC code

* Run format

* Fix unit tests

* Remove unnecessary code

* Run npm format

* Fix enableAadDataPlane feature flag behavior

* Fix  enable AAD dataplane feature flag behavior

* Address feedback comments

* Minor fix

* Add new fixes

* Fix Tables test

* Run npm format

* Address Local storage default setting issue

* Run npm format

* Address lint error

* Run format

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* Fix bug in viewing tables account databases (#1899)

* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Address errors and checks

* Cleanup DP RBAC code

* Run format

* Fix unit tests

* Remove unnecessary code

* Run npm format

* Fix enableAadDataPlane feature flag behavior

* Fix  enable AAD dataplane feature flag behavior

* Address feedback comments

* Minor fix

* Add new fixes

* Fix Tables test

* Run npm format

* Address Local storage default setting issue

* Run npm format

* Address lint error

* Run format

* Address bug in fetching data for Tables Account

* Add fetchAndUpdate Keys

* Add fix for MPAC Tables account issue

* Fix issue with Cosmos Client

* Run np format

* Address bugs

* Remove unused import

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>

---------

Co-authored-by: Asier Isayas <asier.isayas@gmail.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
Co-authored-by: sindhuba <122321535+sindhuba@users.noreply.github.com>
2024-07-09 12:27:57 -04:00
Asier Isayas
375bb5f567 Adding CRI fixes to pre RBAC commit (#1902)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-07-08 17:27:34 -04:00
Asier Isayas
e9f83a8efd Set AllowPartialScopes flag to true (#1900)
* add partial scopes flag

* add partial scopes flag

* add partial scopes flag

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-07-08 16:30:14 -04:00
14 changed files with 449 additions and 63 deletions

View File

@@ -11,6 +11,7 @@ import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { getErrorMessage } from "./ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
const _global = typeof self === "undefined" ? window : self;
@@ -21,6 +22,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth;
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
if (aadDataPlaneFeatureEnabled || (!userContext.features.enableAadDataPlane && dataPlaneRBACOptionEnabled)) {
Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
"Explorer/tokenProvider",
);
if (!userContext.aadToken) {
logConsoleError(
`AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`,
@@ -80,6 +85,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
}
if (userContext.masterKey) {
Logger.logInfo(`Master Key exists`, "Explorer/tokenProvider");
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
verb,

View File

@@ -1,7 +1,8 @@
export interface QueryRequestOptions {
$skipToken?: string;
$top?: number;
subscriptions: string[];
$allowPartialScopes: boolean;
subscriptions?: string[];
}
export interface QueryResponse {

View File

@@ -0,0 +1,68 @@
// Definitions of State data
import { loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
import { userContext } from "UserContext";
import * as ViewModels from "../../../Contracts/ViewModels";
// Component states
export interface DocumentsTabStateData {
leftPaneWidthPercent: number;
}
const defaultState: DocumentsTabStateData = {
leftPaneWidthPercent: 35,
};
const ComponentName = "DocumentsTab";
export const readDocumentsTabState = (): DocumentsTabStateData => {
const state = loadState({ componentName: ComponentName });
return (state as DocumentsTabStateData) || defaultState;
};
export const saveDocumentsTabState = (state: DocumentsTabStateData): void => {
saveStateDebounced({ componentName: ComponentName }, state);
};
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
export type WidthDefinition = { idealWidth?: number; minWidth?: number };
export const readSubComponentState = <T>(
subComponentName: "ColumnSizes" | "FilterHistory",
collection: ViewModels.CollectionBase,
defaultValue: T,
): T => {
const globalAccountName = userContext.databaseAccount?.name;
// TODO what if databaseAccount doesn't exist?
const state = loadState({
componentName: ComponentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
}) as T;
return state || defaultValue;
};
export const saveSubComponentState = <T>(
subComponentName: "ColumnSizes" | "FilterHistory",
collection: ViewModels.CollectionBase,
state: T,
debounce?: boolean,
): void => {
const globalAccountName = userContext.databaseAccount?.name;
// TODO what if databaseAccount doesn't exist?
(debounce ? saveStateDebounced : saveState)(
{
componentName: ComponentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
},
state,
);
};

View File

@@ -13,6 +13,7 @@ import {
SAVE_BUTTON_ID,
UPDATE_BUTTON_ID,
UPLOAD_BUTTON_ID,
addStringsNoDuplicate,
buildQuery,
getDiscardExistingDocumentChangesButtonState,
getDiscardNewDocumentChangesButtonState,
@@ -339,7 +340,10 @@ describe("Documents tab (noSql API)", () => {
const createMockProps = (): IDocumentsTabComponentProps => ({
isPreferredApiMongoDB: false,
documentIds: [],
collection: undefined,
collection: {
id: ko.observable<string>("collectionId"),
databaseId: "databaseId",
} as ViewModels.CollectionBase,
partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
onLoadStartKey: 0,
tabTitle: "",
@@ -474,3 +478,13 @@ describe("Documents tab (noSql API)", () => {
});
});
});
describe("Documents tab", () => {
it("should add strings to array without duplicate", () => {
const array1 = ["a", "b", "c"];
const array2 = ["b", "c", "d"];
const array3 = addStringsNoDuplicate(array1, array2);
expect(array3).toEqual(["a", "b", "c", "d"]);
});
});

View File

@@ -1,5 +1,17 @@
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Button, FluentProvider, Input, TableRowId } from "@fluentui/react-components";
import {
Button,
FluentProvider,
Input,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuProps,
PositioningImperativeRef,
TableRowId,
useRestoreFocusTarget,
} from "@fluentui/react-components";
import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons";
import Split from "@uiw/react-split";
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
@@ -21,6 +33,12 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import {
DocumentsTabStateData,
readDocumentsTabState,
readSubComponentState,
saveSubComponentState,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { getPlatformTheme } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
@@ -420,6 +438,24 @@ export const buildQuery = (
return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey);
};
/**
* Export to expose to unit tests
*
* Add array2 to array1 without duplicates
* @param array1
* @param array2
* @return array1 with array2 added without duplicates
*/
export const addStringsNoDuplicate = (array1: string[], array2: string[]): string[] => {
const result = [...array1];
array2.forEach((item) => {
if (!result.includes(item)) {
result.push(item);
}
});
return result;
};
// Export to expose to unit tests
export interface IDocumentsTabComponentProps {
isPreferredApiMongoDB: boolean;
@@ -434,6 +470,11 @@ export interface IDocumentsTabComponentProps {
isTabActive: boolean;
}
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
const defaultSqlFilters = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC'];
const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
// Export to expose to unit tests
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
isPreferredApiMongoDB,
@@ -480,6 +521,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
ViewModels.DocumentExplorerState.noDocumentSelected,
);
// State
const [tabStateData, setTabStateData] = useState<DocumentsTabStateData>(() => readDocumentsTabState());
const isQueryCopilotSampleContainer =
_collection?.isSampleCollection &&
_collection?.databaseId === QueryCopilotSampleDatabaseId &&
@@ -488,6 +532,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// For Mongo only
const [continuationToken, setContinuationToken] = useState<string>(undefined);
// User's filter history
const [lastFilterContents, setLastFilterContents] = useState<string[]>(() =>
readSubComponentState("FilterHistory", _collection, []),
);
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
useEffect(() => {
@@ -513,8 +562,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
}, [documentIds, clickedRowIndex, editorState]);
let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC'];
const applyFilterButton = {
enabled: true,
visible: true,
@@ -1303,6 +1350,26 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return () => resizeObserver.disconnect(); // clean up
}, []);
const [filterMenuOpen, setFilterMenuOpen] = React.useState(false);
const onFilterMenuOpenChange: MenuProps["onOpenChange"] = (e, data) => {
// do not close menu as an outside click if clicking on the custom trigger/target
// this prevents it from closing & immediately re-opening when clicking custom triggers
if (data.type === "clickOutside" && (e.target === filterInput.current || e.target === inputRef.current)) {
return;
}
setFilterMenuOpen(data.open);
};
const inputRef = React.useRef<HTMLInputElement>(null);
const positioningRef = React.useRef<PositioningImperativeRef>(null);
React.useEffect(() => {
if (filterInput.current) {
positioningRef.current?.setTarget(filterInput.current);
}
}, [filterInput, positioningRef]);
const restoreFocusTargetAttribute = useRestoreFocusTarget();
const columnHeaders = {
idHeader: isPreferredApiMongoDB ? "_id" : "id",
partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [],
@@ -1369,7 +1436,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return partitionKey;
};
lastFilterContents = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => {
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
@@ -1646,6 +1712,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
// ***************** Mongo ***************************
const onApplyFilterClick = (): void => {
refreshDocumentsGrid(true);
// Remove duplicates, but keep order
if (lastFilterContents.includes(filterContent)) {
lastFilterContents.splice(lastFilterContents.indexOf(filterContent), 1);
}
// Save filter content to local storage
lastFilterContents.unshift(filterContent);
setLastFilterContents([...lastFilterContents]);
saveSubComponentState("FilterHistory", _collection, lastFilterContents);
};
const refreshDocumentsGrid = useCallback(
(applyFilterButtonPressed: boolean): void => {
// clear documents grid
@@ -1708,8 +1788,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
<Input
id="filterInput"
ref={filterInput}
{...restoreFocusTargetAttribute}
autocomplete="off"
type="text"
list="filtersList"
list={`filtersList-${getUniqueId(_collection)}`}
className={`${filterContent.length === 0 ? "placeholderVisible" : ""}`}
style={{ width: "100%" }}
title="Type a query predicate or choose one from the list."
@@ -1721,21 +1803,38 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
value={filterContent}
autoFocus={true}
onKeyDown={onFilterKeyDown}
onChange={(e) => setFilterContent(e.target.value)}
onChange={(e) => {
setFilterContent(e.target.value);
setFilterMenuOpen(true);
}}
onBlur={() => setIsFilterFocused(false)}
/>
<datalist id="filtersList">
{lastFilterContents.map((filter) => (
<Menu open={filterMenuOpen} onOpenChange={onFilterMenuOpenChange} positioning={{ positioningRef }}>
<MenuPopover>
<MenuList>
{addStringsNoDuplicate(
lastFilterContents,
isPreferredApiMongoDB ? defaultMongoFilters : defaultSqlFilters,
).map((filter) => (
<MenuItem key={filter}>{filter}</MenuItem>
))}
</MenuList>
</MenuPopover>
</Menu>
{/* <datalist id={`filtersList-${getUniqueId(_collection)}`}>
{addStringsNoDuplicate(
lastFilterContents,
isPreferredApiMongoDB ? defaultMongoFilters : defaultSqlFilters,
).map((filter) => (
<option key={filter} value={filter} />
))}
</datalist>
</datalist> */}
<span className="filterbuttonpad">
<Button
appearance="primary"
style={filterButtonStyle}
onClick={() => refreshDocumentsGrid(true)}
onClick={onApplyFilterClick}
disabled={!applyFilterButton.enabled}
aria-label="Apply filter"
tabIndex={0}
@@ -1772,7 +1871,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
)}
{/* <Split> doesn't like to be a flex child */}
<div style={{ overflow: "hidden", height: "100%" }}>
<Split>
<Split
onDragEnd={(preSize: number) => {
tabStateData.leftPaneWidthPercent = Math.min(100, Math.max(0, Math.round(100 * preSize) / 100));
// saveDocumentsTabState(tabStateData); // Disable saving split position for now
setTabStateData({ ...tabStateData });
}}
>
<div
style={{ minWidth: 120, width: "35%", overflow: "hidden", position: "relative" }}
ref={tableContainerRef}
@@ -1813,6 +1918,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
(partitionKey.systemKey && !isPreferredApiMongoDB) ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
}
collection={_collection}
/>
{tableItems.length > 0 && (
<a

View File

@@ -1,6 +1,7 @@
import { TableRowId } from "@fluentui/react-components";
import { mount } from "enzyme";
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
const PARTITION_KEY_HEADER = "partitionKey";
@@ -25,6 +26,10 @@ describe("DocumentsTableComponent", () => {
partitionKeyHeaders: [PARTITION_KEY_HEADER],
},
isSelectionDisabled: false,
collection: {
databaseId: "db",
id: ((): string => "coll") as ko.Observable<string>,
} as ViewModels.CollectionBase,
});
it("should render documents and partition keys in header", () => {

View File

@@ -1,4 +1,5 @@
import {
createTableColumn,
Menu,
MenuItem,
MenuList,
@@ -16,17 +17,23 @@ import {
TableRow,
TableRowId,
TableSelectionCell,
createTableColumn,
useArrowNavigationGroup,
useTableColumnSizing_unstable,
useTableFeatures,
useTableSelection,
} from "@fluentui/react-components";
import { NormalizedEventKey } from "Common/Constants";
import {
ColumnSizesMap,
readSubComponentState,
saveSubComponentState,
WidthDefinition,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
import React, { useCallback, useMemo } from "react";
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
import * as ViewModels from "../../../Contracts/ViewModels";
export type DocumentsTableComponentItem = {
id: string;
@@ -45,6 +52,7 @@ export interface IDocumentsTableComponentProps {
columnHeaders: ColumnHeaders;
style?: React.CSSProperties;
isSelectionDisabled?: boolean;
collection: ViewModels.CollectionBase;
}
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
@@ -57,6 +65,11 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
data: TableRowData[];
}
const defaultSize: WidthDefinition = {
idealWidth: 200,
minWidth: 50,
};
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
items,
onSelectedRowsChange,
@@ -65,30 +78,32 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
size,
columnHeaders,
isSelectionDisabled,
collection,
}: IDocumentsTableComponentProps) => {
const initialSizingOptions: TableColumnSizingOptions = {
id: {
idealWidth: 280,
minWidth: 50,
},
};
columnHeaders.partitionKeyHeaders.forEach((pkHeader) => {
initialSizingOptions[pkHeader] = {
idealWidth: 200,
minWidth: 50,
};
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
const columnIds = ["id"].concat(columnHeaders.partitionKeyHeaders);
const columnSizesMap: ColumnSizesMap = readSubComponentState("ColumnSizes", collection, {});
const columnSizesPx: ColumnSizesMap = {};
columnIds.forEach((columnId) => {
columnSizesPx[columnId] = (columnSizesMap && columnSizesMap[columnId]) || defaultSize;
});
return columnSizesPx;
});
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
const onColumnResize = React.useCallback((_, { columnId, width }) => {
setColumnSizingOptions((state) => ({
...state,
[columnId]: {
...state[columnId],
idealWidth: width,
},
}));
setColumnSizingOptions((state) => {
const newSizingOptions = {
...state,
[columnId]: {
...state[columnId],
idealWidth: width,
},
};
saveSubComponentState("ColumnSizes", collection, newSizingOptions, true);
return newSizingOptions;
});
}, []);
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes

View File

@@ -485,6 +485,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
>
<Split
mode="horizontal"
onDragEnd={[Function]}
prefixCls="w-split"
visiable={true}
>
@@ -526,6 +527,12 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
}
>
<DocumentsTableComponent
collection={
Object {
"databaseId": "databaseId",
"id": [Function],
}
}
columnHeaders={
Object {
"idHeader": "id",

View File

@@ -2,6 +2,12 @@
exports[`DocumentsTableComponent should not render selection column when isSelectionDisabled is true 1`] = `
<DocumentsTableComponent
collection={
Object {
"databaseId": "db",
"id": [Function],
}
}
columnHeaders={
Object {
"idHeader": "id",
@@ -994,6 +1000,12 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
exports[`DocumentsTableComponent should render documents and partition keys in header 1`] = `
<DocumentsTableComponent
collection={
Object {
"databaseId": "db",
"id": [Function],
}
}
columnHeaders={
Object {
"idHeader": "id",

View File

@@ -0,0 +1,76 @@
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
// The component name whose state is being saved. Component name must not include special characters.
export type ComponentName = "DocumentsTab" | "DocumentsTab.columnSizes";
const SCHEMA_VERSION = 1;
export interface StateData {
schemaVersion: number;
timestamp: number;
data: unknown;
}
type StorePath = {
componentName: string;
subComponentName?: string;
globalAccountName?: string;
databaseName?: string;
containerName?: string;
};
// Load and save state data
export const loadState = (path: StorePath): unknown => {
const appState =
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
const key = createKeyFromPath(path);
return appState[key]?.data;
};
export const saveState = (path: StorePath, state: unknown): void => {
// Retrieve state object
const appState =
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
const key = createKeyFromPath(path);
appState[key] = {
schemaVersion: SCHEMA_VERSION,
timestamp: Date.now(),
data: state,
};
// TODO Add logic to clean up old state data based on timestamp
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
};
// This is for high-frequency state changes
let timeoutId: NodeJS.Timeout | undefined;
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
};
interface ApplicationState {
[statePath: string]: StateData;
}
const orderedPathSegments: (keyof StorePath)[] = [
"subComponentName",
"globalAccountName",
"databaseName",
"containerName",
];
/**
* /componentName/subComponentName/globalAccountName/databaseName/containerName/
* Any of the path segments can be "" except componentName
* @param path
*/
const createKeyFromPath = (path: StorePath): string => {
let key = `/${path.componentName}`; // ComponentName is always there
orderedPathSegments.forEach((segment) => {
const segmentValue = path[segment as keyof StorePath];
key += `/${segmentValue !== undefined ? segmentValue : ""}`;
});
return key;
};

View File

@@ -20,3 +20,14 @@ export const setEntryNumber = (key: StorageKey, value: number): void =>
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
localStorage.setItem(StorageKey[key], value.toString());
export const setEntryObject = (key: StorageKey, value: unknown): void => {
localStorage.setItem(StorageKey[key], JSON.stringify(value));
};
export const getEntryObject = <T>(key: StorageKey): T | null => {
const item = localStorage.getItem(StorageKey[key]);
if (item) {
return JSON.parse(item) as T;
}
return null;
};

View File

@@ -30,6 +30,7 @@ export enum StorageKey {
VisitedAccounts,
PriorityLevel,
DefaultQueryResultsView,
AppState,
}
export const hasRUThresholdBeenConfigured = (): boolean => {

View File

@@ -43,6 +43,7 @@ import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/Messa
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { applyExplorerBindings } from "../applyExplorerBindings";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import * as Logger from "../Common/Logger";
// This hook will create a new instance of Explorer.ts and bind it to the DOM
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
@@ -275,26 +276,55 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
updateUserContext({
databaseAccount: config.databaseAccount,
});
Logger.logInfo(
`Configuring Data Explorer for ${userContext.apiType} account ${account.name}`,
"Explorer/configureHostedWithAAD",
);
if (!userContext.features.enableAadDataPlane) {
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
if (userContext.apiType === "SQL") {
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
Logger.logInfo(
`Local storage RBAC setting for ${userContext.apiType} account ${account.name} is ${isDataPlaneRbacSetting}`,
"Explorer/configureHostedWithAAD",
);
let dataPlaneRbacEnabled;
if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) {
dataPlaneRbacEnabled = account.properties.disableLocalAuth;
Logger.logInfo(
`Data Plane RBAC value for ${userContext.apiType} account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} is ${dataPlaneRbacEnabled}`,
"Explorer/configureHostedWithAAD",
);
} else {
dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption;
Logger.logInfo(
`Data Plane RBAC value for ${userContext.apiType} account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} is ${dataPlaneRbacEnabled}`,
"Explorer/configureHostedWithAAD",
);
}
if (!dataPlaneRbacEnabled) {
Logger.logInfo(
`Calling fetch keys for ${userContext.apiType} account ${account.name} with RBAC setting ${dataPlaneRbacEnabled}`,
"Explorer/configureHostedWithAAD",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
updateUserContext({ dataPlaneRbacEnabled });
} else {
const dataPlaneRbacEnabled = account.properties.disableLocalAuth;
Logger.logInfo(
`Local storage setting does not exist : Data Plane RBAC value for ${userContext.apiType} account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} is ${dataPlaneRbacEnabled}`,
"Explorer/configureHostedWithAAD",
);
if (!dataPlaneRbacEnabled) {
Logger.logInfo(
`Fetching keys for ${userContext.apiType} account ${account.name} with RBAC setting ${dataPlaneRbacEnabled}`,
"Explorer/configureHostedWithAAD",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
@@ -302,10 +332,22 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled });
}
} else {
Logger.logInfo(
`Fetching keys for ${userContext.apiType} account ${account.name}`,
"Explorer/configureHostedWithAAD",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
} else {
Logger.logInfo(
`AAD Feature flag is enabled for account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} `,
"Explorer/configureHostedWithAAD",
);
if (!account.properties.disableLocalAuth) {
Logger.logInfo(
`Fetching keys for ${userContext.apiType} account ${account.name} with AAD data plane feature enabled`,
"Explorer/configureHostedWithAAD",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
}
@@ -425,13 +467,23 @@ function configureEmulator(): Explorer {
async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup: string, account: string) {
try {
Logger.logInfo(`Fetching keys for ${userContext.apiType} account ${account}`, "Explorer/fetchAndUpdateKeys");
const keys = await listKeys(subscriptionId, resourceGroup, account);
Logger.logInfo(`Keys fetched for ${userContext.apiType} account ${account}`, "Explorer/fetchAndUpdateKeys");
updateUserContext({
masterKey: keys.primaryMasterKey,
});
Logger.logInfo(
`User context updated with Master key for ${userContext.apiType} account ${account}`,
"Explorer/fetchAndUpdateKeys",
);
} catch (error) {
console.error("Error during fetching keys or updating user context:", error);
Logger.logError(
`Error during fetching keys or updating user context: ${error} for ${userContext.apiType} account ${account}`,
"Explorer/fetchAndUpdateKeys",
);
throw error;
}
}
@@ -440,6 +492,7 @@ async function configurePortal(): Promise<Explorer> {
updateUserContext({
authType: AuthType.AAD,
});
let explorer: Explorer;
return new Promise((resolve) => {
// In development mode, try to load the iframe message from session storage.
@@ -454,6 +507,7 @@ async function configurePortal(): Promise<Explorer> {
console.dir(message);
updateContextsFromPortalMessage(message);
explorer = new Explorer();
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
@@ -492,33 +546,47 @@ async function configurePortal(): Promise<Explorer> {
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
let dataPlaneRbacEnabled;
if (userContext.apiType === "SQL") {
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
Logger.logInfo(
`Local storage RBAC setting for ${userContext.apiType} account ${account.name} is ${isDataPlaneRbacSetting}`,
"Explorer/configurePortal",
);
let dataPlaneRbacEnabled;
if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) {
dataPlaneRbacEnabled = account.properties.disableLocalAuth;
} else {
dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption;
}
if (!dataPlaneRbacEnabled) {
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
updateUserContext({ dataPlaneRbacEnabled });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled });
} else {
const dataPlaneRbacEnabled = account.properties.disableLocalAuth;
if (!dataPlaneRbacEnabled) {
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
updateUserContext({ dataPlaneRbacEnabled });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled });
Logger.logInfo(
`Local storage does not exist for ${userContext.apiType} account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} is ${dataPlaneRbacEnabled}`,
"Explorer/configurePortal",
);
dataPlaneRbacEnabled = account.properties.disableLocalAuth;
}
} else {
Logger.logInfo(
`Data Plane RBAC value for ${userContext.apiType} account ${account.name} with disable local auth set to ${account.properties.disableLocalAuth} is ${dataPlaneRbacEnabled}`,
"Explorer/configurePortal",
);
if (!dataPlaneRbacEnabled) {
Logger.logInfo(
`Calling fetch keys for ${userContext.apiType} account ${account.name}`,
"Explorer/configurePortal",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}
updateUserContext({ dataPlaneRbacEnabled });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled });
} else if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
Logger.logInfo(
`Calling fetch keys for ${userContext.apiType} account ${account.name}`,
"Explorer/configurePortal",
);
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
}

View File

@@ -51,17 +51,13 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
do {
const body = {
query: subscriptionsQuery,
...(skipToken
? {
options: {
$skipToken: skipToken,
} as QueryRequestOptions,
}
: {
options: {
$top: 150,
} as QueryRequestOptions,
}),
options: {
$allowPartialScopes: true,
$top: 150,
...(skipToken && {
$skipToken: skipToken,
}),
} as QueryRequestOptions,
};
const response = await fetch(managementResourceGraphAPIURL, {