mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 05:41:40 +00:00
Compare commits
9 Commits
defect-227
...
defect2280
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57fa554178 | ||
|
|
ce3a7debb3 | ||
|
|
755b732532 | ||
|
|
1b5a9b83ff | ||
|
|
fb8871cfbf | ||
|
|
874cec26fc | ||
|
|
9d2d0e4754 | ||
|
|
c7c894d6d9 | ||
|
|
547954c3dc |
@@ -18,7 +18,8 @@ module.exports = {
|
|||||||
// clearMocks: false,
|
// clearMocks: false,
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
collectCoverage: true,
|
|
||||||
|
collectCoverage: process.env.skipCodeCoverage === "true" ? false : true,
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
|
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
|||||||
@@ -2576,6 +2576,10 @@ a:link {
|
|||||||
.querydropdown.placeholderVisible {
|
.querydropdown.placeholderVisible {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||||
|
color: #767474;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.querydropdown:hover {
|
.querydropdown:hover {
|
||||||
background-color: @AccentLow;
|
background-color: @AccentLow;
|
||||||
@@ -3087,3 +3091,4 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,17 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.databaseHeader {
|
.databaseHeader {
|
||||||
|
padding: 1px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collectionHeader {
|
.collectionHeader {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadMoreHeader {
|
||||||
|
color: RGB(5, 99, 193);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notebookResourceTree {
|
.notebookResourceTree {
|
||||||
@@ -23,6 +28,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;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
|
||||||
import "./TabComponent.less";
|
import "./TabComponent.less";
|
||||||
|
|
||||||
export interface TabContent {
|
export interface TabContent {
|
||||||
@@ -19,11 +18,15 @@ interface TabComponentProps {
|
|||||||
onTabIndexChange: (newIndex: number) => void;
|
onTabIndexChange: (newIndex: number) => void;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
|
interface TabRefs {
|
||||||
|
[key: string]: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We assume there's at least one tab
|
* We assume there's at least one tab
|
||||||
*/
|
*/
|
||||||
export class TabComponent extends React.Component<TabComponentProps> {
|
export class TabComponent extends React.Component<TabComponentProps> {
|
||||||
|
private tabRefs: TabRefs = {};
|
||||||
public constructor(props: TabComponentProps) {
|
public constructor(props: TabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -33,11 +36,41 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
|||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state = {
|
||||||
|
activeTabIndex: this.props.currentTabIndex,
|
||||||
|
};
|
||||||
|
|
||||||
private setActiveTab(index: number): void {
|
private setActiveTab(index: number): void {
|
||||||
this.setState({ activeTabIndex: index });
|
this.setState({ activeTabIndex: index });
|
||||||
this.props.onTabIndexChange(index);
|
this.props.onTabIndexChange(index);
|
||||||
}
|
}
|
||||||
|
private setIndex = (index: number) => {
|
||||||
|
const tab = this.tabRefs[index];
|
||||||
|
if (tab) {
|
||||||
|
tab.focus();
|
||||||
|
this.setState({ activeTabIndex: index });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private handlekeypress = (event: React.KeyboardEvent<HTMLSpanElement>): void => {
|
||||||
|
const { tabs, onTabIndexChange } = this.props;
|
||||||
|
const { activeTabIndex } = this.state;
|
||||||
|
const count = tabs.length;
|
||||||
|
|
||||||
|
const prevTab = () => {
|
||||||
|
this.setIndex((activeTabIndex - 1 + count) % count);
|
||||||
|
onTabIndexChange((activeTabIndex - 1 + count) % count);
|
||||||
|
};
|
||||||
|
const nextTab = () => {
|
||||||
|
this.setIndex((activeTabIndex + 1) % count);
|
||||||
|
onTabIndexChange((activeTabIndex + 1) % count);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
prevTab();
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
nextTab();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private renderTabTitles(): JSX.Element[] {
|
private renderTabTitles(): JSX.Element[] {
|
||||||
return this.props.tabs.map((tab: Tab, index: number) => {
|
return this.props.tabs.map((tab: Tab, index: number) => {
|
||||||
@@ -46,23 +79,33 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let className = "toggleSwitch";
|
let className = "toggleSwitch";
|
||||||
|
let ariaselected;
|
||||||
|
let tabindex;
|
||||||
if (index === this.props.currentTabIndex) {
|
if (index === this.props.currentTabIndex) {
|
||||||
className += " selectedToggle";
|
className += " selectedToggle";
|
||||||
|
ariaselected = true;
|
||||||
|
tabindex = 0;
|
||||||
} else {
|
} else {
|
||||||
className += " unselectedToggle";
|
className += " unselectedToggle";
|
||||||
|
ariaselected = false;
|
||||||
|
tabindex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab" key={index}>
|
<div className="tab" key={index}>
|
||||||
<AccessibleElement
|
<span
|
||||||
as="span"
|
|
||||||
className={className}
|
className={className}
|
||||||
role="presentation"
|
role="tab"
|
||||||
onActivated={() => this.setActiveTab(index)}
|
onClick={() => this.setActiveTab(index)}
|
||||||
|
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.handlekeypress(event)}
|
||||||
|
onFocus={() => this.setState({ activeTabIndex: index })}
|
||||||
aria-label={`Select tab: ${tab.title}`}
|
aria-label={`Select tab: ${tab.title}`}
|
||||||
|
aria-selected={ariaselected}
|
||||||
|
tabIndex={tabindex}
|
||||||
|
ref={(element) => (this.tabRefs[index] = element)}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</AccessibleElement>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -77,7 +120,11 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabComponentContainer">
|
<div className="tabComponentContainer">
|
||||||
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
|
{!this.props.hideHeader && (
|
||||||
|
<div className="tabs tabSwitch" role="tablist">
|
||||||
|
{this.renderTabTitles()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={className}>{currentTabContent.render()}</div>
|
<div className={className}>{currentTabContent.render()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
.treeComponent {
|
.treeComponent {
|
||||||
.nodeItem {
|
.nodeItem {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 1px dashed @AccentMedium;
|
outline: 2px @AccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeNodeHeader {
|
.treeNodeHeader {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
capabilities: [{ name: "EnableTable" }],
|
capabilities: [{ name: "EnableMongo" }],
|
||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
@@ -38,6 +38,38 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
);
|
);
|
||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Button should not be visible for Tables API", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableTable" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||||
|
);
|
||||||
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Button should not be visible for Cassandra API", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableCassandra" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||||
|
);
|
||||||
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Enable notebook button", () => {
|
describe("Enable notebook button", () => {
|
||||||
|
|||||||
@@ -51,11 +51,13 @@ export function createStaticCommandBarButtons(
|
|||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
buttons.push(newCollectionBtn);
|
buttons.push(newCollectionBtn);
|
||||||
|
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||||
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
|
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
if (addSynapseLink) {
|
||||||
if (addSynapseLink) {
|
buttons.push(createDivider());
|
||||||
buttons.push(createDivider());
|
buttons.push(addSynapseLink);
|
||||||
buttons.push(addSynapseLink);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables") {
|
if (userContext.apiType !== "Tables") {
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
role="button"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
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";
|
||||||
@@ -9,6 +9,8 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa
|
|||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
|
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
||||||
|
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||||
|
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
class MessageType {
|
class MessageType {
|
||||||
@@ -47,7 +49,6 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
IMongoShellTabComponentProps,
|
IMongoShellTabComponentProps,
|
||||||
IMongoShellTabComponentStates
|
IMongoShellTabComponentStates
|
||||||
> {
|
> {
|
||||||
private _runtimeEndpoint: string;
|
|
||||||
private _logTraces: Map<string, number>;
|
private _logTraces: Map<string, number>;
|
||||||
|
|
||||||
constructor(props: IMongoShellTabComponentProps) {
|
constructor(props: IMongoShellTabComponentProps) {
|
||||||
@@ -55,7 +56,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
this._logTraces = new Map();
|
this._logTraces = new Map();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
url: this.getURL(),
|
url: getMongoShellUrl(),
|
||||||
};
|
};
|
||||||
|
|
||||||
props.onMongoShellTabAccessor({
|
props.onMongoShellTabAccessor({
|
||||||
@@ -65,22 +66,6 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
window.addEventListener("message", this.handleMessage.bind(this), false);
|
window.addEventListener("message", this.handleMessage.bind(this), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(): string {
|
|
||||||
const { databaseAccount: account } = userContext;
|
|
||||||
const resourceId = account?.id;
|
|
||||||
const accountName = account?.name;
|
|
||||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
|
||||||
|
|
||||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
|
||||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
|
||||||
let baseUrl = "/content/mongoshell/dist/";
|
|
||||||
if (userContext.portalEnv === "localhost") {
|
|
||||||
baseUrl = "/content/mongoshell/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
public setContentFocus(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {}
|
public setContentFocus(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {}
|
||||||
|
|
||||||
@@ -136,6 +121,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
const collectionId = this.props.collection.id();
|
const collectionId = this.props.collection.id();
|
||||||
const apiEndpoint = configContext.BACKEND_ENDPOINT;
|
const apiEndpoint = configContext.BACKEND_ENDPOINT;
|
||||||
const encryptedAuthToken: string = userContext.accessToken;
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
|
const targetOrigin = getMongoShellOrigin();
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
@@ -151,7 +137,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
apiEndpoint: apiEndpoint,
|
apiEndpoint: apiEndpoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
configContext.BACKEND_ENDPOINT
|
targetOrigin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts
Normal file
86
src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||||
|
import { configContext } from "../../../ConfigContext";
|
||||||
|
import { updateUserContext } from "../../../UserContext";
|
||||||
|
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
||||||
|
|
||||||
|
describe("getMongoShellOrigin", () => {
|
||||||
|
(window as { origin: string }).origin = "window_origin";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1": "false",
|
||||||
|
"feature.enableLegacyMongoShellV2": "false",
|
||||||
|
"feature.enableLegacyMongoShellV1Debug": "false",
|
||||||
|
"feature.enableLegacyMongoShellV2Debug": "false",
|
||||||
|
"feature.loadLegacyMongoShellFromBE": "false",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return by default", () => {
|
||||||
|
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return window.origin when enableLegacyMongoShellV1", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return window.origin when enableLegacyMongoShellV2===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV2": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1Debug": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV2Debug": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.loadLegacyMongoShellFromBE": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts
Normal file
10
src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { configContext } from "../../../ConfigContext";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
|
||||||
|
export function getMongoShellOrigin(): string {
|
||||||
|
if (userContext.features.loadLegacyMongoShellFromBE === true) {
|
||||||
|
return configContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.origin;
|
||||||
|
}
|
||||||
206
src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts
Normal file
206
src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||||
|
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
|
||||||
|
import { updateUserContext, userContext } from "../../../UserContext";
|
||||||
|
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
|
||||||
|
|
||||||
|
const mongoBackendEndpoint = "https://localhost:1234";
|
||||||
|
|
||||||
|
describe("getMongoShellUrl", () => {
|
||||||
|
let queryString = "";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
|
platform: Platform.Hosted,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUserContext({
|
||||||
|
subscriptionId: "fakeSubscriptionId",
|
||||||
|
resourceGroup: "fakeResourceGroup",
|
||||||
|
databaseAccount: {
|
||||||
|
id: "fakeId",
|
||||||
|
name: "fakeName",
|
||||||
|
location: "fakeLocation",
|
||||||
|
type: "fakeType",
|
||||||
|
kind: "fakeKind",
|
||||||
|
properties: {
|
||||||
|
documentEndpoint: "fakeDocumentEndpoint",
|
||||||
|
tableEndpoint: "fakeTableEndpoint",
|
||||||
|
gremlinEndpoint: "fakeGremlinEndpoint",
|
||||||
|
cassandraEndpoint: "fakeCassandraEndpoint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1": "false",
|
||||||
|
"feature.enableLegacyMongoShellV2": "false",
|
||||||
|
"feature.enableLegacyMongoShellV1Debug": "false",
|
||||||
|
"feature.enableLegacyMongoShellV2Debug": "false",
|
||||||
|
"feature.loadLegacyMongoShellFromBE": "false",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
portalEnv: "prod",
|
||||||
|
});
|
||||||
|
|
||||||
|
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/indexv2.html by default ", () => {
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/indexv2.html when portalEnv==localhost ", () => {
|
||||||
|
updateUserContext({
|
||||||
|
portalEnv: "localhost",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV2": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV1Debug": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.enableLegacyMongoShellV2Debug": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadLegacyMongoShellFromBE===true", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
|
platform: Platform.Hosted,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUserContext({
|
||||||
|
features: extractFeatures(
|
||||||
|
new URLSearchParams({
|
||||||
|
"feature.loadLegacyMongoShellFromBE": "true",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /mongoshell/index.html", () => {
|
||||||
|
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
platform: Platform.Portal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
platform: Platform.Portal,
|
||||||
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html ", () => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
platform: Platform.Hosted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
platform: Platform.Portal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getExtensionEndpoint", () => {
|
||||||
|
it("when platform === Platform.Hosted, backendEndpoint is undefined ", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Hosted, backendEndpoint === ''", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Hosted, "")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Hosted, backendEndpoint === null", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Hosted, null)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Hosted, backendEndpoint != '' ", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Portal, backendEndpoint is udefined ", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Portal, backendEndpoint === '' ", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Portal, "")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform === Platform.Portal, backendEndpoint === null", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Portal, null)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when platform !== Platform.Portal, backendEndpoint != '' ", () => {
|
||||||
|
expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo");
|
||||||
|
});
|
||||||
|
});
|
||||||
45
src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts
Normal file
45
src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
|
||||||
|
export function getMongoShellUrl(): string {
|
||||||
|
const { databaseAccount: account } = userContext;
|
||||||
|
const resourceId = account?.id;
|
||||||
|
const accountName = account?.name;
|
||||||
|
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||||
|
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||||
|
|
||||||
|
if (userContext.features.enableLegacyMongoShellV1 === true) {
|
||||||
|
return `/mongoshell/index.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.features.enableLegacyMongoShellV1Debug === true) {
|
||||||
|
return `/mongoshell/debug/index.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.features.enableLegacyMongoShellV2 === true) {
|
||||||
|
return `/mongoshell/indexv2.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.features.enableLegacyMongoShellV2Debug === true) {
|
||||||
|
return `/mongoshell/debug/indexv2.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.portalEnv === "localhost") {
|
||||||
|
return `/mongoshell/indexv2.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.features.loadLegacyMongoShellFromBE === true) {
|
||||||
|
const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||||
|
return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/mongoshell/indexv2.html?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExtensionEndpoint(platform: string, backendEndpoint: string): string {
|
||||||
|
const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : "";
|
||||||
|
|
||||||
|
const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || "";
|
||||||
|
|
||||||
|
return extensionEndpoint;
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={active ? "active tabList" : "tabList"}
|
className={active ? "active tabList" : "tabList"}
|
||||||
|
style={active ? { fontWeight: "bolder" } : {}}
|
||||||
title={useObservable(tab?.tabPath || ko.observable(""))}
|
title={useObservable(tab?.tabPath || ko.observable(""))}
|
||||||
aria-selected={active}
|
aria-selected={active}
|
||||||
aria-expanded={active}
|
aria-expanded={active}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export type Features = {
|
|||||||
readonly mongoProxyAPIs?: string;
|
readonly mongoProxyAPIs?: string;
|
||||||
readonly enableThroughputCap: boolean;
|
readonly enableThroughputCap: boolean;
|
||||||
readonly enableHierarchicalKeys: boolean;
|
readonly enableHierarchicalKeys: boolean;
|
||||||
|
readonly enableLegacyMongoShellV1: boolean;
|
||||||
|
readonly enableLegacyMongoShellV1Debug: boolean;
|
||||||
|
readonly enableLegacyMongoShellV2: boolean;
|
||||||
|
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||||
|
readonly loadLegacyMongoShellFromBE: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -92,6 +97,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||||
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
||||||
|
enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"),
|
||||||
|
enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"),
|
||||||
|
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
||||||
|
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||||
|
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user