mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 23:16:56 +00:00
Lazy load database offer in data explorer (#208)
Co-authored-by: zfoster <notzachfoster@gmail.com>
This commit is contained in:
parent
e62184a1f2
commit
dc56f7e154
83
src/Common/dataAccess/readDatabaseOffer.ts
Normal file
83
src/Common/dataAccess/readDatabaseOffer.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { readOffers } from "./readOffers";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export const readDatabaseOffer = async (
|
||||||
|
params: DataModels.ReadDatabaseOfferParams
|
||||||
|
): Promise<DataModels.OfferWithHeaders> => {
|
||||||
|
let offerId = params.offerId;
|
||||||
|
if (!offerId) {
|
||||||
|
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||||
|
try {
|
||||||
|
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId, params.isServerless);
|
||||||
|
if (!offerId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
return (
|
||||||
|
response && {
|
||||||
|
...response.resource,
|
||||||
|
headers: response.headers
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||||
|
let rpResponse;
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpResponse?.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string, isServerless: boolean): Promise<string> => {
|
||||||
|
const offers = await readOffers(isServerless);
|
||||||
|
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||||
|
return offer?.id;
|
||||||
|
};
|
36
src/Common/dataAccess/readOffers.ts
Normal file
36
src/Common/dataAccess/readOffers.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
|
import { ClientDefaults } from "../Constants";
|
||||||
|
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||||
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { sendCachedDataMessage } from "../MessageHandler";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export const readOffers = async (isServerless?: boolean): Promise<Offer[]> => {
|
||||||
|
if (isServerless) {
|
||||||
|
return []; // Reading offers is not supported for serverless accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (configContext.platform === Platform.Portal) {
|
||||||
|
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
|
||||||
|
userContext.databaseAccount.id,
|
||||||
|
ClientDefaults.portalCacheTimeoutMs
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If error getting cached Offers, continue on and read via SDK
|
||||||
|
}
|
||||||
|
|
||||||
|
return client()
|
||||||
|
.offers.readAll()
|
||||||
|
.fetchAll()
|
||||||
|
.then(response => response.resources)
|
||||||
|
.catch(error => {
|
||||||
|
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
||||||
|
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
@ -289,6 +289,13 @@ export interface CreateCollectionParams {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReadDatabaseOfferParams {
|
||||||
|
databaseId: string;
|
||||||
|
databaseResourceId?: string;
|
||||||
|
isServerless?: boolean;
|
||||||
|
offerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
|
@ -81,15 +81,15 @@ export interface Database extends TreeNode {
|
|||||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||||
|
|
||||||
selectDatabase(): void;
|
selectDatabase(): void;
|
||||||
expandDatabase(): void;
|
expandDatabase(): Promise<void>;
|
||||||
collapseDatabase(): void;
|
collapseDatabase(): void;
|
||||||
|
|
||||||
loadCollections(): Q.Promise<void>;
|
loadCollections(): Promise<void>;
|
||||||
findCollectionWithId(collectionRid: string): Collection;
|
findCollectionWithId(collectionRid: string): Collection;
|
||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||||
readSettings(): void;
|
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
|
loadOffer(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
|
@ -42,7 +42,8 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
const deleteDatabaseMenuItem = {
|
const deleteDatabaseMenuItem = {
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
||||||
label: container.deleteDatabaseText()
|
label: container.deleteDatabaseText(),
|
||||||
|
styleClass: "deleteDatabaseMenuItem"
|
||||||
};
|
};
|
||||||
return [newCollectionMenuItem, deleteDatabaseMenuItem];
|
return [newCollectionMenuItem, deleteDatabaseMenuItem];
|
||||||
}
|
}
|
||||||
@ -112,7 +113,8 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
||||||
},
|
},
|
||||||
label: container.deleteCollectionText()
|
label: container.deleteCollectionText(),
|
||||||
|
styleClass: "deleteCollectionMenuItem"
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@ -159,4 +159,20 @@ describe("TreeNodeComponent", () => {
|
|||||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders loading icon", () => {
|
||||||
|
const node: TreeNode = {
|
||||||
|
label: "label",
|
||||||
|
children: [],
|
||||||
|
isExpanded: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
node,
|
||||||
|
generation: 2,
|
||||||
|
paddingLeft: 9
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,12 +17,14 @@ import {
|
|||||||
|
|
||||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||||
|
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
styleClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
@ -37,6 +39,7 @@ export interface TreeNode {
|
|||||||
data?: any; // Piece of data corresponding to this node
|
data?: any; // Piece of data corresponding to this node
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
||||||
|
isLoading?: boolean;
|
||||||
isSelected?: () => boolean;
|
isSelected?: () => boolean;
|
||||||
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
||||||
onExpanded?: () => void;
|
onExpanded?: () => void;
|
||||||
@ -183,6 +186,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
)}
|
)}
|
||||||
{node.contextMenu && this.renderContextMenuButton(node)}
|
{node.contextMenu && this.renderContextMenuButton(node)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="loadingIconContainer">
|
||||||
|
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
||||||
|
</div>
|
||||||
{node.children && (
|
{node.children && (
|
||||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||||
<div className="nodeChildren" data-test={node.label}>
|
<div className="nodeChildren" data-test={node.label}>
|
||||||
@ -256,13 +262,20 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||||
>
|
>
|
||||||
{props.item.onRenderIcon()}
|
{props.item.onRenderIcon()}
|
||||||
<span className="treeComponentMenuItemLabel">{props.item.text}</span>
|
<span
|
||||||
|
className={
|
||||||
|
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.item.text}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
|
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
|
||||||
key: menuItem.label,
|
key: menuItem.label,
|
||||||
text: menuItem.label,
|
text: menuItem.label,
|
||||||
disabled: menuItem.isDisabled,
|
disabled: menuItem.isDisabled,
|
||||||
|
className: menuItem.styleClass,
|
||||||
onClick: menuItem.onClick,
|
onClick: menuItem.onClick,
|
||||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||||
}))
|
}))
|
||||||
|
@ -63,6 +63,15 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
|||||||
label
|
label
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@ -179,6 +188,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
"isBeakVisible": false,
|
"isBeakVisible": false,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"className": undefined,
|
||||||
"disabled": true,
|
"disabled": true,
|
||||||
"key": "menuLabel",
|
"key": "menuLabel",
|
||||||
"onClick": undefined,
|
"onClick": undefined,
|
||||||
@ -201,6 +211,15 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@ -261,6 +280,77 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||||
|
<div
|
||||||
|
className=" main2 nodeItem "
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="treeNodeHeader "
|
||||||
|
data-test="label"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingLeft": 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="label branch is expanded"
|
||||||
|
className="expandCollapseIcon"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
role="button"
|
||||||
|
src=""
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="nodeLabel"
|
||||||
|
title="label"
|
||||||
|
>
|
||||||
|
label
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AnimateHeight
|
||||||
|
animateOpacity={false}
|
||||||
|
animationStateClasses={
|
||||||
|
Object {
|
||||||
|
"animating": "rah-animating",
|
||||||
|
"animatingDown": "rah-animating--down",
|
||||||
|
"animatingToHeightAuto": "rah-animating--to-height-auto",
|
||||||
|
"animatingToHeightSpecific": "rah-animating--to-height-specific",
|
||||||
|
"animatingToHeightZero": "rah-animating--to-height-zero",
|
||||||
|
"animatingUp": "rah-animating--up",
|
||||||
|
"static": "rah-static",
|
||||||
|
"staticHeightAuto": "rah-static--height-auto",
|
||||||
|
"staticHeightSpecific": "rah-static--height-specific",
|
||||||
|
"staticHeightZero": "rah-static--height-zero",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyInlineTransitions={true}
|
||||||
|
delay={0}
|
||||||
|
duration={200}
|
||||||
|
easing="ease"
|
||||||
|
height="auto"
|
||||||
|
style={Object {}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="nodeChildren"
|
||||||
|
data-test="label"
|
||||||
|
/>
|
||||||
|
</AnimateHeight>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||||
<div
|
<div
|
||||||
className="nodeClassname main12 nodeItem "
|
className="nodeClassname main12 nodeItem "
|
||||||
@ -331,6 +421,15 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@ -450,6 +549,15 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
|||||||
label
|
label
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.showingMenu {
|
&.showingMenu {
|
||||||
background-color: #EEE;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeMenuEllipsis {
|
.treeMenuEllipsis {
|
||||||
@ -78,3 +78,12 @@
|
|||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadingIconContainer {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.loadingIcon {
|
||||||
|
height: 6px;
|
||||||
|
margin-left: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,7 +15,7 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
|||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||||
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||||
@ -1424,71 +1424,40 @@ export default class Explorer {
|
|||||||
|
|
||||||
// TODO: Refactor
|
// TODO: Refactor
|
||||||
const deferred: Q.Deferred<any> = Q.defer();
|
const deferred: Q.Deferred<any> = Q.defer();
|
||||||
|
this._setLoadingStatusText("Fetching databases...");
|
||||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
readDatabases().then(
|
||||||
this._setLoadingStatusText("Fetching databases...");
|
(databases: DataModels.Database[]) => {
|
||||||
readDatabases().then(
|
this._setLoadingStatusText("Successfully fetched databases.");
|
||||||
(databases: DataModels.Database[]) => {
|
TelemetryProcessor.traceSuccess(
|
||||||
this._setLoadingStatusText("Successfully fetched databases.");
|
Action.LoadDatabases,
|
||||||
TelemetryProcessor.traceSuccess(
|
{
|
||||||
Action.LoadDatabases,
|
databaseAccountName: this.databaseAccount().name,
|
||||||
{
|
defaultExperience: this.defaultExperience(),
|
||||||
databaseAccountName: this.databaseAccount().name,
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
defaultExperience: this.defaultExperience(),
|
},
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
startKey
|
||||||
|
);
|
||||||
|
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||||
|
const deltaDatabases = this.getDeltaDatabases(databases);
|
||||||
|
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||||
|
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||||
|
this.selectedNode(currentlySelectedNode);
|
||||||
|
this._setLoadingStatusText("Fetching containers...");
|
||||||
|
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
this._setLoadingStatusText("Successfully fetched containers.");
|
||||||
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
startKey
|
reason => {
|
||||||
);
|
this._setLoadingStatusText("Failed to fetch containers.");
|
||||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
deferred.reject(reason);
|
||||||
const deltaDatabases = this.getDeltaDatabases(databases, offers);
|
}
|
||||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
)
|
||||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
.finally(() => this.isRefreshingExplorer(false));
|
||||||
this.selectedNode(currentlySelectedNode);
|
|
||||||
this._setLoadingStatusText("Fetching containers...");
|
|
||||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
this._setLoadingStatusText("Successfully fetched containers.");
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
reason => {
|
|
||||||
this._setLoadingStatusText("Failed to fetch containers.");
|
|
||||||
deferred.reject(reason);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => this.isRefreshingExplorer(false));
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this._setLoadingStatusText("Failed to fetch databases.");
|
|
||||||
this.isRefreshingExplorer(false);
|
|
||||||
deferred.reject(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.LoadDatabases,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.databaseAccount().name,
|
|
||||||
defaultExperience: this.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
|
||||||
error: JSON.stringify(error)
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
|
||||||
this._setLoadingStatusText("Fetching offers...");
|
|
||||||
offerPromise.then(
|
|
||||||
(offers: DataModels.Offer[]) => {
|
|
||||||
this._setLoadingStatusText("Successfully fetched offers.");
|
|
||||||
refreshDatabases(offers);
|
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this._setLoadingStatusText("Failed to fetch offers.");
|
this._setLoadingStatusText("Failed to fetch databases.");
|
||||||
this.isRefreshingExplorer(false);
|
this.isRefreshingExplorer(false);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@ -2103,16 +2072,13 @@ export default class Explorer {
|
|||||||
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
databasesToLoad.forEach((database: ViewModels.Database) => {
|
databasesToLoad.forEach(async (database: ViewModels.Database) => {
|
||||||
loadCollectionPromises.push(
|
await database.loadCollections();
|
||||||
database.loadCollections().finally(() => {
|
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
||||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
if (isNewDatabase) {
|
||||||
if (isNewDatabase) {
|
database.expandDatabase();
|
||||||
database.expandDatabase();
|
}
|
||||||
}
|
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Q.all(loadCollectionPromises).done(
|
Q.all(loadCollectionPromises).done(
|
||||||
@ -2257,8 +2223,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDeltaDatabases(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[]
|
||||||
updatedOffersList: DataModels.Offer[]
|
|
||||||
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
|
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
|
||||||
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
||||||
const databaseExists = _.some(
|
const databaseExists = _.some(
|
||||||
@ -2267,10 +2232,9 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
return !databaseExists;
|
return !databaseExists;
|
||||||
});
|
});
|
||||||
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
|
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
|
||||||
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
|
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
|
||||||
return new Database(this, newDatabase, databaseOffer);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
let databasesToDelete: ViewModels.Database[] = [];
|
let databasesToDelete: ViewModels.Database[] = [];
|
||||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||||
@ -2320,10 +2284,6 @@ export default class Explorer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOfferForResource(offers: DataModels.Offer[], resourceId: string): DataModels.Offer {
|
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
@ -3160,4 +3120,15 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadSelectedDatabaseOffer(): Promise<void> {
|
||||||
|
const database = this.findSelectedDatabase();
|
||||||
|
await database?.loadOffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadDatabaseOffers(): Promise<void> {
|
||||||
|
this.databases()?.forEach(async (database: ViewModels.Database) => {
|
||||||
|
await database.loadOffer();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -391,31 +391,6 @@ export class CommandBarComponentButtonFactory {
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
let isShared = false;
|
|
||||||
if (container.isDatabaseNodeSelected()) {
|
|
||||||
isShared = container.findSelectedDatabase().isDatabaseShared();
|
|
||||||
} else if (container.isNodeKindSelected("Collection")) {
|
|
||||||
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
|
|
||||||
isShared = database && database.isDatabaseShared();
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = isShared ? "Settings" : "Scale & Settings";
|
|
||||||
|
|
||||||
return {
|
|
||||||
iconSrc: ScaleIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => {
|
|
||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
|
||||||
selectedCollection && (<any>selectedCollection).onSettingsClick();
|
|
||||||
},
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: true,
|
|
||||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "New Notebook";
|
const label = "New Notebook";
|
||||||
return {
|
return {
|
||||||
|
@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public open(databaseId?: string) {
|
public async open(databaseId?: string) {
|
||||||
super.open();
|
super.open();
|
||||||
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
|
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
|
||||||
this.formWarnings("");
|
this.formWarnings("");
|
||||||
@ -715,6 +715,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane
|
dataExplorerArea: Constants.Areas.ContextualPane
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await this.container.loadDatabaseOffers();
|
||||||
this._onDatabasesChange(this.container.databases());
|
this._onDatabasesChange(this.container.databases());
|
||||||
this._setFocus();
|
this._setFocus();
|
||||||
|
|
||||||
@ -748,8 +749,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
|
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
|
||||||
if (database && database.offer && database.offer()) {
|
if (database && database.offer && database.offer()) {
|
||||||
this._databaseOffers.set(database.id(), database.offer());
|
this._databaseOffers.set(database.id(), database.offer());
|
||||||
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
|
|
||||||
database.readSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return database.id();
|
return database.id();
|
||||||
|
@ -268,8 +268,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
|
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
|
||||||
if (keyspace && keyspace.offer && !!keyspace.offer()) {
|
if (keyspace && keyspace.offer && !!keyspace.offer()) {
|
||||||
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
|
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
|
||||||
} else if (keyspace && keyspace.isDatabaseShared && keyspace.isDatabaseShared()) {
|
|
||||||
keyspace.readSettings();
|
|
||||||
}
|
}
|
||||||
return keyspace.id();
|
return keyspace.id();
|
||||||
});
|
});
|
||||||
|
@ -132,7 +132,8 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
super.resetData();
|
super.resetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
public async open() {
|
||||||
|
await this.container.loadSelectedDatabaseOffer();
|
||||||
this.recordDeleteFeedback(this.shouldRecordFeedback());
|
this.recordDeleteFeedback(this.shouldRecordFeedback());
|
||||||
super.open();
|
super.open();
|
||||||
}
|
}
|
||||||
|
@ -598,7 +598,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
() => {
|
() => {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this._setBaseline();
|
this._setBaseline();
|
||||||
this.database.readSettings();
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.UpdateSettings,
|
Action.UpdateSettings,
|
||||||
{
|
{
|
||||||
@ -643,8 +642,9 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onActivate(): Q.Promise<any> {
|
public onActivate(): Q.Promise<any> {
|
||||||
return super.onActivate().then(() => {
|
return super.onActivate().then(async () => {
|
||||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||||
|
await this.database.loadOffer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,7 +346,6 @@ describe("Settings tab", () => {
|
|||||||
|
|
||||||
const offer: DataModels.Offer = null;
|
const offer: DataModels.Offer = null;
|
||||||
const defaultTtl = 200;
|
const defaultTtl = 200;
|
||||||
const database = new Database(explorer, baseDatabase, null);
|
|
||||||
const conflictResolutionPolicy = {
|
const conflictResolutionPolicy = {
|
||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
conflictResolutionPath: "/_ts"
|
conflictResolutionPath: "/_ts"
|
||||||
@ -507,7 +506,6 @@ describe("Settings tab", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const database = new Database(explorer, baseDatabase, null);
|
|
||||||
const container: DataModels.Collection = {
|
const container: DataModels.Collection = {
|
||||||
_rid: "_rid",
|
_rid: "_rid",
|
||||||
_self: "",
|
_self: "",
|
||||||
|
@ -1270,8 +1270,10 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onActivate(): Q.Promise<any> {
|
public onActivate(): Q.Promise<any> {
|
||||||
return super.onActivate().then(() => {
|
return super.onActivate().then(async () => {
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||||
|
const database: ViewModels.Database = this.collection.getDatabase();
|
||||||
|
await database.loadOffer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
|
|||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
|
||||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
|
|||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
|
|
||||||
constructor(container: Explorer, data: any, offer: DataModels.Offer) {
|
constructor(container: Explorer, data: any) {
|
||||||
this.nodeKind = "Database";
|
this.nodeKind = "Database";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
this.rid = data._rid;
|
this.rid = data._rid;
|
||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable();
|
||||||
this.collections = ko.observableArray<Collection>();
|
this.collections = ko.observableArray<Collection>();
|
||||||
this.isDatabaseExpanded = ko.observable<boolean>(false);
|
this.isDatabaseExpanded = ko.observable<boolean>(false);
|
||||||
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||||
@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: "Scale"
|
tabTitle: "Scale"
|
||||||
});
|
});
|
||||||
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
|
pendingNotificationsPromise.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
const pendingNotification: DataModels.Notification = data && data[0];
|
||||||
settingsTab = new DatabaseSettingsTab({
|
settingsTab = new DatabaseSettingsTab({
|
||||||
@ -121,80 +121,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public readSettings(): Q.Promise<void> {
|
|
||||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
|
||||||
this.container.isRefreshingExplorer(true);
|
|
||||||
const databaseDataModel: DataModels.Database = <DataModels.Database>{
|
|
||||||
id: this.id(),
|
|
||||||
_rid: this.rid,
|
|
||||||
_self: this.self
|
|
||||||
};
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience()
|
|
||||||
});
|
|
||||||
|
|
||||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
|
|
||||||
isServerless: this.container.isServerlessEnabled()
|
|
||||||
});
|
|
||||||
Q.all([offerInfoPromise]).then(
|
|
||||||
() => {
|
|
||||||
this.container.isRefreshingExplorer(false);
|
|
||||||
|
|
||||||
const databaseOffer: DataModels.Offer = this._getOfferForDatabase(
|
|
||||||
offerInfoPromise.valueOf(),
|
|
||||||
databaseDataModel
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!databaseOffer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
|
|
||||||
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
|
|
||||||
minimumRUForCollection:
|
|
||||||
offerDetail.content &&
|
|
||||||
offerDetail.content.collectionThroughputInfo &&
|
|
||||||
offerDetail.content.collectionThroughputInfo.minimumRUForCollection,
|
|
||||||
numPhysicalPartitions:
|
|
||||||
offerDetail.content &&
|
|
||||||
offerDetail.content.collectionThroughputInfo &&
|
|
||||||
offerDetail.content.collectionThroughputInfo.numPhysicalPartitions
|
|
||||||
};
|
|
||||||
|
|
||||||
databaseOffer.content.collectionThroughputInfo = offerThroughputInfo;
|
|
||||||
(databaseOffer as DataModels.OfferWithHeaders).headers = offerDetail.headers;
|
|
||||||
this.offer(databaseOffer);
|
|
||||||
this.offer.valueHasMutated();
|
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.LoadOffers,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience()
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
deferred.resolve();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
this.container.isRefreshingExplorer(false);
|
|
||||||
deferred.reject(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.LoadOffers,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience()
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isDatabaseNodeSelected(): boolean {
|
public isDatabaseNodeSelected(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.isDatabaseExpanded() &&
|
!this.isDatabaseExpanded() &&
|
||||||
@ -219,23 +145,13 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollapseDatabase() {
|
public async expandDatabase() {
|
||||||
this.selectDatabase();
|
|
||||||
if (this.isDatabaseExpanded()) {
|
|
||||||
this.collapseDatabase();
|
|
||||||
} else {
|
|
||||||
this.expandDatabase();
|
|
||||||
}
|
|
||||||
this.container.onUpdateTabsButtons([]);
|
|
||||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === this.rid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public expandDatabase() {
|
|
||||||
if (this.isDatabaseExpanded()) {
|
if (this.isDatabaseExpanded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadCollections();
|
await this.loadOffer();
|
||||||
|
await this.loadCollections();
|
||||||
this.isDatabaseExpanded(true);
|
this.isDatabaseExpanded(true);
|
||||||
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
||||||
description: "Database node",
|
description: "Database node",
|
||||||
@ -259,32 +175,19 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadCollections(): Q.Promise<void> {
|
public async loadCollections(): Promise<void> {
|
||||||
let collectionVMs: Collection[] = [];
|
const collectionVMs: Collection[] = [];
|
||||||
let deferred: Q.Deferred<void> = Q.defer<void>();
|
const collections: DataModels.Collection[] = await readCollections(this.id());
|
||||||
|
const deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|
||||||
readCollections(this.id()).then(
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
(collections: DataModels.Collection[]) => {
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
||||||
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
collectionVMs.push(collectionVM);
|
||||||
let deltaCollections = this.getDeltaCollections(collections);
|
});
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
//merge collections
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
this.addCollectionsToList(collectionVMs);
|
||||||
collectionVMs.push(collectionVM);
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
});
|
|
||||||
|
|
||||||
//merge collections
|
|
||||||
this.addCollectionsToList(collectionVMs);
|
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public openAddCollection(database: Database, event: MouseEvent) {
|
public openAddCollection(database: Database, event: MouseEvent) {
|
||||||
@ -296,6 +199,17 @@ export default class Database implements ViewModels.Database {
|
|||||||
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadOffer(): Promise<void> {
|
||||||
|
if (!this.offer()) {
|
||||||
|
const params: DataModels.ReadDatabaseOfferParams = {
|
||||||
|
databaseId: this.id(),
|
||||||
|
databaseResourceId: this.self,
|
||||||
|
isServerless: this.container.isServerlessEnabled()
|
||||||
|
};
|
||||||
|
this.offer(await readDatabaseOffer(params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return Q.resolve(undefined);
|
return Q.resolve(undefined);
|
||||||
@ -387,8 +301,4 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
this.collections(collectionsToKeep);
|
this.collections(collectionsToKeep);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getOfferForDatabase(offers: DataModels.Offer[], database: DataModels.Database): DataModels.Offer {
|
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === database._self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -170,14 +170,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
children: [],
|
children: [],
|
||||||
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
|
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
|
||||||
onClick: isExpanded => {
|
onClick: async isExpanded => {
|
||||||
// Rewritten version of expandCollapseDatabase():
|
// Rewritten version of expandCollapseDatabase():
|
||||||
if (!isExpanded) {
|
if (isExpanded) {
|
||||||
database.expandDatabase();
|
|
||||||
database.loadCollections();
|
|
||||||
} else {
|
|
||||||
database.collapseDatabase();
|
database.collapseDatabase();
|
||||||
|
} else {
|
||||||
|
if (databaseNode.children?.length === 0) {
|
||||||
|
databaseNode.isLoading = true;
|
||||||
|
}
|
||||||
|
await database.expandDatabase();
|
||||||
}
|
}
|
||||||
|
databaseNode.isLoading = false;
|
||||||
database.selectDatabase();
|
database.selectDatabase();
|
||||||
this.container.onUpdateTabsButtons([]);
|
this.container.onUpdateTabsButtons([]);
|
||||||
this.container.tabsManager.refreshActiveTab(
|
this.container.tabsManager.refreshActiveTab(
|
||||||
@ -203,6 +206,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
||||||
|
collections.forEach((collection: ViewModels.Collection) =>
|
||||||
|
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return databaseNode;
|
return databaseNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,24 +56,27 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||||
|
|
||||||
await frame.click(`div[data-test="${dbId}"]`);
|
await frame.click(`div[data-test="${dbId}"]`);
|
||||||
await frame.waitFor(`span[title="${collectionId}"]`);
|
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
|
||||||
|
await frame.waitFor(3000)
|
||||||
|
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
|
||||||
|
|
||||||
// delete container
|
// delete container
|
||||||
|
|
||||||
// click context menu for container
|
// click context menu for container
|
||||||
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`);
|
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true });
|
||||||
|
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
|
||||||
await frame.click(`div[data-test="${collectionId}"] > div > button`);
|
await frame.click(`div[data-test="${collectionId}"] > div > button`);
|
||||||
|
await frame.waitFor(2000)
|
||||||
// click delete container
|
// click delete container
|
||||||
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
|
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
|
||||||
await frame.waitFor(1000);
|
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
|
||||||
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
|
|
||||||
await elements[4].click();
|
|
||||||
|
|
||||||
// confirm delete container
|
// confirm delete container
|
||||||
|
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true })
|
||||||
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
|
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
|
||||||
|
|
||||||
// click delete
|
// click delete
|
||||||
|
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true })
|
||||||
await frame.click('input[data-test="deleteCollection"]');
|
await frame.click('input[data-test="deleteCollection"]');
|
||||||
await frame.waitFor(5000);
|
await frame.waitFor(5000);
|
||||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||||
@ -87,9 +90,8 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await button.asElement().click();
|
await button.asElement().click();
|
||||||
|
|
||||||
// click delete database
|
// click delete database
|
||||||
await frame.waitFor(1000);
|
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||||
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
|
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||||
await dbElements[1].click();
|
|
||||||
|
|
||||||
// confirm delete database
|
// confirm delete database
|
||||||
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
|
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
|
||||||
|
Loading…
Reference in New Issue
Block a user