{userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"}
From 1ee6abf89076ed8542286add3dbcfad24faa27de Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Mon, 27 Mar 2023 10:47:04 -0700
Subject: [PATCH 10/12] Change AAD endpoint from /common to /organizations.
(#1408)
---
src/Utils/AuthorizationUtils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts
index 0da7e310f..6e99d2351 100644
--- a/src/Utils/AuthorizationUtils.ts
+++ b/src/Utils/AuthorizationUtils.ts
@@ -49,7 +49,7 @@ export function getMsalInstance() {
cacheLocation: "localStorage",
},
auth: {
- authority: `${configContext.AAD_ENDPOINT}common`,
+ authority: `${configContext.AAD_ENDPOINT}organizations`,
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
},
};
From 7f220bf8be09a57ce50e30a18c67a94822a7bfd3 Mon Sep 17 00:00:00 2001
From: sindhuba <122321535+sindhuba@users.noreply.github.com>
Date: Mon, 27 Mar 2023 15:33:55 -0700
Subject: [PATCH 11/12] Add additional teaching bubbles in Quickstart (#1407)
* Add additional teaching bubbles in Quickstart
* Run npm format
* Fix lint error
* Add unit tests
* Add Mongo teaching bubbles for Try CosmosDB and Launch full screen
* Add additional tests for UrlUtility
* Run npm format
* Add tests for Notebook Utils
---
src/Common/EnvironmentUtility.test.ts | 14 ++++
src/Common/OfferUtility.test.ts | 24 ++++++-
src/Common/UrlUtility.test.ts | 49 ++++++++++++++
.../CommandBarComponentButtonFactory.tsx | 1 +
.../InMemoryContentProviderUtils.test.ts | 23 +++++++
.../Tutorials/MongoQuickstartTutorial.tsx | 67 ++++++++++++++++---
.../Tutorials/SQLQuickstartTutorial.tsx | 64 +++++++++++++++---
src/Main.tsx | 1 +
8 files changed, 222 insertions(+), 21 deletions(-)
create mode 100644 src/Common/EnvironmentUtility.test.ts
create mode 100644 src/Common/UrlUtility.test.ts
create mode 100644 src/Explorer/Notebook/NotebookComponent/InMemoryContentProviderUtils.test.ts
diff --git a/src/Common/EnvironmentUtility.test.ts b/src/Common/EnvironmentUtility.test.ts
new file mode 100644
index 000000000..6b5b1e218
--- /dev/null
+++ b/src/Common/EnvironmentUtility.test.ts
@@ -0,0 +1,14 @@
+import * as EnvironmentUtility from "./EnvironmentUtility";
+
+describe("Environment Utility Test", () => {
+ it("Test sample URI with /", () => {
+ const uri = "test/";
+ expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(uri);
+ });
+
+ it("Test sample URI without /", () => {
+ const uri = "test";
+ const expectedResult = "test/";
+ expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
+ });
+});
diff --git a/src/Common/OfferUtility.test.ts b/src/Common/OfferUtility.test.ts
index ce86c0d8a..d854f74d0 100644
--- a/src/Common/OfferUtility.test.ts
+++ b/src/Common/OfferUtility.test.ts
@@ -1,6 +1,6 @@
-import * as OfferUtility from "./OfferUtility";
-import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
+import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
+import * as OfferUtility from "./OfferUtility";
describe("parseSDKOfferResponse", () => {
it("manual throughput", () => {
@@ -31,6 +31,26 @@ describe("parseSDKOfferResponse", () => {
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
+ it("offerContent not defined", () => {
+ const mockOfferDefinition = {
+ id: "test",
+ } as SDKOfferDefinition;
+
+ const mockResponse = {
+ resource: mockOfferDefinition,
+ } as OfferResponse;
+
+ expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
+ });
+
+ it("offerDefinition is null", () => {
+ const mockResponse = {
+ resource: undefined,
+ } as OfferResponse;
+
+ expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
+ });
+
it("autoscale throughput", () => {
const mockOfferDefinition = {
content: {
diff --git a/src/Common/UrlUtility.test.ts b/src/Common/UrlUtility.test.ts
new file mode 100644
index 000000000..8170cc63f
--- /dev/null
+++ b/src/Common/UrlUtility.test.ts
@@ -0,0 +1,49 @@
+import * as UrlUtility from "./UrlUtility";
+
+describe("parseDocumentsPath", () => {
+ it("empty resource path", () => {
+ const resourcePath = "";
+
+ expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual({});
+ });
+
+ it("resourcePath does not begin or end with /", () => {
+ const resourcePath = "localhost/portal/home";
+ const expectedResult = {
+ type: "home",
+ objectBody: {
+ id: "portal",
+ self: "/localhost/portal/home/",
+ },
+ };
+
+ expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
+ });
+
+ it("resourcePath length is even", () => {
+ const resourcePath = "/localhost/portal/src/home/";
+ const expectedResult = {
+ type: "src",
+ objectBody: {
+ id: "home",
+ self: resourcePath,
+ },
+ };
+
+ expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
+ });
+
+ it("createUri", () => {
+ const baseUri = "http://foo.com/bar/";
+ const relativeUri = "/index.html";
+ const expectedUri = "http://foo.com/bar/index.html";
+
+ expect(UrlUtility.createUri(baseUri, relativeUri)).toEqual(expectedUri);
+ });
+
+ it("should throw an error if baseUri is empty", () => {
+ expect(() => {
+ UrlUtility.createUri("", "/home");
+ }).toThrow("baseUri is null or empty");
+ });
+});
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
index c02639f09..938383d5b 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
+++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
@@ -202,6 +202,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
+ id: "openFullScreenBtn",
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => {
diff --git a/src/Explorer/Notebook/NotebookComponent/InMemoryContentProviderUtils.test.ts b/src/Explorer/Notebook/NotebookComponent/InMemoryContentProviderUtils.test.ts
new file mode 100644
index 000000000..a5fd5df80
--- /dev/null
+++ b/src/Explorer/Notebook/NotebookComponent/InMemoryContentProviderUtils.test.ts
@@ -0,0 +1,23 @@
+import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
+
+describe("fromContentUri", () => {
+ it("fromContentUri should return valid result", () => {
+ const contentUri = "memory://resource/path";
+ const result = "resource";
+
+ expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(result);
+ });
+
+ it("fromContentUri should return undefined on invalid input", () => {
+ const contentUri = "invalid";
+
+ expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(undefined);
+ });
+
+ it("toContentUri should return valid result", () => {
+ const path = "resource/path";
+ const result = "memory://resource/path";
+
+ expect(InMemoryContentProviderUtils.toContentUri(path)).toEqual(result);
+ });
+});
diff --git a/src/Explorer/Quickstart/Tutorials/MongoQuickstartTutorial.tsx b/src/Explorer/Quickstart/Tutorials/MongoQuickstartTutorial.tsx
index d6655e14b..aa9c5e300 100644
--- a/src/Explorer/Quickstart/Tutorials/MongoQuickstartTutorial.tsx
+++ b/src/Explorer/Quickstart/Tutorials/MongoQuickstartTutorial.tsx
@@ -1,4 +1,4 @@
-import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
+import { DirectionalHint, Link, Stack, TeachingBubble, Text } from "@fluentui/react";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
@@ -18,6 +18,11 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
return <>>;
}
+ let totalSteps = 9;
+ if (userContext.isTryCosmosDBSubscription) {
+ totalSteps = 10;
+ }
+
switch (step) {
case 1:
return isSampleDBExpanded ? (
@@ -33,7 +38,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
},
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 1 of 8"
+ footerContent={"Step 1 of " + totalSteps}
>
Start viewing and working with your data by opening Documents under Data
@@ -55,7 +60,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(1),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 2 of 8"
+ footerContent={"Step 2 of " + totalSteps}
>
View documents here using the documents window. You can also use your favorite MongoDB tools and drivers to do
so.
@@ -78,7 +83,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(2),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 3 of 8"
+ footerContent={"Step 3 of " + totalSteps}
>
Add new document by copy / pasting JSON or uploading a JSON. You can also use your favorite MongoDB tools and
drivers to do so.
@@ -99,7 +104,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(3),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 4 of 8"
+ footerContent={"Step 4 of " + totalSteps}
>
Query your data using the filter function. Azure Cosmos DB for MongoDB provides comprehensive support for
MongoDB query language constructs. You can also use your favorite MongoDB tools and drivers to do so.
@@ -120,7 +125,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(4),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 5 of 8"
+ footerContent={"Step 5 of " + totalSteps}
>
Change throughput provisioned to your collection according to your needs
@@ -140,7 +145,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(5),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 6 of 8"
+ footerContent={"Step 6 of " + totalSteps}
>
Use the indexing policy editor to create and edit your indexes.
@@ -160,12 +165,54 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(6),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 7 of 8"
+ footerContent={"Step 7 of " + totalSteps}
>
Visualize your data, store queries in an interactive document
);
case 8:
+ return (
+
(userContext.isTryCosmosDBSubscription ? setStep(9) : setStep(10)),
+ }}
+ secondaryButtonProps={{
+ text: "Previous",
+ onClick: () => setStep(7),
+ }}
+ onDismiss={() => onDimissTeachingBubble()}
+ footerContent={"Step 8 of " + totalSteps}
+ >
+ This will open a new tab in your browser to use Cosmos DB Explorer. Using the provided URLs you can share
+ read-write or read-only access with other people.
+
+ );
+ case 9:
+ return (
+
setStep(10),
+ }}
+ secondaryButtonProps={{
+ text: "Previous",
+ onClick: () => setStep(8),
+ }}
+ calloutProps={{ directionalHint: DirectionalHint.leftCenter }}
+ onDismiss={() => onDimissTeachingBubble()}
+ footerContent={"Step 9 of " + totalSteps}
+ >
+ Unlock everything Azure Cosmos DB has to offer When you're ready, upgrade to production.
+
+ );
+ case 10:
return (
{
}}
secondaryButtonProps={{
text: "Previous",
- onClick: () => setStep(7),
+ onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(9) : setStep(8)),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 8 of 8"
+ footerContent={"Step " + totalSteps + " of " + totalSteps}
>
diff --git a/src/Explorer/Quickstart/Tutorials/SQLQuickstartTutorial.tsx b/src/Explorer/Quickstart/Tutorials/SQLQuickstartTutorial.tsx
index cced06a50..ddb35aa5e 100644
--- a/src/Explorer/Quickstart/Tutorials/SQLQuickstartTutorial.tsx
+++ b/src/Explorer/Quickstart/Tutorials/SQLQuickstartTutorial.tsx
@@ -1,4 +1,4 @@
-import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
+import { DirectionalHint, Link, Stack, TeachingBubble, Text } from "@fluentui/react";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
@@ -17,6 +17,10 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
if (userContext.apiType !== "SQL") {
return <>>;
}
+ let totalSteps = 8;
+ if (userContext.isTryCosmosDBSubscription) {
+ totalSteps = 9;
+ }
switch (step) {
case 1:
@@ -33,7 +37,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
},
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 1 of 7"
+ footerContent={"Step 1 of " + totalSteps}
>
Start viewing and working with your data by opening Items under Data
@@ -55,7 +59,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(1),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 2 of 7"
+ footerContent={"Step 2 of " + totalSteps}
>
View item here using the items window. Additionally you can also filter items to be reviewed with the filter
function
@@ -78,7 +82,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(2),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 3 of 7"
+ footerContent={"Step 3 of " + totalSteps}
>
Add new item by copy / pasting JSON; or uploading a JSON
@@ -98,7 +102,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(3),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 4 of 7"
+ footerContent={"Step 4 of " + totalSteps}
>
Query your data using either the filter function or new query.
@@ -118,7 +122,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(4),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 5 of 7"
+ footerContent={"Step 5 of " + totalSteps}
>
Change throughput provisioned to your container according to your needs
@@ -138,12 +142,54 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(5),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 6 of 7"
+ footerContent={"Step 6 of " + totalSteps}
>
Visualize your data, store queries in an interactive document
);
case 7:
+ return (
+
(userContext.isTryCosmosDBSubscription ? setStep(8) : setStep(9)),
+ }}
+ secondaryButtonProps={{
+ text: "Previous",
+ onClick: () => setStep(6),
+ }}
+ onDismiss={() => onDimissTeachingBubble()}
+ footerContent={"Step 7 of " + totalSteps}
+ >
+ This will open a new tab in your browser to use Cosmos DB Explorer. Using the provided URLs you can share
+ read-write or read-only access with other people.
+
+ );
+ case 8:
+ return (
+
setStep(9),
+ }}
+ secondaryButtonProps={{
+ text: "Previous",
+ onClick: () => setStep(7),
+ }}
+ calloutProps={{ directionalHint: DirectionalHint.leftCenter }}
+ onDismiss={() => onDimissTeachingBubble()}
+ footerContent={"Step 8 of " + totalSteps}
+ >
+ Unlock everything Azure Cosmos DB has to offer When you're ready, upgrade to production.
+
+ );
+ case 9:
return (
{
}}
secondaryButtonProps={{
text: "Previous",
- onClick: () => setStep(6),
+ onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(8) : setStep(7)),
}}
onDismiss={() => onDimissTeachingBubble()}
- footerContent="Step 7 of 7"
+ footerContent={"Step " + totalSteps + " of " + totalSteps}
>
diff --git a/src/Main.tsx b/src/Main.tsx
index 2115406ce..d8d817847 100644
--- a/src/Main.tsx
+++ b/src/Main.tsx
@@ -80,6 +80,7 @@ const App: React.FunctionComponent = () => {
return (
+
{/* Main Command Bar - Start */}
{/* Collections Tree and Tabs - Begin */}
From 547954c3dc2ea7c9562ee41482016b30d4f9f509 Mon Sep 17 00:00:00 2001
From: Asier Isayas
Date: Thu, 30 Mar 2023 17:53:36 -0400
Subject: [PATCH 12/12] Lazy loading containers (#1411)
Co-authored-by: Asier Isayas
---
less/resourceTree.less | 8 ++-
src/Common/Constants.ts | 2 +-
src/Common/dataAccess/readCollections.ts | 30 ++++++++++
src/Contracts/DataModels.ts | 5 ++
src/Contracts/ViewModels.ts | 4 +-
src/Explorer/Explorer.tsx | 2 +-
.../Panes/SettingsPane/SettingsPane.tsx | 25 ++++++++
.../__snapshots__/SettingsPane.test.tsx.snap | 58 +++++++++++++++++++
src/Explorer/Tree/Database.tsx | 40 +++++++++++--
src/Explorer/Tree/ResourceTree.tsx | 12 ++++
src/Shared/StorageUtility.ts | 1 +
11 files changed, 175 insertions(+), 12 deletions(-)
diff --git a/less/resourceTree.less b/less/resourceTree.less
index 31410ab32..68aa3d0de 100644
--- a/less/resourceTree.less
+++ b/less/resourceTree.less
@@ -11,6 +11,10 @@
.collectionHeader {
font-size: 12px;
}
+
+ .loadMoreHeader {
+ color: RGB(5, 99, 193);
+ }
}
.notebookResourceTree {
@@ -23,6 +27,4 @@
.clickDisabled {
pointer-events: none;
}
-}
-
-
+}
\ No newline at end of file
diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts
index c9a458558..3feba8cc2 100644
--- a/src/Common/Constants.ts
+++ b/src/Common/Constants.ts
@@ -141,7 +141,7 @@ export class Queries {
public static UnlimitedPageOption: string = "unlimited";
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 containersPerPage: number = 50;
public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4;
public static readonly DefaultMaxDegreeOfParallelism = 6;
diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts
index a5c8dd928..4a528b66c 100644
--- a/src/Common/dataAccess/readCollections.ts
+++ b/src/Common/dataAccess/readCollections.ts
@@ -1,3 +1,4 @@
+import { Queries } from "Common/Constants";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@@ -31,6 +32,35 @@ export async function readCollections(databaseId: string): Promise {
+ 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 {
let rpResponse;
diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts
index c913310a3..e4f865c2c 100644
--- a/src/Contracts/DataModels.ts
+++ b/src/Contracts/DataModels.ts
@@ -156,6 +156,11 @@ export interface Collection extends Resource {
requestSchema?: () => void;
}
+export interface CollectionsWithPagination {
+ continuationToken?: string;
+ collections?: Collection[];
+}
+
export interface Database extends Resource {
collections?: Collection[];
}
diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts
index 77ccc1cfe..c9bb351bc 100644
--- a/src/Contracts/ViewModels.ts
+++ b/src/Contracts/ViewModels.ts
@@ -87,13 +87,13 @@ export interface Database extends TreeNode {
isDatabaseExpanded: ko.Observable;
isDatabaseShared: ko.Computed;
isSampleDB?: boolean;
-
+ collectionsContinuationToken?: string;
selectedSubnodeKind: ko.Observable;
expandDatabase(): Promise;
collapseDatabase(): void;
- loadCollections(): Promise;
+ loadCollections(restart?: boolean): Promise;
findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onSettingsClick: () => void;
diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx
index f1c689d47..f8ecb6923 100644
--- a/src/Explorer/Explorer.tsx
+++ b/src/Explorer/Explorer.tsx
@@ -577,7 +577,7 @@ export default class Explorer {
try {
await Promise.all(
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());
if (isNewDatabase) {
database.expandDatabase();
diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
index 9abc63f82..d7dced7a8 100644
--- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
+++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
@@ -21,6 +21,11 @@ export const SettingsPane: FunctionComponent = () => {
const [customItemPerPage, setCustomItemPerPage] = useState(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
);
+ const [containerPaginationEnabled, setContainerPaginationEnabled] = useState(
+ LocalStorageUtility.hasItem(StorageKey.ContainerPaginationEnabled)
+ ? LocalStorageUtility.getEntryString(StorageKey.ContainerPaginationEnabled) === "true"
+ : false
+ );
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
@@ -50,6 +55,7 @@ export const SettingsPane: FunctionComponent = () => {
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
+ LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
@@ -185,6 +191,25 @@ export const SettingsPane: FunctionComponent = () => {
)}
+
+
+
+ Enable container pagination
+
+ Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
+
+
+
setContainerPaginationEnabled(!containerPaginationEnabled)}
+ />
+
+
{shouldShowCrossPartitionOption && (
diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap
index 928e7f7a7..ffa847681 100644
--- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap
+++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap
@@ -97,6 +97,35 @@ exports[`Settings Pane should render Default properly 1`] = `
@@ -182,6 +211,35 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
+
+
+
+ Enable container pagination
+
+ Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
+
+
+
+
+
diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx
index a31b79b97..19cf82e31 100644
--- a/src/Explorer/Tree/Database.tsx
+++ b/src/Explorer/Tree/Database.tsx
@@ -3,7 +3,7 @@ import React from "react";
import * as _ from "underscore";
import { AuthType } from "../../AuthType";
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 { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
@@ -13,6 +13,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient";
+import * as StorageUtility from "../../Shared/StorageUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
@@ -38,6 +39,7 @@ export default class Database implements ViewModels.Database {
public selectedSubnodeKind: ko.Observable;
public junoClient: JunoClient;
public isSampleDB: boolean;
+ public collectionsContinuationToken?: string;
private isOfferRead: boolean;
constructor(container: Explorer, data: DataModels.Database) {
@@ -140,7 +142,11 @@ export default class Database implements ViewModels.Database {
}
await this.loadOffer();
- await this.loadCollections();
+
+ if (this.collections()?.length === 0) {
+ await this.loadCollections(true);
+ }
+
this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node",
@@ -162,9 +168,31 @@ export default class Database implements ViewModels.Database {
});
}
- public async loadCollections(): Promise {
+ public async loadCollections(restart = false) {
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
// 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) {
@@ -199,7 +227,9 @@ export default class Database implements ViewModels.Database {
//merge collections
this.addCollectionsToList(collectionVMs);
- this.deleteCollectionsFromList(deltaCollections.toDelete);
+ if (!containerPaginationEnabled || restart) {
+ this.deleteCollectionsFromList(deltaCollections.toDelete);
+ }
useDatabases.getState().updateDatabase(this);
}
diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx
index 24eb34579..afb5a118e 100644
--- a/src/Explorer/Tree/ResourceTree.tsx
+++ b/src/Explorer/Tree/ResourceTree.tsx
@@ -479,6 +479,18 @@ export const ResourceTree: React.FC = ({ container }: Resourc
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[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection))
diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts
index 0d8477016..ad8a932e1 100644
--- a/src/Shared/StorageUtility.ts
+++ b/src/Shared/StorageUtility.ts
@@ -4,6 +4,7 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey {
ActualItemPerPage,
+ ContainerPaginationEnabled,
CustomItemPerPage,
DatabaseAccountId,
EncryptedKeyToken,