resolve master merge conflict

This commit is contained in:
hardiknai-techm
2021-04-19 17:52:28 +05:30
141 changed files with 12044 additions and 3407 deletions

View File

@@ -75,7 +75,10 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
}
}
let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient {
if (_client) return _client;
const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey,
@@ -89,5 +92,6 @@ export function client(): Cosmos.CosmosClient {
if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
return new Cosmos.CosmosClient(options);
_client = new Cosmos.CosmosClient(options);
return _client;
}

View File

@@ -48,32 +48,18 @@ export function sendCachedDataMessage<TResponseDataModel>(
}
export function sendMessage(data: any): void {
if (canSendMessage()) {
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
data: data,
},
portalChildWindow.document.referrer || "*"
);
}
_sendMessage({
signature: "pcIframe",
data: data,
});
}
export function sendReadyMessage(): void {
if (canSendMessage()) {
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer || "*"
);
}
_sendMessage({
signature: "pcIframe",
kind: "ready",
data: "ready",
});
}
export function canSendMessage(): boolean {
@@ -89,3 +75,17 @@ export function runGarbageCollector() {
}
});
}
const _sendMessage = (message: any): void => {
if (canSendMessage()) {
// Portal window can receive messages from only child windows
const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
} else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
}
}
};

View File

@@ -0,0 +1,39 @@
import { JSONObject, OperationResponse } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export const bulkCreateDocument = async (
collection: CollectionBase,
documents: JSONObject[]
): Promise<OperationResponse[]> => {
const clearMessage = logConsoleProgress(
`Executing ${documents.length} bulk operations for container ${collection.id()}`
);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.bulk(
documents.map((doc) => ({ operationType: "Create", resourceBody: doc })),
{ continueOnError: true }
);
const successCount = response.filter((r) => r.statusCode === 201).length;
const throttledCount = response.filter((r) => r.statusCode === 429).length;
logConsoleInfo(
`${
documents.length
} operations completed for container ${collection.id()}. ${successCount} operations succeeded. ${throttledCount} operations throttled`
);
return response;
} catch (error) {
handleError(error, "BulkCreateDocument", `Error bulk creating items for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,15 +1,15 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { userContext } from "../../UserContext";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
@@ -17,7 +17,6 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if (
userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readCollectionsWithARM(databaseId);

View File

@@ -1,39 +1,37 @@
import { ContainerDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import {
CreateUpdateOptions,
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
MongoDBCollectionResource,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { userContext } from "../../UserContext";
import {
createUpdateCassandraTable,
getCassandraTable,
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
createUpdateGremlinGraph,
getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils";
import {
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateCollection(
databaseId: string,
collectionId: string,
newCollection: Collection,
newCollection: Partial<Collection>,
options: RequestOptions = {}
): Promise<Collection> {
let collection: Collection;
@@ -43,7 +41,6 @@ export async function updateCollection(
if (
userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
@@ -69,7 +66,7 @@ export async function updateCollection(
async function updateCollectionWithARM(
databaseId: string,
collectionId: string,
newCollection: Collection
newCollection: Partial<Collection>
): Promise<Collection> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
@@ -85,6 +82,15 @@ async function updateCollectionWithARM(
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
@@ -96,7 +102,7 @@ async function updateSqlContainer(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
newCollection: Partial<Collection>
): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -115,35 +121,26 @@ async function updateSqlContainer(
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
export async function updateMongoDBCollectionThroughRP(
export async function updateMongoDBCollection(
databaseId: string,
collectionId: string,
newCollection: MongoDBCollectionResource,
updateOptions?: CreateUpdateOptions
): Promise<MongoDBCollectionResource> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
): Promise<Collection> {
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
const updateParams: MongoDBCollectionCreateUpdateParameters = {
properties: {
resource: newCollection,
options: updateOptions,
},
};
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateMongoDBCollection(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
updateParams
getResponse as MongoDBCollectionCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(
@@ -157,7 +154,7 @@ async function updateCassandraTable(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
newCollection: Partial<Collection>
): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -184,7 +181,7 @@ async function updateGremlinGraph(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
newCollection: Partial<Collection>
): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -208,7 +205,7 @@ async function updateTable(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
newCollection: Partial<Collection>
): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {

View File

@@ -121,6 +121,10 @@ export interface ISchemaRequest {
}
export interface Collection extends Resource {
// Only in Mongo collections loaded via ARM
shardKey?: {
[key: string]: string;
};
defaultTtl?: number;
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;

View File

@@ -15,7 +15,6 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType";
@@ -23,6 +22,14 @@ export interface TokenProvider {
getAuthHeader(): Promise<Headers>;
}
export interface UploadDetailsRecord {
fileName: string;
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}
export interface QueryResultsMetadata {
hasMoreResults: boolean;
firstItemIndex: number;
@@ -174,7 +181,7 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<UploadDetails>;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
@@ -269,7 +276,6 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
isActive: ko.Observable<boolean>;
hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>;
@@ -390,6 +396,9 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[];
features?: {
[key: string]: string;
};
}
export interface SelfServeFrameInputs {

View File

@@ -1,7 +0,0 @@
declare module "worker-loader!*" {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}

View File

@@ -73,10 +73,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
it("should register delete-collection-confirmation-pane component", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
});
it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
});

View File

@@ -57,16 +57,11 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Panes
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register(
"delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());

View File

@@ -55,7 +55,7 @@ export class ResourceTreeContextMenuButtonFactory {
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
@@ -80,7 +80,7 @@ export class ResourceTreeContextMenuButtonFactory {
});
}
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
@@ -123,7 +123,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
return [];
}
@@ -137,7 +137,7 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
return [];
}
@@ -154,7 +154,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
return [];
}

View File

@@ -21,18 +21,18 @@ import {
Text,
} from "office-ui-fabric-react";
import * as React from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer";
import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps {
container?: Explorer;
@@ -138,11 +138,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
key: SortBy.MostRecent,
text: GalleryViewerComponent.mostRecentText,
},
{
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
},
];
this.sortingOptions.push({
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
});
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
@@ -654,7 +654,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
private onRenderCell = (data?: IGalleryItem): JSX.Element => {
const isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
const isFavorite =
this.props.container && this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
const props: GalleryCardComponentProps = {
data,
isFavorite,

View File

@@ -1,11 +1,11 @@
import { shallow } from "enzyme";
import ko from "knockout";
import React from "react";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
@@ -23,13 +23,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
changeFeedPolicy: undefined,
analyticalStorageTtl: undefined,
geospatialConfig: undefined,
} as DataModels.Collection),
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
id: undefined,
shardKey: undefined,
indexes: [],
analyticalStorageTtl: undefined,
} as MongoDBCollectionResource),
}),
}));
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
@@ -44,7 +39,6 @@ describe("SettingsComponent", () => {
tabPath: "",
node: undefined,
hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined,
}),
};
@@ -113,7 +107,13 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DataModels.DatabaseAccount,
});
const newCollection = { ...collection };
newCollection.container = newContainer;
@@ -193,7 +193,6 @@ describe("SettingsComponent", () => {
};
await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled();
expect(updateMongoDBCollectionThroughRP).toBeCalled();
expect(updateOffer).toBeCalled();
});

View File

@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
@@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -299,7 +299,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean =>
this.container?.databaseAccount &&
@@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try {
const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection: MongoDBCollectionResource = {
const newMongoCollection = {
...this.mongoDBCollectionResource,
indexes: newMongoIndexes,
};
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.mongoDBCollectionResource = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newMongoCollection

View File

@@ -1,14 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
import { container, collection } from "../TestUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
import ko from "knockout";
import { DatabaseAccount } from "../../../../Contracts/DataModels";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
import { collection, container } from "../TestUtils";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
describe("SubSettingsComponent", () => {
container.isPreferredApiDocumentDB = ko.computed(() => true);
const baseProps: SubSettingsComponentProps = {
collection: collection,
container: container,
@@ -106,8 +105,13 @@ describe("SubSettingsComponent", () => {
it("partitionKey not visible", () => {
const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);

View File

@@ -1,28 +1,38 @@
import {
ChoiceGroup,
IChoiceGroupOption,
Label,
Link,
MessageBar,
Stack,
Text,
TextField,
} from "office-ui-fabric-react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import {
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult,
TtlOn,
TtlOff,
TtlOnNoDefault,
getSanitizedInputValue,
} from "../SettingsUtils";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
import {
getTextFieldStyles,
changeFeedPolicyToolTip,
getChoiceGroupStyles,
getTextFieldStyles,
messageBarStyles,
subComponentStackProps,
titleAndInputStackProps,
getChoiceGroupStyles,
ttlWarning,
messageBarStyles,
} from "../SettingsRenderUtils";
import {
ChangeFeedPolicyState,
GeospatialConfigType,
getSanitizedInputValue,
IsComponentDirtyResult,
isDirty,
TtlOff,
TtlOn,
TtlOnNoDefault,
TtlType,
} from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps {
@@ -60,17 +70,15 @@ export interface SubSettingsComponentProps {
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
private shouldCheckComponentIsDirty = true;
private ttlVisible: boolean;
private geospatialVisible: boolean;
private partitionKeyValue: string;
private partitionKeyName: string;
constructor(props: SubSettingsComponentProps) {
super(props);
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
}
componentDidMount(): void {
@@ -170,39 +178,51 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
): void =>
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
private getTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
private getTtlComponent = (): JSX.Element =>
userContext.apiType === "Mongo" ? (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={{ text: { fontSize: 14 } }}
>
To enable time-to-live (TTL) for your collection/documents,
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
create a TTL index
</Link>
.
</MessageBar>
) : (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
)}
</Stack>
);
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
</Stack>
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true },
@@ -300,7 +320,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public getPartitionKeyVisible = (): boolean => {
if (
this.props.container.isPreferredApiCassandra() ||
userContext.apiType === "Cassandra" ||
this.props.container.isPreferredApiTable() ||
!this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
@@ -315,7 +335,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.ttlVisible && this.getTtlComponent()}
{userContext.apiType !== "Cassandra" && this.getTtlComponent()}
{this.geospatialVisible && this.getGeoSpatialComponent()}

View File

@@ -115,21 +115,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -220,27 +205,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -531,21 +495,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -614,9 +563,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -656,27 +602,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -868,21 +793,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -973,27 +883,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -1284,21 +1173,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -1367,9 +1241,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -1409,27 +1280,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -1634,21 +1484,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1739,27 +1574,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -2050,21 +1864,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -2133,9 +1932,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -2175,27 +1971,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -2387,21 +2162,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -2492,27 +2252,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -2803,21 +2542,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -2886,9 +2610,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -2928,27 +2649,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],

View File

@@ -4,6 +4,7 @@ jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout";
import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
@@ -13,11 +14,8 @@ describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve();
@@ -31,7 +29,7 @@ describe("ContainerSampleGenerator", () => {
it("should insert documents for sql API account", async () => {
const sampleCollectionId = "SampleCollection";
const sampleDatabaseId = "SampleDB";
updateUserContext({});
const sampleData = {
databaseId: sampleDatabaseId,
offerThroughput: 400,
@@ -66,7 +64,7 @@ describe("ContainerSampleGenerator", () => {
database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
@@ -116,7 +114,13 @@ describe("ContainerSampleGenerator", () => {
collection.databaseId = database.id();
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
@@ -125,31 +129,45 @@ describe("ContainerSampleGenerator", () => {
});
it("should not create any sample for Mongo API account", async () => {
const experience = "not supported api";
const experience = "Sample generation not supported for this API Mongo";
const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
explorerStub.defaultExperience = ko.observable<string>(experience);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});
it("should not create any sample for Table API account", async () => {
const experience = "not supported api";
const experience = "Sample generation not supported for this API Tables";
const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => true);
explorerStub.defaultExperience = ko.observable<string>(experience);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});
it("should not create any sample for Cassandra API account", async () => {
const experience = "not supported api";
const experience = "Sample generation not supported for this API Cassandra";
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => true);
explorerStub.defaultExperience = ko.observable<string>(experience);
// Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});

View File

@@ -1,12 +1,12 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[];
@@ -23,16 +23,16 @@ export class ContainerSampleGenerator {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container);
let dataFileContent: any;
if (container.isPreferredApiGraph()) {
if (userContext.apiType === "Gremlin") {
dataFileContent = await import(
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
);
} else if (container.isPreferredApiDocumentDB()) {
} else if (userContext.apiType === "SQL") {
dataFileContent = await import(
/* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json"
);
} else {
return Promise.reject(`Sample generation not supported for this API ${container.defaultExperience()}`);
return Promise.reject(`Sample generation not supported for this API ${userContext.apiType}`);
}
generator.setData(dataFileContent);
@@ -73,7 +73,7 @@ export class ContainerSampleGenerator {
}
const promises: Q.Promise<any>[] = [];
if (this.container.isPreferredApiGraph()) {
if (userContext.apiType === "Gremlin") {
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
// (e.g. adding edge requires vertices to be present)
const queries: string[] = this.sampleDataFile.data;

View File

@@ -1,4 +1,5 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
@@ -56,6 +57,6 @@ export class DataSamplesUtil {
}
public isSampleContainerCreationSupported(): boolean {
return this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph();
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}
}

View File

@@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as PricingUtils from "../Utils/PricingUtils";
import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
@@ -48,11 +49,10 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo
import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePane } from "./Panes/AddDatabasePane";
import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
@@ -65,9 +65,10 @@ import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase";
@@ -78,8 +79,6 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -93,6 +92,7 @@ export interface ExplorerParams {
closeSidePanel: () => void;
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
tabsManager: TabsManager;
}
export default class Explorer {
@@ -116,26 +116,12 @@ export default class Explorer {
* Use userContext.apiType instead
* */
public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
@@ -188,12 +174,11 @@ export default class Explorer {
public tabsManager: TabsManager;
// Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane;
@@ -420,20 +405,6 @@ export default class Explorer {
});
});
this.isPreferredApiDocumentDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase();
});
this.isPreferredApiCassandra = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase();
});
this.isPreferredApiGraph = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase();
});
this.isPreferredApiTable = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase();
@@ -497,7 +468,9 @@ export default class Explorer {
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
configContext.platform === Platform.Portal &&
!this.isRunningOnNationalCloud() &&
userContext.apiType !== "Gremlin"
);
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.selectedDatabaseId = ko.computed<string>(() => {
@@ -521,16 +494,16 @@ export default class Explorer {
}
});
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
id: "addcollectionpane",
this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
id: "deletecollectionconfirmationpane",
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
id: "addcollectionpane",
visible: ko.observable<boolean>(false),
container: this,
@@ -557,13 +530,6 @@ export default class Explorer {
container: this,
});
this.querySelectPane = new QuerySelectPane({
id: "queryselectpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.newVertexPane = new NewVertexPane({
id: "newvertexpane",
visible: ko.observable<boolean>(false),
@@ -592,21 +558,26 @@ export default class Explorer {
container: this,
});
this.tabsManager = new TabsManager();
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
this.selectedNode(undefined);
this.onUpdateTabsButtons([]);
}
});
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
this.deleteCollectionConfirmationPane,
this.graphStylingPane,
this.addTableEntityPane,
this.editTableEntityPane,
this.querySelectPane,
this.newVertexPane,
this.cassandraAddCollectionPane,
this.stringInputPane,
this.setupNotebooksPane,
];
//this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
@@ -634,8 +605,6 @@ export default class Explorer {
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container"
);
this.deleteCollectionConfirmationPane.title("Delete Container");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.refreshTreeTitle("Refresh containers");
break;
case "Mongo":
@@ -662,8 +631,6 @@ export default class Explorer {
this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.deleteCollectionConfirmationPane.title("Delete Graph");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.refreshTreeTitle("Refresh graphs");
break;
case "Tables":
@@ -679,8 +646,6 @@ export default class Explorer {
this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Entity");
this.editTableEntityPane.title("Edit Table Entity");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient();
break;
case "Cassandra":
@@ -696,8 +661,6 @@ export default class Explorer {
this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Row");
this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new CassandraAPIDataClient();
break;
}
@@ -915,10 +878,8 @@ export default class Explorer {
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
@@ -931,20 +892,16 @@ export default class Explorer {
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();
},
(reason) => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
);
},
(error) => {
this._setLoadingStatusText("Failed to fetch databases.");
deferred.reject(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
@@ -1020,7 +977,7 @@ export default class Explorer {
// Facade
public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_self");
window.open(Constants.Urls.feedbackEmail, "_blank");
};
public async getArcadiaToken(): Promise<string> {
@@ -1291,49 +1248,6 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection;
}
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
public findSelectedStoredProcedure(): StoredProcedure {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab) => tab.node && tab.node.rid === storedProcedure.rid
);
return (
storedProcedure.rid === this.selectedNode().rid ||
(!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive())
);
});
}
public findSelectedUDF(): UserDefinedFunction {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab) => tab.node && tab.node.rid === userDefinedFunction.rid
);
return (
userDefinedFunction.rid === this.selectedNode().rid ||
(!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive())
);
});
}
public findSelectedTrigger(): Trigger {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
(tab) => tab.node && tab.node.rid === trigger.rid
);
return (
trigger.rid === this.selectedNode().rid ||
(!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive())
);
});
}
public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
}
@@ -1662,7 +1576,6 @@ export default class Explorer {
collection: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2068,7 +1981,6 @@ export default class Explorer {
tabPath: title,
collection: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2173,7 +2085,7 @@ export default class Explorer {
}
public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel();
@@ -2216,32 +2128,6 @@ export default class Explorer {
}
}
private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") {
if (!text) {
return;
}
const loadingText = document.getElementById("explorerLoadingStatusText");
if (!loadingText) {
Logger.logError(
"getElementById('explorerLoadingStatusText') failed to find element",
"Explorer/_setLoadingStatusText"
);
return;
}
loadingText.innerHTML = text;
const loadingTitle = document.getElementById("explorerLoadingStatusTitle");
if (!loadingTitle) {
Logger.logError(
"getElementById('explorerLoadingStatusTitle') failed to find element",
"Explorer/_setLoadingStatusText"
);
} else {
loadingTitle.innerHTML = title;
}
}
private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)";
const description =
@@ -2309,16 +2195,15 @@ export default class Explorer {
}
public openDeleteCollectionConfirmationPane(): void {
userContext.features.enableKOPanel
? this.deleteCollectionConfirmationPane.open()
: this.openSidePanel(
"Delete Collection",
<DeleteCollectionConfirmationPanel
explorer={this}
closePanel={() => this.closeSidePanel()}
openNotificationConsole={() => this.expandConsole()}
/>
);
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
this.openSidePanel(
"Delete " + collectionName,
<DeleteCollectionConfirmationPanel
explorer={this}
collectionName={collectionName}
closePanel={this.closeSidePanel}
/>
);
}
public openDeleteDatabaseConfirmationPane(): void {
@@ -2341,10 +2226,14 @@ export default class Explorer {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openExecuteSprocParamsPanel(): void {
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
this.openSidePanel(
"Input parameters",
<ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} />
<ExecuteSprocParamsPanel
explorer={this}
storedProcedure={storedProcedure}
closePanel={() => this.closeSidePanel()}
/>
);
}
@@ -2389,4 +2278,11 @@ export default class Explorer {
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel(
"Select Column",
<TableQuerySelectPanel explorer={this} closePanel={this.closeSidePanel} queryViewModel={queryViewModal} />
);
}
}

View File

@@ -1,6 +1,6 @@
import { NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphData from "./GraphData";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as GraphData from "./GraphData";
import { NeighborVertexBasicInfo } from "./GraphExplorer";
interface JoinArrayMaxCharOutput {
result: string; // string output
@@ -13,9 +13,9 @@ interface EdgePropertyType {
inV?: string;
}
export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
}
};
/**
* Collect all edges from this node
@@ -23,11 +23,11 @@ export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
* @param graphData
* @param newNodes (optional) object describing new nodes encountered
*/
export function createEdgesfromNode(
export const createEdgesfromNode = (
vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean }
): void {
): void => {
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
const outE = vertex.outE;
for (const label in outE) {
@@ -66,7 +66,7 @@ export function createEdgesfromNode(
});
}
}
}
};
/**
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
@@ -75,7 +75,7 @@ export function createEdgesfromNode(
* @param maxSize
* @return
*/
export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput {
export const getLimitedArrayString = (array: string[], maxSize: number): JoinArrayMaxCharOutput => {
if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 };
}
@@ -96,16 +96,16 @@ export function getLimitedArrayString(array: string[], maxSize: number): JoinArr
result: output,
consumedCount: i + 1,
};
}
};
export function createFetchEdgePairQuery(
export const createFetchEdgePairQuery = (
outE: boolean,
pkid: string,
excludedEdgeIds: string[],
startIndex: number,
pageSize: number,
withoutStepArgMaxLenght: number
): string {
): string => {
let gremlinQuery: string;
if (excludedEdgeIds.length > 0) {
// build a string up to max char
@@ -128,15 +128,15 @@ export function createFetchEdgePairQuery(
}().as('v').select('e', 'v')`;
}
return gremlinQuery;
}
};
/**
* Trim graph
*/
export function trimGraph(
export const trimGraph = (
currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) {
): void => {
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
graphData.unloadAllVertices(importantNodes);
@@ -144,32 +144,32 @@ export function trimGraph(
$.each(graphData.ids, (index: number, id: string) => {
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
});
}
};
export function addRootChildToGraph(
export const addRootChildToGraph = (
root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) {
): void => {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child);
createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child);
}
};
/**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value
*/
export function escapeDoubleQuotes(value: string): string {
export const escapeDoubleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/"/g, '\\"');
}
};
/**
* Surround with double-quotes if val is a string.
* @param val
*/
export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => {
switch (ip.type) {
case "number":
case "boolean":
@@ -179,12 +179,12 @@ export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
default:
return `"${escapeDoubleQuotes(ip.value as string)}"`;
}
}
};
/**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value
*/
export function escapeSingleQuotes(value: string): string {
export const escapeSingleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/'/g, "\\'");
}
};

