2021-03-19 01:06:13 +00:00
import {
ActionButton ,
Checkbox ,
DefaultButton ,
DirectionalHint ,
Dropdown ,
Icon ,
IconButton ,
IDropdownOption ,
Link ,
2021-04-30 18:23:34 +01:00
Separator ,
2021-03-19 01:06:13 +00:00
Stack ,
Text ,
TooltipHost ,
2021-05-06 00:26:03 +01:00
} from "@fluentui/react" ;
2021-10-12 15:38:34 +01:00
import * as Constants from "Common/Constants" ;
import { createCollection } from "Common/dataAccess/createCollection" ;
import { getErrorMessage , getErrorStack } from "Common/ErrorHandlingUtils" ;
import { configContext , Platform } from "ConfigContext" ;
import * as DataModels from "Contracts/DataModels" ;
import { SubscriptionType } from "Contracts/SubscriptionType" ;
import { useSidePanel } from "hooks/useSidePanel" ;
2021-03-19 01:06:13 +00:00
import React from "react" ;
2021-10-12 15:38:34 +01:00
import { CollectionCreation } from "Shared/Constants" ;
import { Action } from "Shared/Telemetry/TelemetryConstants" ;
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor" ;
import { userContext } from "UserContext" ;
import { getCollectionName } from "Utils/APITypeUtils" ;
import { isCapabilityEnabled , isServerlessAccount } from "Utils/CapabilityUtils" ;
import { getUpsellMessage } from "Utils/PricingUtils" ;
2021-03-19 01:06:13 +00:00
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent" ;
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput" ;
import Explorer from "../Explorer" ;
2021-06-18 19:25:08 +01:00
import { useDatabases } from "../useDatabases" ;
2021-03-19 01:06:13 +00:00
import { PanelFooterComponent } from "./PanelFooterComponent" ;
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent" ;
import { PanelLoadingScreen } from "./PanelLoadingScreen" ;
export interface AddCollectionPanelProps {
explorer : Explorer ;
2021-04-30 18:23:34 +01:00
databaseId? : string ;
2021-03-19 01:06:13 +00:00
}
2021-05-21 02:34:29 +01:00
const SharedDatabaseDefault : DataModels.IndexingPolicy = {
indexingMode : "consistent" ,
automatic : true ,
includedPaths : [ ] ,
excludedPaths : [
{
path : "/*" ,
} ,
] ,
} ;
const AllPropertiesIndexed : DataModels.IndexingPolicy = {
indexingMode : "consistent" ,
automatic : true ,
includedPaths : [
{
path : "/*" ,
indexes : [
{
kind : "Range" ,
dataType : "Number" ,
precision : - 1 ,
} ,
{
kind : "Range" ,
dataType : "String" ,
precision : - 1 ,
} ,
] ,
} ,
] ,
excludedPaths : [ ] ,
} ;
2021-03-19 01:06:13 +00:00
export interface AddCollectionPanelState {
createNewDatabase : boolean ;
newDatabaseId : string ;
isSharedThroughputChecked : boolean ;
selectedDatabaseId : string ;
collectionId : string ;
enableIndexing : boolean ;
isSharded : boolean ;
partitionKey : string ;
enableDedicatedThroughput : boolean ;
createMongoWildCardIndex : boolean ;
2021-04-30 18:23:34 +01:00
useHashV2 : boolean ;
2021-03-19 01:06:13 +00:00
enableAnalyticalStore : boolean ;
uniqueKeys : string [ ] ;
errorMessage : string ;
showErrorDetails : boolean ;
isExecuting : boolean ;
2021-10-31 02:45:16 +00:00
isThroughputCapExceeded : boolean ;
2021-03-19 01:06:13 +00:00
}
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 = {
2021-04-30 18:23:34 +01:00
createNewDatabase : userContext.apiType !== "Tables" && ! this . props . databaseId ,
2021-03-19 01:06:13 +00:00
newDatabaseId : "" ,
isSharedThroughputChecked : this.getSharedThroughputDefault ( ) ,
2021-04-30 18:23:34 +01:00
selectedDatabaseId :
userContext . apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId ,
2021-03-19 01:06:13 +00:00
collectionId : "" ,
enableIndexing : true ,
2021-04-28 20:25:04 +01:00
isSharded : userContext.apiType !== "Tables" ,
2021-07-29 14:48:03 +01:00
partitionKey : this.getPartitionKey ( ) ,
2021-03-19 01:06:13 +00:00
enableDedicatedThroughput : false ,
2021-05-07 03:27:47 +01:00
createMongoWildCardIndex : isCapabilityEnabled ( "EnableMongo" ) ,
2021-04-30 18:23:34 +01:00
useHashV2 : false ,
2021-03-19 01:06:13 +00:00
enableAnalyticalStore : false ,
uniqueKeys : [ ] ,
errorMessage : "" ,
showErrorDetails : false ,
isExecuting : false ,
2021-10-31 02:45:16 +00:00
isThroughputCapExceeded : false ,
2021-03-19 01:06:13 +00:00
} ;
}
render ( ) : JSX . Element {
2021-06-18 19:25:08 +01:00
const isFirstResourceCreated = useDatabases . getState ( ) . isFirstResourceCreated ( ) ;
2021-03-19 01:06:13 +00:00
return (
< form className = "panelFormWrapper" onSubmit = { this . submit . bind ( this ) } >
{ this . state . errorMessage && (
< PanelInfoErrorComponent
message = { this . state . errorMessage }
messageType = "error"
showErrorDetails = { this . state . showErrorDetails }
/ >
) }
{ ! this . state . errorMessage && this . isFreeTierAccount ( ) && (
< PanelInfoErrorComponent
2021-06-18 19:25:08 +01:00
message = { getUpsellMessage ( userContext . portalEnv , true , isFirstResourceCreated , true ) }
2021-03-19 01:06:13 +00:00
messageType = "info"
showErrorDetails = { false }
link = { Constants . Urls . freeTierInformation }
linkText = "Learn more"
/ >
) }
< div className = "panelMainContent" >
2021-04-28 20:25:04 +01:00
< Stack hidden = { userContext . apiType === "Tables" } >
2021-03-19 01:06:13 +00:00
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
2021-04-30 18:23:34 +01:00
Database { userContext . apiType === "Mongo" ? "name" : "id" }
2021-03-19 01:06:13 +00:00
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = { ` A database is analogous to a namespace. It is the unit of management for a set of ${ getCollectionName (
true
) . toLocaleLowerCase ( ) } . ` }
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
< Stack horizontal verticalAlign = "center" >
< input
className = "panelRadioBtn"
checked = { this . state . createNewDatabase }
aria - label = "Create new database"
aria - checked = { this . state . createNewDatabase }
name = "databaseType"
type = "radio"
role = "radio"
id = "databaseCreateNew"
tabIndex = { 0 }
onChange = { this . onCreateNewDatabaseRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Create new < / span >
< input
className = "panelRadioBtn"
checked = { ! this . state . createNewDatabase }
aria - label = "Use existing database"
aria - checked = { ! this . state . createNewDatabase }
name = "databaseType"
type = "radio"
role = "radio"
tabIndex = { 0 }
onChange = { this . onUseExistingDatabaseRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Use existing < / span >
< / Stack >
{ this . state . createNewDatabase && (
< Stack className = "panelGroupSpacing" >
< input
name = "newDatabaseId"
2021-04-30 18:23:34 +01:00
id = "newDatabaseId"
2021-03-19 01:06:13 +00:00
aria - required
required
type = "text"
autoComplete = "off"
pattern = "[^/?#\\]*[^/?# \\]"
title = "May not end with space nor contain characters '\' '/' '#' '?'"
placeholder = "Type a new database id"
size = { 40 }
className = "panelTextField"
2021-04-30 18:23:34 +01:00
aria - label = "New database id"
2021-03-19 01:06:13 +00:00
autoFocus
2021-09-16 22:24:47 +01:00
tabIndex = { 0 }
2021-03-19 01:06:13 +00:00
value = { this . state . newDatabaseId }
onChange = { ( event : React.ChangeEvent < HTMLInputElement > ) = >
this . setState ( { newDatabaseId : event.target.value } )
}
/ >
2021-05-12 19:56:24 +01:00
{ ! isServerlessAccount ( ) && (
2021-03-19 01:06:13 +00:00
< Stack horizontal >
< Checkbox
2021-04-30 18:23:34 +01:00
label = { ` Share throughput across ${ getCollectionName ( true ) . toLocaleLowerCase ( ) } ` }
2021-03-19 01:06:13 +00:00
checked = { this . state . isSharedThroughputChecked }
styles = { {
text : { fontSize : 12 } ,
checkbox : { width : 12 , height : 12 } ,
label : { padding : 0 , alignItems : "center" } ,
} }
onChange = { ( ev : React.FormEvent < HTMLElement > , isChecked : boolean ) = >
this . setState ( { isSharedThroughputChecked : isChecked } )
}
/ >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = { ` Throughput configured at the database level will be shared across all ${ getCollectionName (
true
) . toLocaleLowerCase ( ) } within the database . ` }
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
) }
2021-05-12 19:56:24 +01:00
{ ! isServerlessAccount ( ) && this . state . isSharedThroughputChecked && (
2021-03-19 01:06:13 +00:00
< ThroughputInput
2021-06-18 19:25:08 +01:00
showFreeTierExceedThroughputTooltip = { this . isFreeTierAccount ( ) && ! isFirstResourceCreated }
2021-03-19 01:06:13 +00:00
isDatabase = { true }
2021-04-30 18:23:34 +01:00
isSharded = { this . state . isSharded }
2021-03-19 01:06:13 +00:00
setThroughputValue = { ( throughput : number ) = > ( this . newDatabaseThroughput = throughput ) }
setIsAutoscale = { ( isAutoscale : boolean ) = > ( this . isNewDatabaseAutoscale = isAutoscale ) }
2021-10-31 02:45:16 +00:00
setIsThroughputCapExceeded = { ( isThroughputCapExceeded : boolean ) = >
this . setState ( { isThroughputCapExceeded } )
}
2021-03-19 01:06:13 +00:00
onCostAcknowledgeChange = { ( isAcknowledge : boolean ) = > ( this . isCostAcknowledged = isAcknowledge ) }
/ >
) }
< / Stack >
) }
{ ! this . state . createNewDatabase && (
< Dropdown
styles = { { title : { height : 27 , lineHeight : 27 } , dropdownItem : { fontSize : 12 } } }
style = { { width : 300 , fontSize : 12 } }
placeholder = "Choose an existing database"
options = { this . getDatabaseOptions ( ) }
onChange = { ( event : React.FormEvent < HTMLDivElement > , database : IDropdownOption ) = >
this . setState ( { selectedDatabaseId : database.key as string } )
}
2021-04-30 18:23:34 +01:00
defaultSelectedKey = { this . props . databaseId }
responsiveMode = { 999 }
2021-03-19 01:06:13 +00:00
/ >
) }
2021-04-30 18:23:34 +01:00
< Separator className = "panelSeparator" / >
2021-03-19 01:06:13 +00:00
< / Stack >
< Stack >
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
2021-04-30 18:23:34 +01:00
{ ` ${ getCollectionName ( ) } ${ userContext . apiType === "Mongo" ? "name" : "id" } ` }
2021-03-19 01:06:13 +00:00
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = { ` Unique identifier for the ${ getCollectionName ( ) . toLocaleLowerCase ( ) } and used for id-based routing through REST and all SDKs. ` }
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
< input
name = "collectionId"
2021-04-30 18:23:34 +01:00
id = "collectionId"
2021-03-19 01:06:13 +00:00
type = "text"
aria - required
required
autoComplete = "off"
pattern = "[^/?#\\]*[^/?# \\]"
title = "May not end with space nor contain characters '\' '/' '#' '?'"
2021-04-30 18:23:34 +01:00
placeholder = { ` e.g., ${ getCollectionName ( ) } 1 ` }
2021-03-19 01:06:13 +00:00
size = { 40 }
className = "panelTextField"
2021-04-30 18:23:34 +01:00
aria - label = { ` ${ getCollectionName ( ) } id ` }
2021-03-19 01:06:13 +00:00
value = { this . state . collectionId }
onChange = { ( event : React.ChangeEvent < HTMLInputElement > ) = >
this . setState ( { collectionId : event.target.value } )
}
/ >
< / Stack >
{ this . shouldShowIndexingOptionsForFreeTierAccount ( ) && (
< Stack >
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
Indexing
< / Text >
< / Stack >
< Stack horizontal verticalAlign = "center" >
< input
className = "panelRadioBtn"
checked = { this . state . enableIndexing }
aria - label = "Turn on indexing"
aria - checked = { this . state . enableIndexing }
type = "radio"
role = "radio"
tabIndex = { 0 }
onChange = { this . onTurnOnIndexing . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Automatic < / span >
< input
className = "panelRadioBtn"
checked = { ! this . state . enableIndexing }
aria - label = "Turn off indexing"
aria - checked = { ! this . state . enableIndexing }
type = "radio"
role = "radio"
tabIndex = { 0 }
onChange = { this . onTurnOffIndexing . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Off < / span >
< / Stack >
< Text variant = "small" >
{ this . getFreeTierIndexingText ( ) } { " " }
< Link target = "_blank" href = "https://aka.ms/cosmos-indexing-policy" >
Learn more
< / Link >
< / Text >
< / Stack >
) }
2021-04-28 20:25:04 +01:00
{ userContext . apiType === "Mongo" &&
2021-03-19 01:06:13 +00:00
( ! this . state . isSharedThroughputChecked ||
this . props . explorer . isFixedCollectionWithSharedThroughputSupported ( ) ) && (
< Stack >
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
2021-04-30 18:23:34 +01:00
Sharding
2021-03-19 01:06:13 +00:00
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = {
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
< Stack horizontal verticalAlign = "center" >
< input
className = "panelRadioBtn"
checked = { ! this . state . isSharded }
aria - label = "Unsharded"
aria - checked = { ! this . state . isSharded }
name = "unsharded"
type = "radio"
role = "radio"
id = "unshardedOption"
tabIndex = { 0 }
onChange = { this . onUnshardedRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Unsharded ( 20 GB limit ) < / span >
< input
className = "panelRadioBtn"
checked = { this . state . isSharded }
aria - label = "Sharded"
aria - checked = { this . state . isSharded }
name = "sharded"
type = "radio"
role = "radio"
id = "shardedOption"
tabIndex = { 0 }
onChange = { this . onShardedRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Sharded < / span >
< / Stack >
< / Stack >
) }
{ this . state . isSharded && (
< Stack >
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
{ this . getPartitionKeyName ( ) }
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = { this . getPartitionKeyTooltipText ( ) }
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
2021-07-14 20:10:45 +01:00
< Text variant = "small" aria - label = "pkDescription" >
{ this . getPartitionKeySubtext ( ) }
< / Text >
2021-03-19 01:06:13 +00:00
< input
type = "text"
id = "addCollection-partitionKeyValue"
aria - required
required
size = { 40 }
className = "panelTextField"
placeholder = { this . getPartitionKeyPlaceHolder ( ) }
aria - label = { this . getPartitionKeyName ( ) }
2021-04-28 20:25:04 +01:00
pattern = { userContext . apiType === "Gremlin" ? "^/[^/]*" : ".*" }
title = { userContext . apiType === "Gremlin" ? "May not use composite partition key" : "" }
2021-03-19 01:06:13 +00:00
value = { this . state . partitionKey }
2021-04-30 18:23:34 +01:00
onChange = { ( event : React.ChangeEvent < HTMLInputElement > ) = > {
if (
userContext . apiType !== "Mongo" &&
2021-05-12 19:56:24 +01:00
! this . state . partitionKey &&
2021-04-30 18:23:34 +01:00
! event . target . value . startsWith ( "/" )
) {
this . setState ( { partitionKey : "/" + event . target . value } ) ;
} else {
this . setState ( { partitionKey : event.target.value } ) ;
}
} }
2021-03-19 01:06:13 +00:00
/ >
< / Stack >
) }
2021-05-12 19:56:24 +01:00
{ ! isServerlessAccount ( ) && ! this . state . createNewDatabase && this . isSelectedDatabaseSharedThroughput ( ) && (
2021-03-19 01:06:13 +00:00
< Stack horizontal verticalAlign = "center" >
< Checkbox
2021-04-30 18:23:34 +01:00
label = { ` Provision dedicated throughput for this ${ getCollectionName ( ) . toLocaleLowerCase ( ) } ` }
2021-03-19 01:06:13 +00:00
checked = { this . state . enableDedicatedThroughput }
styles = { {
text : { fontSize : 12 } ,
checkbox : { width : 12 , height : 12 } ,
label : { padding : 0 , alignItems : "center" } ,
} }
onChange = { ( ev : React.FormEvent < HTMLElement > , isChecked : boolean ) = >
this . setState ( { enableDedicatedThroughput : isChecked } )
}
/ >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
2021-04-30 18:23:34 +01:00
content = { ` You can optionally provision dedicated throughput for a ${ getCollectionName ( ) . toLocaleLowerCase ( ) } within a database that has throughput
provisioned . This dedicated throughput amount will not be shared with other $ { getCollectionName (
true
) . toLocaleLowerCase ( ) } in the database and
2021-03-19 01:06:13 +00:00
does not count towards the throughput you provisioned for the database . This throughput amount will be
2021-04-30 18:23:34 +01:00
billed in addition to the throughput amount you provisioned at the database level . ` }
2021-03-19 01:06:13 +00:00
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
) }
{ this . shouldShowCollectionThroughputInput ( ) && (
< ThroughputInput
2021-06-18 19:25:08 +01:00
showFreeTierExceedThroughputTooltip = { this . isFreeTierAccount ( ) && ! isFirstResourceCreated }
2021-03-19 01:06:13 +00:00
isDatabase = { false }
2021-04-30 18:23:34 +01:00
isSharded = { this . state . isSharded }
2021-03-19 01:06:13 +00:00
setThroughputValue = { ( throughput : number ) = > ( this . collectionThroughput = throughput ) }
setIsAutoscale = { ( isAutoscale : boolean ) = > ( this . isCollectionAutoscale = isAutoscale ) }
2021-10-31 02:45:16 +00:00
setIsThroughputCapExceeded = { ( isThroughputCapExceeded : boolean ) = >
this . setState ( { isThroughputCapExceeded } )
}
2021-03-19 01:06:13 +00:00
onCostAcknowledgeChange = { ( isAcknowledged : boolean ) = > {
this . isCostAcknowledged = isAcknowledged ;
} }
/ >
) }
2021-04-28 20:25:04 +01:00
{ userContext . apiType === "SQL" && (
2021-03-19 01:06:13 +00:00
< Stack >
< Stack horizontal >
< Text className = "panelTextBold" variant = "small" >
Unique keys
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
content = " 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 . "
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-03-19 01:06:13 +00:00
< / TooltipHost >
< / Stack >
{ this . state . uniqueKeys . map (
( uniqueKey : string , i : number ) : JSX . Element = > {
return (
< Stack style = { { marginBottom : 8 } } key = { ` uniqueKey ${ i } ` } horizontal >
< input
type = "text"
autoComplete = "off"
placeholder = {
2021-04-28 20:25:04 +01:00
userContext . apiType === "Mongo"
2021-03-19 01:06:13 +00:00
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className = "panelTextField"
autoFocus
value = { uniqueKey }
onChange = { ( event : React.ChangeEvent < HTMLInputElement > ) = > {
const uniqueKeys = this . state . uniqueKeys . map ( ( uniqueKey : string , j : number ) = > {
if ( i === j ) {
return event . target . value ;
}
return uniqueKey ;
} ) ;
this . setState ( { uniqueKeys } ) ;
} }
/ >
< IconButton
iconProps = { { iconName : "Delete" } }
style = { { height : 27 } }
onClick = { ( ) = > {
const uniqueKeys = this . state . uniqueKeys . filter ( ( uniqueKey , j ) = > i !== j ) ;
this . setState ( { uniqueKeys } ) ;
} }
/ >
< / Stack >
) ;
}
) }
< ActionButton
iconProps = { { iconName : "Add" } }
styles = { { root : { padding : 0 } , label : { fontSize : 12 } } }
onClick = { ( ) = > this . setState ( { uniqueKeys : [ . . . this . state . uniqueKeys , "" ] } ) }
>
Add unique key
< / ActionButton >
< / Stack >
) }
2021-08-19 20:18:21 +01:00
{ this . shouldShowAnalyticalStoreOptions ( ) && (
< Stack className = "panelGroupSpacing" >
< Stack horizontal >
< Text className = "panelTextBold" variant = "small" >
Analytical store
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
content = { this . getAnalyticalStorageTooltipContent ( ) }
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-08-19 20:18:21 +01:00
< / TooltipHost >
< / Stack >
< Stack horizontal verticalAlign = "center" >
< input
className = "panelRadioBtn"
checked = { this . state . enableAnalyticalStore }
disabled = { ! this . isSynapseLinkEnabled ( ) }
aria - label = "Enable analytical store"
aria - checked = { this . state . enableAnalyticalStore }
name = "analyticalStore"
type = "radio"
role = "radio"
id = "enableAnalyticalStoreBtn"
tabIndex = { 0 }
onChange = { this . onEnableAnalyticalStoreRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > On < / span >
< input
className = "panelRadioBtn"
checked = { ! this . state . enableAnalyticalStore }
disabled = { ! this . isSynapseLinkEnabled ( ) }
aria - label = "Disable analytical store"
aria - checked = { ! this . state . enableAnalyticalStore }
name = "analyticalStore"
type = "radio"
role = "radio"
id = "disableAnalyticalStoreBtn"
tabIndex = { 0 }
onChange = { this . onDisableAnalyticalStoreRadioBtnChange . bind ( this ) }
/ >
< span className = "panelRadioBtnLabel" > Off < / span >
< / Stack >
{ ! this . isSynapseLinkEnabled ( ) && (
< Stack className = "panelGroupSpacing" >
< Text variant = "small" >
Azure Synapse Link is required for creating an analytical store { " " }
{ getCollectionName ( ) . toLocaleLowerCase ( ) } . Enable Synapse Link for this Cosmos DB account . { " " }
< Link href = "https://aka.ms/cosmosdb-synapselink" target = "_blank" >
Learn more
< / Link >
< / Text >
< DefaultButton
text = "Enable"
onClick = { ( ) = > this . props . explorer . openEnableSynapseLinkDialog ( ) }
style = { { height : 27 , width : 80 } }
styles = { { label : { fontSize : 12 } } }
/ >
< / Stack >
) }
< / Stack >
) }
2021-04-30 18:23:34 +01:00
{ userContext . apiType !== "Tables" && (
< CollapsibleSectionComponent
title = "Advanced"
isExpandedByDefault = { false }
onExpand = { ( ) = > {
TelemetryProcessor . traceOpen ( Action . ExpandAddCollectionPaneAdvancedSection ) ;
this . scrollToAdvancedSection ( ) ;
} }
>
< Stack className = "panelGroupSpacing" id = "collapsibleSectionContent" >
2021-05-07 03:27:47 +01:00
{ isCapabilityEnabled ( "EnableMongo" ) && (
2021-04-30 18:23:34 +01:00
< Stack className = "panelGroupSpacing" >
< Stack horizontal >
< span className = "mandatoryStar" > * & nbsp ; < / span >
< Text className = "panelTextBold" variant = "small" >
Indexing
< / Text >
< TooltipHost
directionalHint = { DirectionalHint . bottomLeftEdge }
content = "The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
2021-09-16 22:24:47 +01:00
< Icon iconName = "Info" className = "panelInfoIcon" tabIndex = { 0 } / >
2021-04-30 18:23:34 +01:00
< / TooltipHost >
< / Stack >
< Checkbox
label = "Create a Wildcard Index on all fields"
checked = { this . state . createMongoWildCardIndex }
styles = { {
text : { fontSize : 12 } ,
checkbox : { width : 12 , height : 12 } ,
label : { padding : 0 , alignItems : "center" } ,
} }
onChange = { ( ev : React.FormEvent < HTMLElement > , isChecked : boolean ) = >
this . setState ( { createMongoWildCardIndex : isChecked } )
}
/ >
2021-03-19 01:06:13 +00:00
< / Stack >
2021-04-30 18:23:34 +01:00
) }
2021-03-19 01:06:13 +00:00
2021-04-30 18:23:34 +01:00
{ userContext . apiType === "SQL" && (
2021-03-19 01:06:13 +00:00
< Checkbox
2021-04-30 18:23:34 +01:00
label = "My partition key is larger than 100 bytes"
checked = { this . state . useHashV2 }
2021-03-19 01:06:13 +00:00
styles = { {
text : { fontSize : 12 } ,
checkbox : { width : 12 , height : 12 } ,
label : { padding : 0 , alignItems : "center" } ,
} }
onChange = { ( ev : React.FormEvent < HTMLElement > , isChecked : boolean ) = >
2021-04-30 18:23:34 +01:00
this . setState ( { useHashV2 : isChecked } )
2021-03-19 01:06:13 +00:00
}
/ >
2021-04-30 18:23:34 +01:00
) }
< / Stack >
< / CollapsibleSectionComponent >
) }
2021-03-19 01:06:13 +00:00
< / div >
2021-10-31 02:45:16 +00:00
< PanelFooterComponent buttonLabel = "OK" isButtonDisabled = { this . state . isThroughputCapExceeded } / >
2021-03-19 01:06:13 +00:00
{ this . state . isExecuting && < PanelLoadingScreen / > }
< / form >
) ;
}
private getDatabaseOptions ( ) : IDropdownOption [ ] {
2021-06-18 19:25:08 +01:00
return useDatabases . getState ( ) . databases ? . map ( ( database ) = > ( {
2021-03-19 01:06:13 +00:00
key : database.id ( ) ,
text : database.id ( ) ,
} ) ) ;
}
2021-04-30 18:23:34 +01:00
private getPartitionKeyName ( isLowerCase? : boolean ) : string {
const partitionKeyName = userContext . apiType === "Mongo" ? "Shard key" : "Partition key" ;
2021-03-19 01:06:13 +00:00
2021-04-30 18:23:34 +01:00
return isLowerCase ? partitionKeyName . toLocaleLowerCase ( ) : partitionKeyName ;
2021-03-19 01:06:13 +00:00
}
private getPartitionKeyPlaceHolder ( ) : string {
2021-04-28 20:25:04 +01:00
switch ( userContext . apiType ) {
case "Mongo" :
2021-03-19 01:06:13 +00:00
return "e.g., address.zipCode" ;
2021-04-28 20:25:04 +01:00
case "Gremlin" :
2021-03-19 01:06:13 +00:00
return "e.g., /address" ;
default :
return "e.g., /address/zipCode" ;
}
}
private onCreateNewDatabaseRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && ! this . state . createNewDatabase ) {
this . setState ( {
createNewDatabase : true ,
} ) ;
}
}
private onUseExistingDatabaseRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && this . state . createNewDatabase ) {
this . setState ( {
createNewDatabase : false ,
} ) ;
}
}
private onUnshardedRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && this . state . isSharded ) {
this . setState ( {
isSharded : false ,
} ) ;
}
}
private onShardedRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && ! this . state . isSharded ) {
this . setState ( {
isSharded : true ,
} ) ;
}
}
private onEnableAnalyticalStoreRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && ! this . state . enableAnalyticalStore ) {
this . setState ( {
enableAnalyticalStore : true ,
} ) ;
}
}
private onDisableAnalyticalStoreRadioBtnChange ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && this . state . enableAnalyticalStore ) {
this . setState ( {
enableAnalyticalStore : false ,
} ) ;
}
}
private onTurnOnIndexing ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && ! this . state . enableIndexing ) {
this . setState ( {
enableIndexing : true ,
} ) ;
}
}
private onTurnOffIndexing ( event : React.ChangeEvent < HTMLInputElement > ) : void {
if ( event . target . checked && this . state . enableIndexing ) {
this . setState ( {
enableIndexing : false ,
} ) ;
}
}
private isSelectedDatabaseSharedThroughput ( ) : boolean {
if ( ! this . state . selectedDatabaseId ) {
return false ;
}
2021-06-18 19:25:08 +01:00
const selectedDatabase = useDatabases
. getState ( )
. databases ? . find ( ( database ) = > database . id ( ) === this . state . selectedDatabaseId ) ;
2021-03-19 01:06:13 +00:00
return ! ! selectedDatabase ? . offer ( ) ;
}
private isFreeTierAccount ( ) : boolean {
return userContext . databaseAccount ? . properties ? . enableFreeTier ;
}
private getSharedThroughputDefault ( ) : boolean {
2021-05-12 19:56:24 +01:00
return userContext . subscriptionType !== SubscriptionType . EA && ! isServerlessAccount ( ) ;
2021-03-19 01:06:13 +00:00
}
private getFreeTierIndexingText ( ) : string {
return this . state . enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations." ;
}
2021-04-30 18:23:34 +01:00
private getPartitionKeyTooltipText ( ) : string {
if ( userContext . apiType === "Mongo" ) {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’ s critical to choose a field that will evenly distribute your data." ;
}
let tooltipText = ` The ${ this . getPartitionKeyName (
true
) } is used to automatically distribute data across partitions for scalability . Choose a property in your JSON document that has a wide range of values and evenly distributes request volume . ` ;
if ( userContext . apiType === "SQL" ) {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice." ;
}
return tooltipText ;
}
2021-07-29 14:48:03 +01:00
private getPartitionKey ( ) : string {
if ( userContext . apiType !== "SQL" && userContext . apiType !== "Mongo" ) {
return "" ;
}
if ( userContext . features . partitionKeyDefault ) {
return userContext . apiType === "SQL" ? "/id" : "_id" ;
}
if ( userContext . features . partitionKeyDefault2 ) {
return userContext . apiType === "SQL" ? "/pk" : "pk" ;
}
return "" ;
}
2021-07-14 20:10:45 +01:00
private getPartitionKeySubtext ( ) : string {
if (
userContext . features . partitionKeyDefault &&
( userContext . apiType === "SQL" || userContext . apiType === "Mongo" )
) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key." ;
return subtext ;
}
return "" ;
}
2021-04-30 18:23:34 +01:00
private getAnalyticalStorageTooltipContent ( ) : JSX . Element {
return (
< Text variant = "small" >
Enable analytical store capability to perform near real - time analytics on your operational data , without
impacting the performance of transactional workloads . { " " }
< Link target = "_blank" href = "https://aka.ms/analytical-store-overview" >
Learn more
< / Link >
< / Text >
) ;
}
2021-03-19 01:06:13 +00:00
private shouldShowCollectionThroughputInput ( ) : boolean {
2021-05-12 19:56:24 +01:00
if ( isServerlessAccount ( ) ) {
2021-03-19 01:06:13 +00:00
return false ;
}
if ( this . state . createNewDatabase ) {
return ! this . state . isSharedThroughputChecked ;
}
if ( this . state . enableDedicatedThroughput ) {
return true ;
}
return this . state . selectedDatabaseId && ! this . isSelectedDatabaseSharedThroughput ( ) ;
}
private shouldShowIndexingOptionsForFreeTierAccount ( ) : boolean {
if ( ! this . isFreeTierAccount ( ) ) {
return false ;
}
return this . state . createNewDatabase
? this . state . isSharedThroughputChecked
: this . isSelectedDatabaseSharedThroughput ( ) ;
}
private shouldShowAnalyticalStoreOptions ( ) : boolean {
if ( configContext . platform === Platform . Emulator ) {
return false ;
}
2021-05-12 19:56:24 +01:00
if ( isServerlessAccount ( ) ) {
2021-03-19 01:06:13 +00:00
return false ;
}
2021-04-28 20:25:04 +01:00
switch ( userContext . apiType ) {
case "SQL" :
case "Mongo" :
2021-03-19 01:06:13 +00:00
return true ;
default :
return false ;
}
}
private isSynapseLinkEnabled ( ) : boolean {
2021-05-05 22:54:50 +01:00
const { properties } = userContext . databaseAccount ;
2021-03-19 01:06:13 +00:00
if ( ! properties ) {
return false ;
}
if ( properties . enableAnalyticalStorage ) {
return true ;
}
2021-05-07 00:51:22 +01:00
return properties . capabilities ? . some (
2021-03-19 01:06:13 +00:00
( capability ) = > capability . name === Constants . CapabilityNames . EnableStorageAnalytics
) ;
}
private parseUniqueKeys ( ) : DataModels . UniqueKeyPolicy {
if ( this . state . uniqueKeys ? . length === 0 ) {
return undefined ;
}
const uniqueKeyPolicy : DataModels.UniqueKeyPolicy = { uniqueKeys : [ ] } ;
this . state . uniqueKeys . forEach ( ( uniqueKey ) = > {
if ( uniqueKey ) {
const validPaths : string [ ] = uniqueKey . split ( "," ) ? . filter ( ( path ) = > path ? . length > 0 ) ;
const trimmedPaths : string [ ] = validPaths ? . map ( ( path ) = > path . trim ( ) ) ;
if ( trimmedPaths ? . length > 0 ) {
2021-04-28 20:25:04 +01:00
if ( userContext . apiType === "Mongo" ) {
2021-03-19 01:06:13 +00:00
trimmedPaths . map ( ( path ) = > {
const transformedPath = path . split ( "." ) . join ( "/" ) ;
if ( transformedPath [ 0 ] !== "/" ) {
return "/" + transformedPath ;
}
return transformedPath ;
} ) ;
}
uniqueKeyPolicy . uniqueKeys . push ( { paths : trimmedPaths } ) ;
}
}
} ) ;
return uniqueKeyPolicy ;
}
private validateInputs ( ) : boolean {
if ( ! this . state . createNewDatabase && ! this . state . selectedDatabaseId ) {
this . setState ( { errorMessage : "Please select an existing database" } ) ;
return false ;
}
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 } ) ;
return false ;
}
2021-04-30 18:23:34 +01:00
if ( throughput > CollectionCreation . MaxRUPerPartition && ! this . state . isSharded ) {
this . setState ( { errorMessage : "Unsharded collections support up to 10,000 RUs" } ) ;
return false ;
}
2021-03-19 01:06:13 +00:00
if (
2021-04-28 20:25:04 +01:00
userContext . apiType === "Gremlin" &&
2021-03-19 01:06:13 +00:00
( this . state . partitionKey === "/id" || this . state . partitionKey === "/label" )
) {
this . setState ( { errorMessage : "/id and /label as partition keys are not allowed for graph." } ) ;
return false ;
}
return true ;
}
private getAnalyticalStorageTtl ( ) : number {
2021-05-12 17:49:25 +01:00
if ( ! this . isSynapseLinkEnabled ( ) ) {
return undefined ;
}
2021-03-19 01:06:13 +00:00
if ( ! this . shouldShowAnalyticalStoreOptions ( ) ) {
return undefined ;
}
if ( this . state . enableAnalyticalStore ) {
// TODO: always default to 90 days once the backend hotfix is deployed
2021-03-22 19:04:06 +00:00
return userContext . features . ttl90Days
2021-03-19 01:06:13 +00:00
? Constants . AnalyticalStorageTtl . Days90
: Constants . AnalyticalStorageTtl . Infinite ;
}
return Constants . AnalyticalStorageTtl . Disabled ;
}
2021-04-30 18:23:34 +01:00
private scrollToAdvancedSection ( ) : void {
document . getElementById ( "collapsibleSectionContent" ) ? . scrollIntoView ( ) ;
}
2021-03-19 01:06:13 +00:00
private async submit ( event : React.FormEvent < HTMLFormElement > ) : Promise < void > {
event . preventDefault ( ) ;
if ( ! this . validateInputs ( ) ) {
return ;
}
const collectionId : string = this . state . collectionId . trim ( ) ;
let databaseId = this . state . createNewDatabase ? this . state . newDatabaseId . trim ( ) : this . state . selectedDatabaseId ;
2021-10-11 20:09:38 +01:00
let partitionKeyString = this . state . isSharded ? this . state . partitionKey . trim ( ) : undefined ;
2021-03-19 01:06:13 +00:00
2021-04-28 20:25:04 +01:00
if ( userContext . apiType === "Tables" ) {
2021-03-19 01:06:13 +00:00
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
databaseId = CollectionCreation . TablesAPIDefaultDatabase ;
partitionKeyString = "/'$pk'" ;
}
const uniqueKeyPolicy : DataModels.UniqueKeyPolicy = this . parseUniqueKeys ( ) ;
2021-04-30 18:23:34 +01:00
const partitionKeyVersion = this . state . useHashV2 ? 2 : undefined ;
2021-03-19 01:06:13 +00:00
const partitionKey : DataModels.PartitionKey = partitionKeyString
? {
paths : [ partitionKeyString ] ,
2021-05-21 02:34:29 +01:00
kind : "Hash" ,
2021-03-19 01:06:13 +00:00
version : partitionKeyVersion ,
}
: undefined ;
const indexingPolicy : DataModels.IndexingPolicy = this . state . enableIndexing
2021-05-21 02:34:29 +01:00
? AllPropertiesIndexed
: SharedDatabaseDefault ;
2021-03-19 01:06:13 +00:00
const telemetryData = {
database : {
id : databaseId ,
new : this . state . createNewDatabase ,
shared : this.state.createNewDatabase
? this . state . isSharedThroughputChecked
: this . isSelectedDatabaseSharedThroughput ( ) ,
} ,
collection : {
id : this.state.collectionId ,
throughput : this.collectionThroughput ,
isAutoscale : this.isCollectionAutoscale ,
partitionKey ,
uniqueKeyPolicy ,
collectionWithDedicatedThroughput : this.state.enableDedicatedThroughput ,
} ,
subscriptionQuotaId : userContext.quotaId ,
dataExplorerArea : Constants.Areas.ContextualPane ,
useIndexingForSharedThroughput : this.state.enableIndexing ,
} ;
const startKey : number = TelemetryProcessor . traceStart ( Action . CreateCollection , telemetryData ) ;
const databaseLevelThroughput : boolean = this . state . createNewDatabase
? this . state . isSharedThroughputChecked
: this . isSelectedDatabaseSharedThroughput ( ) && ! this . state . enableDedicatedThroughput ;
let offerThroughput : number ;
let autoPilotMaxThroughput : number ;
2021-05-21 19:12:13 +01:00
if ( databaseLevelThroughput ) {
if ( this . state . createNewDatabase ) {
if ( this . isNewDatabaseAutoscale ) {
autoPilotMaxThroughput = this . newDatabaseThroughput ;
} else {
offerThroughput = this . newDatabaseThroughput ;
}
2021-03-19 01:06:13 +00:00
}
2021-05-21 19:12:13 +01:00
} else {
2021-03-19 01:06:13 +00:00
if ( this . isCollectionAutoscale ) {
autoPilotMaxThroughput = this . collectionThroughput ;
} else {
offerThroughput = this . collectionThroughput ;
}
}
const createCollectionParams : DataModels.CreateCollectionParams = {
createNewDatabase : this.state.createNewDatabase ,
collectionId ,
databaseId ,
databaseLevelThroughput ,
offerThroughput ,
autoPilotMaxThroughput ,
analyticalStorageTtl : this.getAnalyticalStorageTtl ( ) ,
indexingPolicy ,
partitionKey ,
uniqueKeyPolicy ,
createMongoWildcardIndex : this.state.createMongoWildCardIndex ,
} ;
this . setState ( { isExecuting : true } ) ;
try {
await createCollection ( createCollectionParams ) ;
this . setState ( { isExecuting : false } ) ;
this . props . explorer . refreshAllDatabases ( ) ;
TelemetryProcessor . traceSuccess ( Action . CreateCollection , telemetryData , startKey ) ;
2021-05-27 22:07:07 +01:00
useSidePanel . getState ( ) . closeSidePanel ( ) ;
2021-03-19 01:06:13 +00:00
} catch ( error ) {
const errorMessage : string = getErrorMessage ( error ) ;
this . setState ( { isExecuting : false , errorMessage , showErrorDetails : true } ) ;
const failureTelemetryData = { . . . telemetryData , error : errorMessage , errorStack : getErrorStack ( error ) } ;
TelemetryProcessor . traceFailure ( Action . CreateCollection , failureTelemetryData , startKey ) ;
}
}
}