Compare commits

..

4 Commits

Author SHA1 Message Date
sunilyadav840
4b8ed7a555 Merge branch 'master' 2021-05-06 14:08:02 +05:30
sunilyadav840
c2923768f8 update snapshot 2021-04-30 10:29:45 +05:30
sunilyadav840
101ddbec17 Merge branch 'master' 2021-04-30 09:20:13 +05:30
sunilyadav840
b8ed534c90 Move Explorer._refreshNotebooksEnabledStateForAccount into React 2021-04-29 17:51:05 +05:30
73 changed files with 6024 additions and 5320 deletions

View File

@@ -54,6 +54,7 @@ src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInput.test.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts
@@ -108,18 +109,24 @@ src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActions.test.ts
src/Explorer/OpenActions.ts
src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddCollectionPane.test.ts
src/Explorer/Panes/AddCollectionPane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts

View File

@@ -188,3 +188,7 @@ Cosmos Explorer has been under constant development for over 5 years. As a resul
✅ DO
- Support all [browsers supported by the Azure Portal](https://docs.microsoft.com/en-us/azure/azure-portal/azure-portal-supported-browsers-devices)
- Support IE11
- In practice, this should not need to be considered as part of a normal development workflow
- Polyfills and transpilation are already provided by our engineering systems.
- This requirement will be removed on March 30th, 2021 when Azure drops IE11 support.

View File

@@ -1757,7 +1757,7 @@ input::-webkit-calendar-picker-indicator {
cursor: pointer;
}
.paneMainContent {
.contextual-pane .paneMainContent {
flex: 1;
padding-left: 34px;
padding-right: 34px;

1027
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"@azure/ms-rest-nodeauth": "3.0.7",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.14.3",
"@fluentui/react": "8.10.1",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1",
@@ -43,6 +43,7 @@
"@testing-library/jest-dom": "5.11.9",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",

View File

@@ -1,16 +0,0 @@
import { Icon, TooltipHost } from "@fluentui/react";
import * as React from "react";
export interface TooltipProps {
children: string;
}
export const InfoTooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
return (
<span>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
</TooltipHost>
</span>
);
};

View File

@@ -0,0 +1,24 @@
import { ITooltipHostStyles, TooltipHost } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import * as React from "react";
import InfoBubble from "../../../images/info-bubble.svg";
const calloutProps = { gapSpace: 0 };
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
export interface TooltipProps {
children: string;
}
export const Tooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
const tooltipId = useId("tooltip");
return children ? (
<span>
<TooltipHost content={children} id={tooltipId} calloutProps={calloutProps} styles={hostStyles}>
<img className="infoImg" src={InfoBubble} alt="More information" />
</TooltipHost>
</span>
) : (
<></>
);
};

View File

@@ -2,7 +2,7 @@ import { Image, Stack, TextField } from "@fluentui/react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../Constants";
import { InfoTooltip } from "../Tooltip/InfoTooltip";
import { Tooltip } from "../Tooltip/Tooltip";
interface UploadProps {
label: string;
@@ -51,7 +51,7 @@ export const Upload: FunctionComponent<UploadProps> = ({
return (
<div>
<span className="renewUploadItemsHeader">{label}</span>
{tooltip && <InfoTooltip>{tooltip}</InfoTooltip>}
<Tooltip>{tooltip}</Tooltip>
<Stack horizontal>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input

View File

@@ -20,6 +20,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true);
});
it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});

View File