View File

@@ -4,15 +4,15 @@
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil";
import Explorer from "../../Explorer";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
@@ -23,17 +23,14 @@ export class CommandBarComponentAdapter implements ReactAdapter {
constructor(container: Explorer) {
this.container = container;
this.tabsButtons = [];
this.isNotebookTabActive = ko.computed(() =>
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
this.isNotebookTabActive = ko.computed(
() => container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2
);
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [
container.isPreferredApiTable,
container.isPreferredApiMongoDB,
container.isPreferredApiDocumentDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText,
container.deleteDatabaseText,
container.addCollectionText,

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
@@ -17,7 +18,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -56,7 +56,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -119,7 +118,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -208,15 +206,26 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
beforeEach(() => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
});
it("Cassandra Api not available - button should be hidden", () => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
@@ -281,7 +290,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
@@ -337,7 +345,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -347,6 +354,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("should only show New SQL Query and Open Query buttons", () => {
updateUserContext({
databaseAccount: {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");

View File

@@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenMongoTerminalButton(container));
}
if (container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container));
}
}
@@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createDivider());
}
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSqlQuerySupported) {
const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn);
}
const isSupportedOpenQueryApi =
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
@@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenQueryFromDiskButton(container));
}
if (areScriptsSupported(container)) {
if (areScriptsSupported()) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
@@ -154,25 +154,18 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
}
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (configContext.platform === Platform.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
const buttons: CommandButtonComponentProps[] = [
{
iconSrc: SettingsIcon,
iconAlt: label,
iconAlt: "Settings",
onCommandClick: () => container.openSettingPane(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
ariaLabel: "Settings",
tooltipText: "Settings",
hasPopup: true,
disabled: false,
};
buttons.push(settingsPaneButton);
}
},
];
if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen";
@@ -223,8 +216,8 @@ export function createDivider(): CommandButtonComponentProps {
};
}
function areScriptsSupported(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
@@ -286,7 +279,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () => {
container.openAddDatabasePane();
container.addDatabasePane.open();
// container.openAddDatabasePane();
document.getElementById("linkAddDatabase").focus();
},
commandButtonLabel: label,
@@ -296,7 +290,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
}
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
@@ -310,7 +304,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
} else if (container.isPreferredApiMongoDB()) {
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
return {
iconSrc: AddSqlQueryIcon,
@@ -332,8 +326,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";

View File

@@ -1,9 +1,9 @@
import React from "react";
import { shallow } from "enzyme";
import React from "react";
import {
NotificationConsoleComponentProps,
NotificationConsoleComponent,
ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps,
} from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => {
@@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => {
consoleData: undefined,
isConsoleExpanded: false,
inProgressConsoleDataIdToBeDeleted: "",
setIsConsoleExpanded: (isExpanded: boolean): void => {},
setIsConsoleExpanded: (): void => undefined,
};
};
@@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true);
};
it("renders progress notifications", () => {
@@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
wrapper.find(".clearNotificationsButton").simulate("click");
expect(!wrapper.exists(".notificationConsoleData"));
expect(wrapper.exists(".notificationConsoleData")).toBe(true);
});
it("collapses and hide content", () => {
@@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
wrapper.find(".notificationConsoleHeader").simulate("click");
expect(!wrapper.exists(".notificationConsoleContent"));
expect(wrapper.exists(".notificationConsoleContent")).toBe(false);
});
it("display latest data in header", () => {

View File

@@ -2,19 +2,20 @@
* React component for control bar
*/
import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import LoadingIcon from "../../../../images/loading.svg";
import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import LoadingIcon from "../../../../images/loading.svg";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
/**
* Log levels
@@ -76,7 +77,7 @@ export class NotificationConsoleComponent extends React.Component<
public componentDidUpdate(
prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState
) {
): void {
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
if (
@@ -97,7 +98,7 @@ export class NotificationConsoleComponent extends React.Component<
}
}
public setElememntRef = (element: HTMLElement) => {
public setElememntRef = (element: HTMLElement): void => {
this.consoleHeaderElement = element;
};
@@ -116,7 +117,7 @@ export class NotificationConsoleComponent extends React.Component<
className="notificationConsoleHeader"
id="notificationConsoleHeader"
ref={this.setElememntRef}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
>
@@ -135,6 +136,7 @@ export class NotificationConsoleComponent extends React.Component<
<span className="numInfoItems">{numInfoItems}</span>
</span>
</span>
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" />
<span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
@@ -304,3 +306,18 @@ export class NotificationConsoleComponent extends React.Component<
);
};
}
const PrPreview = (props: { pr: string }) => {
const url = new URL(props.pr);
const [, ref] = url.hash.split("#");
url.hash = "";
return (
<>
<span className="consoleSplitter" />
<a target="_blank" rel="noreferrer" href={url.href} style={{ marginRight: "1em", fontWeight: "bold" }}>
{ref}
</a>
</>
);
};

View File

@@ -1,15 +1,16 @@
// Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient?
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
// Vendor modules
import {
actions,
AppState,
ContentRecord,
createHostRef,
createKernelspecsRef,
HostRecord,
HostRef,
IContentProvider,
KernelspecsRef,
makeAppRecord,
makeCommsRecord,
makeContentsRecord,
@@ -19,23 +20,22 @@ import {
makeJupyterHostRecord,
makeStateRecord,
makeTransformsRecord,
ContentRecord,
HostRecord,
HostRef,
KernelspecsRef,
IContentProvider,
} from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import configureStore from "./NotebookComponent/store";
import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript";
import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML";
export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -168,8 +168,10 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json,
"application/javascript": Media.JavaScript,
"text/html": Media.HTML,
"application/javascript": userContext.features.sandboxNotebookOutputs
? SandboxJavaScript
: Media.JavaScript,
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML,
"text/markdown": Media.Markdown,
"text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG,

View File

@@ -1,18 +1,20 @@
import * as React from "react";
import "./base.css";
import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import "./default.css";
import "./NotebookReadOnlyRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
export interface NotebookRendererProps {
contentRef: any;
@@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,

View File

@@ -1,37 +1,33 @@
import * as React from "react";
import "./base.css";
import "./default.css";
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef } from "@nteract/core";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt from "./Prompt";
import { promptContent } from "./PromptContent";
import { AzureTheme } from "./AzureTheme";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react";
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import { CellId } from "@nteract/commutable";
import loadTransform from "../NotebookComponent/loadTransform";
import DraggableCell from "./decorators/draggable";
import CellCreator from "./decorators/CellCreator";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import CellToolbar from "./Toolbar";
import StatusBar from "./StatusBar";
import HijackScroll from "./decorators/hijack-scroll";
import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import { userContext } from "../../../UserContext";
import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
import CellLabeler from "./decorators/CellLabeler";
import HoverableCell from "./decorators/HoverableCell";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css";
import MarkdownCell from "./markdown-cell";
import "./NotebookRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
import Prompt from "./Prompt";
import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar";
import CellToolbar from "./Toolbar";
export interface NotebookRendererBaseProps {
contentRef: any;
@@ -112,6 +108,16 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</Prompt>
),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
}}
</CodeCell>
),

View File

@@ -0,0 +1,160 @@
// TODO The purpose of importing this source file https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/cells/markdown-cell.tsx
// into our source is to be able to overwrite the version of react-markdown which has this fix ("escape html to false")
// https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631 without having to upgrade
// @nteract/stateful-component which causes runtime issues.
import { ImmutableCell } from "@nteract/commutable/src";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { MarkdownPreviewer } from "@nteract/markdown";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { Source as BareSource } from "@nteract/presentational-components";
import Editor, { EditorSlots } from "@nteract/stateful-components/lib/inputs/editor";
import React from "react";
import { ReactMarkdownProps } from "react-markdown";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
const { selector: markdownConfig } = defineConfigOption({
key: "markdownOptions",
label: "Markdown Editor Options",
defaultValue: {},
});
interface NamedMDCellSlots {
editor?: EditorSlots;
toolbar?: () => JSX.Element;
}
interface ComponentProps {
id: string;
contentRef: ContentRef;
cell_type?: "markdown";
children?: NamedMDCellSlots;
}
interface StateProps {
isCellFocused: boolean;
isEditorFocused: boolean;
cell?: ImmutableCell;
markdownOptions: ReactMarkdownProps;
}
interface DispatchProps {
focusAboveCell: () => void;
focusBelowCell: () => void;
focusEditor: () => void;
unfocusEditor: () => void;
}
// Add missing style to make the editor show https://github.com/nteract/nteract/commit/7fa580011578350e56deac81359f6294fdfcad20#diff-07829a1908e4bf98d4420f868a1c6f890b95d77297b9805c9590d2dba11e80ce
export const Source = styled(BareSource)`
width: 100%;
width: -webkit-fill-available;
width: -moz-available;
`;
export class PureMarkdownCell extends React.Component<ComponentProps & DispatchProps & StateProps> {
render() {
const { contentRef, id, cell, children } = this.props;
const { isEditorFocused, isCellFocused, markdownOptions } = this.props;
const { focusAboveCell, focusBelowCell, focusEditor, unfocusEditor } = this.props;
/**
* We don't set the editor slots as defaults to support dynamic imports
* Users can continue to add the editorSlots as children
*/
const editor = children?.editor;
const toolbar = children?.toolbar;
const source = cell ? cell.get("source", "") : "";
return (
<div className="nteract-md-cell nteract-cell">
<div className="nteract-cell-row">
<div className="nteract-cell-gutter">{toolbar && toolbar()}</div>
<div className="nteract-cell-body">
<MarkdownPreviewer
focusAbove={focusAboveCell}
focusBelow={focusBelowCell}
focusEditor={focusEditor}
cellFocused={isCellFocused}
editorFocused={isEditorFocused}
unfocusEditor={unfocusEditor}
source={source}
markdownOptions={markdownOptions}
>
<Source className="nteract-cell-source">
<Editor id={id} contentRef={contentRef}>
{editor}
</Editor>
</Source>
</MarkdownPreviewer>
</div>
</div>
</div>
);
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const { id, contentRef } = ownProps;
const mapStateToProps = (state: AppState): StateProps => {
const model = selectors.model(state, { contentRef });
let isCellFocused = false;
let isEditorFocused = false;
let cell;
if (model && model.type === "notebook") {
cell = selectors.notebook.cellById(model, { id });
isCellFocused = model.cellFocused === id;
isEditorFocused = model.editorFocused === id;
}
const markdownOptionsDefaults = {
linkTarget: "_blank",
};
const currentMarkdownOptions = markdownConfig(state);
const markdownOptions = Object.assign({}, markdownOptionsDefaults, currentMarkdownOptions);
return {
cell,
isCellFocused,
isEditorFocused,
markdownOptions,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = (
initialDispatch: Dispatch,
ownProps: ComponentProps
): ((dispatch: Dispatch) => DispatchProps) => {
const { id, contentRef } = ownProps;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
focusAboveCell: () => {
dispatch(actions.focusPreviousCell({ id, contentRef }));
dispatch(actions.focusPreviousCellEditor({ id, contentRef }));
},
focusBelowCell: () => {
dispatch(actions.focusNextCell({ id, createCellIfUndefined: true, contentRef }));
dispatch(actions.focusNextCellEditor({ id, contentRef }));
},
focusEditor: () => dispatch(actions.focusCellEditor({ id, contentRef })),
unfocusEditor: () => dispatch(actions.focusCellEditor({ id: undefined, contentRef })),
});
return mapDispatchToProps;
};
const MarkdownCell = connect(makeMapStateToProps, makeMapDispatchToProps)(PureMarkdownCell);
export default MarkdownCell;

View File

@@ -0,0 +1,70 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import { Output } from "@nteract/outputs";
import Immutable from "immutable";
import React from "react";
import { connect } from "react-redux";
import { SandboxFrame } from "./SandboxFrame";
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
// to add support for sandboxing using <iframe>
interface ComponentProps {
id: string;
contentRef: ContentRef;
children: React.ReactNode;
}
interface StateProps {
hidden: boolean;
expanded: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputs: Immutable.List<any>;
}
export class IFrameOutputs extends React.PureComponent<ComponentProps & StateProps> {
render(): JSX.Element {
const { outputs, children, hidden, expanded } = this.props;
return (
<SandboxFrame
style={{ border: "none", width: "100%" }}
sandbox="allow-downloads allow-forms allow-pointer-lock allow-same-origin allow-scripts"
>
<div className={`nteract-cell-outputs ${hidden ? "hidden" : ""} ${expanded ? "expanded" : ""}`}>
{outputs.map((output, index) => (
<Output output={output} key={index}>
{children}
</Output>
))}
</div>
</SandboxFrame>
);
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState): StateProps => {
let outputs = Immutable.List();
let hidden = false;
let expanded = false;
const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
hidden = cell.cell_type === "code" && cell.getIn(["metadata", "jupyter", "outputs_hidden"]);
expanded = cell.cell_type === "code" && cell.getIn(["metadata", "collapsed"]) === false;
}
}
return { outputs, hidden, expanded };
};
return mapStateToProps;
};
export default connect<StateProps, void, ComponentProps, AppState>(makeMapStateToProps)(IFrameOutputs);

