Lazy loading containers (#1411)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
This commit is contained in:
parent
7f220bf8be
commit
547954c3dc
|
@ -11,6 +11,10 @@
|
||||||
.collectionHeader {
|
.collectionHeader {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadMoreHeader {
|
||||||
|
color: RGB(5, 99, 193);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notebookResourceTree {
|
.notebookResourceTree {
|
||||||
|
@ -23,6 +27,4 @@
|
||||||
.clickDisabled {
|
.clickDisabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ export class Queries {
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
public static itemsPerPage: number = 100;
|
public static itemsPerPage: number = 100;
|
||||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||||
|
public static containersPerPage: number = 50;
|
||||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Queries } from "Common/Constants";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
@ -31,6 +32,35 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readCollectionsWithPagination(
|
||||||
|
databaseId: string,
|
||||||
|
continuationToken?: string
|
||||||
|
): Promise<DataModels.CollectionsWithPagination> {
|
||||||
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
try {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.containers.query(
|
||||||
|
{ query: "SELECT * FROM c" },
|
||||||
|
{
|
||||||
|
continuationToken,
|
||||||
|
maxItemCount: Queries.containersPerPage,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.fetchNext();
|
||||||
|
const collectionsWithPagination: DataModels.CollectionsWithPagination = {
|
||||||
|
collections: sdkResponse.resources as DataModels.Collection[],
|
||||||
|
continuationToken: sdkResponse.continuationToken,
|
||||||
|
};
|
||||||
|
return collectionsWithPagination;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
|
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
let rpResponse;
|
let rpResponse;
|
||||||
|
|
||||||
|
|
|
@ -156,6 +156,11 @@ export interface Collection extends Resource {
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CollectionsWithPagination {
|
||||||
|
continuationToken?: string;
|
||||||
|
collections?: Collection[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Database extends Resource {
|
export interface Database extends Resource {
|
||||||
collections?: Collection[];
|
collections?: Collection[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,13 +87,13 @@ export interface Database extends TreeNode {
|
||||||
isDatabaseExpanded: ko.Observable<boolean>;
|
isDatabaseExpanded: ko.Observable<boolean>;
|
||||||
isDatabaseShared: ko.Computed<boolean>;
|
isDatabaseShared: ko.Computed<boolean>;
|
||||||
isSampleDB?: boolean;
|
isSampleDB?: boolean;
|
||||||
|
collectionsContinuationToken?: string;
|
||||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||||
|
|
||||||
expandDatabase(): Promise<void>;
|
expandDatabase(): Promise<void>;
|
||||||
collapseDatabase(): void;
|
collapseDatabase(): void;
|
||||||
|
|
||||||
loadCollections(): Promise<void>;
|
loadCollections(restart?: boolean): Promise<void>;
|
||||||
findCollectionWithId(collectionId: string): Collection;
|
findCollectionWithId(collectionId: string): Collection;
|
||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
|
|
|
@ -577,7 +577,7 @@ export default class Explorer {
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
databasesToLoad.map(async (database: ViewModels.Database) => {
|
databasesToLoad.map(async (database: ViewModels.Database) => {
|
||||||
await database.loadCollections();
|
await database.loadCollections(true);
|
||||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
|
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
|
||||||
if (isNewDatabase) {
|
if (isNewDatabase) {
|
||||||
database.expandDatabase();
|
database.expandDatabase();
|
||||||
|
|
|
@ -21,6 +21,11 @@ export const SettingsPane: FunctionComponent = () => {
|
||||||
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
||||||
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
|
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
|
||||||
);
|
);
|
||||||
|
const [containerPaginationEnabled, setContainerPaginationEnabled] = useState<boolean>(
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.ContainerPaginationEnabled)
|
||||||
|
? LocalStorageUtility.getEntryString(StorageKey.ContainerPaginationEnabled) === "true"
|
||||||
|
: false
|
||||||
|
);
|
||||||
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
|
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
|
||||||
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
|
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
|
||||||
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||||
|
@ -50,6 +55,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
|
||||||
);
|
);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||||
|
|
||||||
|
@ -185,6 +191,25 @@ export const SettingsPane: FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable container pagination
|
||||||
|
<InfoTooltip>
|
||||||
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: { padding: 0 },
|
||||||
|
}}
|
||||||
|
className="padding"
|
||||||
|
ariaLabel="Enable container pagination"
|
||||||
|
checked={containerPaginationEnabled}
|
||||||
|
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
|
|
|
@ -97,6 +97,35 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
|
>
|
||||||
|
Enable container pagination
|
||||||
|
<InfoTooltip>
|
||||||
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
ariaLabel="Enable container pagination"
|
||||||
|
checked={false}
|
||||||
|
className="padding"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
@ -182,6 +211,35 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||||
<div
|
<div
|
||||||
className="paneMainContent"
|
className="paneMainContent"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
|
>
|
||||||
|
Enable container pagination
|
||||||
|
<InfoTooltip>
|
||||||
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
ariaLabel="Enable container pagination"
|
||||||
|
checked={false}
|
||||||
|
className="padding"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
|
@ -13,6 +13,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient";
|
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient";
|
||||||
|
import * as StorageUtility from "../../Shared/StorageUtility";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
@ -38,6 +39,7 @@ export default class Database implements ViewModels.Database {
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
public junoClient: JunoClient;
|
public junoClient: JunoClient;
|
||||||
public isSampleDB: boolean;
|
public isSampleDB: boolean;
|
||||||
|
public collectionsContinuationToken?: string;
|
||||||
private isOfferRead: boolean;
|
private isOfferRead: boolean;
|
||||||
|
|
||||||
constructor(container: Explorer, data: DataModels.Database) {
|
constructor(container: Explorer, data: DataModels.Database) {
|
||||||
|
@ -140,7 +142,11 @@ export default class Database implements ViewModels.Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadOffer();
|
await this.loadOffer();
|
||||||
await this.loadCollections();
|
|
||||||
|
if (this.collections()?.length === 0) {
|
||||||
|
await this.loadCollections(true);
|
||||||
|
}
|
||||||
|
|
||||||
this.isDatabaseExpanded(true);
|
this.isDatabaseExpanded(true);
|
||||||
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
||||||
description: "Database node",
|
description: "Database node",
|
||||||
|
@ -162,9 +168,31 @@ export default class Database implements ViewModels.Database {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadCollections(): Promise<void> {
|
public async loadCollections(restart = false) {
|
||||||
const collectionVMs: Collection[] = [];
|
const collectionVMs: Collection[] = [];
|
||||||
const collections: DataModels.Collection[] = await readCollections(this.id());
|
let collections: DataModels.Collection[] = [];
|
||||||
|
if (restart) {
|
||||||
|
this.collectionsContinuationToken = undefined;
|
||||||
|
}
|
||||||
|
const containerPaginationEnabled =
|
||||||
|
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) ===
|
||||||
|
"true";
|
||||||
|
if (containerPaginationEnabled) {
|
||||||
|
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
|
||||||
|
this.id(),
|
||||||
|
this.collectionsContinuationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
|
||||||
|
this.collectionsContinuationToken = collectionsWithPagination.continuationToken;
|
||||||
|
} else {
|
||||||
|
this.collectionsContinuationToken = undefined;
|
||||||
|
}
|
||||||
|
collections = collectionsWithPagination.collections;
|
||||||
|
} else {
|
||||||
|
collections = await readCollections(this.id());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Remove
|
// TODO Remove
|
||||||
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
|
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
|
||||||
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
|
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
|
||||||
|
@ -199,7 +227,9 @@ export default class Database implements ViewModels.Database {
|
||||||
|
|
||||||
//merge collections
|
//merge collections
|
||||||
this.addCollectionsToList(collectionVMs);
|
this.addCollectionsToList(collectionVMs);
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
if (!containerPaginationEnabled || restart) {
|
||||||
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
|
}
|
||||||
|
|
||||||
useDatabases.getState().updateDatabase(this);
|
useDatabases.getState().updateDatabase(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -479,6 +479,18 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||||
databaseNode.children.push(buildCollectionNode(database, collection))
|
databaseNode.children.push(buildCollectionNode(database, collection))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (database.collectionsContinuationToken) {
|
||||||
|
const loadMoreNode: TreeNode = {
|
||||||
|
label: "load more",
|
||||||
|
className: "loadMoreHeader",
|
||||||
|
onClick: async () => {
|
||||||
|
await database.loadCollections();
|
||||||
|
useDatabases.getState().updateDatabase(database);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
databaseNode.children.push(loadMoreNode);
|
||||||
|
}
|
||||||
|
|
||||||
database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
||||||
collections.forEach((collection: ViewModels.Collection) =>
|
collections.forEach((collection: ViewModels.Collection) =>
|
||||||
databaseNode.children.push(buildCollectionNode(database, collection))
|
databaseNode.children.push(buildCollectionNode(database, collection))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||||
export { LocalStorageUtility, SessionStorageUtility };
|
export { LocalStorageUtility, SessionStorageUtility };
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
ActualItemPerPage,
|
ActualItemPerPage,
|
||||||
|
ContainerPaginationEnabled,
|
||||||
CustomItemPerPage,
|
CustomItemPerPage,
|
||||||
DatabaseAccountId,
|
DatabaseAccountId,
|
||||||
EncryptedKeyToken,
|
EncryptedKeyToken,
|
||||||
|
|
Loading…
Reference in New Issue