@@ -19,7 +19,9 @@ ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
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("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

@@ -1959,7 +1959,7 @@ exports[`test render renders with filters 1`] = `
</div>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -1,23 +1,20 @@
import {
BaseButton,
Button,
DocumentCard,
DocumentCardActivity,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardTitle,
FontWeights,
Icon,
IconButton,
IDocumentCardPreviewProps,
IDocumentCardStyles,
Image,
ImageFit,
Link,
Persona,
Separator,
Spinner,
SpinnerSize,
Text,
TooltipHost,
} from "@fluentui/react";
import { Card } from "@uifabric/react-cards";
import React, { FunctionComponent, useState } from "react";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { IGalleryItem } from "../../../../Juno/JunoClient";
@@ -51,6 +48,7 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
const CARD_WIDTH = 256;
const cardImageHeight = 144;
const cardDescriptionMaxChars = 80;
const cardItemGapBig = 10;
const cardItemGapSmall = 8;
const cardDeleteSpinnerHeight = 360;
const smallTextLineHeight = 18;
@@ -66,9 +64,9 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
const dateString = new Date(data.created).toLocaleString("default", options);
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
const renderTruncated = (text: string, totalLength: number): string => {
let truncatedDescription = text.substr(0, totalLength);
if (text.length > totalLength) {
const renderTruncatedDescription = (): string => {
let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars);
if (data.description.length > cardDescriptionMaxChars) {
truncatedDescription = `${truncatedDescription} ...`;
}
return truncatedDescription;
@@ -122,35 +120,42 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
event.preventDefault();
activate();
};
const DocumentCardActivityPeople = [{ name: data.author, profileImageSrc: data.isSample && CosmosDBLogo }];
const previewProps: IDocumentCardPreviewProps = {
previewImages: [
{
previewImageSrc: data.thumbnailUrl,
imageFit: ImageFit.cover,
width: CARD_WIDTH,
height: cardImageHeight,
},
],
};
const cardStyles: IDocumentCardStyles = {
root: { display: "inline-block", marginRight: 20, width: CARD_WIDTH },
};
return (
<DocumentCard aria-label={cardTitle} styles={cardStyles} onClick={onClick}>
<Card
style={{ background: "white" }}
aria-label={cardTitle}
data-is-focusable="true"
tokens={{ width: CARD_WIDTH, childrenGap: 0 }}
onClick={(event) => handlerOnClick(event, onClick)}
>
{isDeletingPublishedNotebook && (
<Spinner
size={SpinnerSize.large}
label={`Deleting '${cardTitle}'`}
styles={{ root: { height: cardDeleteSpinnerHeight } }}
/>
<Card.Item tokens={{ padding: cardItemGapBig }}>
<Spinner
size={SpinnerSize.large}
label={`Deleting '${cardTitle}'`}
styles={{ root: { height: cardDeleteSpinnerHeight } }}
/>
</Card.Item>
)}
{!isDeletingPublishedNotebook && (
<>
<DocumentCardActivity activity={dateString} people={DocumentCardActivityPeople} />
<DocumentCardPreview {...previewProps} />
<DocumentCardDetails>
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight, padding: "2px 16px" } }}>
<Card.Item tokens={{ padding: cardItemGapBig }}>
<Persona imageUrl={data.isSample && CosmosDBLogo} text={data.author} secondaryText={dateString} />
</Card.Item>
<Card.Item>
<Image
src={data.thumbnailUrl}
width={CARD_WIDTH}
height={cardImageHeight}
imageFit={ImageFit.cover}
alt={`${cardTitle} cover image`}
/>
</Card.Item>
<Card.Section styles={{ root: { padding: cardItemGapBig } }}>
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight } }}>
{data.tags ? (
data.tags.map((tag, index, array) => (
<span key={tag}>
@@ -162,22 +167,43 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
<br />
)}
</Text>
<DocumentCardTitle title={renderTruncated(cardTitle, 20)} shouldTruncate />
<DocumentCardTitle
title={renderTruncated(data.description, cardDescriptionMaxChars)}
showAsSecondaryTitle
/>
<span style={{ padding: "8px 16px" }}>
<Text
styles={{
root: {
fontWeight: FontWeights.semibold,
paddingTop: cardItemGapSmall,
paddingBottom: cardItemGapSmall,
},
}}
nowrap
>
{cardTitle}
</Text>
<Text variant="small" styles={{ root: { height: smallTextLineHeight * 2 } }}>
{renderTruncatedDescription()}
</Text>
<span>
{data.views !== undefined && generateIconText("RedEye", data.views.toString())}
{data.downloads !== undefined && generateIconText("Download", data.downloads.toString())}
{data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())}
</span>
</DocumentCardDetails>
</Card.Section>
{cardButtonsVisible && (
<DocumentCardDetails>
<Card.Section
styles={{
root: {
marginLeft: cardItemGapBig,
marginRight: cardItemGapBig,
},
}}
>
<Separator styles={{ root: { padding: 0, height: 1 } }} />
<span style={{ padding: "0px 16px" }}>
<span>
{isFavorite !== undefined &&
generateIconButtonWithTooltip(
isFavorite ? "HeartFill" : "Heart",
@@ -196,10 +222,10 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
)
)}
</span>
</DocumentCardDetails>
</Card.Section>
)}
</>
)}
</DocumentCard>
</Card>
);
};

View File

@@ -1,49 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GalleryCardComponent renders 1`] = `
<StyledDocumentCardBase
<Card
aria-label="name"
styles={
data-is-focusable="true"
onClick={[Function]}
style={
Object {
"root": Object {
"display": "inline-block",
"marginRight": 20,
"width": 256,
},
"background": "white",
}
}
tokens={
Object {
"childrenGap": 0,
"width": 256,
}
}
>
<StyledDocumentCardActivityBase
activity="Invalid Date"
people={
Array [
Object {
"name": "author",
"profileImageSrc": false,
},
]
<CardItem
tokens={
Object {
"padding": 10,
}
}
/>
<StyledDocumentCardPreviewBase
previewImages={
Array [
Object {
"height": 144,
"imageFit": 2,
"previewImageSrc": "thumbnailUrl",
"width": 256,
>
<StyledPersonaBase
imageUrl={false}
secondaryText="Invalid Date"
text="author"
/>
</CardItem>
<CardItem>
<Image
alt="name cover image"
height={144}
imageFit={2}
src="thumbnailUrl"
width={256}
/>
</CardItem>
<CardSection
styles={
Object {
"root": Object {
"padding": 10,
},
]
}
}
/>
<StyledDocumentCardDetailsBase>
>
<Text
nowrap={true}
styles={
Object {
"root": Object {
"height": 18,
"padding": "2px 16px",
},
}
}
@@ -59,21 +69,33 @@ exports[`GalleryCardComponent renders 1`] = `
</StyledLinkBase>
</span>
</Text>
<StyledDocumentCardTitleBase
shouldTruncate={true}
title="name"
/>
<StyledDocumentCardTitleBase
showAsSecondaryTitle={true}
title="description"
/>
<span
style={
<Text
nowrap={true}
styles={
Object {
"padding": "8px 16px",
"root": Object {
"fontWeight": 600,
"paddingBottom": 8,
"paddingTop": 8,
},
}
}
>
name
</Text>
<Text
styles={
Object {
"root": Object {
"height": 36,
},
}
}
variant="small"
>
description
</Text>
<span>
<Text
styles={
Object {
@@ -147,8 +169,17 @@ exports[`GalleryCardComponent renders 1`] = `
0
</Text>
</span>
</StyledDocumentCardDetailsBase>
<StyledDocumentCardDetailsBase>
</CardSection>
<CardSection
styles={
Object {
"root": Object {
"marginLeft": 10,
"marginRight": 10,
},
}
}
>
<Separator
styles={
Object {
@@ -159,13 +190,7 @@ exports[`GalleryCardComponent renders 1`] = `
}
}
/>
<span
style={
Object {
"padding": "0px 16px",
}
}
>
<span>
<StyledTooltipHostBase
calloutProps={
Object {
@@ -251,6 +276,6 @@ exports[`GalleryCardComponent renders 1`] = `
/>
</StyledTooltipHostBase>
</span>
</StyledDocumentCardDetailsBase>
</StyledDocumentCardBase>
</CardSection>
</Card>
`;

View File

@@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent";
import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent";
const CARD_WIDTH = 256;
export interface GalleryViewerComponentProps {
container?: Explorer;
junoClient: JunoClient;
@@ -86,7 +87,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
public static readonly PublishedTitle = "My published work";
private static readonly rowsPerPage = 5;
private static readonly CARD_WIDTH = 256;
private static readonly mostViewedText = "Most viewed";
private static readonly mostDownloadedText = "Most downloaded";
private static readonly mostFavoritedText = "Most favorited";
@@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
if (itemIndex === 0) {
this.columnCount = Math.floor(visibleRect.width / GalleryViewerComponent.CARD_WIDTH) || this.columnCount;
this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount;
this.rowCount = GalleryViewerComponent.rowsPerPage;
}
@@ -671,7 +672,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
return (
<div style={{ float: "left", padding: 5 }}>
<div style={{ float: "left", padding: 10 }}>
<GalleryCardComponent {...props} />
</div>
);

View File

@@ -252,7 +252,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
>
capacity calculator
<FontIcon
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
@@ -526,7 +526,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
>
capacity calculator
<FontIcon
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>

View File

@@ -75,6 +75,88 @@ exports[`SettingsComponent renders 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -141,8 +223,89 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -1060,7 +1223,7 @@ exports[`SettingsComponent renders 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -1079,6 +1242,7 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
@@ -1187,6 +1351,88 @@ exports[`SettingsComponent renders 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1253,8 +1499,89 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -2172,7 +2499,7 @@ exports[`SettingsComponent renders 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -2191,6 +2518,7 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
@@ -2312,6 +2640,88 @@ exports[`SettingsComponent renders 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -2378,8 +2788,89 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -3297,7 +3788,7 @@ exports[`SettingsComponent renders 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -3316,6 +3807,7 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
@@ -3424,6 +3916,88 @@ exports[`SettingsComponent renders 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -3490,8 +4064,89 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -4409,7 +5064,7 @@ exports[`SettingsComponent renders 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -4428,6 +5083,7 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],

View File

@@ -1,13 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { CostEstimateText } from "./CostEstimateText";
const props = {
requestUnits: 5,
isAutoscale: false,
};
describe("CostEstimateText Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<CostEstimateText {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,77 +0,0 @@
import { Text } from "@fluentui/react";
import React, { FunctionComponent } from "react";
import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import {
calculateEstimateNumber,
computeRUUsagePriceHourly,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../../Utils/PricingUtils";
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
requestUnits,
isAutoscale,
}: CostEstimateTextProps) => {
const { databaseAccount } = userContext;
if (!databaseAccount?.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return (
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
);
};

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CostEstimateText Pane should render Default properly 1`] = `<Fragment />`;

View File

@@ -1,36 +0,0 @@
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { ThroughputInput } from "./ThroughputInput";
const props = {
isDatabase: false,
showFreeTierExceedThroughputTooltip: true,
isSharded: false,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
onCostAcknowledgeChange: () => jest.fn(),
};
describe("ThroughputInput Pane", () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(<ThroughputInput {...props} />);
});
it("should render Default properly", () => {
expect(wrapper).toMatchSnapshot();
});
it("test Autoscale Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: true });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toBe(
"Estimate your required RU/s with capacity calculator."
);
expect(wrapper.find('[aria-label="maxRUDescription"]').at(0).text()).toContain("Max RU/s");
});
it("test Manual Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: false });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toContain("Estimate your required RU/s with");
expect(wrapper.find('[aria-label="capacityLink"]').at(0).text()).toContain("capacity calculator");
});
});

View File

@@ -1,14 +1,11 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import { Checkbox, DirectionalHint, Icon, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import React from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import { CostEstimateText } from "./CostEstimateText/CostEstimateText";
import "./ThroughputInput.less";
export interface ThroughputInputProps {
isDatabase: boolean;
@@ -17,25 +14,178 @@ export interface ThroughputInputProps {
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
isAutoscaleSelected?: boolean;
throughput?: number;
}
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isDatabase,
showFreeTierExceedThroughputTooltip,
setThroughputValue,
setIsAutoscale,
isSharded,
isAutoscaleSelected = true,
throughput = AutoPilotUtils.minAutoPilotThroughput,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
const getThroughputLabelText = (): string => {
export interface ThroughputInputState {
isAutoscaleSelected: boolean;
throughput: number;
isCostAcknowledged: boolean;
throughputError: string;
}
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
constructor(props: ThroughputInputProps) {
super(props);
this.state = {
isAutoscaleSelected: true,
throughput: AutoPilotUtils.minAutoPilotThroughput,
isCostAcknowledged: false,
throughputError: undefined,
};
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
this.props.setIsAutoscale(true);
}
render(): JSX.Element {
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{this.getThroughputLabelText()}
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onAutoscaleRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onManualRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{this.props.isDatabase ? "Database" : getCollectionName()} max RU/s
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
errorMessage={this.state.throughputError}
/>
<Text variant="small">
Your {this.props.isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will
automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
{this.state.throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
this.props.showFreeTierExceedThroughputTooltip &&
this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={this.state.throughputError}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={this.state.throughput} isAutoscale={this.state.isAutoscaleSelected} />
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<span className="mandatoryStar">*&nbsp;</span>
<Checkbox
checked={this.state.isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
this.setState({ isCostAcknowledged: isChecked });
this.props.onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
);
}
private getThroughputLabelText(): string {
let throughputHeaderText: string;
if (isAutoscaleSelected) {
if (this.state.isAutoscaleSelected) {
throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase();
} else {
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
@@ -44,26 +194,29 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
: "unlimited";
throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`;
}
return `${isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
};
const onThroughputValueChange = (newInput: string): void => {
return `${this.props.isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
}
private onThroughputValueChange(newInput: string): void {
const newThroughput = parseInt(newInput);
setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs");
} else {
setThroughputError("");
}
};
this.setState({ throughput: newThroughput });
this.props.setThroughputValue(newThroughput);
const getAutoScaleTooltip = (): string => {
if (!this.props.isSharded && newThroughput > 10000) {
this.setState({ throughputError: "Unsharded collections support up to 10,000 RUs" });
} else {
this.setState({ throughputError: undefined });
}
}
private getAutoScaleTooltip(): string {
const collectionName = getCollectionName().toLocaleLowerCase();
return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`;
};
}
const getCostAcknowledgeText = (): string => {
const databaseAccount = userContext.databaseAccount;
private getCostAcknowledgeText(): string {
const { databaseAccount } = userContext;
if (!databaseAccount || !databaseAccount.properties) {
return "";
}
@@ -72,159 +225,98 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
return PricingUtils.getEstimatedSpendAcknowledgeString(
throughput,
this.state.throughput,
userContext.portalEnv,
numberOfRegions,
multimasterEnabled,
isAutoscaleSelected
this.state.isAutoscaleSelected
);
};
}
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
} else {
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
private onAutoscaleRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.isAutoscaleSelected) {
this.setState({ isAutoscaleSelected: true, throughput: AutoPilotUtils.minAutoPilotThroughput });
this.props.setIsAutoscale(true);
}
};
}
private onManualRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && this.state.isAutoscaleSelected) {
this.setState({
isAutoscaleSelected: false,
throughput: SharedConstants.CollectionCreation.DefaultCollectionRUs400,
});
this.props.setIsAutoscale(false);
this.props.setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
}
}
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props: CostEstimateTextProps) => {
const { requestUnits, isAutoscale } = props;
const { databaseAccount } = userContext;
if (!databaseAccount?.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = PricingUtils.computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = PricingUtils.getPriceCurrency(serverId);
const currencySign: string = PricingUtils.getCurrencySign(serverId);
const multiplier = PricingUtils.getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
: PricingUtils.getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = (
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={PricingUtils.estimatedCostDisclaimer}
styles={{ root: { verticalAlign: "bottom" } }}
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
);
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{getThroughputLabelText()}
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with{" "}
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="ruDescription">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Max RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={throughputError}
/>
<Text variant="small">
Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale
from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="capacityLink">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
showFreeTierExceedThroughputTooltip &&
throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={throughputError}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
{throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox
checked={isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
setIsCostAcknowledged(isChecked);
onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
);
};

View File

@@ -1,5 +1,6 @@
import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout";
import * as path from "path";
import Q from "q";
import React from "react";
import _ from "underscore";
@@ -31,7 +32,7 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
import { getCollectionName } from "../Utils/APITypeUtils";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@@ -49,9 +50,9 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
@@ -96,6 +97,8 @@ export interface ExplorerParams {
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
tabsManager: TabsManager;
refreshSparkEnabledStateForAccount: () => void;
isSparkEnabledForAccount: boolean;
}
export default class Explorer {
@@ -150,6 +153,7 @@ export default class Explorer {
// Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient;
@@ -172,7 +176,6 @@ export default class Explorer {
public notebookWorkspaceManager: NotebookWorkspaceManager;
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
public isSparkEnabled: ko.Observable<boolean>;
public isSparkEnabledForAccount: ko.Observable<boolean>;
public arcadiaToken: ko.Observable<string>;
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
@@ -191,6 +194,10 @@ export default class Explorer {
content: string;
};
// Refresh spark
public refreshSparkEnabledStateForAccount: () => void;
public isSparkEnabledForAccount: boolean;
// React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter;
@@ -206,6 +213,8 @@ export default class Explorer {
this.closeSidePanel = params?.closeSidePanel;
this.closeDialog = params?.closeDialog;
this.openDialog = params?.openDialog;
this.refreshSparkEnabledStateForAccount = params?.refreshSparkEnabledStateForAccount;
this.isSparkEnabledForAccount = params?.isSparkEnabledForAccount;
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
@@ -231,8 +240,7 @@ export default class Explorer {
});
this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false);
this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
// this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.hasStorageAnalyticsAfecFeature = ko.observable(false);
this.hasStorageAnalyticsAfecFeature.subscribe((enabled: boolean) => this.refreshCommandBarButtons());
this.isSynapseLinkUpdating = ko.observable<boolean>(false);
@@ -248,7 +256,7 @@ export default class Explorer {
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then((isRegistered) =>
this.hasStorageAnalyticsAfecFeature(isRegistered)
);
Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then(
Promise.all([this._refreshNotebooksEnabledStateForAccount(), this.refreshSparkEnabledStateForAccount()]).then(
async () => {
this.isNotebookEnabled(
userContext.authType !== AuthType.ResourceToken &&
@@ -271,7 +279,7 @@ export default class Explorer {
this.isSparkEnabled(
(this.isNotebookEnabled() &&
this.isSparkEnabledForAccount() &&
this.isSparkEnabledForAccount &&
this.arcadiaWorkspaces() &&
this.arcadiaWorkspaces().length > 0) ||
userContext.features.enableSpark
@@ -411,6 +419,14 @@ export default class Explorer {
container: this,
});
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"),
id: "addcollectionpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
@@ -433,7 +449,12 @@ export default class Explorer {
}
});
this._panes = [this.addDatabasePane, this.graphStylingPane, this.cassandraAddCollectionPane];
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
this.graphStylingPane,
this.cassandraAddCollectionPane,
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
@@ -457,6 +478,11 @@ export default class Explorer {
this.collectionTreeNodeAltText("Container");
this.deleteCollectionText("Delete Container");
this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Container");
this.addCollectionPane.collectionIdTitle("Container id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container"
);
this.refreshTreeTitle("Refresh containers");
break;
case "Mongo":
@@ -466,6 +492,11 @@ export default class Explorer {
this.collectionTreeNodeAltText("Collection");
this.deleteCollectionText("Delete Collection");
this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Collection");
this.addCollectionPane.collectionIdTitle("Collection id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this collection"
);
this.refreshTreeTitle("Refresh collections");
break;
case "Gremlin":
@@ -475,6 +506,9 @@ export default class Explorer {
this.deleteDatabaseText("Delete Database");
this.collectionTitle("Gremlin API");
this.collectionTreeNodeAltText("Graph");
this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.refreshTreeTitle("Refresh graphs");
break;
case "Tables":
@@ -484,6 +518,9 @@ export default class Explorer {
this.deleteDatabaseText("Delete Database");
this.collectionTitle("Azure Table API");
this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new TablesAPIDataClient();
break;
@@ -494,6 +531,9 @@ export default class Explorer {
this.deleteDatabaseText("Delete Keyspace");
this.collectionTitle("Cassandra API");
this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new CassandraAPIDataClient();
break;
@@ -1553,34 +1593,6 @@ export default class Explorer {
}
}
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const { subscriptionId, authType } = userContext;
const armEndpoint = configContext.ARM_ENDPOINT;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet
this.isSparkEnabledForAccount(false);
return;
}
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try {
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri,
Constants.ArmApiVersions.armFeatures
);
const isEnabled =
(sparkNotebooksFeature &&
sparkNotebooksFeature.properties &&
sparkNotebooksFeature.properties.state === "Registered") ||
false;
this.isSparkEnabledForAccount(isEnabled);
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
this.isSparkEnabledForAccount(false);
}
};
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const { subscriptionId, authType } = userContext;
const armEndpoint = configContext.ARM_ENDPOINT;
@@ -1809,9 +1821,45 @@ export default class Explorer {
}
}
public async openNotebookViewer(notebookUrl: string) {
const title = path.basename(notebookUrl);
const hashLocation = notebookUrl;
const NotebookViewerTab = await (
await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab")
).default;
const notebookViewerTab = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2).find((tab) => {
return tab.hashLocation() == hashLocation && tab instanceof NotebookViewerTab && tab.notebookUrl === notebookUrl;
});
if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab);
} else {
const notebookViewerTab = new NotebookViewerTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null,
title: title,
tabPath: title,
collection: null,
hashLocation: hashLocation,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
container: this,
notebookUrl,
});
this.tabsManager.activateNewTab(notebookViewerTab);
}
}
public onNewCollectionClicked(databaseId?: string): void {
if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableKOPanel) {
this.addCollectionPane.open(this.selectedDatabaseId());
document.getElementById("linkAddCollection").focus();
} else {
this.openAddCollectionPanel(databaseId);
}
@@ -1925,7 +1973,7 @@ export default class Explorer {
public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel(
"Delete " + getDatabaseName(),
"Delete Database",
<DeleteDatabaseConfirmationPanel
explorer={this}
openNotificationConsole={this.expandConsole}
@@ -1936,12 +1984,12 @@ export default class Explorer {
}
public openUploadItemsPanePane(): void {
this.openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openSettingPane(): void {
this.openSidePanel(
"Setting",
"Settings",
<SettingsPane expandConsole={() => this.expandConsole()} closePanel={this.closeSidePanel} />
);
}
@@ -1969,21 +2017,6 @@ export default class Explorer {
/>
);
}
public openAddDatabasePane(): void {
if (userContext.features.enableKOPanel) {
this.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus();
} else {
this.openSidePanel(
"Add " + getDatabaseName(),
<AddDatabasePanel
explorer={this}
openNotificationConsole={this.expandConsole}
closePanel={this.closeSidePanel}
/>
);
}
}
public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />);
@@ -2000,7 +2033,7 @@ export default class Explorer {
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel(
"Upload file to notebook server",
"Upload File",
<UploadFilePane
expandConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel}

View File

@@ -733,16 +733,15 @@ export class D3ForceGraph implements GraphRenderer {
.attr("aria-label", (d: D3Node) => {
return this.retrieveNodeCaption(d);
})
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
.on("dblclick", function (_: MouseEvent, d: D3Node) {
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
.on("click", function (_: MouseEvent, d: D3Node) {
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("keypress", function (this: Element, event: KeyboardEvent, d: D3Node) {
.on("keypress", function (event: KeyboardEvent, d: D3Node) {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
// this is the <g> element

View File

@@ -266,7 +266,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () => {
container.openAddDatabasePane();
container.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus();
},
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -0,0 +1,602 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="setTemplateReady: true, click: cancel, clickBubble: false"></div>
<div class="contextual-pane" data-bind="attr: { id: id }">
<!-- Add collection form -- Start -->
<div class="contextual-pane-in">
<form data-bind="submit: submit" style="height: 100%">
<div
class="paneContentContainer"
role="dialog"
aria-labelledby="containerTitle"
data-bind="template: { name: 'add-collection-inputs' }"
></div>
</form>
</div>
<!-- Add collection form -- End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>
<script type="text/html" id="add-collection-inputs">
<!-- Add collection header - Start -->
<div class="firstdivbg headerline">
<span id="containerTitle" role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
id="closeBtnAddCollection"
role="button"
aria-label="Add collection close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
tabindex="0"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add collection header - End -->
<!-- Add collection 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="visible: formErrorsDetails() && formErrorsDetails() !== '' , click: showErrorDetails, event: { keypress: onMoreDetailsKeyPress }"
tabindex="0"
>
More details</a
>
</span>
</div>
</div>
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: formWarnings() && formWarnings() !== ''">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formWarnings, attr: { title: formWarnings }"></span>
</span>
</div>
</div>
<!-- Add collection errors - End -->
<!-- upsell message - start -->
<div
class="infoBoxContainer"
aria-live="assertive"
data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()"
>
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo" /></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a
class="underlinedLink"
id="linkAddCollection"
data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank"
href=""
tabindex="0"
></a>
</span>
</div>
</div>
<!-- upsell message - end -->
<!-- Add collection inputs - Start -->
<div class="paneMainContent" data-bind="visible: !maxCollectionsReached()">
<div data-bind="visible: !isPreferredApiTable()">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Database id</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>A database is analogous to a namespace. It is the unit of management for a set of containers.</span
>
</span>
</p>
<div class="createNewDatabaseOrUseExisting">
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Create new database"
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
data-test="addCollection-createNewDatabase"
tabindex="0"
data-bind="checked: databaseCreateNew, checkedValue: true, attr: { 'aria-checked': databaseCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="databaseCreateNew">Create new</span>
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Use existing database"
name="databaseType"
type="radio"
role="radio"
id="databaseUseExisting"
data-test="addCollection-existingDatabase"
tabindex="0"
data-bind="checked: databaseCreateNew, checkedValue: false, attr: { 'aria-checked': !databaseCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="databaseUseExisting">Use existing</span>
</div>
<input
name="newDatabaseId"
id="databaseId"
data-test="addCollection-newDatabaseId"
aria-required="true"
type="text"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new database id"
size="40"
class="collid"
data-bind="visible: databaseCreateNew, textInput: databaseId, hasFocus: firstFieldHasFocus"
autofocus
/>
<input
name="existingDatabaseId"
id="existingDatabaseId"
data-test="addCollection-existingDatabaseId"
aria-required="true"
type="text"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
list="databasesList"
placeholder="Choose an existing database"
size="40"
class="collid"
data-bind="visible: !databaseCreateNew(), textInput: databaseId, hasFocus: firstFieldHasFocus"
/>
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
<option data-bind="value: $data"></option>
</datalist>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="Provision database throughput" data-bind="visible: databaseCreateNew">
<input
tabindex="0"
type="checkbox"
data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput"
title="Provision database throughput"
data-bind="checked: databaseCreateNewShared"
/>
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext provisionDatabaseThroughput"
>Provisioned throughput at the database level will be shared across all containers within the
database.</span
>
</span>
</div>
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input-autopilot-v3
params="{
testId: 'databaseThroughputValue',
value: throughputDatabase,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared() && databaseCreateNew(),
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAck',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newContainer-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newContainer-databaseThroughput-manualRadio',
throughputModeRadioName: 'sharedThroughputModeRadio',
isAutoPilotSelected: isSharedAutoPilotSelected,
maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel" data-bind="text: collectionIdTitle"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Unique identifier for the container and used for id-based routing through REST and all SDKs</span
>
</span>
</p>
<input
name="collectionId"
id="containerId"
data-test="addCollection-collectionId"
type="text"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="e.g., Container1"
size="40"
class="textfontclr collid"
data-bind="value: collectionId"
/>
</div>
<!-- Indexing For Shared Throughput - start -->
<div class="seconddivpadding" data-bind="visible: showIndexingOptionsForSharedThroughput() && !isMongo()">
<div
class="useIndexingForSharedThroughput createNewDatabaseOrUseExisting"
aria-label="Indexing For Shared Throughput"
>
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Indexing</span>
</p>
<div>
<input
type="radio"
id="useIndexingForSharedThroughputOn"
name="useIndexingForSharedThroughput"
value="on"
class="createNewDatabaseOrUseExistingRadio"
data-bind="checked: useIndexingForSharedThroughput, checkedValue: true"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="useIndexingForSharedThroughputOn">Automatic</span>
<input
type="radio"
id="useIndexingForSharedThroughputOff"
name="useIndexingForSharedThroughput"
value="off"
class="createNewDatabaseOrUseExistingRadio"
data-bind="checked: useIndexingForSharedThroughput, checkedValue: false"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="useIndexingForSharedThroughputOff">Off</span>
</div>
<p data-bind="visible: useIndexingForSharedThroughput">
All properties in your documents will be indexed by default for flexible and efficient queries.
<a class="errorLink" href="https://aka.ms/cosmos-indexing-policy" target="_blank">Learn more</a>
</p>
<p data-bind="visible: useIndexingForSharedThroughput() === false">
Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.
<a class="errorLink" href="https://aka.ms/cosmos-indexing-policy" target="_blank">Learn more</a>
</p>
</div>
</div>
<!-- Indexing For Shared Throughput - end -->
<p
class="seconddivpadding"
data-bind="visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Storage capacity</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>This is the maximum storage size of the container. Storage is billed per GB based on consumption.</span
>
</span>
</p>
<div class="tabs">
<div
tabindex="0"
data-bind="event: { keydown: onStorageOptionsKeyDown }, visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
aria-label="Storage capacity"
>
<!-- Fixed option button - Start -->
<div class="tab">
<input type="radio" id="tab1" name="storage" value="10" class="radio" data-bind="checked: storage" />
<label for="tab1">Fixed (20 GB)</label>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input type="radio" id="tab2" name="storage" value="100" class="radio" data-bind="checked: storage" />
<label for="tab2">Unlimited</label>
</div>
<!-- Unlimited option button - End -->
</div>
<!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: partitionKeyVisible">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel" data-bind="text: partitionKeyName"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>The <span data-bind="text: partitionKeyName"></span> is used to automatically partition data among
multiple servers for scalability. Choose a JSON property name that has a wide range of values and is
likely to have evenly distributed access patterns.</span
>
</span>
</p>
<input
type="text"
id="addCollection-partitionKeyValue"
data-test="addCollection-partitionKeyValue"
aria-required="true"
size="40"
class="textfontclr collid"
data-bind="textInput: partitionKey,
attr: {
placeholder: partitionKeyPlaceholder,
required: partitionKeyVisible(),
pattern: partitionKeyPattern,
title: partitionKeyTitle
}"
/>
</div>
<!-- large parition key - start -->
<div class="largePartitionKey" aria-label="Large Partition Key" data-bind="visible: partitionKeyVisible">
<input
tabindex="0"
type="checkbox"
id="largePartitionKey"
data-test="addCollection-largePartitionKey"
title="Large Partition Key"
data-bind="checked: largePartitionKey"
/>
<span for="largePartitionKey"
>My <span data-bind="text: lowerCasePartitionKeyName"></span> is larger than 100 bytes</span
>
<p
data-bind="visible: largePartitionKey"
class="largePartitionKeyDescription"
data-test="addCollection-largePartitionKeyDescription"
>
Old SDKs do not work with containers that support large
<span data-bind="text: lowerCasePartitionKeyName"></span>s, ensure you are using the right SDK version.
<a class="errorLink" href="https://aka.ms/cosmosdb/pkv2" target="_blank">Learn more</a>
</p>
</div>
<!-- large parition key - end -->
<!-- ko if: canConfigureThroughput -->
<!-- Provision collection throughput checkbox - start -->
<div class="pkPadding" data-bind="visible: databaseHasSharedOffer() && !databaseCreateNew()">
<input
type="checkbox"
id="collectionSharedThroughput"
data-bind="checked: collectionWithThroughputInShared, attr: {title:collectionWithThroughputInSharedTitle}"
/>
<span for="collectionSharedThroughput" data-bind="text: collectionWithThroughputInSharedTitle"></span>
<span class="leftAlignInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext sharedCollectionThroughputTooltipWidth"
>You can optionally provision dedicated throughput for a container within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other containers in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.</span
>
</span>
</div>
<!-- Provision collection throughput checkbox - end -->
<!-- Provision collection throughput spinner - start -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input-autopilot-v3
params="{
testId: 'collectionThroughputValue',
value: throughputMultiPartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: displayCollectionThroughput,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: dedicatedRequestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCollection',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newContainer-containerThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newContainer-containerThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- Provision collection throughput spinner - end -->
<!-- /ko -->
<!-- Provision collection throughput - end -->
<!-- Custom indexes for mongo checkbox - start -->
<div class="pkPadding" data-bind="visible: container.isEnableMongoCapabilityPresent()">
<p>
<span class="addCollectionLabel">Indexing</span>
</p>
<input
type="checkbox"
id="mongoWildcardIndex"
title="mongoWildcardIndex"
data-bind="checked: shouldCreateMongoWildcardIndex"
/>
<span>Create a Wildcard Index on all fields</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext mongoWildcardIndexTooltipWidth">
By default, only the field _id is indexed. Creating a wildcard index on all fields will quickly optimize
query performance and is recommended during development.
</span>
</span>
</div>
<!-- Custom indexes for mongo checkbox - end -->
<!-- Enable analytical storage - start -->
<div
class="enableAnalyticalStorage pkPadding"
aria-label="Enable Analytical Store"
data-bind="visible: isSynapseLinkSupported"
>
<div>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span>
<span
class="infoTooltip"
role="tooltip"
tabindex="0"
data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }"
>
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }">
Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads. Learn more
<a
id="link1"
class="errorLink"
href="https://aka.ms/analytical-store-overview"
target="_blank"
data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }"
>here</a
>
</span>
</span>
</div>
<div class="paragraph">
<input
class="enableAnalyticalStorageRadio"
id="enableAnalyticalStorageRadioOn"
name="analyticalStore"
type="radio"
role="radio"
tabindex="0"
data-bind="
disable: showEnableSynapseLink,
checked: isAnalyticalStorageOn,
checkedValue: true,
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'true' : 'false'
}"
/>
<label for="enableAnalyticalStorageRadioOn" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink"> On </span>
</label>
<input
class="enableAnalyticalStorageRadio"
id="enableAnalyticalStorageRadioOff"
name="analyticalStore"
type="radio"
role="radio"
tabindex="0"
data-bind="
disable: showEnableSynapseLink,
checked: isAnalyticalStorageOn,
checkedValue: false,
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'false' : 'true'
}"
/>
<label for="enableAnalyticalStorageRadioOff" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink"> Off </span>
</label>
</div>
<div class="paragraph italic" data-bind="visible: ttl90DaysEnabled() && isAnalyticalStorageOn()">
By default, Analytical Time-to-Live will be configured to retain 90 days of data in the analytical store.
You can configure a custom retention policy in the 'Settings' tab.
<span
><a class="errorLink" href="https://aka.ms/cosmosdb-analytical-ttl" target="_blank">Learn more</a></span
>
</div>
<div class="paragraph" data-bind="visible: showEnableSynapseLink">
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this
Cosmos DB account.
<span><a class="errorLink" href="https://aka.ms/cosmosdb-synapselink" target="_blank">Learn more</a></span>
</div>
<div class="paragraph" data-bind="visible: showEnableSynapseLink">
<button
class="button"
type="button"
data-bind="
click: onEnableSynapseLinkButtonClicked,
disable: isSynapseLinkUpdating,
css: {
enabled: !isSynapseLinkUpdating(),
disabled: isSynapseLinkUpdating
}
"
>
Enable
</button>
</div>
</div>
<!-- Enable analytical storage - end -->
</div>
<!-- Unlimited Button Content - End -->
</div>
<div class="uniqueIndexesContainer" data-bind="visible: uniqueKeysVisible">
<p class="uniqueKeys">
<span class="addCollectionLabel">Unique keys</span>
<span class="uniqueInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="uniqueTooltiptext infoTooltipWidth"
>Unique keys provide developers with the ability to add a layer of data integrity to their database. By
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
per partition key.</span
>
</span>
</p>
<dynamic-list
params="{ listItems: uniqueKeys, placeholder: uniqueKeysPlaceholder(), ariaLabel: 'Write a comma separated path list of unique keys', buttonText: 'Add unique key' }"
>
</dynamic-list>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
name="createCollection"
id="submitBtnAddCollection"
data-test="addCollection-createCollection"
type="submit"
value="OK"
class="btncreatecoll1"
/>
</div>
</div>
<div data-bind="visible: maxCollectionsReached">
<error-display params="{ errorMsg: maxCollectionsReachedMessage }"></error-display>
</div>
<!-- Add collection inputs - End -->
</script>

View File

@@ -0,0 +1,108 @@
import * as Constants from "../../Common/Constants";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddCollectionPane from "./AddCollectionPane";
const mockDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false,
},
type: undefined,
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true,
},
type: undefined,
};
describe("Add Collection Pane", () => {
describe("isValid()", () => {
it("should be true if graph API and partition key is not /id nor /label", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const explorer = new Explorer();
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/blah");
expect(addCollectionPane.isValid()).toBe(true);
});
it("should be false if graph API and partition key is /id or /label", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const explorer = new Explorer();
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(false);
addCollectionPane.partitionKey("/label");
expect(addCollectionPane.isValid()).toBe(false);
});
it("should be true for any non-graph API with /id or /label partition key", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const explorer = new Explorer();
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(true);
addCollectionPane.partitionKey("/label");
expect(addCollectionPane.isValid()).toBe(true);
});
it("should display free tier text in upsell messaging", () => {
updateUserContext({ databaseAccount: mockFreeTierDatabaseAccount });
const explorer = new Explorer();
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
expect(addCollectionPane.isFreeTierAccount()).toBe(true);
expect(addCollectionPane.upsellMessage()).toContain("With free tier");
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addCollectionPane.upsellAnchorText()).toBe("Learn more");
});
it("should display standard texr in upsell messaging", () => {
updateUserContext({ databaseAccount: mockDatabaseAccount });
const explorer = new Explorer();
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
expect(addCollectionPane.isFreeTierAccount()).toBe(false);
expect(addCollectionPane.upsellMessage()).toContain("Start at");
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.cosmosPricing);
expect(addCollectionPane.upsellAnchorText()).toBe("More details");
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,6 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -58,16 +57,18 @@ export interface AddCollectionPanelState {
errorMessage: string;
showErrorDetails: boolean;
isExecuting: boolean;
newDatabaseThroughput: number;
isNewDatabaseAutoscale: boolean;
collectionThroughput: number;
isCollectionAutoscale: boolean;
isCostAcknowledged: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
private newDatabaseThroughput: number;
private isNewDatabaseAutoscale: boolean;
private collectionThroughput: number;
private isCollectionAutoscale: boolean;
private isCostAcknowledged: boolean;
constructor(props: AddCollectionPanelProps) {
super(props);
this.state = {
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
newDatabaseId: "",
@@ -79,18 +80,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded: userContext.apiType !== "Tables",
partitionKey: "",
enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
createMongoWildCardIndex: true,
useHashV2: false,
enableAnalyticalStore: false,
uniqueKeys: [],
errorMessage: "",
showErrorDetails: false,
isExecuting: false,
newDatabaseThroughput: undefined,
isNewDatabaseAutoscale: true,
collectionThroughput: undefined,
isCollectionAutoscale: true,
isCostAcknowledged: false,
};
}
@@ -216,14 +212,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={true}
isAutoscaleSelected={this.state.isNewDatabaseAutoscale}
throughput={this.state.newDatabaseThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => this.setState({ newDatabaseThroughput: throughput })}
setIsAutoscale={(isAutoscale: boolean) => this.setState({ isNewDatabaseAutoscale: isAutoscale })}
onCostAcknowledgeChange={(isAcknowledge: boolean) =>
this.setState({ isCostAcknowledged: isAcknowledge })
}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
</Stack>
@@ -449,13 +441,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={false}
isAutoscaleSelected={this.state.isCollectionAutoscale}
throughput={this.state.collectionThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => this.setState({ collectionThroughput: throughput })}
setIsAutoscale={(isAutoscale: boolean) => this.setState({ isCollectionAutoscale: isAutoscale })}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
this.setState({ isCostAcknowledged: isAcknowledged });
this.isCostAcknowledged = isAcknowledged;
}}
/>
)}
@@ -535,7 +525,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}}
>
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && (
{userContext.databaseAccount.properties.capabilities.find((c) => c.name === "EnableMongo") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
@@ -861,7 +851,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return true;
}
return properties.capabilities?.some(
return properties.capabilities.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics
);
}
@@ -900,11 +890,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
const throughput = this.state.createNewDatabase
? this.state.newDatabaseThroughput
: this.state.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.state.isCostAcknowledged) {
const errorMessage = this.state.isNewDatabaseAutoscale
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
this.setState({ errorMessage });
@@ -987,8 +975,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
},
collection: {
id: this.state.collectionId,
throughput: this.state.collectionThroughput,
isAutoscale: this.state.isCollectionAutoscale,
throughput: this.collectionThroughput,
isAutoscale: this.isCollectionAutoscale,
partitionKey,
uniqueKeyPolicy,
collectionWithDedicatedThroughput: this.state.enableDedicatedThroughput,
@@ -1006,16 +994,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (this.state.createNewDatabase) {
if (this.state.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.state.newDatabaseThroughput;
if (this.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.newDatabaseThroughput;
} else {
offerThroughput = this.state.newDatabaseThroughput;
offerThroughput = this.newDatabaseThroughput;
}
} else if (!databaseLevelThroughput) {
if (this.state.isCollectionAutoscale) {
autoPilotMaxThroughput = this.state.collectionThroughput;
if (this.isCollectionAutoscale) {
autoPilotMaxThroughput = this.collectionThroughput;
} else {
offerThroughput = this.state.collectionThroughput;
offerThroughput = this.collectionThroughput;
}
}

View File

@@ -1,4 +1,4 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" data-bind="attr: { id: id }">
<!-- Add database form -- Start -->
@@ -126,31 +126,31 @@
<div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3
params="{
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckDatabase',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newDatabase-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newDatabase-databaseThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckDatabase',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newDatabase-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newDatabase-databaseThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
>
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">

View File

@@ -1,17 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { AddDatabasePanel } from "./AddDatabasePanel";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
};
describe("AddDatabasePane Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<AddDatabasePanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,342 +0,0 @@
import { Checkbox, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext, Platform } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
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 { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface AddDatabasePaneProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
}
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
explorer: container,
closePanel,
openNotificationConsole,
}: AddDatabasePaneProps) => {
const { subscriptionType } = userContext;
const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled());
const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => {
if (isAutoPilotSelected && maxAutoPilotThroughputSet) {
return {
maxThroughput: maxAutoPilotThroughputSet * 1,
};
}
return undefined;
};
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
const [databaseId, setDatabaseId] = useState<string>("");
const databaseIdTooltipText = `A ${
isCassandraAccount ? "keyspace" : "database"
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(getSharedThroughputDefault);
const [formErrorsDetails, setFormErrorsDetails] = useState<string>();
const [formErrors, setFormErrors] = useState<string>("");
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [throughput, setThroughput] = useState<number>(
isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const canRequestSupport = () => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = throughput;
return offerThroughput <= 100000;
}
return false;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const upsellMessage: string = PricingUtils.getUpsellMessage(
userContext.portalEnv,
isFreeTierAccount,
container.isFirstResourceCreated(),
false
);
const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details";
const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput;
const canConfigureThroughput = !container.isServerlessEnabled();
const showUpsellMessage = () => {
if (container.isServerlessEnabled()) {
return false;
}
if (isFreeTierAccount) {
return databaseCreateNewShared;
}
return true;
};
const [isExecuting, setIsExecuting] = useState<boolean>(false);
useEffect(() => {
setDatabaseCreateNewShared(getSharedThroughputDefault);
}, [subscriptionType]);
const addDatabasePaneMessage = {
database: {
id: databaseId,
shared: databaseCreateNewShared,
},
subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
useEffect(() => {
const addDatabasePaneOpenMessage = {
subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: throughput,
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
}, []);
const onSubmit = () => {
if (!_isValid()) {
return;
}
const offerThroughput: number = _computeOfferThroughput();
const addDatabasePaneStartMessage = {
...addDatabasePaneMessage,
offerThroughput,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
setFormErrors("");
setIsExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = {
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (isAutoPilotSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput;
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
() => {
_onCreateDatabaseSuccess(offerThroughput, startKey);
},
(error: string) => {
_onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
};
const _onCreateDatabaseSuccess = (offerThroughput: number, startKey: number): void => {
setIsExecuting(false);
closePanel();
container.refreshAllDatabases();
const addDatabasePaneSuccessMessage = {
...addDatabasePaneMessage,
offerThroughput,
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
};
const _onCreateDatabaseFailure = (error: string, offerThroughput: number, startKey: number): void => {
setIsExecuting(false);
const errorMessage = getErrorMessage(error);
setFormErrors(errorMessage);
setFormErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = {
...addDatabasePaneMessage,
offerThroughput,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
};
const _getThroughput = (): number => {
return isNaN(throughput) ? 0 : Number(throughput);
};
const _computeOfferThroughput = (): number => {
if (!canConfigureThroughput) {
return undefined;
}
return _getThroughput();
};
const _isValid = (): boolean => {
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoPilotSelected) {
const autoPilot = _isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = _getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
setFormErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = maxAutoPilotThroughputSet * 1;
if (
isAutoPilotSelected &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
return true;
};
const handleonChangeDBId = React.useCallback(
(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
setDatabaseId(newValue || "");
},
[]
);
const props: RightPaneFormProps = {
expandConsole: container.expandConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting,
submitButtonText: "OK",
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="paneContentContainer" role="dialog" aria-labelledby="databaseTitle">
{showUpsellMessage && formErrors === "" && (
<PanelInfoErrorComponent
message={upsellMessage}
messageType="info"
showErrorDetails={false}
openNotificationConsole={openNotificationConsole}
link={upsellAnchorUrl}
linkText={upsellAnchorText}
/>
)}
<div className="paneMainContent">
<div>
<p>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</p>
<TextField
id="database-id"
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}
value={databaseId}
onChange={handleonChangeDBId}
autoFocus
/>
<div
className="databaseProvision"
aria-label="New database provision support"
style={{ display: "block ruby" }}
>
<Checkbox
title="Provision shared throughput"
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>{" "}
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</div>
{databaseCreateNewShared && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isAutoscaleSelected={isAutoPilotSelected}
throughput={throughput}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
/>
{canRequestSupport() && (
<p>
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">
Contact support{" "}
</a>
for more than <span>{throughputDefaults.unlimitedmax?.toLocaleString()} </span> RU/s.
</p>
)}
</div>
)}
</div>
</div>
</div>
</RightPaneForm>
);
};

View File

@@ -1,104 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddDatabasePane Pane should render Default properly 1`] = `
<RightPaneForm
expandConsole={[Function]}
formError=""
isExecuting={false}
onSubmit={[Function]}
submitButtonText="OK"
>
<div
aria-labelledby="databaseTitle"
className="paneContentContainer"
role="dialog"
>
<PanelInfoErrorComponent
link="https://aka.ms/azure-cosmos-db-pricing"
linkText="More details"
message="Start at $24/mo per database, multiple containers included"
messageType="info"
openNotificationConsole={[Function]}
showErrorDetails={false}
/>
<div
className="paneMainContent"
>
<div>
<p>
<span
className="mandatoryStar"
>
*
</span>
<Text
variant="small"
>
Database id
</Text>
<InfoTooltip>
A database is a logical container of one or more collections
</InfoTooltip>
</p>
<StyledTextFieldBase
aria-label="Database id"
aria-required="true"
autoComplete="off"
autoFocus={true}
id="database-id"
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new database id"
size={40}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
type="text"
value=""
/>
<div
aria-label="New database provision support"
className="databaseProvision"
style={
Object {
"display": "block ruby",
}
}
>
<StyledCheckboxBase
checked={true}
label="Provision throughput"
onChange={[Function]}
styles={
Object {
"checkbox": Object {
"height": 12,
"width": 12,
},
"label": Object {
"alignItems": "center",
"padding": 0,
},
}
}
title="Provision shared throughput"
/>
<InfoTooltip>
Provisioned throughput at the database level will be shared across all collections within the database.
</InfoTooltip>
</div>
<div>
<ThroughputInput
isAutoscaleSelected={false}
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
throughput={400}
/>
</div>
</div>
</div>
</div>
</RightPaneForm>
`;

View File

@@ -1099,7 +1099,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<button
aria-label="Close pane"
className="ms-Button ms-Button--icon closePaneBtn root-102"
className="ms-Button ms-Button--icon closePaneBtn root-202"
data-is-focusable={true}
onClick={[Function]}
onKeyDown={[Function]}
@@ -1112,16 +1112,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="button"
>
<span
className="ms-Button-flexContainer flexContainer-103"
className="ms-Button-flexContainer flexContainer-203"
data-automationid="splitbuttonprimary"
>
<FontIcon
className="ms-Button-icon icon-105"
<Component
className="ms-Button-icon icon-205"
iconName="Cancel"
>
<i
aria-hidden={true}
className="ms-Icon root-37 css-110 ms-Button-icon icon-105"
className="ms-Icon root-37 css-210 ms-Button-icon icon-205"
data-icon-name="Cancel"
style={
Object {
@@ -1131,10 +1131,10 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>
@@ -1190,7 +1190,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small"
>
<span
className="css-111"
className="css-211"
>
Confirm by typing the
container
@@ -1494,18 +1494,18 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<div
className="ms-TextField root-113"
className="ms-TextField root-213"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-114"
className="ms-TextField-fieldGroup fieldGroup-214"
>
<input
aria-invalid={false}
autoFocus={true}
className="ms-TextField-field field-115"
className="ms-TextField-field field-215"
id="confirmCollectionId"
onBlur={[Function]}
onChange={[Function]}
@@ -1528,7 +1528,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small"
>
<span
className="css-124"
className="css-224"
>
Help us improve Azure Cosmos DB!
</span>
@@ -1538,7 +1538,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small"
>
<span
className="css-124"
className="css-224"
>
What is the reason why you are deleting this
container
@@ -1844,17 +1844,17 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<div
className="ms-TextField ms-TextField--multiline root-113"
className="ms-TextField ms-TextField--multiline root-213"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-125"
className="ms-TextField-fieldGroup fieldGroup-225"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-126"
className="ms-TextField-field field-226"
id="deleteCollectionFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
@@ -3616,7 +3616,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<button
aria-label="Submit"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-128"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-228"
data-is-focusable={true}
onClick={[Function]}
onKeyDown={[Function]}
@@ -3634,14 +3634,14 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="button"
>
<span
className="ms-Button-flexContainer flexContainer-103"
className="ms-Button-flexContainer flexContainer-203"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-104"
className="ms-Button-textContainer textContainer-204"
>
<span
className="ms-Button-label label-129"
className="ms-Button-label label-229"
id="id__9"
key="id__9"
>
@@ -3650,7 +3650,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -1106,7 +1106,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="ms-Button-flexContainer flexContainer-54"
data-automationid="splitbuttonprimary"
>
<FontIcon
<Component
className="ms-Button-icon icon-56"
iconName="Cancel"
>
@@ -1122,10 +1122,10 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>
@@ -8213,7 +8213,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -18,6 +18,10 @@ export interface GenericRightPaneProps {
children?: ReactNode;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
expandConsole,
formError,

View File

@@ -64,6 +64,88 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -130,8 +212,89 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -1049,7 +1212,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -1068,6 +1231,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],

View File

@@ -1,6 +1,9 @@
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
export class PaneComponent {
constructor(data: any) {
@@ -17,6 +20,15 @@ export class AddDatabasePaneComponent {
}
}
export class AddCollectionPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: AddCollectionPaneTemplate,
};
}
}
export class GraphStylingPaneComponent {
constructor() {
return {
@@ -26,6 +38,23 @@ export class GraphStylingPaneComponent {
}
}
export class TableAddEntityPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableAddEntityPaneTemplate,
};
}
}
export class TableEditEntityPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableEditEntityPaneTemplate,
};
}
}
export class CassandraAddCollectionPaneComponent {
constructor() {
return {

View File

@@ -1,14 +1,14 @@
import { PrimaryButton } from "@fluentui/react";
import React from "react";
import { PrimaryButton } from "@fluentui/react";
export interface PanelFooterProps {
buttonLabel: string;
}
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
buttonLabel,
}: PanelFooterProps): JSX.Element => (
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
props: PanelFooterProps
): JSX.Element => (
<div className="panelFooter">
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
<PrimaryButton type="submit" id="sidePanelOkButton" text={props.buttonLabel} />
</div>
);

View File

@@ -8,47 +8,38 @@ export interface PanelInfoErrorProps {
link?: string;
linkText?: string;
openNotificationConsole?: () => void;
formError?: boolean;
}
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({
message,
messageType,
showErrorDetails,
link,
linkText,
openNotificationConsole,
formError = true,
}: PanelInfoErrorProps): JSX.Element => {
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = (
props: PanelInfoErrorProps
): JSX.Element => {
let icon: JSX.Element;
if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />;
} else if (messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
if (props.messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" />;
} else if (props.messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" />;
} else if (props.messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" />;
}
return (
formError && (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
{message}
{link && linkText && (
<Link target="_blank" href={link}>
{linkText}
</Link>
)}
</Text>
{showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={openNotificationConsole}>
More details
</a>
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small">
{props.message}{" "}
{props.link && props.linkText && (
<Link target="_blank" href={props.link}>
{props.linkText}
</Link>
)}
</span>
</Stack>
)
</Text>
{props.showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
More details
</a>
)}
</span>
</Stack>
);
};

View File

@@ -1,40 +0,0 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { RightPaneForm } from "./RightPaneForm";
const onSubmit = jest.fn();
const expandConsole = jest.fn();
const props = {
expandConsole,
formError: "",
formErrorDetail: "",
isExecuting: false,
submitButtonText: "Load",
onSubmit,
};
describe("Right Pane Form", () => {
let wrapper: ReactWrapper;
it("should render Default properly", () => {
wrapper = mount(<RightPaneForm {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should call submit method enter in form", () => {
render(<RightPaneForm {...props} />);
fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should call submit method click on submit button", () => {
render(<RightPaneForm {...props} />);
fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should render error in header", () => {
render(<RightPaneForm {...props} formError="file already Exist" />);
expect(screen.getByLabelText("error")).toBeDefined();
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
});
});

View File

@@ -1,50 +0,0 @@
import React, { FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface RightPaneFormProps {
expandConsole: () => void;
formError: string;
formErrorDetail: string;
isExecuting: boolean;
onSubmit: () => void;
submitButtonText: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
expandConsole,
formError,
formErrorDetail,
isExecuting,
onSubmit,
submitButtonText,
isSubmitButtonHidden = false,
children,
}: RightPaneFormProps) => {
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit();
};
const panelInfoErrorProps: PanelInfoErrorProps = {
messageType: "error",
message: formError,
formError: formError !== "",
showErrorDetails: formErrorDetail !== "",
openNotificationConsole: expandConsole,
};
return (
<>
<PanelInfoErrorComponent {...panelInfoErrorProps} />
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
</form>
{isExecuting && <PanelLoadingScreen />}
</>
);
};

View File

@@ -1,13 +1,16 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { Tooltip } from "../../../Common/Tooltip/Tooltip";
import { configContext } from "../../../ConfigContext";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface SettingsPaneProps {
expandConsole: () => void;
@@ -102,12 +105,15 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
setGraphAutoVizDisabled(option.key);
};
const genericPaneProps: RightPaneFormProps = {
const genericPaneProps: GenericRightPaneProps = {
expandConsole,
formError: formErrors,
formErrorDetail: "",
id: "settingspane",
isExecuting,
title: "Setting",
submitButtonText: "Apply",
onClose: () => closePanel(),
onSubmit: () => handlerOnSubmit(undefined),
};
const pageOptionList: IChoiceGroupOption[] = [
@@ -124,17 +130,17 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
setPageOption(option.key);
};
return (
<RightPaneForm {...genericPaneProps}>
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<InfoTooltip>
<Tooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page.
</InfoTooltip>
</Tooltip>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</div>
@@ -143,7 +149,7 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="tabcontent">
<div className="settingsSectionLabel">
Query results per page
<InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
<Tooltip>Enter the number of query results that should be shown per page.</Tooltip>
</div>
<SpinButton
@@ -170,10 +176,10 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable cross-partition query
<InfoTooltip>
<Tooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
</Tooltip>
</div>
<Checkbox
@@ -193,11 +199,11 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Max degree of parallelism
<InfoTooltip>
<Tooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</Tooltip>
</div>
<SpinButton
@@ -221,10 +227,10 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<InfoTooltip>
<Tooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
JSON.
</InfoTooltip>
</Tooltip>
</div>
<ChoiceGroup
@@ -243,6 +249,6 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
</div>
</div>
</div>
</RightPaneForm>
</GenericRightPaneComponent>
);
};

View File

@@ -1,13 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Pane should render Default properly 1`] = `
<RightPaneForm
<GenericRightPaneComponent
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="settingspane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Apply"
title="Setting"
>
<div
className="paneMainContent"
@@ -22,9 +25,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Page options
<InfoTooltip>
<Tooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</InfoTooltip>
</Tooltip>
</div>
<StyledChoiceGroup
onChange={[Function]}
@@ -53,9 +56,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Query results per page
<InfoTooltip>
<Tooltip>
Enter the number of query results that should be shown per page.
</InfoTooltip>
</Tooltip>
</div>
<StyledSpinButton
ariaLabel="Custom query items per page"
@@ -82,9 +85,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Enable cross-partition query
<InfoTooltip>
<Tooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
</Tooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable cross partition query"
@@ -111,9 +114,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Max degree of parallelism
<InfoTooltip>
<Tooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</Tooltip>
</div>
<StyledSpinButton
ariaLabel="Max degree of parallelism"
@@ -145,17 +148,20 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
</div>
</div>
</RightPaneForm>
</GenericRightPaneComponent>
`;
exports[`Settings Pane should render Gremlin properly 1`] = `
<RightPaneForm
<GenericRightPaneComponent
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="settingspane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Apply"
title="Setting"
>
<div
className="paneMainContent"
@@ -170,9 +176,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="settingsSectionLabel"
>
Display Gremlin query results as: 
<InfoTooltip>
<Tooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
</InfoTooltip>
</Tooltip>
</div>
<StyledChoiceGroup
aria-label="Graph Auto-visualization"
@@ -208,5 +214,5 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</div>
</div>
</div>
</RightPaneForm>
</GenericRightPaneComponent>
`;

View File

@@ -1760,7 +1760,7 @@ exports[`Setup Notebooks Panel should render Default properly 1`] = `
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -54,6 +54,88 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -120,8 +202,89 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -1039,7 +1202,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -1058,6 +1221,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
@@ -2199,7 +2363,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<button
aria-label="Close pane"
className="ms-Button ms-Button--icon closePaneBtn root-53"
className="ms-Button ms-Button--icon closePaneBtn root-153"
data-is-focusable={true}
onClick={[Function]}
onKeyDown={[Function]}
@@ -2212,16 +2376,16 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="button"
>
<span
className="ms-Button-flexContainer flexContainer-54"
className="ms-Button-flexContainer flexContainer-154"
data-automationid="splitbuttonprimary"
>
<FontIcon
className="ms-Button-icon icon-56"
<Component
className="ms-Button-icon icon-156"
iconName="Cancel"
>
<i
aria-hidden={true}
className="ms-Icon root-37 css-61 ms-Button-icon icon-56"
className="ms-Icon root-37 css-161 ms-Button-icon icon-156"
data-icon-name="Cancel"
style={
Object {
@@ -2231,10 +2395,10 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>
@@ -2571,7 +2735,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
value=""
>
<div
className="ms-TextField is-required root-63"
className="ms-TextField is-required root-163"
>
<div
className="ms-TextField-wrapper"
@@ -2862,7 +3026,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
}
>
<label
className="ms-Label root-74"
className="ms-Label root-174"
htmlFor="TextField3"
id="TextFieldLabel5"
>
@@ -2871,13 +3035,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</LabelBase>
</StyledLabelBase>
<div
className="ms-TextField-fieldGroup fieldGroup-64"
className="ms-TextField-fieldGroup fieldGroup-164"
>
<input
aria-invalid={false}
aria-labelledby="TextFieldLabel5"
autoFocus={true}
className="ms-TextField-field field-65"
className="ms-TextField-field field-165"
id="TextField3"
name="collectionIdConfirmation"
onBlur={[Function]}
@@ -4639,7 +4803,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<button
aria-label="Submit"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-75"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-175"
data-is-focusable={true}
onClick={[Function]}
onKeyDown={[Function]}
@@ -4657,14 +4821,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="button"
>
<span
className="ms-Button-flexContainer flexContainer-54"
className="ms-Button-flexContainer flexContainer-154"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-55"
className="ms-Button-textContainer textContainer-155"
>
<span
className="ms-Button-label label-76"
className="ms-Button-label label-176"
id="id__6"
key="id__6"
>
@@ -4673,7 +4837,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -0,0 +1,21 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: close, clickBubble: false"></div>
<div class="contextual-pane" id="switchdirectorypane">
<!-- Switch Directory -- Start -->
<div class="contextual-pane-in">
<!-- Switch Directory 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: close">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Switch Directory header - End -->
<!-- Switch Directory content - Start -->
<div class="paneMainContent" data-bind="react: directoryComponentAdapter"></div>
<!-- Switch Directory content - Start -->
</div>
<!-- Switch Directory -- End -->
</div>
</div>

View File

@@ -0,0 +1,88 @@
import * as ko from "knockout";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { DirectoryListProps } from "../Controls/Directory/DirectoryListComponent";
import { DefaultDirectoryDropdownProps } from "../Controls/Directory/DefaultDirectoryDropdownComponent";
import { DirectoryComponentAdapter } from "../Controls/Directory/DirectoryComponentAdapter";
import SwitchDirectoryPaneTemplate from "./SwitchDirectoryPane.html";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
class PaneComponent {
constructor(data: any) {
return data.data;
}
}
export class SwitchDirectoryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SwitchDirectoryPaneTemplate,
};
}
}
export class SwitchDirectoryPane {
public id: string;
public firstFieldHasFocus: ko.Observable<boolean>;
public title: ko.Observable<string>;
public visible: ko.Observable<boolean>;
public directoryComponentAdapter: DirectoryComponentAdapter;
constructor(
dropdownProps: ko.Observable<DefaultDirectoryDropdownProps>,
listProps: ko.Observable<DirectoryListProps>
) {
this.id = "switchdirectorypane";
this.title = ko.observable("Switch directory");
this.visible = ko.observable(false);
this.firstFieldHasFocus = ko.observable(false);
this.resetData();
this.directoryComponentAdapter = new DirectoryComponentAdapter(dropdownProps, listProps);
}
public open() {
this.visible(true);
this.firstFieldHasFocus(true);
this.resizePane();
TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Open, {
paneTitle: this.title(),
});
this.directoryComponentAdapter.forceRender();
}
public close() {
this.visible(false);
this.resetData();
this.directoryComponentAdapter.forceRender();
}
public resetData() {
this.firstFieldHasFocus(false);
}
public onCloseKeyPress(source: any, event: KeyboardEvent): void {
if (event.key === " " || event.key === "Enter") {
this.close();
}
}
public onPaneKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.key === "Escape") {
this.close();
return false;
}
return true;
}
private resizePane(): void {
const paneElement: HTMLElement = document.getElementById(this.id);
const headerElement: HTMLElement = document.getElementsByTagName("header")[0];
const newPaneElementHeight = window.innerHeight - headerElement.offsetHeight;
paneElement.style.height = `${newPaneElementHeight}px`;
}
}

View File

@@ -0,0 +1,225 @@
import * as ko from "knockout";
import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
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;
visible: ko.Observable<boolean>;
public originEntity: Entities.ITableEntity;
public originalNumberOfProperties: number;
private originalDocument: any;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Update Entity");
if (userContext.apiType === "Cassandra") {
this.submitButtonText("Update Row");
}
this.scrollId = ko.observable<string>("editEntityScroll");
}
public submit() {
if (!this.canApply()) {
return;
}
let entity: Entities.ITableEntity = this.updateEntity(this.displayedAttributes());
this.container.tableDataClient
.updateDocument(this.tableViewModel.queryTablesTab.collection, this.originalDocument, entity)
.then((newEntity: Entities.ITableEntity) => {
var numberOfProperties = 0;
for (var property in newEntity) {
if (
property !== TableEntityProcessor.keyProperties.attachments &&
property !== TableEntityProcessor.keyProperties.etag &&
property !== TableEntityProcessor.keyProperties.resourceId &&
property !== TableEntityProcessor.keyProperties.self &&
(userContext.apiType !== "Cassandra" || property !== TableConstants.EntityKeyNames.RowKey)
) {
numberOfProperties++;
}
}
var propertiesDelta = numberOfProperties - this.originalNumberOfProperties;
return this.tableViewModel
.updateCachedEntity(newEntity)
.then(() => {
if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) {
this.tableViewModel.redrawTableThrottled();
}
})
.then(() => {
// Selecting updated entity
this.tableViewModel.selected.removeAll();
this.tableViewModel.selected.push(newEntity);
});
});
this.close();
}
public open() {
this.displayedAttributes(this.constructDisplayedAttributes(this.originEntity));
if (userContext.apiType === "Tables") {
this.originalDocument = TableEntityProcessor.convertEntitiesToDocuments(
[<Entities.ITableEntityForTablesAPI>this.originEntity],
this.tableViewModel.queryTablesTab.collection
)[0]; // TODO change for Cassandra
this.originalDocument.id = ko.observable<string>(this.originalDocument.id);
} else {
this.originalDocument = this.originEntity;
}
this.updateIsActionEnabled();
super.open();
}
private constructDisplayedAttributes(entity: Entities.ITableEntity): EntityPropertyViewModel[] {
var displayedAttributes: EntityPropertyViewModel[] = [];
const keys = Object.keys(entity);
keys &&
keys.forEach((key: string) => {
if (
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
var entityAttributeType: string = entityAttribute.$;
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
var removable: boolean = false;
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean =
entityAttributeType === TableConstants.CassandraType.Blob ||
entityAttributeType === TableConstants.CassandraType.Inet;
displayedAttributes.push(
new EntityPropertyViewModel(
this,
key,
entityAttributeType,
displayValue,
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
false,
/* default valid name */ true,
/* default valid value */ true,
/* isRequired */ false,
removable,
/*value editable*/ !_.contains<string>(cassandraKeys, key) && !nonEditableType
)
);
} else {
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
var entityAttributeType: string = entityAttribute.$;
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
var editable: boolean = this.isAttributeEditable(key, entityAttributeType);
// As per VSO:189935, Binary properties are read-only, we still want to be able to remove them.
var removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary;
displayedAttributes.push(
new EntityPropertyViewModel(
this,
key,
entityAttributeType,
displayValue,
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* isRequired */ false,
removable
)
);
}
}
});
if (userContext.apiType === "Cassandra") {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((properties: CassandraTableKey[]) => {
properties &&
properties.forEach((property) => {
if (!_.contains(keys, property.property)) {
this.insertAttribute(property.property, property.type);
}
});
});
}
return displayedAttributes;
}
private updateEntity(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
var updatedEntity: any = {};
displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if (attribute.name() && (userContext.apiType !== "Cassandra" || attribute.value() !== "")) {
var value = attribute.getPropertyValue();
var type = attribute.type();
if (type === TableConstants.TableType.Int64) {
value = Utilities.padLongWithZeros(value);
}
updatedEntity[attribute.name()] = {
_: value,
$: type,
};
}
});
return updatedEntity;
}
private isAttributeEditable(attributeName: string, entityAttributeType: string) {
return !(
attributeName === TableConstants.EntityKeyNames.PartitionKey ||
attributeName === TableConstants.EntityKeyNames.RowKey ||
attributeName === TableConstants.EntityKeyNames.Timestamp ||
// As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it.
entityAttributeType === TableConstants.TableType.Binary
);
}
private getPropertyDisplayValue(entity: Entities.ITableEntity, name: string, type: string): any {
var attribute: Entities.ITableEntityAttribute = entity[name];
var displayValue: any = attribute._;
var isBinary: boolean = type === TableConstants.TableType.Binary;
// Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects.
// This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array
// *base64 - encoded * as the value for the updated property or the whole update operation will fail.
if (isBinary && displayValue && $.isArray(displayValue.data)) {
var bytes: number[] = displayValue.data;
displayValue = this.getBase64DisplayValue(bytes);
}
return displayValue;
}
private getBase64DisplayValue(bytes: number[]): string {
var displayValue: string = null;
try {
var chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte));
var toEncode: string = chars.join("");
displayValue = window.btoa(toEncode);
} catch (error) {
// Error
}
return displayValue;
}
}

View File

@@ -0,0 +1,164 @@
import * as ko from "knockout";
import * as DateTimeUtilities from "../../Tables/QueryBuilder/DateTimeUtilities";
import * as EntityPropertyNameValidator from "./Validators/EntityPropertyNameValidator";
import EntityPropertyValueValidator from "./Validators/EntityPropertyValueValidator";
import * as Constants from "../../Tables/Constants";
import * as Utilities from "../../Tables/Utilities";
import TableEntityPane from "./TableEntityPane";
export interface IValidationResult {
isInvalid: boolean;
help: string;
}
export interface IActionEnabledDialog {
updateIsActionEnabled: () => void;
}
/**
* View model for an entity proprety
*/
export default class EntityPropertyViewModel {
/* Constants */
public static noTooltip = "";
// Maximum number of custom properties, see Azure Service Data Model
// At https://msdn.microsoft.com/library/azure/dd179338.aspx
public static maximumNumberOfProperties = 252;
// Labels
public closeButtonLabel: string = "Close"; // localize
/* Observables */
public name: ko.Observable<string>;
public type: ko.Observable<string>;
public value: ko.Observable<any>;
public inputType: ko.Computed<string>;
public nameTooltip: ko.Observable<string>;
public isInvalidName: ko.Observable<boolean>;
public valueTooltip: ko.Observable<string>;
public isInvalidValue: ko.Observable<boolean>;
public namePlaceholder: ko.Observable<string>;
public valuePlaceholder: ko.Observable<string>;
public hasFocus: ko.Observable<boolean>;
public valueHasFocus: ko.Observable<boolean>;
public isDateType: ko.Computed<boolean>;
public editable: boolean; // If a property's name or type is editable, these two are always the same regarding editability.
public valueEditable: boolean; // If a property's value is editable, could be different from name or type.
public removable: boolean; // If a property is removable, usually, PartitionKey, RowKey and TimeStamp (if applicable) are not removable.
public isRequired: boolean; // If a property's value is required, used to differentiate the place holder label.
public ignoreEmptyValue: boolean;
/* Members */
private tableEntityPane: TableEntityPane;
private _validator: EntityPropertyValueValidator;
constructor(
tableEntityPane: TableEntityPane,
name: string,
type: string,
value: any,
namePlaceholder: string = "",
valuePlaceholder: string = "",
editable: boolean = false,
defaultValidName: boolean = true,
defaultValidValue: boolean = false,
isRequired: boolean = false,
removable: boolean = editable,
valueEditable: boolean = editable,
ignoreEmptyValue: boolean = false
) {
this.name = ko.observable<string>(name);
this.type = ko.observable<string>(type);
this.isDateType = ko.pureComputed<boolean>(() => this.type() === Constants.TableType.DateTime);
if (this.isDateType()) {
value = value ? DateTimeUtilities.getLocalDateTime(value) : value;
}
this.value = ko.observable(value);
this.inputType = ko.pureComputed<string>(() => {
if (!this.valueHasFocus() && !this.value() && this.isDateType()) {
return Constants.InputType.Text;
}
return Utilities.getInputTypeFromDisplayedName(this.type());
});
this.namePlaceholder = ko.observable<string>(namePlaceholder);
this.valuePlaceholder = ko.observable<string>(valuePlaceholder);
this.editable = editable;
this.isRequired = isRequired;
this.removable = removable;
this.valueEditable = valueEditable;
this._validator = new EntityPropertyValueValidator(isRequired);
this.tableEntityPane = tableEntityPane;
this.nameTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
this.isInvalidName = ko.observable<boolean>(!defaultValidName);
this.name.subscribe((name: string) => this.validateName(name));
if (!defaultValidName) {
this.validateName(name);
}
this.valueTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
this.isInvalidValue = ko.observable<boolean>(!defaultValidValue);
this.value.subscribe((value: string) => this.validateValue(value, this.type()));
if (!defaultValidValue) {
this.validateValue(value, type);
}
this.type.subscribe((type: string) => this.validateValue(this.value(), type));
this.hasFocus = ko.observable<boolean>(false);
this.valueHasFocus = ko.observable<boolean>(false);
}
/**
* Gets the Javascript value of the entity property based on its EDM type.
*/
public getPropertyValue(): any {
var value: string = this.value();
if (this.type() === Constants.TableType.DateTime) {
value = DateTimeUtilities.getUTCDateTime(value);
}
return this._validator.parseValue(value, this.type());
}
private validateName(name: string): void {
var result: IValidationResult = this.isInvalidNameInput(name);
this.isInvalidName(result.isInvalid);
this.nameTooltip(result.help);
this.namePlaceholder(result.help);
this.tableEntityPane.updateIsActionEnabled();
}
private validateValue(value: string, type: string): void {
var result: IValidationResult = this.isInvalidValueInput(value, type);
if (!result) {
return;
}
this.isInvalidValue(result.isInvalid);
this.valueTooltip(result.help);
this.valuePlaceholder(result.help);
this.tableEntityPane.updateIsActionEnabled();
}
private isInvalidNameInput(name: string): IValidationResult {
return EntityPropertyNameValidator.validate(name);
}
private isInvalidValueInput(value: string, type: string): IValidationResult {
if (this.ignoreEmptyValue && this.value() === "") {
return { isInvalid: false, help: "" };
}
return this._validator.validate(value, type);
}
}

View File

@@ -0,0 +1,190 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" style="width: 700px" id="addtableentitypane">
<!-- Add table entity form - Start -->
<div
class="contextual-pane-in"
data-bind="
visible: !isEditing()"
>
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Add table entity header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
id="closeAddEntityPane"
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>
<!-- Add table entity header - End -->
<div class="tableParamContent paneContentContainer">
<div class="entity-table">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
</div>
</div>
<div class="entity-table-scroll-box" id="addEntityScroll">
<div class="entity-table" data-bind="foreach: displayedAttributes">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-column">
<input
type="text"
class="entity-table-field entity-table-property-column"
required
data-bind="
textInput: name,
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
css: { 'invalid-field': isInvalidName },
readOnly: !editable,
hasFocus: hasFocus"
/>
</div>
<div class="entity-table-cell entity-table-type-column">
<select
class="entity-table-field"
data-bind="
options: $parent.edmTypes,
optionsAfterRender: $parent.setOptionDisable,
value: type,
attr: { 'aria-label': 'type' },
enable: editable,
readOnly: !editable"
></select>
</div>
<!-- ko ifnot: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
id="addTableEntityValue"
step="1"
data-bind="
textInput: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable"
/>
</div>
<!-- /ko -->
<!-- ko if: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
value: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable,
hasFocus: valueHasFocus"
autocomplete="off"
/>
</div>
<!-- /ko -->
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
<span
class="entity-Edit-Cancel"
title="Edit property"
role="button"
aria-label="Edit property"
tabindex="0"
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
>
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit" />
</span>
<span
class="entity-Edit-Cancel"
title="Delete property"
role="button"
aria-label="Delete property"
tabindex="0"
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
>
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Cancel" />
</span>
</div>
</div>
</div>
<div class="entity-table addProperty">
<div class="entity-table-row">
<div class="entity-table-cell">
<span
class="commandButton"
id="addProperty"
role="button"
aria-label="Add property"
tabindex="0"
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
autofocus
>
<img class="addPropertyImg" src="/Add-property.svg" alt="Insert attribute" />
<span data-bind="text: addButtonLabel"> </span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
class="btncreatecoll1"
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
/>
</div>
</div>
</form>
</div>
<!-- Add table entity form - End -->
<!-- Add table entity editor - Start -->
<div id="editor-panel-addEntity" data-bind="visible: isEditing()" style="display: none">
<div data-bind="with: editingProperty()">
<!-- Add table entity editor header - Start -->
<div class="firstdivbg headerline">
<span
class="backBtn"
aria-label="Back"
role="button"
tabindex="0"
data-bind="
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
>
<img src="/RevertBack.svg" alt="BackIcon" />
</span>
<span class="edit-value-text" data-bind="text: name"></span>
</div>
<!-- Add table entity editor header - End -->
<div class="seconddivbg paddingspan2">
<textarea
class="entity-editor-expanded"
id="textAreaEditProperty"
tabindex="0"
rows="21"
data-bind="value: value, attr: { 'aria-label': name }"
style="width: 95%"
autofocus
></textarea>
</div>
</div>
</div>
<!-- Add table entity editor - End -->
</div>
</div>

View File

@@ -0,0 +1,187 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" style="width: 700px" id="edittableentitypane">
<!-- Edit table entity form - Start -->
<div
class="contextual-pane-in"
data-bind="
visible: !isEditing()"
>
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Edit table entity 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: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Edit table entity header - End -->
<div class="tableParamContent paneContentContainer">
<div class="entity-table">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
</div>
</div>
<div class="entity-table-scroll-box" id="editEntityScroll">
<div class="entity-table" data-bind="foreach: displayedAttributes">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-column">
<input
type="text"
class="entity-table-field entity-table-property-column"
required
data-bind="
textInput: name,
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
css: { 'invalid-field': isInvalidName },
readOnly: !editable,
hasFocus: hasFocus"
/>
</div>
<div class="entity-table-cell entity-table-type-column">
<select
class="entity-table-field"
data-bind="
options: $parent.edmTypes,
optionsAfterRender: $parent.setOptionDisable,
value: type,
attr: { 'aria-label': 'type' },
enable: editable,
readOnly: !editable"
></select>
</div>
<!-- ko ifnot: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
textInput: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable"
/>
</div>
<!-- /ko -->
<!-- ko if: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
value: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable,
hasFocus: valueHasFocus"
autocomplete="off"
/>
</div>
<!-- /ko -->
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
<span
class="entity-Edit-Cancel"
role="button"
aria-label="Edit property"
tabindex="0"
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
title="Edit property"
>
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit attribute" />
</span>
<span
class="entity-Edit-Cancel"
role="button"
aria-label="Delete property"
tabindex="0"
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
title="Delete property"
>
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Remove attribute" />
</span>
</div>
</div>
</div>
<div class="entity-table addProperty">
<div class="entity-table-row">
<div class="entity-table-cell">
<span
class="commandButton"
role="button"
aria-label="Add property"
tabindex="0"
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
autofocus
>
<img class="addPropertyImg" src="/Add-property.svg" alt="Add attribute" />
<span data-bind="text: addButtonLabel"> </span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
value="Update Entity"
class="btncreatecoll1"
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
/>
</div>
</div>
</form>
</div>
<!-- Edit table entity form - End -->
<!-- Edit table entity editor - Start -->
<div id="editor-panel-editEntity" data-bind="visible: isEditing()" style="display: none">
<div data-bind="with: editingProperty()">
<!-- Edit table entity editor header - Start -->
<div class="firstdivbg headerline">
<span
class="backBtn"
aria-label="Back"
role="button"
tabindex="0"
data-bind="
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
>
<img src="/RevertBack.svg" alt="BackIcon" />
</span>
<span class="edit-value-text" data-bind="text: name"></span>
</div>
<!-- Edit table entity editor header - End -->
<div class="seconddivbg paddingspan2">
<textarea
class="entity-editor-expanded"
id="editor-area"
tabindex="0"
rows="21"
data-bind="value: value, attr: { 'aria-label': name }"
autofocus
></textarea>
</div>
</div>
</div>
<!-- Edit table entity editor - End -->
</div>
</div>

View File

@@ -0,0 +1,279 @@
import * as ko from "knockout";
import _ from "underscore";
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 { 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 {
protected static requiredFieldsForTablesAPI: string[] = [
TableConstants.EntityKeyNames.PartitionKey,
TableConstants.EntityKeyNames.RowKey,
];
/* Labels */
public attributeNameLabel = "Property Name"; // localize
public dataTypeLabel = "Type"; // localize
public attributeValueLabel = "Value"; // localize
/* Controls */
public removeButtonLabel = "Remove"; // localize
public editButtonLabel = "Edit"; // localize
public addButtonLabel = "Add Property"; // localize
public edmTypes: ko.ObservableArray<string> = ko.observableArray([
TableConstants.TableType.String,
TableConstants.TableType.Boolean,
TableConstants.TableType.Binary,
TableConstants.TableType.DateTime,
TableConstants.TableType.Double,
TableConstants.TableType.Guid,
TableConstants.TableType.Int32,
TableConstants.TableType.Int64,
]);
public canAdd: ko.Computed<boolean>;
public canApply: ko.Observable<boolean>;
public displayedAttributes = ko.observableArray<EntityPropertyViewModel>();
public editingProperty = ko.observable<EntityPropertyViewModel>();
public isEditing = ko.observable<boolean>(false);
public submitButtonText = ko.observable<string>();
public tableViewModel: TableEntityListViewModel;
protected scrollId: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
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 (userContext.apiType === "Cassandra") {
return false;
}
// Adding '2' to the maximum to take into account PartitionKey and RowKey
return this.displayedAttributes().length < EntityPropertyViewModel.maximumNumberOfProperties + 2;
});
this.canApply = ko.observable<boolean>(true);
this.editingProperty(this.displayedAttributes()[0]);
}
public removeAttribute = (index: number, data: any): void => {
this.displayedAttributes.splice(index, 1);
this.updateIsActionEnabled();
document.getElementById("addProperty").focus();
};
public editAttribute = (index: number, data: EntityPropertyViewModel): void => {
this.editingProperty(data);
this.isEditing(true);
document.getElementById("textAreaEditProperty").focus();
};
public finishEditingAttribute = (): void => {
this.isEditing(false);
this.editingProperty(null);
};
public onKeyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = Utilities.onEsc(event, ($sourceElement: JQuery) => {
this.finishEditingAttribute();
});
return !handled;
};
public onAddPropertyKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.insertAttribute();
event.stopPropagation();
return false;
}
return true;
};
public onEditPropertyKeyDown = (
index: number,
data: EntityPropertyViewModel,
event: KeyboardEvent,
source: any
): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.editAttribute(index, data);
event.stopPropagation();
return false;
}
return true;
};
public onDeletePropertyKeyDown = (
index: number,
data: EntityPropertyViewModel,
event: KeyboardEvent,
source: any
): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.removeAttribute(index, data);
event.stopPropagation();
return false;
}
return true;
};
public onBackButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.finishEditingAttribute();
event.stopPropagation();
return false;
}
return true;
};
public insertAttribute = (name?: string, type?: string): void => {
let entityProperty: EntityPropertyViewModel;
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;
entityProperty = new EntityPropertyViewModel(
this,
name,
type,
"", // default to empty string
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
/* editable */ false,
/* default valid name */ false,
/* default valid value */ true,
/* isRequired */ false,
/* removable */ false,
/*value editable*/ !nonEditableType
);
} else {
entityProperty = new EntityPropertyViewModel(
this,
"",
this.edmTypes()[0], // default to the first Edm type: 'string'
"", // default to empty string
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
/* editable */ true,
/* default valid name */ false,
/* default valid value */ true
);
}
this.displayedAttributes.push(entityProperty);
this.updateIsActionEnabled();
this.scrollToBottom();
entityProperty.hasFocus(true);
};
public updateIsActionEnabled(needRequiredFields: boolean = true): void {
var properties: EntityPropertyViewModel[] = this.displayedAttributes() || [];
var disable: boolean = _.some(properties, (property: EntityPropertyViewModel) => {
return property.isInvalidName() || property.isInvalidValue();
});
this.canApply(!disable);
}
protected entityFromAttributes(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
var entity: any = {};
displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if (attribute.name() && (attribute.value() !== "" || attribute.isRequired)) {
var value = attribute.getPropertyValue();
var type = attribute.type();
if (type === TableConstants.TableType.Int64) {
value = Utilities.padLongWithZeros(value);
}
entity[attribute.name()] = {
_: value,
$: type,
};
}
});
return entity;
}
// Removing Binary from Add Entity dialog until we have a full story for it.
protected setOptionDisable(option: Node, value: string): void {
ko.applyBindingsToNode(option, { disable: value === TableConstants.TableType.Binary }, value);
}
/**
* Parse the updated entity to see if there are any new attributes that old headers don't have.
* In this case, add these attributes names as new headers.
* Remarks: adding new headers will automatically trigger table redraw.
*/
protected tryInsertNewHeaders(viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean {
var newHeaders: string[] = [];
const keys = Object.keys(newEntity);
keys &&
keys.forEach((key: string) => {
if (
!_.contains(viewModel.headers, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) {
newHeaders.push(key);
}
});
var newHeadersInserted: boolean = false;
if (newHeaders.length) {
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
newHeaders = viewModel.headers.concat(newHeaders);
}
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
newHeadersInserted = true;
}
return newHeadersInserted;
}
protected scrollToBottom(): void {
var scrollBox = document.getElementById(this.scrollId());
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
if (isScrolledToBottom) {
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
}
}
}

View File

@@ -1110,7 +1110,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
className="ms-Button-flexContainer flexContainer-54"
data-automationid="splitbuttonprimary"
>
<FontIcon
<Component
className="ms-Button-icon icon-56"
iconName="Cancel"
>
@@ -1126,10 +1126,10 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>
@@ -4180,7 +4180,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -2663,7 +2663,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
className="ms-Button-flexContainer flexContainer-73"
data-automationid="splitbuttonprimary"
>
<FontIcon
<Component
className="ms-Button-icon icon-75"
iconName="Cancel"
>
@@ -2679,10 +2679,10 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>

View File

@@ -2664,7 +2664,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
className="ms-Button-flexContainer flexContainer-73"
data-automationid="splitbuttonprimary"
>
<FontIcon
<Component
className="ms-Button-icon icon-75"
iconName="Cancel"
>
@@ -2680,10 +2680,10 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
</i>
</FontIcon>
</Component>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</IconButton>
</CustomizedIconButton>

View File

@@ -2,7 +2,10 @@ import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface UploadFilePanelProps {
expandConsole: () => void;
@@ -15,6 +18,9 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
closePanel,
uploadFile,
}: UploadFilePanelProps) => {
const title = "Upload file to notebook server";
const submitButtonLabel = "Upload";
const selectFileInputLabel = "Select file to upload";
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
@@ -36,8 +42,11 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
}
const file: File = files.item(0);
// const id: string = logConsoleProgress(
// `${inProgressMessage}: ${file.name}`
// );
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
@@ -55,7 +64,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
)
.finally(() => {
setIsExecuting(false);
clearMessage();
// clearInProgressMessageWithId(id);
});
};
@@ -82,20 +91,23 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
return uploadFile(file.name, fileContent);
};
const genericPaneProps: RightPaneFormProps = {
const genericPaneProps: GenericRightPaneProps = {
expandConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "uploadFilePane",
isExecuting: isExecuting,
submitButtonText: "Upload",
title,
submitButtonText: submitButtonLabel,
onClose: closePanel,
onSubmit: submit,
};
return (
<RightPaneForm {...genericPaneProps}>
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
<Upload label={selectFileInputLabel} accept={extensions} onUpload={updateSelectedFiles} />
</div>
</RightPaneForm>
</GenericRightPaneComponent>
);
};

View File

@@ -2,16 +2,34 @@ import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@flu
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface UploadItemsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
const getTitle = (): string => {
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
return "Upload Tables";
} else if (userContext.apiType === "Gremlin") {
return "Upload Graph";
} else {
return "Upload Items";
}
};
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
explorer,
closePanel,
}: UploadItemsPaneProps) => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@@ -52,12 +70,15 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
setFiles(event.target.files);
};
const genericPaneProps: RightPaneFormProps = {
const genericPaneProps: GenericRightPaneProps = {
expandConsole: () => explorer.expandConsole(),
formError,
formErrorDetail,
id: "uploaditemspane",
isExecuting: isExecuting,
title: getTitle(),
submitButtonText: "Upload",
onClose: closePanel,
onSubmit,
};
@@ -92,7 +113,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
};
return (
<RightPaneForm {...genericPaneProps}>
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
<Upload
label="Select JSON Files"
@@ -118,6 +139,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
</div>
)}
</div>
</RightPaneForm>
</GenericRightPaneComponent>
);
};

View File

@@ -1,12 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Upload Items Pane should render Default properly 1`] = `
<RightPaneForm
<GenericRightPaneComponent
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="uploaditemspane"
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Upload"
title="Upload Items"
>
<div
className="paneMainContent"
@@ -20,5 +23,5 @@ exports[`Upload Items Pane should render Default properly 1`] = `
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/>
</div>
</RightPaneForm>
</GenericRightPaneComponent>
`;

View File

@@ -52,6 +52,88 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -118,8 +200,89 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -1040,7 +1203,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isSelectedDatabaseShared": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSparkEnabledForAccount": undefined,
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"junoClient": JunoClient {
@@ -1060,6 +1223,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
},
"refreshAllDatabases": [Function],
"refreshNotebookList": [Function],
"refreshSparkEnabledStateForAccount": undefined,
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
@@ -1125,16 +1289,14 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
verticalAlign="center"
>
<div
className="ms-Stack panelInfoErrorContainer css-102"
className="ms-Stack panelInfoErrorContainer css-202"
>
<StyledIconBase
aria-label="warning"
className="panelWarningIcon"
iconName="WarningSolid"
key=".0:$.0"
>
<IconBase
aria-label="warning"
className="panelWarningIcon"
iconName="WarningSolid"
styles={[Function]}
@@ -1413,10 +1575,9 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
}
>
<i
aria-label="warning"
className="panelWarningIcon root-104"
aria-hidden={true}
className="panelWarningIcon root-204"
data-icon-name="WarningSolid"
role="img"
>
</i>
@@ -1427,15 +1588,14 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
key=".0:$.1"
>
<Text
aria-label="message"
className="panelWarningErrorMessage"
variant="small"
>
<span
aria-label="message"
className="panelWarningErrorMessage css-105"
className="panelWarningErrorMessage css-205"
>
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span>
</Text>
</span>
@@ -1457,7 +1617,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-105"
className="css-205"
>
Confirm by typing the database id
</span>
@@ -1757,18 +1917,18 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true}
>
<div
className="ms-TextField root-107"
className="ms-TextField root-207"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-108"
className="ms-TextField-fieldGroup fieldGroup-208"
>
<input
aria-invalid={false}
autoFocus={true}
className="ms-TextField-field field-109"
className="ms-TextField-field field-209"
id="confirmDatabaseId"
onBlur={[Function]}
onChange={[Function]}
@@ -1791,7 +1951,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-126"
className="css-226"
>
Help us improve Azure Cosmos DB!
</span>
@@ -1801,7 +1961,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-126"
className="css-226"
>
What is the reason why you are deleting this database?
</span>
@@ -2103,17 +2263,17 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true}
>
<div
className="ms-TextField ms-TextField--multiline root-107"
className="ms-TextField ms-TextField--multiline root-207"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-127"
className="ms-TextField-fieldGroup fieldGroup-227"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-128"
className="ms-TextField-field field-228"
id="deleteDatabaseFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
@@ -2136,13 +2296,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
id="sidePanelOkButton"
text="OK"
theme={
@@ -2421,7 +2579,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<CustomizedDefaultButton
ariaLabel="OK"
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -2702,7 +2859,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<DefaultButton
ariaLabel="OK"
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -2983,7 +3139,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -3838,8 +3993,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variantClassName="ms-Button--primary"
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-118"
className="ms-Button ms-Button--primary root-218"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@@ -3851,14 +4005,14 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-119"
className="ms-Button-flexContainer flexContainer-219"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-120"
className="ms-Button-textContainer textContainer-220"
>
<span
className="ms-Button-label label-122"
className="ms-Button-label label-222"
id="id__3"
key="id__3"
>
@@ -3867,7 +4021,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
</span>
</span>
</button>
<FocusRects />
<Component />
</BaseButton>
</DefaultButton>
</CustomizedDefaultButton>

View File

@@ -292,7 +292,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: AddDatabaseIcon,
title: this.container.addDatabaseText(),
description: null,
onClick: () => this.container.openAddDatabasePane(),
onClick: () => this.container.addDatabasePane.open(),
});
}

View File

@@ -183,13 +183,11 @@ export function convertEntityToNewDocument(entity: Entities.ITableEntityForTable
parsedValue = DateTimeUtilities.convertJSDateToTicksWithPadding(propertyValue);
break;
case Constants.TableType.Boolean:
parsedValue = propertyValue.toString().toLowerCase() === "true";
parsedValue = propertyValue.toLowerCase() === "true";
break;
case Constants.TableType.Int32:
parsedValue = parseInt(propertyValue, 10);
break;
case Constants.TableType.Int64:
parsedValue = propertyValue.toString();
parsedValue = parseInt(propertyValue, 10);
break;
case Constants.TableType.Double:
parsedValue = parseFloat(propertyValue);

View File

@@ -0,0 +1,73 @@
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import {
NotebookViewerComponent,
NotebookViewerComponentProps,
} from "../Controls/NotebookViewer/NotebookViewerComponent";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase";
interface NotebookViewerTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount;
container: Explorer;
notebookUrl: string;
}
/**
* Notebook Viewer tab
*/
class NotebookViewerComponentAdapter implements ReactAdapter {
// parameters: true: show, false: hide
public parameters: ko.Computed<boolean>;
constructor(private notebookUrl: string) {}
public renderComponent(): JSX.Element {
const props: NotebookViewerComponentProps = {
notebookUrl: this.notebookUrl,
backNavigationText: undefined,
onBackClick: undefined,
onTagClick: undefined,
};
return this.parameters() ? <NotebookViewerComponent {...props} /> : <></>;
}
}
export default class NotebookViewerTab extends TabsBase {
public readonly html = '<div style="height: 100%" data-bind="react:notebookViewerComponentAdapter"></div>';
private container: Explorer;
public notebookUrl: string;
public notebookViewerComponentAdapter: NotebookViewerComponentAdapter;
constructor(options: NotebookViewerTabOptions) {
super(options);
this.container = options.container;
this.notebookUrl = options.notebookUrl;
this.notebookViewerComponentAdapter = new NotebookViewerComponentAdapter(options.notebookUrl);
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
if (this.isTemplateReady() && this.container.isNotebookEnabled()) {
return true;
}
return false;
});
}
public getContainer(): Explorer {
return this.container;
}
protected getTabsButtons(): CommandButtonComponentProps[] {
return [];
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
}

View File

@@ -28,6 +28,11 @@ import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less";
import { AuthType } from "./AuthType";
import { AfecFeatures, ArmApiVersions } from "./Common/Constants";
import { getErrorMessage } from "./Common/ErrorHandlingUtils";
import { logError } from "./Common/Logger";
import { configContext } from "./ConfigContext";
import { AfecFeature } from "./Contracts/DataModels";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog";
@@ -35,6 +40,7 @@ import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/ThroughputInput/ThroughputInput.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
@@ -53,6 +59,7 @@ import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel";
import { useTabs } from "./hooks/useTabs";
import "./Libs/jquery";
import { ResourceProviderClientFactory } from "./ResourceProvider/ResourceProviderClientFactory";
import "./Shared/appInsights";
import { userContext } from "./UserContext";
@@ -66,6 +73,36 @@ const App: React.FunctionComponent = () => {
const [dialogProps, setDialogProps] = useState<DialogProps>();
const [showDialog, setShowDialog] = useState<boolean>(false);
const [isSparkEnabledForAccount, setIsSparkEnabledForAccount] = useState<boolean>(false);
const refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT;
const authType = userContext.authType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet
setIsSparkEnabledForAccount(false);
return;
}
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${AfecFeatures.Spark}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try {
const sparkNotebooksFeature: AfecFeature = await resourceProviderClient.getAsync(
featureUri,
ArmApiVersions.armFeatures
);
const isEnabled =
(sparkNotebooksFeature &&
sparkNotebooksFeature.properties &&
sparkNotebooksFeature.properties.state === "Registered") ||
false;
setIsSparkEnabledForAccount(isEnabled);
} catch (error) {
logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
setIsSparkEnabledForAccount(false);
}
};
const openDialog = (props: DialogProps) => {
setDialogProps(props);
@@ -87,6 +124,8 @@ const App: React.FunctionComponent = () => {
openDialog,
closeDialog,
tabsManager,
refreshSparkEnabledStateForAccount,
isSparkEnabledForAccount,
};
const config = useConfig();
@@ -227,6 +266,7 @@ const App: React.FunctionComponent = () => {
isConsoleExpanded={isNotificationConsoleExpanded}
/>
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
{showDialog && <Dialog {...dialogProps} />}

View File

@@ -1,3 +1,4 @@
import { TFunction } from "i18next";
import {
CommandBar,
ICommandBarItemProps,
@@ -9,7 +10,6 @@ import {
Stack,
Text,
} from "@fluentui/react";
import { TFunction } from "i18next";
import promiseRetry, { AbortError } from "p-retry";
import React from "react";
import { WithTranslation } from "react-i18next";
@@ -130,7 +130,10 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
const initialValues = await this.props.descriptor.initialize();
this.props.descriptor.inputNames.map((inputName) => {
const initialValue = initialValues.get(inputName);
let initialValue = initialValues.get(inputName);
if (!initialValue) {
initialValue = { value: undefined, hidden: false, disabled: false };
}
currentValues = currentValues.set(inputName, initialValue);
baselineValues = baselineValues.set(inputName, initialValue);
initialValues.delete(inputName);

View File

@@ -34,7 +34,6 @@ export enum BladeType {
CassandraKeys = "cassandraDbKeys",
GremlinKeys = "keys",
TableKeys = "tableKeys",
Metrics = "metrics",
}
export interface DecoratorProperties {

View File

@@ -28,30 +28,3 @@ export const getCollectionName = (isPlural?: boolean): string => {
return collectionName;
};
export const getDatabaseName = (): string => {
const { apiType } = userContext;
switch (apiType) {
case "SQL":
case "Mongo":
case "Gremlin":
case "Tables":
return "Database";
case "Cassandra":
return "Keyspace";
default:
throw new Error(`Unknown API type: ${apiType}`);
}
};
export const getUploadName = (): string => {
switch (userContext.apiType) {
case "Cassandra":
case "Tables":
return "Tables";
case "Gremlin":
return "Graph";
default:
return "Items";
}
};

View File

@@ -1,4 +0,0 @@
import { userContext } from "../UserContext";
export const isCapabilityEnabled = (capabilityName: string): boolean =>
userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName);

View File

@@ -23,7 +23,7 @@ test("Mongo CRUD", async () => {
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
// Create indexing policy
await safeClick(explorer, ".nodeItem >> text=Setting");
await safeClick(explorer, ".nodeItem >> text=Settings");
await explorer.click('button[role="tab"]:has-text("Indexing Policy")');
await explorer.click('[aria-label="Index Field Name 0"]');
await explorer.fill('[aria-label="Index Field Name 0"]', "foo");

View File

@@ -19,7 +19,7 @@ test("Notebooks", async () => {
await explorer.click('[data-test="My Notebooks"] [aria-label="More"]');
await explorer.click('button[role="menuitem"]:has-text("Upload File")');
await explorer.setInputFiles("#importFileInput", path.join(__dirname, fileToUpload));
await explorer.click('[aria-label="Upload"]');
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${fileToUpload}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete")');
await explorer.click('button:has-text("Delete")');

View File

@@ -3,7 +3,6 @@
"allowJs": true,
"sourceMap": false,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false,
@@ -15,24 +14,13 @@
"target": "es2017",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": [
"es5",
"es6",
"dom"
],
"lib": ["es5", "es6", "dom"],
"jsx": "react",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"types": [
"jest"
]
"types": ["jest"]
},
"include": [
"./src/**/*",
"./utils/**/*"
],
"exclude": [
"./src/**/__mocks__/**/*"
]
}
"include": ["./src/**/*", "./utils/**/*"],
"exclude": ["./src/**/__mocks__/**/*"]
}

View File

@@ -43,7 +43,6 @@
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts",
"./src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts",
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
"./src/Explorer/LazyMonaco.ts",
"./src/Explorer/Notebook/FileSystemUtil.ts",
"./src/Explorer/Notebook/NTeractUtil.ts",
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
@@ -55,8 +54,6 @@
"./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx",
"./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx",
"./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/OpenFullScreen.test.tsx",
"./src/Explorer/OpenFullScreen.tsx",
"./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/PanelFooterComponent.tsx",
"./src/Explorer/Panes/PanelLoadingScreen.tsx",
@@ -73,7 +70,6 @@
"./src/HostedExplorerChildFrame.ts",
"./src/Index.ts",
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
"./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/SignInButton.tsx",
"./src/Platform/Hosted/extractFeatures.test.ts",
"./src/Platform/Hosted/extractFeatures.ts",
@@ -91,7 +87,6 @@
"./src/Shared/StringUtility.ts",
"./src/Shared/appInsights.ts",
"./src/UserContext.ts",
"./src/Utils/APITypeUtils.ts",
"./src/Utils/AutoPilotUtils.ts",
"./src/Utils/Base64Utils.test.ts",
"./src/Utils/Base64Utils.ts",
@@ -102,19 +97,15 @@
"./src/Utils/MessageValidation.ts",
"./src/Utils/PricingUtils.ts",
"./src/Utils/StringUtils.ts",
"./src/Utils/StyleUtils.ts",
"./src/Utils/WindowUtils.test.ts",
"./src/Utils/WindowUtils.ts",
"./src/hooks/useDirectories.tsx",
"./src/hooks/useFullScreenURLs.tsx",
"./src/hooks/useObservable.ts",
"./src/i18n.ts",
"./src/quickstart.ts",
"./src/setupTests.ts",
"./src/userContext.test.ts"
],
"include": [
"src/CellOutputViewer/transforms/**/*",
"src/Controls/**/*",
"src/Definitions/**/*",
"src/Explorer/Controls/ErrorDisplayComponent/**/*",