View File

@@ -0,0 +1,69 @@
import React from "react";
import ReactDOM from "react-dom";
import { copyStyles } from "../../../../Utils/StyleUtils";
interface SandboxFrameProps {
style: React.CSSProperties;
sandbox: string;
}
interface SandboxFrameState {
frame: HTMLIFrameElement;
frameBody: HTMLElement;
frameHeight: number;
}
export class SandboxFrame extends React.PureComponent<SandboxFrameProps, SandboxFrameState> {
private resizeObserver: ResizeObserver;
private mutationObserver: MutationObserver;
constructor(props: SandboxFrameProps) {
super(props);
this.state = {
frame: undefined,
frameBody: undefined,
frameHeight: 0,
};
}
render(): JSX.Element {
return (
<iframe
ref={(ele) => this.setState({ frame: ele })}
srcDoc={`<!DOCTYPE html>`}
onLoad={(event) => this.onFrameLoad(event)}
style={this.props.style}
sandbox={this.props.sandbox}
height={this.state.frameHeight}
>
{this.state.frameBody && ReactDOM.createPortal(this.props.children, this.state.frameBody)}
</iframe>
);
}
componentWillUnmount(): void {
this.resizeObserver?.disconnect();
this.mutationObserver?.disconnect();
}
onFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
const doc = (event.target as HTMLIFrameElement).contentDocument;
copyStyles(document, doc);
this.setState({ frameBody: doc.body });
this.mutationObserver = new MutationObserver(() => {
const bodyFirstElementChild = this.state.frameBody?.firstElementChild;
if (!this.resizeObserver && bodyFirstElementChild) {
this.resizeObserver = new ResizeObserver(() =>
this.setState({
frameHeight: this.state.frameBody?.firstElementChild.scrollHeight,
})
);
this.resizeObserver.observe(bodyFirstElementChild);
}
});
this.mutationObserver.observe(doc.body, { childList: true });
}
}

View File

@@ -0,0 +1,26 @@
import { Media } from "@nteract/outputs";
import React from "react";
interface Props {
/**
* The JavaScript code that we would like to execute.
*/
data: string;
/**
* The media type associated with our component.
*/
mediaType: "text/javascript";
}
export class SandboxJavaScript extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "application/javascript",
};
render(): JSX.Element {
return <Media.HTML data={`<script>${this.props.data}</script>`} />;
}
}
export default SandboxJavaScript;

View File

@@ -0,0 +1,38 @@
import { Media } from "@nteract/outputs";
import React from "react";
import sanitizeHtml from "sanitize-html";
interface Props {
/**
* The HTML string that will be rendered.
*/
data: string;
/**
* The media type associated with the HTML
* string. This defaults to text/html.
*/
mediaType: "text/html";
}
export class SanitizedHTML extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "text/html",
};
render(): JSX.Element {
return <Media.HTML data={sanitize(this.props.data)} />;
}
}
function sanitize(html: string): string {
return sanitizeHtml(html, {
allowedTags: false, // allow all tags
allowedAttributes: false, // allow all attrs
transformTags: {
iframe: "iframe-disabled", // disable iframes
},
});
}
export default SanitizedHTML;

View File

@@ -1,7 +1,8 @@
import * as Constants from "../../Common/Constants";
import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddCollectionPane from "./AddCollectionPane";
describe("Add Collection Pane", () => {
describe("isValid()", () => {
@@ -50,7 +51,14 @@ describe("Add Collection Pane", () => {
});
it("should be false if graph API and partition key is /id or /label", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(false);
@@ -60,7 +68,13 @@ describe("Add Collection Pane", () => {
});
it("should be true for any non-graph API with /id or /label partition key", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id");

View File

@@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.partitionKey.extend({ rateLimit: 100 });
this.partitionKeyPattern = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) {
if (userContext.apiType === "Gremlin") {
return "^/[^/]*";
}
return ".*";
});
this.partitionKeyTitle = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) {
if (userContext.apiType === "Gremlin") {
return "May not use composite partition key";
}
return "";
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (currentCollections >= maxCollections) {
let typeOfContainer = "collection";
if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) {
if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) {
typeOfContainer = "container";
}
@@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return "e.g., address.zipCode";
}
if (this.container && !!this.container.isPreferredApiGraph()) {
if (userContext.apiType === "Gremlin") {
return "e.g., /address";
}
@@ -384,17 +384,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.uniqueKeysVisible = ko.pureComputed<boolean>(() => {
if (
this.container == null ||
!!this.container.isPreferredApiMongoDB() ||
!!this.container.isPreferredApiTable() ||
!!this.container.isPreferredApiCassandra() ||
!!this.container.isPreferredApiGraph()
) {
return false;
if (userContext.apiType === "SQL") {
return true;
}
return true;
return false;
});
this.partitionKeyVisible = ko.computed<boolean>(() => {
@@ -591,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false;
}
if (this.container.isPreferredApiDocumentDB()) {
if (userContext.apiType === "SQL") {
return true;
}
@@ -599,7 +593,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
}
if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) {
if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) {
return true;
}
@@ -1011,7 +1005,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false;
}
if (this.container.isPreferredApiGraph() && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) {
if (userContext.apiType === "Gremlin" && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) {
this.formErrors("/id and /label as partition keys are not allowed for graph.");
return false;
}

View File

@@ -0,0 +1,460 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>;
public databaseIdPlaceHolder: ko.Computed<string>;
public databaseId: ko.Observable<string>;
public databaseIdTooltipText: ko.Computed<string>;
public databaseLevelThroughputTooltipText: ko.Computed<string>;
public databaseCreateNewShared: ko.Observable<boolean>;
public formErrorsDetails: ko.Observable<string>;
public throughput: ViewModels.Editable<number>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public maxThroughputRUText: ko.PureComputed<string>;
public throughputRangeText: ko.Computed<string>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public maxAutoPilotThroughputSet: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public freeTierExceedThroughputTooltip: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title((this.container && this.container.addDatabaseText()) || "New Database");
this.databaseId = ko.observable<string>();
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
// TODO 388844: get defaults from parent frame
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.databaseIdLabel = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id"
);
this.databaseIdPlaceHolder = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id"
);
this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"
}`;
});
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
});
this.throughput = editable.observable<number>();
this.maxThroughputRU = ko.observable<number>();
this.minThroughputRU = ko.observable<number>();
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (!autoPilot) {
return "";
}
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */);
});
this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.requestUnitsUsageCost = ko.computed(() => {
const offerThroughput: number = this.throughput();
if (
offerThroughput < this.minThroughputRU() ||
(offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue())
) {
return "";
}
const account = this.container.databaseAccount();
if (!account) {
return "";
}
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
let estimatedSpendAcknowledge: string;
let estimatedSpend: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
}
// TODO: change throughputSpendAckText to be a computed value, instead of having this side effect
this.throughputSpendAckText(estimatedSpendAcknowledge);
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =
databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier;
return isFreeTierAccount;
});
this.showUpsellMessage = ko.pureComputed(() => {
if (this.container.isServerlessEnabled()) {
return false;
}
if (this.isFreeTierAccount()) {
return this.databaseCreateNewShared();
}
return true;
});
this.maxThroughputRUText = ko.pureComputed(() => {
return this.maxThroughputRU().toLocaleString();
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
const selectedThroughput: number = this.throughput();
const maxRU: number = this.maxThroughputRU && this.maxThroughputRU();
const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
const isThroughputSetGreaterThanDefault: boolean =
selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
if (this.canExceedMaximumValue()) {
return isThroughputSetGreaterThanDefault;
}
return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault;
});
this.databaseCreateNewShared.subscribe((useShared: boolean) => {
this._updateThroughputLimitByDatabase();
});
this.resetData();
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: ""
);
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(
userContext.portalEnv,
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
false
);
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`;
});
this.upsellAnchorUrl = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
});
this.upsellAnchorText = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? "Learn more" : "More details";
});
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public open() {
super.open();
this.resetData();
const addDatabasePaneOpenMessage = {
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("database-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
const offerThroughput: number = this._computeOfferThroughput();
const addDatabasePaneStartMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
this.formErrors("");
this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = {
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (this.isAutoPilotSelected()) {
createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet();
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey);
},
(error: any) => {
this._onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
}
public resetData() {
this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase();
this.throughputSpendAck(false);
super.resetData();
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = userContext.subscriptionType;
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
return true;
}
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
const addDatabasePaneSuccessMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
this.resetData();
}
private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}
private _getThroughput(): number {
const throughput: number = this.throughput();
return isNaN(throughput) ? 0 : Number(throughput);
}
private _computeOfferThroughput(): number {
if (!this.canConfigureThroughput()) {
return undefined;
}
if (this.isAutoPilotSelected()) {
return undefined;
}
return this._getThroughput();
}
private _isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts
if (this.isAutoPilotSelected()) {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
this.formErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = this._getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (
this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
return true;
}
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) {
return {
maxThroughput: this.maxAutoPilotThroughputSet() * 1,
};
}
return undefined;
}
private _updateThroughputLimitByDatabase() {
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.throughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
}
}

View File

@@ -1,7 +1,7 @@
import { shallow } from "enzyme";
import React from "react";
import { AddDatabasePane } from ".";
import Explorer from "../../Explorer";
import { AddDatabasePane } from "../AddDatabasePane";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,

View File

@@ -1,108 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="deletecollectionconfirmationpane">
<!-- Delete Collection Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Collection Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete Collection Confirmation header - End -->
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
<div class="warningErrorContent">
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
resource and all of its children resources.
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="
visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - End -->
<!-- Delete Collection Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: collectionIdConfirmationText"></span>
<p>
<input
type="text"
data-test="confirmCollectionId"
name="collectionIdConfirmation"
required
class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
/>
</p>
</div>
<div data-bind="visible: recordDeleteFeedback">
<div>Help us improve Azure Cosmos DB!</div>
<div>What is the reason why you are deleting this container?</div>
<p>
<textarea
type="text"
data-test="containerDeleteFeedback"
name="containerDeleteFeedback"
rows="3"
cols="53"
class="collid"
maxlength="512"
data-bind="value: containerDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this container?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteCollection" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Collection Confirmation inputs - End -->
</form>
</div>
<!-- Delete Collection Confirmation form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,128 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {
public collectionIdConfirmationText: ko.Observable<string>;
public collectionIdConfirmation: ko.Observable<string>;
public containerDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.collectionIdConfirmationText = ko.observable<string>("Confirm by typing the collection id");
this.collectionIdConfirmation = ko.observable<string>();
this.containerDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Collection");
this.resetData();
}
public submit(): Promise<any> {
if (!this._isValid()) {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.formErrors("Input collection name does not match the selected collection");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}`
);
return Promise.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedCollection = <ViewModels.Collection>this.container.findSelectedCollection();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then(
() => {
this.isExecuting(false);
this.close();
this.container.selectedNode(selectedCollection.database);
this.container.tabsManager?.closeTabsByComparator(
(tab) =>
tab.node?.id() === selectedCollection.id() &&
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
);
this.container.refreshAllDatabases();
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteCollection,
{
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
if (this.shouldRecordFeedback()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.containerDeleteFeedback()
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
this.containerDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
);
}
public resetData() {
this.collectionIdConfirmation("");
super.resetData();
}
public open() {
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return this.container.isLastCollection() && !this.container.isSelectedDatabaseShared();
}
private _isValid(): boolean {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
if (!selectedCollection) {
return false;
}
return this.collectionIdConfirmation() === selectedCollection.id();
}
}

View File

@@ -1,178 +0,0 @@
import { Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import { Areas } from "../../Common/Constants";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
}
export interface DeleteCollectionConfirmationPanelState {
formError: string;
isExecuting: boolean;
}
export class DeleteCollectionConfirmationPanel extends React.Component<
DeleteCollectionConfirmationPanelProps,
DeleteCollectionConfirmationPanelState
> {
private inputCollectionName: string;
private deleteCollectionFeedback: string;
constructor(props: DeleteCollectionConfirmationPanelProps) {
super(props);
this.state = {
formError: "",
isExecuting: false,
};
}
render(): JSX.Element {
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
<PanelInfoErrorComponent {...this.getPanelErrorProps()} />
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the collection id</Text>
<TextField
id="confirmCollectionId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
this.inputCollectionName = newInput;
}}
/>
</div>
{this.shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this container?
</Text>
<TextField
id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
rows={3}
onChange={(event, newInput?: string) => {
this.deleteCollectionFeedback = newInput;
}}
/>
</div>
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
{this.state.isExecuting && <PanelLoadingScreen />}
</form>
);
}
private getPanelErrorProps(): PanelInfoErrorProps {
if (this.state.formError) {
return {
messageType: "error",
message: this.state.formError,
showErrorDetails: true,
openNotificationConsole: this.props.openNotificationConsole,
};
}
return {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
}
private shouldRecordFeedback(): boolean {
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
}
public async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
const collection = this.props.explorer.findSelectedCollection();
if (!collection || this.inputCollectionName !== collection.id()) {
const errorMessage = "Input collection name does not match the selected collection";
this.setState({ formError: errorMessage });
NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`);
return;
}
this.setState({ formError: "", isExecuting: true });
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
});
try {
await deleteCollection(collection.databaseId, collection.id());
this.setState({ isExecuting: false });
this.props.explorer.selectedNode(collection.database);
this.props.explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
this.props.explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(
Action.DeleteCollection,
{
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
},
startKey
);
if (this.shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
this.deleteCollectionFeedback
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
this.props.closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
this.setState({ formError: errorMessage, isExecuting: false });
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
}
}

View File

@@ -1,19 +1,18 @@
jest.mock("../../Common/dataAccess/deleteCollection");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
jest.mock("../../../Common/dataAccess/deleteCollection");
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import DeleteFeedback from "../../Common/DeleteFeedback";
import Explorer from "../Explorer";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { updateUserContext } from "../../UserContext";
import { DeleteCollectionConfirmationPanel } from ".";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => {
@@ -64,7 +63,7 @@ describe("Delete Collection Confirmation Pane", () => {
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
collectionName: "container",
};
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
@@ -118,7 +117,7 @@ describe("Delete Collection Confirmation Pane", () => {
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
collectionName: "container",
};
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
});
@@ -132,8 +131,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes()
.simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount();
@@ -153,8 +152,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes()
.simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback(

View File

@@ -0,0 +1,152 @@
import { Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../../Common/Constants";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer;
collectionName: string;
closePanel: () => void;
}
export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollectionConfirmationPanelProps> = ({
explorer,
closePanel,
collectionName,
}: DeleteCollectionConfirmationPanelProps) => {
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => {
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
};
const paneTitle = "Delete " + collectionName;
const submit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
);
return;
}
const paneInfo = {
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle,
};
setFormError("");
setIsExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, paneInfo);
try {
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
explorer.selectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
if (shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
deleteCollectionFeedback
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
setIsExecuting(false);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
...paneInfo,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
};
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formError,
id: "deleteCollectionpane",
isExecuting,
title: paneTitle,
submitButtonText: "OK",
onClose: closePanel,
onSubmit: submit,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField
id="confirmCollectionId"
autoFocus
value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setInputCollectionName(newInput);
}}
/>
</div>
{shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this {collectionName}?
</Text>
<TextField
id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
value={deleteCollectionFeedback}
rows={3}
onChange={(event, newInput?: string) => {
setDeleteCollectionFeedback(newInput);
}}
/>
</div>
)}
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -4,6 +4,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<ExecuteSprocParamsPanel
closePanel={[Function]}
explorer={Object {}}
storedProcedure={Object {}}
>
<GenericRightPaneComponent
container={Object {}}
@@ -1148,7 +1149,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
isAddRemoveVisible={false}
onParamKeyChange={[Function]}
onParamValueChange={[Function]}
paramValue=""
selectedKey="string"
>
<StyledLabelBase>
@@ -2683,7 +2683,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
key=".0:$.1"
label="Value"
onChange={[Function]}
value=""
>
<TextFieldBase
autoFocus={true}
@@ -2968,7 +2967,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
}
}
validateOnLoad={true}
value=""
>
<div
className="ms-TextField root-102"

View File

@@ -1,12 +1,15 @@
import { mount } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import { ExecuteSprocParamsPanel } from "./index";
describe("Excute Sproc Param Pane", () => {
const fakeExplorer = {} as Explorer;
const fakeSproc = {} as StoredProcedure;
const props = {
explorer: fakeExplorer,
storedProcedure: fakeSproc,
closePanel: (): void => undefined,
};

View File

@@ -3,11 +3,13 @@ import { IDropdownOption, IImageProps, Image, Stack, Text } from "office-ui-fabr
import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps {
explorer: Explorer;
storedProcedure: StoredProcedure;
closePanel: () => void;
}
@@ -23,11 +25,12 @@ interface UnwrappedExecuteSprocParam {
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
explorer,
storedProcedure,
closePanel,
}: ExecuteSprocParamsPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const [partitionValue, setPartitionValue] = useState<string>("");
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
@@ -76,9 +79,15 @@ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPanePr
return;
}
setLoadingTrue();
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text);
const currentSelectedSproc = explorer.findSelectedStoredProcedure();
currentSelectedSproc.execute(sprocParams, partitionValue);
const sprocParams =
wrappedSprocParams &&
wrappedSprocParams.map((sprocParam) => {
if (sprocParam.key === "custom") {
return JSON.parse(sprocParam.text);
}
return sprocParam.text;
});
storedProcedure.execute(sprocParams, partitionKey === "custom" ? JSON.parse(partitionValue) : partitionValue);
setLoadingFalse();
closePanel();
};

View File

@@ -1,142 +0,0 @@
import { Subscription } from "knockout";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import ErrorRedIcon from "../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer";
export interface GenericRightPaneProps {
container: Explorer;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export class GenericRightPaneComponent extends React.Component<GenericRightPaneProps, GenericRightPaneState> {
private notificationConsoleSubscription: Subscription;
constructor(props: GenericRightPaneProps) {
super(props);
this.state = {
panelHeight: this.getPanelHeight(),
};
}
public componentWillUnmount(): void {
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
}
public render(): JSX.Element {
return (
<div tabIndex={-1} onKeyDown={this.onKeyDown}>
<div className="contextual-pane-out" onClick={this.props.onClose}></div>
<div
className="contextual-pane"
id={this.props.id}
style={{ height: this.state.panelHeight }}
onKeyDown={this.onKeyDown}
>
<div className="panelContentWrapper">
{this.renderPanelHeader()}
{this.renderErrorSection()}
{this.props.children}
{this.renderPanelFooter()}
</div>
{this.renderLoadingScreen()}
</div>
</div>
);
}
private renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{this.props.title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={this.props.onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
private renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={this.props.formError}>
{this.props.formError}
</span>
<a className="errorLink" role="link" hidden={!this.props.formErrorDetail} onClick={this.showErrorDetail}>
More details
</a>
</span>
</div>
</div>
);
};
private renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={this.props.onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={this.props.submitButtonText}
/>
</div>
</div>
);
};
private renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
this.props.onClose();
event.stopPropagation();
}
};
private showErrorDetail = (): void => {
this.props.container.expandConsole();
};
private getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
}

View File

@@ -0,0 +1,135 @@
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
export interface GenericRightPaneProps {
container: Explorer;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
container,
formError,
formErrorDetail,
id,
isExecuting,
onClose,
onSubmit,
submitButtonText,
title,
isSubmitButtonHidden,
children,
}: GenericRightPaneProps) => {
const getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
const panelHeight: number = getPanelHeight();
const renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
const renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={formError}>
{formError}
</span>
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={showErrorDetail}>
More details
</a>
</span>
</div>
</div>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={submitButtonText}
/>
</div>
</div>
);
};
const renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
onClose();
event.stopPropagation();
}
};
const showErrorDetail = (): void => {
container.expandConsole();
};
return (
<div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose}></div>
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
<div className="panelContentWrapper">
{renderPanelHeader()}
{renderErrorSection()}
{children}
{renderPanelFooter()}
</div>
{renderLoadingScreen()}
</div>
</div>
);
};

View File

@@ -1,6 +1,5 @@
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
@@ -8,7 +7,6 @@ import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
export class PaneComponent {
constructor(data: any) {
@@ -25,15 +23,6 @@ export class AddCollectionPaneComponent {
}
}
export class DeleteCollectionConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteCollectionConfirmationPaneTemplate,
};
}
}
export class GraphNewVertexPaneComponent {
constructor() {
return {
@@ -69,16 +58,6 @@ export class TableEditEntityPaneComponent {
};
}
}
export class TableQuerySelectPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableQuerySelectPaneTemplate,
};
}
}
export class CassandraAddCollectionPaneComponent {
constructor() {
return {

View File

@@ -152,3 +152,6 @@
.removeIcon {
color: @InfoIconColor;
}
.column-select-view {
margin: 20px 0px 0px 0px;
}

View File

@@ -91,21 +91,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -196,27 +181,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -507,21 +471,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -590,9 +539,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -632,27 +578,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -750,6 +675,41 @@ exports[`Settings Pane should render Default properly 1`] = `
<div
className="paneMainContent"
>
<div
className="settingsSection"
>
<div
className="settingsSectionPart pageOptionsPart"
>
<div
className="settingsSectionLabel"
>
Page options
<Tooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</Tooltip>
</div>
<StyledChoiceGroupBase
onChange={[Function]}
options={
Array [
Object {
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="unlimited"
/>
</div>
<div
className="tabs settingsSectionPart"
/>
</div>
<div
className="settingsSection"
>
@@ -932,21 +892,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1037,27 +982,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -1348,21 +1272,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -1431,9 +1340,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -1473,27 +1379,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],

View File

@@ -1,10 +1,11 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import * as TableConstants from "../../Tables/Constants";
import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane";
@@ -24,11 +25,9 @@ export default class AddTableEntityPane extends TableEntityPane {
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Add Entity");
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
if (isCassandra) {
this.submitButtonText("Add Row");
}
});
if (userContext.apiType === "Cassandra") {
this.submitButtonText("Add Row");
}
this.scrollId = ko.observable<string>("addEntityScroll");
}
@@ -57,7 +56,7 @@ export default class AddTableEntityPane extends TableEntityPane {
headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey];
}
}
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((columns: CassandraTableKey[]) => {
@@ -94,7 +93,7 @@ export default class AddTableEntityPane extends TableEntityPane {
headers &&
headers.forEach((key: string) => {
if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) {
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);

View File

@@ -1,14 +1,15 @@
import * as ko from "knockout";
import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as Entities from "../../Tables/Entities";
import TableEntityPane from "./TableEntityPane";
import * as Utilities from "../../Tables/Utilities";
import * as TableConstants from "../../Tables/Constants";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import { userContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import * as TableConstants from "../../Tables/Constants";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane";
export default class EditTableEntityPane extends TableEntityPane {
container: Explorer;
@@ -21,11 +22,9 @@ export default class EditTableEntityPane extends TableEntityPane {
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Update Entity");
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
if (isCassandra) {
this.submitButtonText("Update Row");
}
});
if (userContext.apiType === "Cassandra") {
this.submitButtonText("Update Row");
}
this.scrollId = ko.observable<string>("editEntityScroll");
}
@@ -44,7 +43,7 @@ export default class EditTableEntityPane extends TableEntityPane {
property !== TableEntityProcessor.keyProperties.etag &&
property !== TableEntityProcessor.keyProperties.resourceId &&
property !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey)
(userContext.apiType !== "Cassandra" || property !== TableConstants.EntityKeyNames.RowKey)
) {
numberOfProperties++;
}
@@ -93,9 +92,9 @@ export default class EditTableEntityPane extends TableEntityPane {
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || key !== TableConstants.EntityKeyNames.RowKey)
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) {
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
@@ -150,7 +149,7 @@ export default class EditTableEntityPane extends TableEntityPane {
}
}
});
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((properties: CassandraTableKey[]) => {
@@ -169,10 +168,7 @@ export default class EditTableEntityPane extends TableEntityPane {
var updatedEntity: any = {};
displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if (
attribute.name() &&
(!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "")
) {
if (attribute.name() && (userContext.apiType !== "Cassandra" || attribute.value() !== "")) {
var value = attribute.getPropertyValue();
var type = attribute.type();
if (type === TableConstants.TableType.Int64) {

View File

@@ -1,174 +0,0 @@
import * as ko from "knockout";
import _ from "underscore";
import * as Constants from "../../Tables/Constants";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ContextualPaneBase } from "../ContextualPaneBase";
export interface ISelectColumn {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
editable: ko.Observable<boolean>;
}
export class QuerySelectPane extends ContextualPaneBase {
public titleLabel: string = "Select Columns";
public instructionLabel: string = "Select the columns that you want to query.";
public availableColumnsTableQueryLabel: string = "Available Columns";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<ISelectColumn>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public allSelected: ko.Computed<boolean>;
private selectedColumnOption: ISelectColumn = null;
public queryViewModel: QueryViewModel;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<ISelectColumn>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return !value.selected();
});
});
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: (value) => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this,
});
}
public submit() {
this.queryViewModel.selectText(this.getParameters());
this.queryViewModel.getSelectMessage();
this.close();
}
public open() {
this.setTableColumns(this.queryViewModel.columnOptions());
this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions());
super.open();
}
private getParameters(): string[] {
if (this.canSelectAll() === false) {
return [];
}
var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true);
var columns: string[] = selectedColumns.map((value: ISelectColumn) => {
var name: string = value.columnName();
return name;
});
return columns;
}
public setTableColumns(columnNames: string[]): void {
var columns: ISelectColumn[] = columnNames.map((value: string) => {
var columnOption: ISelectColumn = {
columnName: ko.observable<string>(value),
selected: ko.observable<boolean>(true),
editable: ko.observable<boolean>(this.isEntityEditable(value)),
};
return columnOption;
});
this.columnOptions(columns);
}
public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void {
if (querySelect == null || _.isEmpty(querySelect)) {
return;
}
this.setSelected(querySelect, columns);
}
private setSelected(querySelect: string[], columns: ISelectColumn[]): void {
this.clearAll();
querySelect &&
querySelect.forEach((value: string) => {
for (var i = 0; i < columns.length; i++) {
if (value === columns[i].columnName()) {
columns[i].selected(true);
}
}
});
}
public availableColumnsCheckboxClick(): boolean {
if (this.canSelectAll()) {
return this.selectAll();
} else {
return this.clearAll();
}
}
public selectAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: ISelectColumn) => {
value.selected(true);
});
return true;
}
public clearAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((column: ISelectColumn) => {
if (this.isEntityEditable(column.columnName())) {
column.selected(false);
} else {
column.selected(true);
}
});
return true;
}
public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void {
this.selectedColumnOption = targetColumn;
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private isEntityEditable(name: string) {
if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !_.contains<string>(cassandraKeys, name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
}
}

View File

@@ -1,15 +1,16 @@
import * as ko from "knockout";
import _ from "underscore";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import { KeyCodes } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import * as ViewModels from "../../../Contracts/ViewModels";
import { KeyCodes } from "../../../Common/Constants";
import { ContextualPaneBase } from "../ContextualPaneBase";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
// Class with variables and functions that are common to both adding and editing entities
export default abstract class TableEntityPane extends ContextualPaneBase {
@@ -52,31 +53,29 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
constructor(options: ViewModels.PaneOptions) {
super(options);
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
if (isCassandra) {
this.edmTypes([
TableConstants.CassandraType.Text,
TableConstants.CassandraType.Ascii,
TableConstants.CassandraType.Bigint,
TableConstants.CassandraType.Blob,
TableConstants.CassandraType.Boolean,
TableConstants.CassandraType.Decimal,
TableConstants.CassandraType.Double,
TableConstants.CassandraType.Float,
TableConstants.CassandraType.Int,
TableConstants.CassandraType.Uuid,
TableConstants.CassandraType.Varchar,
TableConstants.CassandraType.Varint,
TableConstants.CassandraType.Inet,
TableConstants.CassandraType.Smallint,
TableConstants.CassandraType.Tinyint,
]);
}
});
if (userContext.apiType === "Cassandra") {
this.edmTypes([
TableConstants.CassandraType.Text,
TableConstants.CassandraType.Ascii,
TableConstants.CassandraType.Bigint,
TableConstants.CassandraType.Blob,
TableConstants.CassandraType.Boolean,
TableConstants.CassandraType.Decimal,
TableConstants.CassandraType.Double,
TableConstants.CassandraType.Float,
TableConstants.CassandraType.Int,
TableConstants.CassandraType.Uuid,
TableConstants.CassandraType.Varchar,
TableConstants.CassandraType.Varint,
TableConstants.CassandraType.Inet,
TableConstants.CassandraType.Smallint,
TableConstants.CassandraType.Tinyint,
]);
}
this.canAdd = ko.computed<boolean>(() => {
// Cassandra can't add since the schema can't be changed once created
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
return false;
}
// Adding '2' to the maximum to take into account PartitionKey and RowKey
@@ -163,7 +162,7 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
public insertAttribute = (name?: string, type?: string): void => {
let entityProperty: EntityPropertyViewModel;
if (!!name && !!type && this.container.isPreferredApiCassandra()) {
if (!!name && !!type && userContext.apiType === "Cassandra") {
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean =
type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet;
@@ -253,8 +252,7 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!viewModel.queryTablesTab.container.isPreferredApiCassandra() ||
key !== TableConstants.EntityKeyNames.RowKey)
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) {
newHeaders.push(key);
}

View File

@@ -1,79 +0,0 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="queryselectpane">
<!-- Query Select form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Query Select header - Start -->
<div class="firstdivbg headerline">
Select Column
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Query Select header - End -->
<div class="paneMainContent paneContentContainer">
<!--<div class="row">
<label id="instructionLabel" data-bind="text: instructionLabel"></label>
</div>-->
<div><span>Select the columns that you want to query.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight: 700"
id="availableColumnsTableQueryLabel"
data-bind="text: availableColumnsTableQueryLabel"
></label>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsTableQueryLabel" tabindex="0">
<!-- ko template: {if: editable} -->
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input type="checkbox" data-bind="attr: { title: columnName }, checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
<!-- ko template: {ifnot: editable} -->
<li class="list-item columns-border" data-bind="attr: { title: columnName } ">
<input type="checkbox" disabled data-bind="checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Query Select form - End -->
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./index";
describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer;
const fakeQueryViewModal = {} as QueryViewModel;
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
queryViewModel: fakeQueryViewModal,
};
it("should render Default properly", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("Should exist availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.exists("#availableCheckbox")).toBe(true);
});
it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
id: "availableCheckbox",
label: "Available Columns",
checked: true,
onChange: expect.any(Function),
});
});
});

View File

@@ -0,0 +1,155 @@
import { Checkbox, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import * as Constants from "../../../Tables/Constants";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../../GenericRightPaneComponent";
interface TableQuerySelectPanelProps {
explorer: Explorer;
closePanel: () => void;
queryViewModel: QueryViewModel;
}
interface ISelectColumn {
columnName: string;
selected: boolean;
editable: boolean;
}
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
explorer,
closePanel,
queryViewModel,
}: TableQuerySelectPanelProps): JSX.Element => {
const [columnOptions, setColumnOptions] = useState<ISelectColumn[]>([
{ columnName: "", selected: true, editable: false },
]);
const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true);
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: "",
formErrorDetail: "",
id: "querySelectPane",
isExecuting: false,
title: "Select Column",
submitButtonText: "OK",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const submit = (): void => {
queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage();
closePanel();
};
const handleClick = (isChecked: boolean, selectedColumn: string): void => {
const columns = columnOptions.map((column) => {
if (column.columnName === selectedColumn) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
canSelectAll();
setColumnOptions(columns);
};
useEffect(() => {
queryViewModel && setTableColumns(queryViewModel.columnOptions());
}, []);
const setTableColumns = (columnNames: string[]): void => {
const columns: ISelectColumn[] =
columnNames &&
columnNames.length &&
columnNames.map((value: string) => {
const columnOption: ISelectColumn = {
columnName: value,
selected: true,
editable: isEntityEditable(value),
};
return columnOption;
});
setColumnOptions(columns);
};
const isEntityEditable = (name: string): boolean => {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !cassandraKeys.includes(name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
};
const availableColumnsCheckboxClick = (event: React.FormEvent<HTMLElement>, isChecked: boolean): void => {
setIsAvailableColumnChecked(isChecked);
selectClearAll(isChecked);
};
const selectClearAll = (isChecked: boolean): void => {
const columns: ISelectColumn[] = columnOptions.map((column: ISelectColumn) => {
if (isEntityEditable(column.columnName)) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
setColumnOptions(columns);
};
const getParameters = (): string[] => {
const selectedColumns = columnOptions.filter((value: ISelectColumn) => value.selected === true);
const columns: string[] = selectedColumns.map((value: ISelectColumn) => {
const name: string = value.columnName;
return name;
});
return columns;
};
const canSelectAll = (): void => {
const canSelectAllColumn: boolean = columnOptions.some((value: ISelectColumn) => {
return !value.selected;
});
setIsAvailableColumnChecked(!canSelectAllColumn);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Text>Select the columns that you want to query.</Text>
<div className="column-select-view">
<Checkbox
id="availableCheckbox"
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
/>
{columnOptions.map((column) => {
return (
<Checkbox
label={column.columnName}
onChange={(_event, isChecked: boolean) => handleClick(isChecked, column.columnName)}
key={column.columnName}
checked={column.selected}
disabled={!column.editable}
/>
);
})}
</div>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -91,21 +91,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -196,27 +181,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -507,21 +471,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -590,9 +539,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -632,27 +578,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],

View File

@@ -1,9 +1,9 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { UploadDetails, UploadDetailsRecord } from "../../../workers/upload/definitions";
import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
@@ -13,12 +13,6 @@ export interface UploadItemsPaneProps {
closePanel: () => void;
}
interface IUploadFileData {
numSucceeded: number;
numFailed: number;
fileName: string;
}
const getTitle = (): string => {
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
return "Upload Tables";
@@ -54,7 +48,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
selectedCollection
?.uploadFiles(files)
.then(
(uploadDetails: UploadDetails) => {
(uploadDetails) => {
setUploadFileData(uploadDetails.data);
setFiles(undefined);
},
@@ -84,6 +78,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
onClose: closePanel,
onSubmit,
};
const columns: IColumn[] = [
{
key: "fileName",
@@ -105,12 +100,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
},
];
const _renderItemColumn = (item: IUploadFileData, index: number, column: IColumn) => {
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
switch (column.key) {
case "status":
return <span>{item.numSucceeded + " items created, " + item.numFailed + " errors"}</span>;
return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
default:
return <span>{item.fileName}</span>;
return item.fileName;
}
};

View File

@@ -92,21 +92,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -197,27 +182,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function],
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
@@ -508,21 +472,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
@@ -593,9 +542,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
@@ -636,27 +582,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshAllDatabases": [Function],
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],

View File

@@ -50,10 +50,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions = [];
}
public shouldComponentUpdate() {
return this.container.tabsManager.openedTabs.length === 0;
}
public componentWillUnmount() {
while (this.subscriptions.length) {
this.subscriptions.pop().dispose();
@@ -62,7 +58,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})),
this.container.selectedNode.subscribe(() => this.setState({})),
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
);
@@ -80,7 +75,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent;
return (
const formContainer = (jsx: JSX.Element) => (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">{jsx}</form>
</div>
);
return formContainer(
<div className="splashScreenContainer">
<div className="splashScreen">
<div className="title">
@@ -226,7 +227,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}
if (!this.container.isDatabaseNodeOrNoneSelected()) {
if (this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
@@ -255,7 +256,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
onClick: () => this.container.openBrowseQueriesPanel(),
});
if (!this.container.isPreferredApiCassandra()) {
if (userContext.apiType !== "Cassandra") {
items.push({
iconSrc: NewStoredProcedureIcon,
title: "New Stored Procedure",

View File

@@ -1,4 +1,5 @@
import Q from "q";
import { userContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities";
@@ -73,7 +74,7 @@ export default class TableCommands {
}
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?";
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
deleteMessage = "Are you sure you want to delete the selected rows?";
}
if (window.confirm(deleteMessage)) {

View File

@@ -5,6 +5,7 @@ import { Areas } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
@@ -412,10 +413,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
}
var entities = this.cache.data;
if (
this.queryTablesTab.container.isPreferredApiCassandra() &&
DataTableUtilities.checkForDefaultHeader(this.headers)
) {
if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
.getTableSchema(this.queryTablesTab.collection)
.then((headers: CassandraTableKey[]) => {
@@ -427,7 +425,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
} else {
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
entities,
this.queryTablesTab.container.isPreferredApiCassandra()
userContext.apiType === "Cassandra"
);
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
if (newHeaders.length > 0) {
@@ -512,7 +510,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(finalEntities);
}
);
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
promise = Q(
this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
@@ -523,7 +521,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
);
} else {
let query = this.sqlQuery();
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
query = this.cqlQuery();
}
promise = Q(

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout";
import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
@@ -70,7 +71,7 @@ export default class QueryBuilderViewModel {
private scrollEventListener: boolean;
constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) {
if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
this.edmTypes([
Constants.CassandraType.Text,
Constants.CassandraType.Ascii,

View File

@@ -1,9 +1,10 @@
import * as ko from "knockout";
import _ from "underscore";
import { userContext } from "../../../UserContext";
import * as QueryBuilderConstants from "../Constants";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
import ClauseGroup from "./ClauseGroup";
import * as Utilities from "../Utilities";
import ClauseGroup from "./ClauseGroup";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
export default class QueryClauseViewModel {
public checkedForGrouping: ko.Observable<boolean>;
@@ -68,7 +69,7 @@ export default class QueryClauseViewModel {
this.getValueType();
this.isOperaterEditable = ko.pureComputed<boolean>(() => {
const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra();
const isPreferredApiCassandra = userContext.apiType === "Cassandra";
const cassandraKeys = isPreferredApiCassandra
? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map(
(key) => key.property
@@ -84,7 +85,7 @@ export default class QueryClauseViewModel {
this.field() !== "Timestamp" &&
this.field() !== "PartitionKey" &&
this.field() !== "RowKey" &&
!this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()
userContext.apiType !== "Cassandra"
);
this.and_or.subscribe((value) => {
@@ -170,7 +171,7 @@ export default class QueryClauseViewModel {
this.type(QueryBuilderConstants.TableType.String);
} else {
this.resetFromTimestamp();
if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection
.cassandraSchema;
for (let i = 0, len = cassandraSchema.length; i < len; i++) {

View File

@@ -1,13 +1,13 @@
import * as ko from "knockout";
import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
import QueryClauseViewModel from "./QueryClauseViewModel";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import { KeyCodes } from "../../../Common/Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
export default class QueryViewModel {
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
@@ -47,7 +47,7 @@ export default class QueryViewModel {
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
return !this.queryTablesTab.container.isPreferredApiCassandra();
return userContext.apiType !== "Cassandra";
});
let initialOptions = this._tableEntityListViewModel.headers;
this.columnOptions = ko.observableArray<string>(initialOptions);
@@ -127,7 +127,7 @@ export default class QueryViewModel {
private setFilter = (): string => {
var queryString = this.isEditorActive()
? this.queryText()
: this.queryTablesTab.container.isPreferredApiCassandra()
: userContext.apiType === "Cassandra"
? this.queryBuilderViewModel().getCqlFilterFromClauses()
: this.queryBuilderViewModel().getODataFilterFromClauses();
var filter = queryString;
@@ -160,7 +160,7 @@ export default class QueryViewModel {
public runQuery = (): DataTables.DataTable => {
var filter = this.setFilter();
if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) {
if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'");
}
var top = this.topValue();
@@ -198,8 +198,7 @@ export default class QueryViewModel {
};
public selectQueryOptions(): Promise<any> {
this.queryTablesTab.container.querySelectPane.queryViewModel = this;
this.queryTablesTab.container.querySelectPane.open();
this.queryTablesTab.container.openTableSelectQueryPanel(this);
return null;
}

View File

@@ -16,8 +16,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -89,8 +87,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -106,8 +102,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -123,8 +117,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -140,8 +132,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -157,8 +147,6 @@ describe("Documents tab", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});

View File

@@ -1,14 +1,9 @@
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
import MongoUtility from "../../Common/MongoUtility";
import ObjectId from "../Tree/ObjectId";
import Q from "q";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import {
createDocument,
deleteDocument,
@@ -16,10 +11,14 @@ import {
readDocument,
updateDocument,
} from "../../Common/MongoProxyClient";
import { extractPartitionKey } from "@azure/cosmos";
import * as Logger from "../../Common/Logger";
import { PartitionKeyDefinition } from "@azure/cosmos";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import MongoUtility from "../../Common/MongoUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import DocumentsTab from "./DocumentsTab";
export default class MongoDocumentsTab extends DocumentsTab {
public collection: ViewModels.Collection;

View File

@@ -88,7 +88,6 @@ export default class NotebookTabV2 extends TabsBase {
public onCloseTabButtonClick(): Q.Promise<any> {
const cleanup = () => {
this.notebookComponentAdapter.notebookShutdown();
this.isActive(false);
super.onCloseTabButtonClick();
};

View File

@@ -1,9 +1,10 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
describe("Query Tab", () => {
function getNewQueryTabForContainer(container: Explorer): QueryTab {
@@ -24,7 +25,6 @@ describe("Query Tab", () => {
database: database,
title: "",
tabPath: "",
isActive: ko.observable<boolean>(false),
hashLocation: "",
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
});
@@ -52,13 +52,19 @@ describe("Query Tab", () => {
});
it("should be true for accounts using SQL API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(true);
});
it("should be false for accounts using other APIs", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(false);
});
@@ -72,13 +78,19 @@ describe("Query Tab", () => {
});
it("should be visible when using a supported API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB);
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(true);
});
it("should not be visible when using an unsupported API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(false);
});

View File

@@ -13,6 +13,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./QueryTab.html";
@@ -95,9 +96,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))
);
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return (
(this.collection && this.collection.container && this.collection.container.isPreferredApiDocumentDB()) || false
);
return userContext.apiType === "SQL" || false;
});
this.activityId = ko.observable<string>();
this.roundTrips = ko.observable<number>();
@@ -117,7 +116,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
const container = this.collection && this.collection.container;
return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false;
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
});
this.maybeSubQuery = ko.computed<boolean>(function () {

View File

@@ -1,21 +1,21 @@
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import TableCommands from "../Tables/DataTable/TableCommands";
import { TableDataClient } from "../Tables/TableDataClient";
import AddEntityIcon from "../../../images/AddEntity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import EditEntityIcon from "../../../images/Edit-entity.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import AddEntityIcon from "../../../images/AddEntity.svg";
import EditEntityIcon from "../../../images/Edit-entity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import Explorer from "../Explorer";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import TableCommands from "../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import { TableDataClient } from "../Tables/TableDataClient";
import template from "./QueryTablesTab.html";
import TabsBase from "./TabsBase";
// Will act as table explorer class
export default class QueryTablesTab extends TabsBase {
@@ -176,7 +176,7 @@ export default class QueryTablesTab extends TabsBase {
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.queryBuilderButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "CQL Query Builder" : "Query Builder";
const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
buttons.push({
iconSrc: QueryBuilderIcon,
iconAlt: label,
@@ -190,7 +190,7 @@ export default class QueryTablesTab extends TabsBase {
}
if (this.queryTextButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "CQL Query Text" : "Query Text";
const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
buttons.push({
iconSrc: QueryTextIcon,
iconAlt: label,
@@ -217,7 +217,7 @@ export default class QueryTablesTab extends TabsBase {
}
if (this.addEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Add Row" : "Add Entity";
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
buttons.push({
iconSrc: AddEntityIcon,
iconAlt: label,
@@ -230,7 +230,7 @@ export default class QueryTablesTab extends TabsBase {
}
if (this.editEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Edit Row" : "Edit Entity";
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
buttons.push({
iconSrc: EditEntityIcon,
iconAlt: label,
@@ -243,7 +243,7 @@ export default class QueryTablesTab extends TabsBase {
}
if (this.deleteEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Delete Rows" : "Delete Entities";
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
buttons.push({
iconSrc: DeleteEntitiesIcon,
iconAlt: label,

View File

@@ -208,7 +208,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: () => {
this.collection && this.collection.container.openExecuteSprocParamsPanel();
this.collection && this.collection.container.openExecuteSprocParamsPanel(this.node);
},
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -1,15 +1,16 @@
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ThemeUtility from "../../Common/ThemeUtility";
import Explorer from "../Explorer";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel {
@@ -20,18 +21,16 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public database: ViewModels.Database;
public rid: string;
public hasFocus: ko.Observable<boolean>;
public isActive: ko.Observable<boolean>;
public isMouseOver: ko.Observable<boolean>;
public tabId: string;
public tabKind: ViewModels.CollectionTabKind;
public tabTitle: ko.Observable<string>;
public tabPath: ko.Observable<string>;
public closeButtonTabIndex: ko.Computed<number>;
public errorDetailsTabIndex: ko.Computed<number>;
public hashLocation: ko.Observable<string>;
public isExecutionError: ko.Observable<boolean>;
public isExecuting: ko.Observable<boolean>;
public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string;
public onLoadStartKey: number;
@@ -46,7 +45,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
this.database = options.database;
this.rid = options.rid || (this.collection && this.collection.rid) || "";
this.hasFocus = ko.observable<boolean>(false);
this.isActive = options.isActive || ko.observable<boolean>(false);
this.isMouseOver = ko.observable<boolean>(false);
this.tabId = `tab${id}`;
this.tabKind = options.tabKind;
@@ -55,21 +53,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
(options.tabPath && ko.observable<string>(options.tabPath)) ||
(this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.closeButtonTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.errorDetailsTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.isExecutionError = ko.observable<boolean>(false);
this.isExecuting = ko.observable<boolean>(false);
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
this.onLoadStartKey = options.onLoadStartKey;
this.hashLocation = ko.observable<string>(options.hashLocation || "");
this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation));
this.isActive.subscribe((isActive: boolean) => {
if (isActive) {
this.onActivate();
}
});
this.closeTabButton = {
enabled: ko.computed<boolean>(() => {
return true;
@@ -82,12 +71,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onCloseTabButtonClick(): void {
const explorer = this.getContainer();
explorer.tabsManager.closeTab(this.tabId, explorer);
this.manager?.closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
tabId: this.tabId,
@@ -95,7 +81,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onTabClick(): void {
this.getContainer().tabsManager.activateTab(this);
this.manager?.activateTab(this);
}
protected updateSelectedNode(): void {
@@ -127,6 +113,11 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
};
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() {
return this === this.manager?.activeTab();
}
public onActivate(): void {
this.updateSelectedNode();
if (!!this.collection) {

View File

@@ -1,4 +1,8 @@
<div id="content" class="flexContainer hideOverflows" data-bind="visible: openedTabs && openedTabs().length > 0">
<div
id="content"
class="flexContainer hideOverflows"
data-bind="visible: activeTab() && openedTabs && openedTabs().length > 0"
>
<!-- Tabs - Start -->
<div class="nav-tabs-margin">
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
@@ -8,12 +12,12 @@
data-bind="
attr: {
title: $data.tabPath,
'aria-selected': $data.isActive,
'aria-expanded': $data.isActive,
'aria-selected': $parent.activeTab() === $data,
'aria-expanded': $parent.activeTab() === $data,
'aria-controls': $data.tabId
},
css:{
active: $data.isActive
active: $parent.activeTab() === $data
},
hasFocus: $data.hasFocus,
event: { keypress: onKeyPressActivate },
@@ -33,8 +37,8 @@
data-bind="
click: onErrorDetailsClick,
event: { keypress: onErrorDetailsKeyPress },
attr: { tabindex: errorDetailsTabIndex },
css: { actionsEnabled: isActive },
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
css: { actionsEnabled: $parent.activeTab() === $data },
visible: isExecutionError"
>
<span class="errorIcon"></span>
@@ -56,11 +60,14 @@
data-bind="
click: $data.onCloseTabButtonClick,
event: { keypress: onKeyPressClose },
attr: { tabindex: $data.closeButtonTabIndex },
visible: $data.isActive() || $data.isMouseOver()"
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
visible: $parent.activeTab() === $data || $data.isMouseOver()"
title="Close"
>
<span class="tabIcon close-Icon" data-bind="visible: $data.isActive() || $data.isMouseOver()">
<span
class="tabIcon close-Icon"
data-bind="visible: $parent.activeTab() === $data || $data.isMouseOver()"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</span>
</span>
@@ -77,7 +84,7 @@
<!-- Tabs Panes -- Start -->
<div class="tabPanesContainer">
<!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $data.isActive">
<div class="tabs-container" data-bind="visible: $parent.activeTab() === $data">
<span
data-bind="class: $data.constructor.component.name, component: { name: $data.constructor.component.name, params: $data }"
></span>

View File

@@ -1,11 +1,11 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import { TabsManager } from "./TabsManager";
import DocumentsTab from "./DocumentsTab";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
import QueryTab from "./QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => {
let tabsManager: TabsManager;
@@ -50,7 +50,6 @@ describe("Tabs manager tests", () => {
database,
title: "",
tabPath: "",
isActive: ko.observable<boolean>(false),
hashLocation: "",
onUpdateTabsButtons: undefined,
});
@@ -63,7 +62,6 @@ describe("Tabs manager tests", () => {
title: "",
tabPath: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: undefined,
});
@@ -72,10 +70,7 @@ describe("Tabs manager tests", () => {
documentsTab.tabId = "2";
});
beforeEach(() => {
tabsManager = new TabsManager();
explorer.tabsManager = tabsManager;
});
beforeEach(() => (tabsManager = new TabsManager()));
it("open new tabs", () => {
tabsManager.activateNewTab(queryTab);
@@ -122,7 +117,7 @@ describe("Tabs manager tests", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.closeTab(documentsTab.tabId, explorer);
tabsManager.closeTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);

View File

@@ -1,16 +1,10 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase";
export class TabsManager {
public openedTabs: ko.ObservableArray<TabsBase>;
public activeTab: ko.Observable<TabsBase>;
constructor() {
this.openedTabs = ko.observableArray<TabsBase>([]);
this.activeTab = ko.observable<TabsBase>();
}
public openedTabs = ko.observableArray<TabsBase>([]);
public activeTab = ko.observable<TabsBase>();
public activateNewTab(tab: TabsBase): void {
this.openedTabs.push(tab);
@@ -18,66 +12,43 @@ export class TabsManager {
}
public activateTab(tab: TabsBase): void {
this.activeTab() && this.activeTab().isActive(false);
tab.isActive(true);
this.activeTab(tab);
if (this.openedTabs().includes(tab)) {
tab.manager = this;
this.activeTab(tab);
tab.onActivate();
}
}
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
return this.openedTabs().filter((openedTab: TabsBase) => {
return openedTab.tabKind === tabKind && (!comparator || comparator(openedTab));
});
return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
}
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.openedTabs().forEach((tab: TabsBase) => {
if (comparator(tab) && tab.isActive()) {
tab.onActivate();
}
});
}
public removeTabById(tabId: string): void {
this.openedTabs.remove((tab: TabsBase) => tab.tabId === tabId);
}
public removeTabByComparator(comparator: (tab: TabsBase) => boolean): void {
this.openedTabs.remove((tab: TabsBase) => comparator(tab));
this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
}
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
this.activeTab() && this.activeTab().isActive(false);
this.activeTab(undefined);
this.openedTabs().forEach((tab: TabsBase) => {
if (comparator(tab)) {
tab.onCloseTabButtonClick();
}
});
this.openedTabs()
.filter(comparator)
.forEach((tab) => tab.onCloseTabButtonClick());
}
public closeTabs(): void {
this.openedTabs([]);
}
public closeTab(tabId: string, explorer: Explorer): void {
const tabIndex: number = this.openedTabs().findIndex((tab: TabsBase) => tab.tabId === tabId);
public closeTab(tab: TabsBase): void {
const tabIndex = this.openedTabs().indexOf(tab);
if (tabIndex !== -1) {
const tabToActive: TabsBase = this.openedTabs()[tabIndex + 1] || this.openedTabs()[tabIndex - 1];
this.openedTabs()[tabIndex].isActive(false);
this.removeTabById(tabId);
if (tabToActive) {
tabToActive.isActive(true);
this.activeTab(tabToActive);
} else {
explorer.selectedNode(undefined);
explorer.onUpdateTabsButtons([]);
this.openedTabs.remove(tab);
tab.manager = undefined;
if (this.openedTabs().length === 0) {
this.activeTab(undefined);
}
if (tab === this.activeTab()) {
const tabToTheRight = this.openedTabs()[tabIndex];
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
this.activateTab(tabToTheRight ?? lastOpenTab);
}
}
}
public isTabActive(tabKind: ViewModels.CollectionTabKind): boolean {
return this.activeTab() && this.activeTab().tabKind === tabKind;
}
}

View File

@@ -1,8 +1,7 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Collection from "./Collection";
import * as DataModels from "../../Contracts/DataModels";
import Explorer from "../Explorer";
import Collection from "./Collection";
jest.mock("monaco-editor");
describe("Collection", () => {
@@ -35,18 +34,11 @@ describe("Collection", () => {
mockContainer.isPreferredApiMongoDB = ko.computed(() => {
return false;
});
mockContainer.isPreferredApiCassandra = ko.computed(() => {
return false;
});
mockContainer.isDatabaseNodeOrNoneSelected = () => {
return false;
};
mockContainer.isPreferredApiDocumentDB = ko.computed(() => {
return true;
});
mockContainer.isPreferredApiGraph = ko.computed(() => {
return false;
});
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);

View File

@@ -1,9 +1,8 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
@@ -13,16 +12,14 @@ import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefine
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab";
@@ -196,7 +193,7 @@ export default class Collection implements ViewModels.Collection {
.map((node) => <Trigger>node);
});
const showScriptsMenus: boolean = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
const showScriptsMenus: boolean = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
@@ -304,7 +301,6 @@ export default class Collection implements ViewModels.Collection {
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items",
isActive: ko.observable<boolean>(false),
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,
@@ -352,7 +348,6 @@ export default class Collection implements ViewModels.Collection {
conflictIds: ko.observableArray<ConflictId>([]),
tabKind: ViewModels.CollectionTabKind.Conflicts,
title: "Conflicts",
isActive: ko.observable<boolean>(false),
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Conflicts`,
@@ -377,7 +372,7 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree,
});
if (this.container.isPreferredApiCassandra() && !this.cassandraKeys) {
if (userContext.apiType === "Cassandra" && !this.cassandraKeys) {
(<CassandraAPIDataClient>this.container.tableDataClient).getTableKeys(this).then((keys: CassandraTableKeys) => {
this.cassandraKeys = keys;
});
@@ -394,7 +389,7 @@ export default class Collection implements ViewModels.Collection {
} else {
this.documentIds([]);
let title = `Entities`;
if (this.container.isPreferredApiCassandra()) {
if (userContext.apiType === "Cassandra") {
title = `Rows`;
}
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@@ -409,12 +404,9 @@ export default class Collection implements ViewModels.Collection {
tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title,
tabPath: "",
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -466,7 +458,6 @@ export default class Collection implements ViewModels.Collection {
collectionPartitionKeyProperty: this.partitionKeyProperty,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
collectionId: this.id(),
isActive: ko.observable(false),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
@@ -513,12 +504,9 @@ export default class Collection implements ViewModels.Collection {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Documents",
tabPath: "",
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -561,7 +549,6 @@ export default class Collection implements ViewModels.Collection {
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
};
@@ -604,7 +591,6 @@ export default class Collection implements ViewModels.Collection {
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
isActive: ko.observable(false),
queryText: queryText,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
@@ -634,7 +620,6 @@ export default class Collection implements ViewModels.Collection {
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
isActive: ko.observable(false),
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
@@ -666,7 +651,6 @@ export default class Collection implements ViewModels.Collection {
collectionPartitionKeyProperty: this.partitionKeyProperty,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
collectionId: this.id(),
isActive: ko.observable(false),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
@@ -685,7 +669,6 @@ export default class Collection implements ViewModels.Collection {
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -960,73 +943,6 @@ export default class Collection implements ViewModels.Collection {
this.uploadFiles(event.originalEvent.dataTransfer.files);
}
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
if (configContext.platform === Platform.Hosted && userContext.authType === AuthType.AAD) {
return this._uploadFilesCors(fileList);
}
const documentUploader: Worker = new UploadWorker();
let inProgressNotificationId: string = "";
if (!fileList || fileList.length === 0) {
return Promise.reject("No files specified");
}
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
const numSuccessful: number = event.data.numUploadsSuccessful;
const numFailed: number = event.data.numUploadsFailed;
const runtimeError: string = event.data.runtimeError;
const uploadDetails: UploadDetails = event.data.uploadDetails;
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
documentUploader.terminate();
if (!!runtimeError) {
reject(runtimeError);
} else if (numSuccessful === 0) {
// all uploads failed
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
} else if (numFailed > 0) {
NotificationConsoleUtils.logConsoleError(
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
);
} else {
NotificationConsoleUtils.logConsoleInfo(
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
);
}
this._logUploadDetailsInConsole(uploadDetails);
resolve(uploadDetails);
};
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
documentUploader.terminate();
reject(event.error);
}
const uploaderMessage: StartUploadMessageParams = {
files: fileList,
documentClientParams: {
databaseId: this.databaseId,
containerId: this.id(),
masterKey: userContext.masterKey,
endpoint: userContext.endpoint,
accessToken: userContext.accessToken,
platform: configContext.platform,
databaseAccount: userContext.databaseAccount,
},
};
return new Promise<UploadDetails>((resolve, reject) => {
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
documentUploader.onerror = onerror.bind(null, reject);
documentUploader.postMessage(uploaderMessage);
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
});
};
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
if (!this.container) {
return undefined;
@@ -1062,13 +978,13 @@ export default class Collection implements ViewModels.Collection {
}
}
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
return { data };
}
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
private uploadFile(file: File): Promise<UploadDetailsRecord> {
const reader = new FileReader();
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
const fileData: string = evt.target.result;
@@ -1079,6 +995,7 @@ export default class Collection implements ViewModels.Collection {
resolve({
fileName: file.name,
numSucceeded: 0,
numThrottled: 0,
numFailed: 1,
errors: [(evt as any).error.message],
});
@@ -1096,21 +1013,47 @@ export default class Collection implements ViewModels.Collection {
fileName: fileName,
numSucceeded: 0,
numFailed: 0,
numThrottled: 0,
errors: [],
};
try {
const content = JSON.parse(documentContent);
if (Array.isArray(content)) {
await Promise.all(
content.map(async (documentContent) => {
await createDocument(this, documentContent);
record.numSucceeded++;
})
const parsedContent = JSON.parse(documentContent);
if (Array.isArray(parsedContent)) {
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize)
);
for (const chunk of chunkedContent) {
let retryAttempts = 0;
let chunkComplete = false;
let documentsToAttempt = chunk;
while (retryAttempts < 10 && !chunkComplete) {
const responses = await bulkCreateDocument(this, documentsToAttempt);
const attemptedDocuments = [...documentsToAttempt];
documentsToAttempt = [];
responses.forEach((response, index) => {
if (response.statusCode === 201) {
record.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else {
record.numFailed++;
}
});
if (documentsToAttempt.length === 0) {
chunkComplete = true;
break;
}
logConsoleInfo(
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`
);
retryAttempts++;
await sleep(retryAttempts);
}
}
} else {
await createDocument(this, documentContent);
await createDocument(this, parsedContent);
record.numSucceeded++;
}
@@ -1122,40 +1065,6 @@ export default class Collection implements ViewModels.Collection {
}
}
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
const numFiles: number = uploadDetailsRecords.length;
const stackTraceLimit: number = 100;
let stackTraceCount: number = 0;
let currentFileIndex = 0;
while (stackTraceCount < stackTraceLimit && currentFileIndex < numFiles) {
const errors: string[] = uploadDetailsRecords[currentFileIndex].errors;
for (let i = 0; i < errors.length; i++) {
if (stackTraceCount >= stackTraceLimit) {
break;
}
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Document creation error for container ${this.id()} - file ${
uploadDetailsRecords[currentFileIndex].fileName
}: ${errors[i]}`
);
stackTraceCount++;
}
currentFileIndex++;
}
uploadDetailsRecords.forEach((record: UploadDetailsRecord) => {
const consoleDataType: ConsoleDataType = record.numFailed > 0 ? ConsoleDataType.Error : ConsoleDataType.Info;
NotificationConsoleUtils.logConsoleMessage(
consoleDataType,
`Item creation summary for container ${this.id()} - file ${record.fileName}: ${
record.numSucceeded
} items created, ${record.numFailed} errors`
);
});
}
/**
* Top-level method that will open the correct tab type depending on account API
*/
@@ -1163,10 +1072,10 @@ export default class Collection implements ViewModels.Collection {
if (this.container.isPreferredApiTable()) {
this.onTableEntitiesClick();
return;
} else if (this.container.isPreferredApiCassandra()) {
} else if (userContext.apiType === "Cassandra") {
this.onTableEntitiesClick();
return;
} else if (this.container.isPreferredApiGraph()) {
} else if (userContext.apiType === "Gremlin") {
this.onGraphDocumentsClick();
return;
} else if (this.container.isPreferredApiMongoDB()) {
@@ -1183,9 +1092,9 @@ export default class Collection implements ViewModels.Collection {
public getLabel(): string {
if (this.container.isPreferredApiTable()) {
return "Entities";
} else if (this.container.isPreferredApiCassandra()) {
} else if (userContext.apiType === "Cassandra") {
return "Rows";
} else if (this.container.isPreferredApiGraph()) {
} else if (userContext.apiType === "Gremlin") {
return "Graph";
} else if (this.container.isPreferredApiMongoDB()) {
return "Documents";
@@ -1240,3 +1149,7 @@ export default class Collection implements ViewModels.Collection {
}
}
}
function sleep(seconds: number) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout";
import * as _ from "underscore";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
@@ -78,7 +79,6 @@ export default class Database implements ViewModels.Database {
rid: this.rid,
database: this,
hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
};
@@ -172,6 +172,27 @@ export default class Database implements ViewModels.Database {
public async loadCollections(): Promise<void> {
const collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id());
// TODO Remove
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
collections.map((collection) => {
if (collection.shardKey) {
const shardKey = Object.keys(collection.shardKey)[0];
collection.partitionKey = {
version: undefined,
kind: "Hash",
paths: [`/"$v"/"${shardKey.split(".").join(`"/"$v"/"`)}"/"$v"`],
};
} else {
collection.partitionKey = {
paths: ["/'$v'/'_partitionKey'/'$v'"],
kind: "Hash",
version: 2,
systemKey: true,
};
}
});
}
const deltaCollections = this.getDeltaCollections(collections);
collections.forEach((collection: DataModels.Collection) => {

View File

@@ -92,7 +92,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
isActive: ko.observable(false),
queryText: queryText,
partitionKey: collection.partitionKey,
resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(),
@@ -139,7 +138,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items",
isActive: ko.observable<boolean>(false),
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,

View File

@@ -253,7 +253,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
* @param container
*/
private static showScriptNodes(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
@@ -273,7 +273,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
});
if (!this.container.isPreferredApiCassandra() || !this.container.isServerlessEnabled()) {
if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) {
children.push({
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),

View File

@@ -76,7 +76,6 @@ export default class StoredProcedure {
collection: source,
node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
});
@@ -123,7 +122,6 @@ export default class StoredProcedure {
this.collection.databaseId,
this.collection.id()
)}/sprocs/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -138,7 +136,7 @@ export default class StoredProcedure {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => {
this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}

View File

@@ -58,7 +58,6 @@ export default class Trigger {
collection: source,
node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
});
@@ -98,7 +97,6 @@ export default class Trigger {
this.collection.databaseId,
this.collection.id()
)}/triggers/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -113,7 +111,7 @@ export default class Trigger {
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => {
this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}

View File

@@ -44,7 +44,6 @@ export default class UserDefinedFunction {
collection: source,
node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
});
@@ -82,7 +81,6 @@ export default class UserDefinedFunction {
this.collection.databaseId,
this.collection.id()
)}/udfs/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
});
@@ -106,7 +104,7 @@ export default class UserDefinedFunction {
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => {
this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}

View File

@@ -52,6 +52,7 @@ import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel";
import { useTabs } from "./hooks/useTabs";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/jquery";
import "./Shared/appInsights";
@@ -77,6 +78,7 @@ const App: React.FunctionComponent = () => {
};
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
const { tabs, tabsManager } = useTabs();
const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded,
@@ -86,7 +88,9 @@ const App: React.FunctionComponent = () => {
closeSidePanel,
openDialog,
closeDialog,
tabsManager,
};
const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams);
@@ -199,11 +203,7 @@ const App: React.FunctionComponent = () => {
{/* Splitter - End */}
</div>
{/* Collections Tree - End */}
<div className="connectExplorerContainer" data-bind="visible: tabsManager.openedTabs().length === 0">
<form className="connectExplorerFormContainer">
<SplashScreen explorer={explorer} />
</form>
</div>
{tabs.length === 0 && <SplashScreen explorer={explorer} />}
<div className="tabsManagerContainer" data-bind='component: { name: "tabs-manager", params: tabsManager }' />
</div>
{/* Collections Tree and Tabs - End */}
@@ -229,12 +229,10 @@ const App: React.FunctionComponent = () => {
isConsoleExpanded={isNotificationConsoleExpanded}
/>
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
<div data-bind='component: { name: "delete-collection-confirmation-pane", params: { data: deleteCollectionConfirmationPane} }' />
<div data-bind='component: { name: "graph-new-vertex-pane", params: { data: newVertexPane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
<div data-bind='component: { name: "table-query-select-pane", params: { data: querySelectPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
<div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' />

View File

@@ -17,15 +17,17 @@ export type Features = {
readonly notebookBasePath?: string;
readonly notebookServerToken?: string;
readonly notebookServerUrl?: string;
readonly sandboxNotebookOutputs: boolean;
readonly selfServeType?: string;
readonly pr?: string;
readonly showMinRUSurvey: boolean;
readonly ttl90Days: boolean;
};
export function extractFeatures(given = new URLSearchParams()): Features {
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
const downcased = new URLSearchParams();
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string) => downcased.get("feature." + key) ?? undefined;
const get = (key: string, defaultValue?: string) => downcased.get("feature." + key) ?? defaultValue;
try {
new URLSearchParams(window.parent.location.search).forEach(set);
@@ -54,7 +56,9 @@ export function extractFeatures(given = new URLSearchParams()): Features {
notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"),
notebookServerUrl: get("notebookserverurl"),
sandboxNotebookOutputs: "true" === get("sandboxnotebookoutputs", "true"),
selfServeType: get("selfservetype"),
pr: get("pr"),
showMinRUSurvey: "true" === get("showminrusurvey"),
ttl90Days: "true" === get("ttl90days"),
};

View File

@@ -5,6 +5,7 @@ import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase";
import TabsBase from "../Explorer/Tabs/TabsBase";
import { userContext } from "../UserContext";
export class TabRouteHandler {
private _tabRouter: any;
@@ -134,10 +135,7 @@ export class TabRouteHandler {
databaseId,
collectionId
);
collection &&
collection.container &&
collection.container.isPreferredApiDocumentDB() &&
collection.onDocumentDBDocumentsClick();
userContext.apiType === "SQL" && collection.onDocumentDBDocumentsClick();
});
}
@@ -149,7 +147,7 @@ export class TabRouteHandler {
);
collection &&
collection.container &&
(collection.container.isPreferredApiTable() || collection.container.isPreferredApiCassandra()) &&
(collection.container.isPreferredApiTable() || userContext.apiType === "Cassandra") &&
collection.onTableEntitiesClick();
});
}
@@ -160,10 +158,7 @@ export class TabRouteHandler {
databaseId,
collectionId
);
collection &&
collection.container &&
collection.container.isPreferredApiGraph() &&
collection.onGraphDocumentsClick();
userContext.apiType === "Gremlin" && collection.onGraphDocumentsClick();
});
}

View File

@@ -41,13 +41,15 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
await loadTranslations("SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
const selfServeExample = new SelfServeExample.default();
await loadTranslations(selfServeExample.constructor.name);
return selfServeExample.toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
await loadTranslations("SqlX");
return new SqlX.default().toSelfServeDescriptor();
const sqlX = new SqlX.default();
await loadTranslations(sqlX.constructor.name);
return sqlX.toSelfServeDescriptor();
}
default:
return undefined;

View File

@@ -89,6 +89,8 @@ export abstract class SelfServeBaseClass {
selfServeDescriptor.initialize = this.initialize;
selfServeDescriptor.onSave = this.onSave;
selfServeDescriptor.onRefresh = this.onRefresh;
selfServeDescriptor.root.id = className;
return selfServeDescriptor;
}
}

View File

@@ -150,7 +150,6 @@ describe("SelfServeUtils", () => {
]);
const expectedDescriptor = {
root: {
id: "TestClass",
children: [
{
id: "dbThroughput",
@@ -270,7 +269,7 @@ describe("SelfServeUtils", () => {
"invalidRegions",
],
};
const descriptor = mapToSmartUiDescriptor("TestClass", context);
const descriptor = mapToSmartUiDescriptor(context);
expect(descriptor).toEqual(expectedDescriptor);
});
});

View File

@@ -112,21 +112,18 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
const smartUiDescriptor = mapToSmartUiDescriptor(context);
Reflect.defineMetadata(className, smartUiDescriptor, target);
};
export const mapToSmartUiDescriptor = (
className: string,
context: Map<string, DecoratorProperties>
): SelfServeDescriptor => {
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => {
const inputNames: string[] = [];
const root = context.get("root");
context.delete("root");
const smartUiDescriptor: SelfServeDescriptor = {
root: {
id: className,
id: undefined,
info: undefined,
children: [],
},

View File

@@ -177,7 +177,7 @@ export default class SqlX extends SelfServeBaseClass {
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => {
selfServeTrace({ selfServeClassName: "SqlX" });
selfServeTrace({ selfServeClassName: SqlX.name });
const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean;
const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;

View File

@@ -36,6 +36,7 @@ const features = extractFeatures();
const { enableSDKoperations: useSDKOperations } = features;
const userContext: UserContext = {
apiType: "SQL",
hasWriteAccess: true,
isTryCosmosDBSubscription: false,
portalEnv: "prod",

Some files were not shown because too many files have changed in this diff Show More