Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

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" data-bind="text: title"></span>
<div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="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: 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="attr: { 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="https://aka.ms/azure-cosmos-db-pricing" tabindex="0">
More details</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"
aria-label="Database id" 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"
aria-label="Database id">
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
<option data-bind="value: $data">
</datalist>
<!-- Database provisioned throughput - Start -->
<div class="databaseProvision" aria-label="New database provision support"
data-bind="visible: databaseCreateNew">
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput" title="Provision shared 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>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input-autopilot-v3 params="{
testId: 'databaseThroughputValue',
value: throughputDatabase,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
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,
showAutoPilot: !isFreeTierAccount()
}">
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input params="{
testId: 'databaseThroughputValue',
value: throughputDatabase,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
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,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
autoPilotTiersList: sharedAutoPilotTiersList,
selectedAutoPilotTier: selectedSharedAutoPilotTier
}">
</throughput-input>
</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, attr: { 'aria-label': collectionIdTitle }">
</div>
<!-- <p class="seconddivpadding" data-bind="visible:(container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) && !databaseHasSharedOffer()">
Where did 'fixed' containers go?
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext noFixedCollectionsTooltipWidth">
We lowered the minimum throughput for partitioned containers to 400 RU/s, removing the only drawback partitioned containers had. <br/><br/>
We are planning to deprecate ability to create non-partitioned containers, as they do not allow you to scale elastically.
If for some reason you still need a container without partition key, you can use our SDKs to create one. <br/><br/>
Please <a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback">contact us</a> if you have questions or concerns.
</span>
</span>
</p> -->
<!-- Indexing For Shared Throughput - start -->
<div class="seconddivpadding"
data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()">
<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: container.isPreferredApiMongoDB() && !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: container.isPreferredApiMongoDB() && !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 (10 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>
<!-- Fixed Button Content - Start -->
<div class="tabcontent" data-bind="visible: isFixedStorageSelected() && !databaseHasSharedOffer()">
<!-- 2 -->
<throughput-input params="{
testId: 'fixedThroughputValue',
value: throughputSinglePartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: isFixedStorageSelected() && !databaseHasSharedOffer(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost
showAsMandatory: true,
isFixed: true,
infoBubbleText: ruToolTipText,
canExceedMaximumValue: canExceedMaximumValue
}">
</throughput-input>
<div data-bind="visible: rupmVisible">
<div class="tabs">
<p class="pkPadding">
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">RU/m</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext throughputRuInfo">
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
per
minute
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
with
RU/m
enabled, the RU/m budget will be 50,000 RU/m.
</span>
</span>
</p>
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
<div class="tab">
<input type="radio" id="rupmOn" name="rupmcoll" value="on" class="radio"
data-bind="checked: rupm">
<label for="rupmOn">ON</label>
</div>
<div class="tab">
<input type="radio" id="rupmOff" name="rupmcoll" value="off" class="radio"
data-bind="checked: rupm">
<label for="rupmOff">OFF</label>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Button Content - End -->
<!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: rupmVisible">
<div class="tabs">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">RU/m</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext throughputRuInfo">
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
per
minute
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
with
RU/m
enabled, the RU/m budget will be 50,000 RU/m.
</span>
</span>
</p>
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
<div class="tab">
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
data-bind="checked: rupm">
<label for="rupmOn2">ON</label>
</div>
<div class="tab">
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
data-bind="checked: rupm">
<label for="rupmOff2">OFF</label>
</div>
</div>
</div>
</div>
<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" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey,
attr: {
placeholder: partitionKeyPlaceholder,
required: partitionKeyVisible(),
'aria-label': partitionKeyName,
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 -->
<!-- Provision collection throughput checkox - 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 checkox - end -->
<!-- Provision collection throughput spinner - start -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input-autopilot-v3 params="{
testId: 'collectionThroughputValue',
value: throughputMultiPartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !databaseHasSharedOffer() || collectionWithThroughputInShared(),
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,
showAutoPilot: !isFixedStorageSelected()
}">
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input params="{
testId: 'collectionThroughputValue',
value: throughputMultiPartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !databaseHasSharedOffer() || collectionWithThroughputInShared(),
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,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
showAutoPilot: !isFixedStorageSelected()
}">
</throughput-input>
</div>
<!-- /ko -->
<!-- Provision collection throughput spinner - 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">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth">
Enable analytical store capability to perform near real-time analytics on your operational
data, without impacting the performance of transactional workloads.
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank">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'
}" />
<span for="enableAnalyticalStorageRadioOn" data-bind="disable: showEnableSynapseLink">
On
</span>
<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'
}" />
<span for="enableAnalyticalStorageRadioOff" data-bind="disable: showEnableSynapseLink">
Off
</span>
</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,74 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import ko from "knockout";
import { AutopilotTier } from "../../Contracts/DataModels";
jest.mock("../Tabs/NotebookTab");
describe("Add Collection Pane", () => {
describe("isValid()", () => {
let explorer: ViewModels.Explorer;
const mockDatabaseAccount: ViewModels.DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: undefined,
type: undefined,
tags: []
};
beforeEach(() => {
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
it("should be true if autopilot enabled and select valid tier", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
addCollectionPane.isAutoPilotSelected(true);
addCollectionPane.selectedAutoPilotTier(AutopilotTier.Tier2);
expect(addCollectionPane.isValid()).toBe(true);
});
it("should be false if autopilot enabled and select invalid tier", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
addCollectionPane.isAutoPilotSelected(true);
addCollectionPane.selectedAutoPilotTier(0);
expect(addCollectionPane.isValid()).toBe(false);
});
it("should be true if graph API and partition key is not /id nor /label", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
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", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
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", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(true);
addCollectionPane.partitionKey("/label");
expect(addCollectionPane.isValid()).toBe(true);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
<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 -->
<div class="contextual-pane-in">
<form data-bind="submit: submit" style="height: 100%">
<div
class="paneContentContainer"
role="dialog"
aria-labelledby="databaseTitle"
data-bind="template: { name: 'add-database-inputs' }"
></div>
</form>
</div>
<!-- Add database 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-database-inputs">
<!-- Add database header - Start -->
<div class="firstdivbg headerline">
<span id="databaseTitle" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add database header - End -->
<!-- Add database 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>
<!-- Add database errors - End -->
<!-- upsell message - start -->
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: 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="linkAddDatabase" data-bind="attr: { 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="https://aka.ms/azure-cosmos-db-pricing" tabindex="0">
More details</a>
</span>
</div>
</div>
<!-- upsell message - end -->
<!-- Add database inputs - Start -->
<div class="paneMainContent">
<div>
<p>
<span class="mandatoryStar">*</span>
<span data-bind="text: databaseIdLabel"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth" data-bind="text: databaseIdTooltipText"></span>
</span>
</p>
<input id="database-id" type="text" aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="Type a new database id"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus"
aria-label="Database id" autofocus>
<div class="databaseProvision" aria-label="New database provision support">
<input tabindex="0" type="checkbox" id="addDatabasePane-databaseSharedThroughput"
title="Provision shared throughput" data-bind="checked: databaseCreateNewShared" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext provisionDatabaseThroughput"
data-bind="text: databaseLevelThroughputTooltipText"></span>
</span>
</div>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<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,
showAutoPilot: !isFreeTierAccount()
}">
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared">
<throughput-input 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,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue
}">
</throughput-input>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" value="OK" class="btncreatecoll1">
</div>
</div>
<!-- Add database inputs - End -->
</script>

View File

@@ -0,0 +1,49 @@
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
jest.mock("../Tabs/NotebookTab");
describe("Add Database Pane", () => {
describe("getSharedThroughputDefault()", () => {
let explorer: ViewModels.Explorer;
beforeEach(() => {
explorer = new Explorer({
documentClientUtility: null,
notificationsClient: null,
isEmulator: false
});
});
it("should be true if subscription type is Benefits", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be false if subscription type is EA", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
});
it("should be true if subscription type is Free", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is Internal", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is PAYG", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
});
});

View File

@@ -0,0 +1,600 @@
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import editable from "../../Common/EditableUtility";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { CosmosClient } from "../../Common/CosmosClient";
import { PlatformType } from "../../PlatformType";
export default class AddDatabasePane extends ContextualPaneBase implements ViewModels.AddDatabasePane {
public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>;
public databaseId: ko.Observable<string>;
public databaseIdTooltipText: ko.Computed<string>;
public databaseLevelThroughputTooltipText: ko.Computed<string>;
public databaseCreateNewShared: ko.Observable<boolean>;
public formErrorsDetails: ko.Observable<string>;
public throughput: ViewModels.Editable<number>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public maxThroughputRUText: ko.PureComputed<string>;
public throughputRangeText: ko.Computed<string>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public maxAutoPilotThroughputSet: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title((this.container && this.container.addDatabaseText()) || "New Database");
this.databaseId = ko.observable<string>();
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
// TODO 388844: get defaults from parent frame
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.container.subscriptionType &&
this.container.subscriptionType.subscribe(subscriptionType => {
this.databaseCreateNewShared(this.getSharedThroughputDefault());
});
this.databaseIdLabel = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id"
);
this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = this.container.isPreferredApiCassandra();
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${
isCassandraAccount ? "tables" : "collections"
}`;
});
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = this.container.isPreferredApiCassandra();
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
});
this.throughput = editable.observable<number>();
this.maxThroughputRU = ko.observable<number>();
this.minThroughputRU = ko.observable<number>();
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.selectedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.autoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (!autoPilot) {
return "";
}
return !this.hasAutoPilotV2FeatureFlag()
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, true /* isDatabaseThroughput */);
});
this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag());
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.requestUnitsUsageCost = ko.computed(() => {
const offerThroughput: number = this.throughput();
if (
offerThroughput < this.minThroughputRU() ||
(offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue())
) {
return "";
}
const account = this.container.databaseAccount();
if (!account) {
return "";
}
const serverId = this.container.serverId();
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
let estimatedSpendAcknowledge: string;
let estimatedSpend: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(),
serverId,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected()
);
}
// TODO: change throughputSpendAckText to be a computed value, instead of having this side effect
this.throughputSpendAckText(estimatedSpendAcknowledge);
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
!this.container.isEmulator &&
!this.container.isTryCosmosDBSubscription() &&
this.container.getPlatformType() !== PlatformType.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =
databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier;
return isFreeTierAccount;
});
this.maxThroughputRUText = ko.pureComputed(() => {
return this.maxThroughputRU().toLocaleString();
});
this.costsVisible = ko.pureComputed(() => {
return !this.container.isEmulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
const selectedThroughput: number = this.throughput();
const maxRU: number = this.maxThroughputRU && this.maxThroughputRU();
const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
const isThroughputSetGreaterThanDefault: boolean =
selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
if (this.canExceedMaximumValue()) {
return isThroughputSetGreaterThanDefault;
}
return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault;
});
this.databaseCreateNewShared.subscribe((useShared: boolean) => {
this._updateThroughputLimitByDatabase();
});
this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(this.container.serverId());
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click for more details`;
});
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public open() {
super.open();
this.resetData();
const addDatabasePaneOpenMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
throughput: this.throughput(),
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane
};
const focusElement = document.getElementById("database-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
const offerThroughput: number = this._computeOfferThroughput();
const addDatabasePaneStartMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
}),
offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
this.formErrors("");
this.isExecuting(true);
const createDatabaseParameters: DataModels.RpParameters = {
db: addDatabasePaneStartMessage.database.id,
st: addDatabasePaneStartMessage.database.shared,
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: addDatabasePaneStartMessage.databaseAccountName
};
const autopilotSettings = this._getAutopilotSettings();
if (this.container.isPreferredApiCassandra()) {
this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) {
this._createGremlinDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) {
this._createSqlDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else {
this._createDatabase(offerThroughput, startKey);
}
}
private _createSqlDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
}
);
}
private _createMongoDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createMongoDatabaseWithARM(
this.container.armEndpoint(),
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
}
private _createGremlinDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createGremlinDatabase(
this.container.armEndpoint(),
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
}
public resetData() {
this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.selectedAutoPilotTier(undefined);
this.isAutoPilotSelected(false);
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase();
this.throughputSpendAck(false);
super.resetData();
}
public getSharedThroughputDefault(): boolean {
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA) {
return false;
}
return true;
}
private _createDatabase(offerThroughput: number, telemetryStartKey: number): void {
const autoPilot: DataModels.AutoPilotCreationSettings = this._isAutoPilotSelectedAndWhatTier();
const createRequest: DataModels.CreateDatabaseRequest = {
databaseId: this.databaseId().trim(),
offerThroughput,
databaseLevelThroughput: this.databaseCreateNewShared(),
autoPilot,
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
};
this.container.documentClientUtility.createDatabase(createRequest).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
}
);
}
private _createKeyspace(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
): void {
if (EnvironmentUtility.isAadUser()) {
this._createKeyspaceUsingRP(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings, startKey);
} else {
this._createKeyspaceUsingProxy(createDatabaseParameters.offerThroughput, startKey);
}
}
private _createKeyspaceUsingProxy(offerThroughput: number, telemetryStartKey: number): void {
const provisionThroughputQueryPart: string = this.databaseCreateNewShared()
? `AND cosmosdb_provisioned_throughput=${offerThroughput}`
: "";
const createKeyspaceQuery: string = `CREATE KEYSPACE ${this.databaseId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } ${provisionThroughputQueryPart};`;
(this.container.tableDataClient as CassandraAPIDataClient)
.createKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
this.container,
createKeyspaceQuery
)
.then(
() => {
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, telemetryStartKey);
}
);
}
private _createKeyspaceUsingRP(
armEndpoint: string,
createKeyspaceParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
): void {
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
});
});
}
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
const addDatabasePaneSuccessMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
this.resetData();
}
private _onCreateDatabaseFailure(reason: any, offerThroughput: number, startKey: number): void {
this.isExecuting(false);
const message = ErrorParserUtility.parse(reason);
this.formErrors(message[0].message);
this.formErrorsDetails(message[0].message);
const addDatabasePaneFailedMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: reason
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}
private _getThroughput(): number {
const throughput: number = this.throughput();
return isNaN(throughput) ? 0 : Number(throughput);
}
private _computeOfferThroughput(): number {
return this.isAutoPilotSelected() ? undefined : this._getThroughput();
}
private _isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts
if (this.isAutoPilotSelected()) {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (
(!this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) {
this.formErrors(
!this.hasAutoPilotV2FeatureFlag()
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
);
return false;
}
}
const throughput = this._getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
return true;
}
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if (
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.maxAutoPilotThroughputSet() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
}
return undefined;
}
private _getAutopilotSettings(): DataModels.RpOptions {
if (
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
[Constants.HttpHeaders.autoPilotThroughput]: `{ "maxThroughput": ${this.maxAutoPilotThroughputSet() * 1} }`
}
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() };
}
return undefined;
}
private _updateThroughputLimitByDatabase() {
const subscriptionType = this.container.subscriptionType();
const flight = this.container.flight();
const throughputDefaults: AddCollectionUtility.ThroughputDefaults = AddCollectionUtility.Utilities.getDefaultThroughput(
flight,
subscriptionType
);
this.throughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
}
}

View File

@@ -0,0 +1,33 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="browsequeriespane">
<!-- Save Query form -- Start -->
<div class="contextual-pane-in">
<div class="paneContentContainer">
<!-- Save Query header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Save Query header - End -->
<!-- Save Query inputs - Start -->
<div class="paneMainContent"><div class="pkPadding" data-bind="react: queriesGridComponentAdapter"></div></div>
</div>
</div>
<!-- Save Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,104 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { Logger } from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
export class BrowseQueriesPane extends ContextualPaneBase implements ViewModels.BrowseQueriesPane {
public queriesGridComponentAdapter: QueriesGridComponentAdapter;
public canSaveQueries: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Open Saved Queries");
this.resetData();
this.canSaveQueries = this.container && this.container.canSaveQueries;
this.queriesGridComponentAdapter = new QueriesGridComponentAdapter(this.container);
}
public open() {
super.open();
this.queriesGridComponentAdapter.forceRender();
}
public close() {
super.close();
this.queriesGridComponentAdapter.forceRender();
}
public submit() {
// override default behavior because this is not a form
}
public setupQueries = async (src: any, event: MouseEvent): Promise<void> => {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
});
try {
this.isExecuting(true);
await this.container.queriesClient.setupQueriesCollection();
this.container.refreshAllDatabases().done(() => this.queriesGridComponentAdapter.forceRender());
TelemetryProcessor.traceSuccess(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
this.formErrors("Failed to setup a collection for saved queries");
this.formErrors(`Failed to setup a collection for saved queries: ${JSON.stringify(error)}`);
} finally {
this.isExecuting(false);
}
};
public loadSavedQuery = (savedQuery: DataModels.Query): void => {
const selectedCollection: ViewModels.Collection = this.container && this.container.findSelectedCollection();
if (!selectedCollection) {
// should never get into this state because this pane is only accessible through the query tab
Logger.logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");
return;
} else if (this.container.isPreferredApiMongoDB()) {
selectedCollection.onNewMongoQueryClick(selectedCollection, null);
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const queryTab: ViewModels.QueryTab = this.container.findActiveTab() as ViewModels.QueryTab;
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);
queryTab.sqlQueryEditorContent(savedQuery.query);
TelemetryProcessor.trace(Action.LoadSavedQuery, ActionModifiers.Mark, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
queryName: savedQuery.queryName,
paneTitle: this.title()
});
this.close();
};
}

View File

@@ -0,0 +1,333 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="cassandraaddcollectionpane">
<!-- Add Cassandra collection form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
role="dialog"
aria-label="Add Table"
data-bind="
submit: submit"
>
<!-- Add Cassandra collection header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add Cassandra collection header - End -->
<!-- Add Cassandra 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>
</span>
</div>
</div>
<!-- Add Cassandra collection errors - End -->
<div class="paneMainContent">
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span> Keyspace name
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Select an existing keyspace or enter a new keyspace id.</span
>
</span>
</p>
<div class="createNewDatabaseOrUseExisting">
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Create new keyspace"
name="databaseType"
type="radio"
role="radio"
id="keyspaceCreateNew"
data-test="addCollection-newDatabase"
tabindex="0"
data-bind="checked: keyspaceCreateNew, checkedValue: true, attr: { 'aria-checked': keyspaceCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="keyspaceCreateNew">Create new</span>
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Use existing keyspace"
name="databaseType"
type="radio"
role="radio"
id="keyspaceUseExisting"
data-test="addCollection-existingDatabase"
tabindex="0"
data-bind="checked: keyspaceCreateNew, checkedValue: false, attr: { 'aria-checked': !keyspaceCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="keyspaceUseExisting">Use existing</span>
</div>
<input
id="keyspace-id"
data-test="addCollection-keyspaceId"
type="text"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new keyspace id"
size="40"
class="collid"
data-bind="visible: keyspaceCreateNew, textInput: keyspaceId, hasFocus: firstFieldHasFocus"
aria-label="Keyspace id"
aria-required="true"
autofocus
/>
<input
type="text"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
list="keyspacesList"
placeholder="Choose existing keyspace id"
size="40"
class="collid"
data-bind="visible: !keyspaceCreateNew(), textInput: keyspaceId, hasFocus: firstFieldHasFocus"
aria-label="Keyspace id"
/>
<datalist id="keyspacesList" data-bind="foreach: container.nonSystemDatabases">
<option data-bind="value: $data.id"> </option>
</datalist>
<div
class="databaseProvision"
aria-label="New database provision support"
data-bind="visible: keyspaceCreateNew"
>
<input
tabindex="0"
type="checkbox"
id="keyspaceSharedThroughput"
title="Provision shared throughput"
data-bind="checked: keyspaceHasSharedOffer"
/>
<span class="databaseProvisionText" for="keyspaceSharedThroughput">Provision keyspace 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 keyspace level will be shared across unlimited number of tables within
the keyspace</span
>
</span>
</div>
<!-- 1 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input-autopilot-v3
params="{
testId: 'cassandraThroughputValue-v3-shared',
value: keyspaceThroughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: keyspaceHasSharedOffer,
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
requestUnitsUsageCost: requestUnitsUsageCostShared,
spendAckChecked: sharedThroughputSpendAck,
spendAckId: 'sharedThroughputSpendAck-v3-shared',
spendAckText: sharedThroughputSpendAckText,
spendAckVisible: sharedThroughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-databaseThroughput-autoPilotRadio-v3-shared',
throughputProvisionedRadioId: 'newKeyspace-databaseThroughput-manualRadio-v3-shared',
isAutoPilotSelected: isSharedAutoPilotSelected,
maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
costsVisible: costsVisible,
showAutoPilot: !isFreeTierAccount()
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input
params="{
testId: 'cassandraThroughputValue-v2-shared',
value: keyspaceThroughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: keyspaceHasSharedOffer,
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
requestUnitsUsageCost: requestUnitsUsageCostShared,
spendAckChecked: sharedThroughputSpendAck,
spendAckId: 'sharedThroughputSpendAck-v2-shared',
spendAckText: sharedThroughputSpendAckText,
spendAckVisible: sharedThroughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-databaseThroughput-autoPilotRadio-v2-shared',
throughputProvisionedRadioId: 'newKeyspace-databaseThroughput-manualRadio-v2-shared',
isAutoPilotSelected: isSharedAutoPilotSelected,
autoPilotTiersList: sharedAutoPilotTiersList,
costsVisible: costsVisible,
selectedAutoPilotTier: selectedSharedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue
}"
>
</throughput-input>
</div>
<!-- /ko -->
</div>
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span> Enter CQL command to create the table.
<a href="https://aka.ms/cassandra-create-table" target="_blank">Learn More</a>
</p>
<div data-bind="text: createTableQuery" style="float:left; padding-top:3px; padding-right:3px;"></div>
<input
type="text"
data-test="addCollection-tableId"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
data-test="addCollection-tableId"
placeholder="Enter tableId"
size="20"
class="textfontclr"
data-bind="value: tableId"
style="margin-bottom: 5px;"
/>
<textarea
id="editor-area"
rows="15"
aria-label="Table Schema"
data-bind="value: userTableQuery"
style="height:125px; width: calc(100% - 80px); resize: vertical;"
></textarea>
</div>
<div class="seconddivpadding" data-bind="visible: keyspaceHasSharedOffer() && !keyspaceCreateNew()">
<input
type="checkbox"
id="tableSharedThroughput"
title="Provision dedicated throughput for this table"
data-bind="checked: dedicateTableThroughput"
/>
<span for="tableSharedThroughput">Provision dedicated throughput for this table</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 table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.</span
>
</span>
</div>
<!-- 2 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input-autopilot-v3
params="{
testId: 'cassandraSharedThroughputValue-v3-dedicated',
value: throughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !keyspaceHasSharedOffer() || dedicateTableThroughput(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCostDedicated,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCassandra-v3-dedicated',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-containerThroughput-autoPilotRadio-v3-dedicated',
throughputProvisionedRadioId: 'newKeyspace-containerThroughput-manualRadio-v3-dedicated',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: selectedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
overrideWithAutoPilotSettings: false,
overrideWithProvisionedThroughputSettings: false
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input
params="{
testId: 'cassandraSharedThroughputValue-v2-dedicated',
value: throughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !keyspaceHasSharedOffer() || dedicateTableThroughput(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCostDedicated,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCassandra-v2-dedicated',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-containerThroughput-autoPilotRadio-v2-dedicated',
throughputProvisionedRadioId: 'newKeyspace-containerThroughput-manualRadio-v2-dedicated',
isAutoPilotSelected: isAutoPilotSelected,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
showAutoPilot: false,
canExceedMaximumValue: canExceedMaximumValue
}"
>
</throughput-input>
</div>
<!-- /ko -->
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="addCollection-createCollection" value="OK" class="btncreatecoll1" />
</div>
</div>
</form>
<!-- Add Cassandra collection form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" alt="loading indicator" />
</div>
<!-- Loader - End -->
</div>
</div>
</div>

View File

@@ -0,0 +1,615 @@
import * as _ from "underscore";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap";
export default class CassandraAddCollectionPane extends ContextualPaneBase
implements ViewModels.CassandraAddCollectionPane {
public createTableQuery: ko.Observable<string>;
public keyspaceId: ko.Observable<string>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public tableId: ko.Observable<string>;
public throughput: ko.Observable<number>;
public throughputRangeText: ko.Computed<string>;
public sharedThroughputRangeText: ko.Computed<string>;
public userTableQuery: ko.Observable<string>;
public requestUnitsUsageCostDedicated: ko.Computed<string>;
public requestUnitsUsageCostShared: ko.Computed<string>;
public costsVisible: ko.PureComputed<boolean>;
public keyspaceHasSharedOffer: ko.Observable<boolean>;
public keyspaceIds: ko.ObservableArray<string>;
public keyspaceThroughput: ko.Observable<number>;
public keyspaceCreateNew: ko.Observable<boolean>;
public dedicateTableThroughput: ko.Observable<boolean>;
public canRequestSupport: ko.PureComputed<boolean>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAckText: ko.Observable<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public selectedSharedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public sharedAutoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public isSharedAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotThroughput: ko.Observable<number>;
public sharedAutoPilotThroughput: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public sharedThroughputSpendAckVisible: ko.Computed<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public isFreeTierAccount: ko.Computed<boolean>;
public ruToolTipText: ko.Computed<string>;
private keyspaceOffers: HashMap<DataModels.Offer>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Add Table");
this.createTableQuery = ko.observable<string>("CREATE TABLE ");
this.keyspaceCreateNew = ko.observable<boolean>(true);
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.keyspaceOffers = new HashMap<DataModels.Offer>();
this.keyspaceIds = ko.observableArray<string>();
this.keyspaceHasSharedOffer = ko.observable<boolean>(false);
this.keyspaceThroughput = ko.observable<number>();
this.keyspaceId = ko.observable<string>("");
this.keyspaceId.subscribe((keyspaceId: string) => {
if (this.keyspaceIds.indexOf(keyspaceId) >= 0) {
this.keyspaceHasSharedOffer(this.keyspaceOffers.has(keyspaceId));
}
});
this.keyspaceId.extend({ rateLimit: 100 });
this.dedicateTableThroughput = ko.observable<boolean>(false);
const flight = this.container.flight();
const subscriptionType = this.container.subscriptionType();
const throughputDefaults = AddCollectionUtility.Utilities.getDefaultThroughput(flight, subscriptionType);
this.maxThroughputRU = ko.observable<number>(throughputDefaults.unlimitedmax);
this.minThroughputRU = ko.observable<number>(throughputDefaults.unlimitedmin);
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =
databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier;
return isFreeTierAccount;
});
this.tableId = ko.observable<string>("");
this.selectedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.selectedSharedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.isSharedAutoPilotSelected = ko.observable<boolean>(false);
this.selectedAutoPilotThroughput = ko.observable<number>();
this.sharedAutoPilotThroughput = ko.observable<number>();
this.throughput = ko.observable<number>();
this.throughputRangeText = ko.pureComputed<string>(() => {
const enableAutoPilot = this.isAutoPilotSelected();
if (!enableAutoPilot) {
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag());
});
this.sharedThroughputRangeText = ko.pureComputed<string>(() => {
if (this.isSharedAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag());
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.userTableQuery = ko.observable<string>("(userid int, name text, email text, PRIMARY KEY (userid))");
this.keyspaceId.subscribe(keyspaceId => {
this.createTableQuery(`CREATE TABLE ${keyspaceId}.`);
});
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.sharedThroughputSpendAck = ko.observable<boolean>(false);
this.sharedThroughputSpendAckText = ko.observable<string>();
this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.requestUnitsUsageCostDedicated = ko.computed(() => {
const account = this.container.databaseAccount();
if (!account) {
return "";
}
const serverId = this.container.serverId();
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const offerThroughput: number = this.throughput();
let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.selectedAutoPilotThroughput(),
serverId,
regions,
multimaster
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.selectedAutoPilotThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected()
);
}
this.throughputSpendAckText(estimatedDedicatedSpendAcknowledge);
return estimatedSpend;
});
this.requestUnitsUsageCostShared = ko.computed(() => {
const account = this.container.databaseAccount();
if (!account) {
return "";
}
const serverId = this.container.serverId();
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.keyspaceThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(),
serverId,
regions,
multimaster
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected()
);
}
this.sharedThroughputSpendAckText(estimatedSharedSpendAcknowledge);
return estimatedSpend;
});
this.costsVisible = ko.pureComputed(() => {
return !this.container.isEmulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
return this.keyspaceThroughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
return this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
});
if (!!this.container) {
const updateKeyspaceIds: (keyspaces: ViewModels.Database[]) => void = (
newKeyspaceIds: ViewModels.Database[]
): void => {
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) {
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
} else if (keyspace && keyspace.isDatabaseShared && keyspace.isDatabaseShared()) {
keyspace.readSettings();
}
return keyspace.id();
});
this.keyspaceIds(cachedKeyspaceIdsList);
};
this.container.nonSystemDatabases.subscribe((newDatabases: ViewModels.Database[]) =>
updateKeyspaceIds(newDatabases)
);
updateKeyspaceIds(this.container.nonSystemDatabases());
}
this.autoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.sharedAutoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._getAutoPilot();
if (!autoPilot) {
return "";
}
const isDatabaseThroughput: boolean = this.keyspaceCreateNew();
return !this.hasAutoPilotV2FeatureFlag()
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, isDatabaseThroughput);
});
}
public decreaseThroughput() {
let offerThroughput: number = this.throughput();
if (offerThroughput > this.minThroughputRU()) {
offerThroughput -= 100;
this.throughput(offerThroughput);
}
}
public increaseThroughput() {
let offerThroughput: number = this.throughput();
if (offerThroughput < this.maxThroughputRU()) {
offerThroughput += 100;
this.throughput(offerThroughput);
}
}
public open() {
super.open();
const addCollectionPaneOpenMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
rupm: false
}),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane
};
const focusElement = document.getElementById("keyspace-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
this.isExecuting(true);
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
let createTableAndKeyspacePromise: Q.Promise<any>;
const toCreateKeyspace: boolean = this.keyspaceCreateNew();
const useAutoPilotForKeyspace: boolean =
(!this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected() && !!this.selectedSharedAutoPilotTier());
const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = this.keyspaceHasSharedOffer()
? useAutoPilotForKeyspace
? !this.hasAutoPilotV2FeatureFlag()
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};`
: `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.selectedSharedAutoPilotTier()};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};`
: `${createKeyspaceQueryPrefix};`;
const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`;
let createTableQuery: string;
if (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer()) {
if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) {
createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`;
} else {
createTableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${this.throughput()};`;
}
} else {
createTableQuery = `${createTableQueryPrefix};`;
}
const addCollectionPaneStartMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
if (toCreateKeyspace) {
createTableAndKeyspacePromise = (<CassandraAPIDataClient>this.container.tableDataClient).createTableAndKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
this.container,
createTableQuery,
createKeyspaceQuery
);
} else {
createTableAndKeyspacePromise = (<CassandraAPIDataClient>this.container.tableDataClient).createTableAndKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
this.container,
createTableQuery
);
}
createTableAndKeyspacePromise.then(
() => {
this.container.refreshAllDatabases();
this.isExecuting(false);
this.close();
const addCollectionPaneSuccessMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
},
reason => {
this.formErrors(reason.exceptionMessage);
this.isExecuting(false);
const addCollectionPaneFailedMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collection: {
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput()
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery,
error: reason
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
);
}
public resetData() {
super.resetData();
const throughputDefaults = AddCollectionUtility.Utilities.getDefaultThroughput(
this.container.flight(),
this.container.subscriptionType()
);
this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(false);
this.selectedAutoPilotTier(null);
this.selectedSharedAutoPilotTier(null);
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.throughput(throughputDefaults.unlimited(this.container));
this.keyspaceThroughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
this.createTableQuery("CREATE TABLE ");
this.userTableQuery("(userid int, name text, email text, PRIMARY KEY (userid))");
this.tableId("");
this.keyspaceId("");
this.throughputSpendAck(false);
this.keyspaceHasSharedOffer(false);
this.keyspaceCreateNew(true);
}
private _isValid(): boolean {
const throughput = this.throughput();
const keyspaceThroughput = this.keyspaceThroughput();
const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isSharedAutoPilotSelected() &&
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.sharedThroughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() &&
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
if (
(this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected()) ||
this.isAutoPilotSelected()
) {
const autoPilot = this._getAutoPilot();
if (
(!this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) {
this.formErrors(
!this.hasAutoPilotV2FeatureFlag()
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
);
return false;
}
return true;
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
if (
this.keyspaceHasSharedOffer() &&
this.keyspaceCreateNew() &&
keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.sharedThroughputSpendAck()
) {
this.formErrors("Please acknowledge the estimated daily spend");
return false;
}
return true;
}
private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if (
(!this.hasAutoPilotV2FeatureFlag() &&
this.keyspaceCreateNew() &&
this.keyspaceHasSharedOffer() &&
this.isSharedAutoPilotSelected() &&
this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() &&
this.keyspaceCreateNew() &&
this.keyspaceHasSharedOffer() &&
this.isSharedAutoPilotSelected() &&
this.selectedSharedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.sharedAutoPilotThroughput() * 1
}
: { autopilotTier: this.selectedSharedAutoPilotTier() };
}
if (
(!this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.selectedAutoPilotThroughput() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
}
return undefined;
}
}

View File

@@ -0,0 +1,59 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="clusterLibraryPane">
<!-- Cluster Library -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Cluster Library header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Cluster Library header - End -->
<!-- Cluster Library 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"
>More details</a
>
</span>
</div>
</div>
<!-- Cluster Library errors - End -->
<!-- Cluster Library inputs - Start -->
<div class="paneMainContent"><div data-bind="react: clusterLibraryGridAdapter"></div></div>
<!-- Cluster Library inputs - End -->
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Save" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Cluster Library - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,236 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ClusterLibraryGridAdapter } from "../Controls/LibraryManagement/ClusterLibraryGridAdapter";
import { ClusterLibraryGridProps, ClusterLibraryItem } from "../Controls/LibraryManagement/ClusterLibraryGrid";
import { Library, SparkCluster, SparkClusterLibrary } from "../../Contracts/DataModels";
import { Logger } from "../../Common/Logger";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class ClusterLibraryPane extends ContextualPaneBase {
public clusterLibraryGridAdapter: ClusterLibraryGridAdapter;
private _clusterLibraryProps: ko.Observable<ClusterLibraryGridProps>;
private _originalCluster: SparkCluster;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Cluster Libraries");
this._clusterLibraryProps = ko.observable<ClusterLibraryGridProps>({
libraryItems: [],
onInstalledChanged: this._onInstalledChanged
});
this.clusterLibraryGridAdapter = new ClusterLibraryGridAdapter();
this.clusterLibraryGridAdapter.parameters = this._clusterLibraryProps;
this.resetData();
}
public open(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
Promise.all([this._getLibraries(resourceId), this._getDefaultCluster(resourceId)]).then(
result => {
const [libraries, cluster] = result;
this._originalCluster = cluster;
const libraryItems = this._mapClusterLibraries(cluster, libraries);
this._updateClusterLibraryGridStates({ libraryItems });
},
reason => {
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
super.open();
}
public submit(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
if (this._areLibrariesChanged()) {
const newLibraries = this._clusterLibraryProps()
.libraryItems.filter(lib => lib.installed)
.map(lib => ({ name: lib.name }));
this._updateClusterLibraries(resourceId, this._originalCluster, newLibraries).then(
() => {
this.isExecuting(false);
this.close();
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
} else {
this.isExecuting(false);
this.close();
}
}
private _updateClusterLibraryGridStates(states: Partial<ClusterLibraryGridProps>): void {
const merged = { ...this._clusterLibraryProps(), ...states };
this._clusterLibraryProps(merged);
this._clusterLibraryProps.valueHasMutated();
}
private _onInstalledChanged = (libraryName: string, installed: boolean): void => {
const items = this._clusterLibraryProps().libraryItems;
const library = items.find(item => item.name === libraryName);
library.installed = installed;
this._clusterLibraryProps.valueHasMutated();
};
private _areLibrariesChanged(): boolean {
const original = this._originalCluster.properties && this._originalCluster.properties.libraries;
const changed = this._clusterLibraryProps()
.libraryItems.filter(lib => lib.installed)
.map(lib => lib.name);
if (original.length !== changed.length) {
return true;
}
const newLibraries = new Set(changed);
for (let o of original) {
if (!newLibraries.has(o.name)) {
return false;
}
newLibraries.delete(o.name);
}
return newLibraries.size === 0;
}
private _mapClusterLibraries(cluster: SparkCluster, libraries: Library[]): ClusterLibraryItem[] {
const clusterLibraries = cluster && cluster.properties && cluster.properties.libraries;
const libraryItems = libraries.map(lib => ({
...lib,
installed: clusterLibraries.some(clusterLib => clusterLib.name === lib.name)
}));
return libraryItems;
}
private async _getLibraries(resourceId: string): Promise<Library[]> {
if (!resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Fetching libraries...`
);
try {
return await this.container.sparkClusterManager.getLibrariesAsync(resourceId);
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch libraries. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _getDefaultCluster(resourceId: string, clusterId: string = "default"): Promise<SparkCluster> {
if (!resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Fetching cluster...`);
try {
const cluster = await this.container.sparkClusterManager.getClusterAsync(resourceId, clusterId);
return cluster;
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch cluster. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getCluster");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _updateClusterLibraries(
resourceId: string,
originalCluster: SparkCluster,
newLibrarys: SparkClusterLibrary[]
): Promise<void> {
if (!originalCluster || !resourceId) {
return Promise.reject("Invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("Cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.ClusterLibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
originalCluster,
newLibrarys
});
let newCluster = originalCluster;
newCluster.properties.libraries = newLibrarys;
const consoleId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating ${newCluster.name} libraries...`
);
try {
const cluster = await this.container.sparkClusterManager.updateClusterAsync(
resourceId,
originalCluster.name,
newCluster
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated ${newCluster.name} libraries.`
);
TelemetryProcessor.traceSuccess(Action.ClusterLibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
cluster
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${newCluster.name} libraries. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.ClusterLibraryManage, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
error: e
});
Logger.logError(e, "Explorer/_updateClusterLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(consoleId);
}
}
}

View File

@@ -0,0 +1,129 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { KeyCodes } from "../../Common/Constants";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
// TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel implements ViewModels.ContextualPane {
public id: string;
public container: ViewModels.Explorer;
public firstFieldHasFocus: ko.Observable<boolean>;
public formErrorsDetails: ko.Observable<string>;
public formErrors: ko.Observable<string>;
public title: ko.Observable<string>;
public visible: ko.Observable<boolean>;
public documentClientUtility: DocumentClientUtilityBase;
public isExecuting: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super();
this.id = options.id;
this.container = options.container;
this.documentClientUtility = options.documentClientUtility;
this.visible = options.visible || ko.observable(false);
this.firstFieldHasFocus = ko.observable<boolean>(false);
this.formErrors = ko.observable<string>();
this.title = ko.observable<string>();
this.formErrorsDetails = ko.observable<string>();
this.isExecuting = ko.observable<boolean>(false);
this.container.isNotificationConsoleExpanded.subscribe((isExpanded: boolean) => {
this.resizePane();
});
this.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
}
public cancel() {
this.close();
this.container.isAccountReady() &&
TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Close, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
}
public close() {
this.visible(false);
this.isExecuting(false);
this.resetData();
}
public open() {
this.visible(true);
this.firstFieldHasFocus(true);
this.resizePane();
this.container.isAccountReady() &&
TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Open, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
}
public resetData() {
this.firstFieldHasFocus(false);
this.formErrors(null);
this.formErrorsDetails(null);
}
public showErrorDetails() {
this.container.expandConsole();
}
public submit() {
this.close();
event.stopPropagation();
this.container.isAccountReady() &&
TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Submit, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
}
public onCloseKeyPress(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.close();
event.stopPropagation();
return false;
}
return true;
}
public onPaneKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Escape) {
this.close();
event.stopPropagation();
return false;
}
return true;
}
public onSubmitKeyPress(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.submit();
event.stopPropagation();
return false;
}
return true;
}
private resizePane(): void {
const paneElement: HTMLElement = document.getElementById(this.id);
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
const newPaneElementHeight = window.innerHeight - $(notificationConsoleElement).height();
$(paneElement).height(newPaneElementHeight);
}
}

View File

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

View File

@@ -0,0 +1,158 @@
import * as ko from "knockout";
import * as sinon from "sinon";
import Q from "q";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CollectionStub, DatabaseStub, ExplorerStub } from "../OpenActionsStubs";
import { TreeNode } from "../../Contracts/ViewModels";
describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => {
let explorer: ViewModels.Explorer;
beforeEach(() => {
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
});
it("should be true if 1 database and 1 collection", () => {
let database: ViewModels.Database = new DatabaseStub({});
database.collections = ko.observableArray<ViewModels.Collection>([new CollectionStub({})]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastCollection()).toBe(true);
});
it("should be false if if 1 database and 2 collection", () => {
let database: ViewModels.Database = new DatabaseStub({});
database.collections = ko.observableArray<ViewModels.Collection>([
new CollectionStub({}),
new CollectionStub({})
]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastCollection()).toBe(false);
});
it("should be false if 2 database and 1 collection each", () => {
let database: ViewModels.Database = new DatabaseStub({});
database.collections = ko.observableArray<ViewModels.Collection>([new CollectionStub({})]);
let database2: ViewModels.Database = new DatabaseStub({});
database2.collections = ko.observableArray<ViewModels.Collection>([new CollectionStub({})]);
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastCollection()).toBe(false);
});
it("should be false if 0 databases", () => {
let database: ViewModels.Database = new DatabaseStub({});
explorer.databases = ko.observableArray<ViewModels.Database>();
database.collections = ko.observableArray<ViewModels.Collection>();
expect(explorer.isLastCollection()).toBe(false);
});
});
describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
let fakeDocumentClientUtility = sinon.createStubInstance<DocumentClientUtilityBase>(
DocumentClientUtilityBase as any
);
let fakeExplorer = sinon.createStubInstance<ExplorerStub>(ExplorerStub as any);
sinon.stub(fakeExplorer, "isNotificationConsoleExpanded").value(ko.observable<boolean>(false));
let pane = new DeleteCollectionConfirmationPane({
documentClientUtility: fakeDocumentClientUtility as any,
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
});
fakeExplorer.isLastCollection.returns(true);
fakeExplorer.isSelectedDatabaseShared.returns(false);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastCollection.returns(true);
fakeExplorer.isSelectedDatabaseShared.returns(true);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(false);
fakeExplorer.isLastCollection.returns(false);
fakeExplorer.isSelectedDatabaseShared.returns(false);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(false);
});
});
describe("submit()", () => {
let telemetryProcessorSpy: sinon.SinonSpy;
beforeEach(() => {
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
});
afterEach(() => {
telemetryProcessorSpy.restore();
});
it("it should log feedback if last collection and database is not shared", () => {
let selectedCollectionId = "testCol";
let fakeDocumentClientUtility = sinon.createStubInstance<DocumentClientUtilityBase>(
DocumentClientUtilityBase as any
);
fakeDocumentClientUtility.deleteCollection.returns(Q.resolve(null));
let fakeExplorer = sinon.createStubInstance<ExplorerStub>(ExplorerStub as any);
fakeExplorer.findSelectedCollection.returns(
new CollectionStub({
id: ko.observable<string>(selectedCollectionId),
rid: "test"
})
);
sinon.stub(fakeExplorer, "isNotificationConsoleExpanded").value(ko.observable<boolean>(false));
sinon.stub(fakeExplorer, "selectedCollectionId").value(ko.observable<string>(selectedCollectionId));
fakeExplorer.isSelectedDatabaseShared.returns(false);
const SubscriptionId = "testId";
const AccountName = "testAccount";
sinon.stub(fakeExplorer, "databaseAccount").value(
ko.observable<ViewModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName
} as ViewModels.DatabaseAccount)
);
sinon.stub(fakeExplorer, "defaultExperience").value(ko.observable<string>("DocumentDB"));
sinon.stub(fakeExplorer, "isPreferredApiCassandra").value(
ko.computed(() => {
return false;
})
);
sinon.stub(fakeExplorer, "documentClientUtility").value(fakeDocumentClientUtility);
sinon.stub(fakeExplorer, "selectedNode").value(ko.observable<TreeNode>());
fakeExplorer.isLastCollection.returns(true);
fakeExplorer.isSelectedDatabaseShared.returns(false);
let pane = new DeleteCollectionConfirmationPane({
documentClientUtility: fakeDocumentClientUtility as any,
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
});
pane.collectionIdConfirmation = ko.observable<string>(selectedCollectionId);
const Feedback = "my feedback";
pane.containerDeleteFeedback(Feedback);
return pane.submit().then(() => {
expect(telemetryProcessorSpy.called).toBe(true);
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(
telemetryProcessorSpy.calledWith(
Action.DeleteCollection,
ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
)
).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,143 @@
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
implements ViewModels.DeleteCollectionConfirmationPane {
public collectionIdConfirmationText: ko.Observable<string>;
public collectionIdConfirmation: ko.Observable<string>;
public containerDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.collectionIdConfirmationText = ko.observable<string>("Confirm by typing the collection id");
this.collectionIdConfirmation = ko.observable<string>();
this.containerDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Collection");
this.resetData();
}
public submit(): Q.Promise<any> {
if (!this._isValid()) {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.formErrors("Input collection name does not match the selected collection");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}`
);
return Q.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedCollection = <ViewModels.Collection>this.container.findSelectedCollection();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
let promise: Q.Promise<any>;
if (this.container.isPreferredApiCassandra()) {
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
`DROP TABLE ${selectedCollection.databaseId}.${selectedCollection.id()};`,
this.container
);
} else {
promise = this.container.documentClientUtility.deleteCollection(selectedCollection);
}
return promise.then(
() => {
this.isExecuting(false);
this.close();
this.container.selectedNode(selectedCollection.database);
this.container.closeAllTabsForResource(selectedCollection.rid);
this.container.refreshAllDatabases();
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteCollection,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
if (this.shouldRecordFeedback()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.containerDeleteFeedback()
);
TelemetryProcessor.trace(
Action.DeleteCollection,
ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.containerDeleteFeedback("");
}
},
(reason: any) => {
this.isExecuting(false);
const message = ErrorParserUtility.parse(reason);
this.formErrors(message[0].message);
this.formErrorsDetails(message[0].message);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
}
);
}
public resetData() {
this.collectionIdConfirmation("");
super.resetData();
}
public open() {
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return this.container.isLastCollection() && !this.container.isSelectedDatabaseShared();
}
private _isValid(): boolean {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
if (!selectedCollection) {
return false;
}
return this.collectionIdConfirmation() === selectedCollection.id();
}
}

View File

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

View File

@@ -0,0 +1,151 @@
import * as ko from "knockout";
import * as sinon from "sinon";
import Q from "q";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer";
import { CollectionStub, DatabaseStub, ExplorerStub } from "../OpenActionsStubs";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels";
describe("Delete Database Confirmation Pane", () => {
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: ViewModels.Explorer;
beforeEach(() => {
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
});
it("should be true if only 1 database", () => {
let database: ViewModels.Database = new DatabaseStub({});
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
let database: ViewModels.Database = new DatabaseStub({});
let database2: ViewModels.Database = new DatabaseStub({});
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
let database: ViewModels.Database = new DatabaseStub({});
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
let database: ViewModels.Database = new DatabaseStub({});
database.collections = ko.observableArray<ViewModels.Collection>([new CollectionStub({})]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
let fakeDocumentClientUtility = sinon.createStubInstance<DocumentClientUtilityBase>(
DocumentClientUtilityBase as any
);
let fakeExplorer = sinon.createStubInstance<ExplorerStub>(ExplorerStub as any);
sinon.stub(fakeExplorer, "isNotificationConsoleExpanded").value(ko.observable<boolean>(false));
let pane = new DeleteDatabaseConfirmationPane({
documentClientUtility: fakeDocumentClientUtility as any,
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
});
fakeExplorer.isLastNonEmptyDatabase.returns(true);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastDatabase.returns(true);
fakeExplorer.isSelectedDatabaseShared.returns(true);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastNonEmptyDatabase.returns(false);
fakeExplorer.isLastDatabase.returns(true);
fakeExplorer.isSelectedDatabaseShared.returns(false);
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(false);
});
});
describe("submit()", () => {
let telemetryProcessorSpy: sinon.SinonSpy;
beforeEach(() => {
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
});
afterEach(() => {
telemetryProcessorSpy.restore();
});
it("on submit() it should log feedback if last non empty database or is last database that has shared throughput", () => {
let selectedDatabaseId = "testDB";
let fakeDocumentClientUtility = sinon.createStubInstance<DocumentClientUtilityBase>(
DocumentClientUtilityBase as any
);
fakeDocumentClientUtility.deleteDatabase.returns(Q.resolve(null));
let fakeExplorer = sinon.createStubInstance<ExplorerStub>(ExplorerStub as any);
fakeExplorer.findSelectedDatabase.returns(
new DatabaseStub({
id: ko.observable<string>(selectedDatabaseId),
rid: "test",
collections: ko.observableArray<ViewModels.Collection>()
})
);
sinon.stub(fakeExplorer, "isNotificationConsoleExpanded").value(ko.observable<boolean>(false));
sinon.stub(fakeExplorer, "selectedDatabaseId").value(ko.observable<string>(selectedDatabaseId));
fakeExplorer.isSelectedDatabaseShared.returns(false);
const SubscriptionId = "testId";
const AccountName = "testAccount";
sinon.stub(fakeExplorer, "databaseAccount").value(
ko.observable<ViewModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName
} as ViewModels.DatabaseAccount)
);
sinon.stub(fakeExplorer, "defaultExperience").value(ko.observable<string>("DocumentDB"));
sinon.stub(fakeExplorer, "isPreferredApiCassandra").value(
ko.computed(() => {
return false;
})
);
sinon.stub(fakeExplorer, "documentClientUtility").value(fakeDocumentClientUtility);
sinon.stub(fakeExplorer, "selectedNode").value(ko.observable<TreeNode>());
fakeExplorer.isLastNonEmptyDatabase.returns(true);
let pane = new DeleteDatabaseConfirmationPane({
documentClientUtility: fakeDocumentClientUtility as any,
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
});
pane.databaseIdConfirmation = ko.observable<string>(selectedDatabaseId);
const Feedback = "my feedback";
pane.databaseDeleteFeedback(Feedback);
return pane.submit().then(() => {
expect(telemetryProcessorSpy.called).toBe(true);
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(
telemetryProcessorSpy.calledWith(
Action.DeleteDatabase,
ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
)
).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,150 @@
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase
implements ViewModels.DeleteDatabaseConfirmationPane {
public databaseIdConfirmationText: ko.Observable<string>;
public databaseIdConfirmation: ko.Observable<string>;
public databaseDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.databaseIdConfirmationText = ko.observable<string>("Confirm by typing the database id");
this.databaseIdConfirmation = ko.observable<string>();
this.databaseDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Database");
this.resetData();
}
public submit(): Q.Promise<any> {
if (!this._isValid()) {
const selectedDatabase: ViewModels.Database = this.container.findSelectedDatabase();
this.formErrors("Input database name does not match the selected database");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}: ${this.formErrors()}`
);
return Q.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedDatabase = this.container.findSelectedDatabase();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDatabase, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
let promise: Q.Promise<any>;
if (this.container.isPreferredApiCassandra()) {
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
`DROP KEYSPACE ${selectedDatabase.id()};`,
this.container
);
} else {
promise = this.container.documentClientUtility.deleteDatabase(selectedDatabase);
}
return promise.then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.closeAllTabsForResource(selectedDatabase.rid);
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) => this.container.closeAllTabsForResource(collection.rid));
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
if (this.shouldRecordFeedback()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.databaseDeleteFeedback()
);
TelemetryProcessor.trace(
Action.DeleteDatabase,
ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.databaseDeleteFeedback("");
}
},
(reason: any) => {
this.isExecuting(false);
const message = ErrorParserUtility.parse(reason);
this.formErrors(message[0].message);
this.formErrorsDetails(message[0].message);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
}
);
}
public resetData() {
this.databaseIdConfirmation("");
super.resetData();
}
public open() {
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return (
this.container.isLastNonEmptyDatabase() ||
(this.container.isLastDatabase() && this.container.isSelectedDatabaseShared())
);
}
private _isValid(): boolean {
const selectedDatabase = this.container.findSelectedDatabase();
if (!selectedDatabase) {
return false;
}
return this.databaseIdConfirmation() === selectedDatabase.id();
}
}

View File

@@ -0,0 +1,165 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="executesprocparamspane">
<!-- Input params form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: execute">
<!-- Input params header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Input params header - End -->
<!-- Input params 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"
>More details</a
>
</span>
</div>
</div>
<!-- Input params errors - End -->
<!-- Script for each param clause to be used for executing a stored procedure -->
<script type="text/html" id="param-template">
<tr>
<td class="paramTemplateRow">
<select class="dataTypeSelector" data-bind="value: type, attr: { 'aria-label': type }">
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input class="valueTextBox" aria-label="Param" data-bind="textInput: value" />
<span class="spEntityAddCancel" data-bind="click: $parent.deleteParam.bind($parent, $index()), event: { keypress: $parent.onDeleteParamKeyPress.bind($parent, $index()) }" role="button" tabindex="0">
<img src="/Entity_cancel.svg" alt="Delete param">
</span>
<span class="spEntityAddCancel" data-bind="click: $parent.addNewParamAtIndex.bind($parent, $index()), event: { keypress: $parent.onAddNewParamAtIndexKeyPress.bind($parent, $index()) }" role="button" tabindex="0">
<img src="/Add-property.svg" alt="Add param">
</span>
</td>
</tr>
</script>
<!-- Input params input - Start -->
<div class="paneMainContent">
<div>
<!-- Partition key input - Start -->
<div class="partitionKeyContainer" data-bind="visible: collectionHasPartitionKey">
<div class="inputHeader">Partition key value</div>
<div class="scrollBox">
<table class="paramsClauseTable">
<thead>
<tr>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="paramTemplateRow">
<select
class="dataTypeSelector"
data-bind="value: partitionKeyType, attr: { 'aria-label': partitionKeyType }"
>
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input
class="partitionKeyValue"
id="partitionKeyValue"
role="textbox"
tabindex="0"
aria-label="Partition key value"
data-bind="textInput: partitionKeyValue"
autofocus
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Partition key input - End -->
<!-- Input params table - Start -->
<div class="paramsTable">
<div class="enterInputParams">Enter input parameters (if any)</div>
<div class="scrollBox" id="executeSprocParamsScroll">
<table class="paramsClauseTable">
<thead>
<tr>
<th class="paramTableTypeHead">Type</th>
<th>Param</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'param-template', foreach: params }"></tbody>
</table>
</div>
<div
id="addNewParamLink"
class="addNewParam"
data-bind="click: addNewParam, event: { keypress: onAddNewParamKeyPress }, attr:{ title: addNewParamLabel }"
role="button"
tabindex="0"
>
<span>
<img src="/Add-property.svg" alt="Add new param" />
<span class="addNewParamLabel" data-bind="text: addNewParamLabel" />
</span>
</div>
</div>
<!-- Input params table - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
value="Execute"
class="btncreatecoll1"
data-bind="{ css: { btnDisabled: !executeButtonEnabled() }}"
/>
</div>
</div>
<!-- Input param input - End -->
</form>
</div>
<!-- Input params form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,172 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export interface ExecuteSprocParam {
type: ko.Observable<string>;
value: ko.Observable<string>;
}
type UnwrappedExecuteSprocParam = {
type: string;
value: any;
};
export class ExecuteSprocParamsPane extends ContextualPaneBase implements ViewModels.ExecuteSprocParamsPane {
public params: ko.ObservableArray<ExecuteSprocParam>;
public partitionKeyType: ko.Observable<string>;
public partitionKeyValue: ko.Observable<string>;
public collectionHasPartitionKey: ko.Observable<boolean>;
public addNewParamLabel: string = "Add New Param";
public executeButtonEnabled: ko.Computed<boolean>;
private _selectedSproc: ViewModels.StoredProcedure;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Input parameters");
this.partitionKeyType = ko.observable<string>("custom");
this.partitionKeyValue = ko.observable<string>();
this.executeButtonEnabled = ko.computed<boolean>(() => this.validPartitionKeyValue());
this.params = ko.observableArray<ExecuteSprocParam>([
{ type: ko.observable<string>("string"), value: ko.observable<string>() }
]);
this.collectionHasPartitionKey = ko.observable<boolean>();
this.resetData();
}
public open() {
super.open();
const currentSelectedSproc: ViewModels.StoredProcedure =
this.container && this.container.findSelectedStoredProcedure();
if (!!currentSelectedSproc && !!this._selectedSproc && this._selectedSproc.rid !== currentSelectedSproc.rid) {
this.params([]);
this.partitionKeyValue("");
}
this._selectedSproc = currentSelectedSproc;
this.collectionHasPartitionKey((this.container && !!this.container.findSelectedCollection().partitionKey) || false);
const focusElement = document.getElementById("partitionKeyValue");
focusElement && focusElement.focus();
}
public execute = () => {
this.formErrors("");
const partitionKeyValue: string = (() => {
if (!this.collectionHasPartitionKey()) {
return undefined;
}
const type: string = this.partitionKeyType();
let value: string = this.partitionKeyValue();
if (type === "custom") {
if (value === "undefined" || value === undefined) {
return undefined;
}
if (value === "null" || value === null) {
return null;
}
try {
value = JSON.parse(value);
} catch (e) {
this.formErrors(`Invalid param specified: ${value}`);
this.formErrorsDetails(`Invalid param specified: ${value} is not a valid literal value`);
}
}
return value;
})();
const unwrappedParams: UnwrappedExecuteSprocParam[] = ko.toJS(this.params());
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = !this.params()
? undefined
: _.map(unwrappedParams, (unwrappedParam: UnwrappedExecuteSprocParam) => {
let paramValue: string = unwrappedParam.value;
if (unwrappedParam.type === "custom" && (paramValue === "undefined" || paramValue === "")) {
paramValue = undefined;
} else if (unwrappedParam.type === "custom") {
try {
paramValue = JSON.parse(paramValue);
} catch (e) {
this.formErrors(`Invalid param specified: ${paramValue}`);
this.formErrorsDetails(`Invalid param specified: ${paramValue} is not a valid literal value`);
}
}
unwrappedParam.value = paramValue;
return unwrappedParam;
});
if (this.formErrors()) {
return;
}
const sprocParams = wrappedSprocParams && _.pluck(wrappedSprocParams, "value");
this._selectedSproc.execute(sprocParams, partitionKeyValue);
this.close();
};
private validPartitionKeyValue = (): boolean => {
return !this.collectionHasPartitionKey || (this.partitionKeyValue() != null && this.partitionKeyValue().length > 0);
};
public addNewParam = (): void => {
this.params.push({ type: ko.observable<string>("string"), value: ko.observable<string>() });
this._maintainFocusOnAddNewParamLink();
};
public onAddNewParamKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParam();
event.stopPropagation();
return false;
}
return true;
};
public addNewParamAtIndex = (index: number): void => {
this.params.splice(index, 0, { type: ko.observable<string>("string"), value: ko.observable<string>() });
};
public onAddNewParamAtIndexKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParamAtIndex(index);
event.stopPropagation();
return false;
}
return true;
};
public deleteParam = (indexToRemove: number): void => {
const params = _.reject(this.params(), (param: ExecuteSprocParam, index: number) => {
return index === indexToRemove;
});
this.params(params);
};
public onDeleteParamKeyPress = (indexToRemove: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.deleteParam(indexToRemove);
event.stopPropagation();
return false;
}
return true;
};
public close(): void {
super.close();
this.formErrors("");
this.formErrorsDetails("");
}
private _maintainFocusOnAddNewParamLink(): void {
const addNewParamLink = document.getElementById("addNewParamLink");
addNewParamLink.scrollIntoView();
}
}

View File

@@ -0,0 +1,14 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="gitHubReposPane">
<div class="contextual-pane-in">
<div class="paneContentContainer" data-bind="react: gitHubReposAdapter" />
</div>
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" alt="loading indicator image" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,349 @@
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { Logger } from "../../Common/Logger";
import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { GitHubUtils } from "../../Utils/GitHubUtils";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponentProps, RepoListItem, GitHubReposComponent } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
export class GitHubReposPane extends ContextualPaneBase {
private static readonly PageSize = 30;
private gitHubClient: GitHubClient;
private junoClient: JunoClient;
private branchesProps: Record<string, BranchesProps>;
private pinnedReposProps: PinnedReposProps;
private unpinnedReposProps: UnpinnedReposProps;
private gitHubReposProps: GitHubReposComponentProps;
private gitHubReposAdapter: GitHubReposComponentAdapter;
private allGitHubRepos: IGitHubRepo[];
private pinnedReposUpdated: boolean;
constructor(options: ViewModels.GitHubReposPaneOptions) {
super(options);
this.gitHubClient = options.gitHubClient;
this.junoClient = options.junoClient;
this.branchesProps = {};
this.pinnedReposProps = {
repos: []
};
this.unpinnedReposProps = {
repos: [],
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreUnpinnedRepos()
};
this.gitHubReposProps = {
showAuthorizeAccess: true,
authorizeAccessProps: {
scope: this.getOAuthScope(),
authorizeAccess: (scope): void => this.connectToGitHub(scope)
},
reposListProps: {
branchesProps: this.branchesProps,
pinnedReposProps: this.pinnedReposProps,
unpinnedReposProps: this.unpinnedReposProps,
pinRepo: (item): Promise<void> => this.pinRepo(item),
unpinRepo: (item): Promise<void> => this.unpinRepo(item)
},
addRepoProps: {
container: this.container,
getRepo: (owner, repo): Promise<IGitHubRepo> => this.getRepo(owner, repo),
pinRepo: (item): Promise<void> => this.pinRepo(item)
},
resetConnection: (): void => this.setup(true),
onOkClick: (): Promise<void> => this.submit(),
onCancelClick: (): void => this.cancel()
};
this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps);
this.allGitHubRepos = [];
this.pinnedReposUpdated = false;
}
public open(): void {
this.resetData();
this.setup();
super.open();
}
public async submit(): Promise<void> {
const pinnedReposUpdated = this.pinnedReposUpdated;
const reposToPin: IPinnedRepo[] = this.pinnedReposProps.repos.map(repo => GitHubUtils.toPinnedRepo(repo));
// Submit resets data too
super.submit();
if (pinnedReposUpdated) {
try {
const response = await this.junoClient.updatePinnedRepos(reposToPin);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
}
} catch (error) {
const message = `Failed to save pinned repos: ${error}`;
Logger.logError(message, "GitHubReposPane/submit");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
}
}
}
public resetData(): void {
// Reset cached branches
this.branchesProps = {};
this.gitHubReposProps.reposListProps.branchesProps = this.branchesProps;
// Reset cached pinned and unpinned repos
this.pinnedReposProps.repos = [];
this.unpinnedReposProps.repos = [];
// Reset cached repos
this.allGitHubRepos = [];
// Reset flags
this.pinnedReposUpdated = false;
this.unpinnedReposProps.hasMore = true;
this.unpinnedReposProps.isLoading = true;
this.triggerRender();
super.resetData();
}
private getOAuthScope(): string {
return (
this.container.gitHubOAuthService?.getTokenObservable()()?.scope || AuthorizeAccessComponent.Scopes.Public.key
);
}
private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.container.gitHubOAuthService.isLoggedIn()
? this.setupForConnectToGitHub()
: this.setupForManageRepos();
}
private setupForConnectToGitHub(): void {
this.gitHubReposProps.showAuthorizeAccess = true;
this.gitHubReposProps.authorizeAccessProps.scope = this.getOAuthScope();
this.isExecuting(false);
this.title(GitHubReposComponent.ConnectToGitHubTitle); // Used for telemetry
this.triggerRender();
}
private async setupForManageRepos(): Promise<void> {
this.gitHubReposProps.showAuthorizeAccess = false;
this.isExecuting(false);
this.title(GitHubReposComponent.ManageGitHubRepoTitle); // Used for telemetry
TelemetryProcessor.trace(Action.NotebooksGitHubManageRepo, ActionModifiers.Mark, {
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
dataExplorerArea: Areas.Notebook
});
this.triggerRender();
this.refreshManageReposComponent();
}
private calculateUnpinnedRepos(): RepoListItem[] {
const unpinnedGitHubRepos = this.allGitHubRepos.filter(
gitHubRepo =>
this.pinnedReposProps.repos.findIndex(
pinnedRepo => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name)
) === -1
);
return unpinnedGitHubRepos.map(gitHubRepo => ({
key: GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name),
repo: gitHubRepo,
branches: []
}));
}
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner.login, repo.name)];
branchesProps.hasMore = true;
branchesProps.isLoading = true;
this.triggerRender();
const nextPage = Math.floor(branchesProps.branches.length / GitHubReposPane.PageSize) + 1;
try {
const response = await this.gitHubClient.getBranchesAsync(
repo.owner.login,
repo.name,
nextPage,
GitHubReposPane.PageSize
);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching branches`);
}
if (response.data) {
branchesProps.branches = branchesProps.branches.concat(response.data);
}
} catch (error) {
const message = `Failed to fetch branches: ${error}`;
Logger.logError(message, "GitHubReposPane/loadMoreBranches");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
}
branchesProps.isLoading = false;
branchesProps.hasMore = branchesProps.branches.length === GitHubReposPane.PageSize * nextPage;
this.triggerRender();
}
private async loadMoreUnpinnedRepos(): Promise<void> {
this.unpinnedReposProps.isLoading = true;
this.unpinnedReposProps.hasMore = true;
this.triggerRender();
const nextPage = Math.floor(this.allGitHubRepos.length / GitHubReposPane.PageSize) + 1;
try {
const response = await this.gitHubClient.getReposAsync(nextPage, GitHubReposPane.PageSize);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`);
}
if (response.data) {
this.allGitHubRepos = this.allGitHubRepos.concat(response.data);
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
}
} catch (error) {
const message = `Failed to fetch unpinned repos: ${error}`;
Logger.logError(message, "GitHubReposPane/loadMoreUnpinnedRepos");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
}
this.unpinnedReposProps.isLoading = false;
this.unpinnedReposProps.hasMore = this.allGitHubRepos.length === GitHubReposPane.PageSize * nextPage;
this.triggerRender();
}
private async getRepo(owner: string, repo: string): Promise<IGitHubRepo> {
try {
const response = await this.gitHubClient.getRepoAsync(owner, repo);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching repo`);
}
return response.data;
} catch (error) {
const message = `Failed to fetch repo: ${error}`;
Logger.logError(message, "GitHubReposPane/getRepo");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
return Promise.resolve(undefined);
}
}
private async pinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
const initialReposLength = this.pinnedReposProps.repos.length;
const existingRepo = this.pinnedReposProps.repos.find(repo => repo.key === item.key);
if (existingRepo) {
existingRepo.branches = item.branches;
} else {
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
}
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
this.triggerRender();
if (this.pinnedReposProps.repos.length > initialReposLength) {
this.refreshBranchesForPinnedRepos();
}
}
private async unpinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
this.pinnedReposProps.repos = this.pinnedReposProps.repos.filter(pinnedRepo => pinnedRepo.key !== item.key);
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
this.triggerRender();
}
private async refreshManageReposComponent(): Promise<void> {
await this.refreshPinnedRepoListItems();
this.refreshBranchesForPinnedRepos();
this.refreshUnpinnedRepoListItems();
}
private async refreshPinnedRepoListItems(): Promise<void> {
this.pinnedReposProps.repos = [];
this.triggerRender();
try {
const response = await this.junoClient.getPinnedRepos(
this.container.gitHubOAuthService?.getTokenObservable()()?.scope
);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
}
if (response.data) {
const pinnedRepos = response.data.map(
pinnedRepo =>
({
key: GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name),
branches: pinnedRepo.branches,
repo: GitHubUtils.toGitHubRepo(pinnedRepo)
} as RepoListItem)
);
this.pinnedReposProps.repos = pinnedRepos;
this.triggerRender();
}
} catch (error) {
const message = `Failed to fetch pinned repos: ${error}`;
Logger.logError(message, "GitHubReposPane/refreshPinnedReposListItems");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
}
}
private refreshBranchesForPinnedRepos(): void {
this.pinnedReposProps.repos.map(item => {
if (!this.branchesProps[item.key]) {
this.branchesProps[item.key] = {
branches: [],
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo)
};
this.loadMoreBranches(item.repo);
}
});
}
private async refreshUnpinnedRepoListItems(): Promise<void> {
this.allGitHubRepos = [];
this.unpinnedReposProps.repos = [];
this.loadMoreUnpinnedRepos();
}
private connectToGitHub(scope: string): void {
this.isExecuting(true);
TelemetryProcessor.trace(Action.NotebooksGitHubAuthorize, ActionModifiers.Mark, {
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
dataExplorerArea: Areas.Notebook,
scopesSelected: scope
});
this.container.gitHubOAuthService.startOAuth(scope);
}
private triggerRender(): void {
this.gitHubReposAdapter.triggerRender();
}
}

View File

@@ -0,0 +1,60 @@
<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 }">
<!-- New Vertex form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- New Vertex header - Start -->
<div class="firstdivbg headerline">
<span>New Vertex</span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
tabindex="0"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- New Vertex header - End -->
<!-- New Vertex errors - Start -->
<div
aria-live="assertive"
class="warningErrorContainer"
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>
<!-- New Vertex errors - End -->
<!-- New Vertex inputs - Start -->
<div class="paneMainContent">
<new-vertex-form
class="newvertexContainer"
params="{ newVertexData: tempVertexData, firstFieldHasFocus: firstFieldHasFocus, partitionKeyProperty: partitionKeyProperty }"
></new-vertex-form>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
<!-- New Vertex inputs - End -->
</form>
</div>
<!-- New Vertex form - End -->
</div>
</div>

View File

@@ -0,0 +1,10 @@
@import "../../../less/Common/Constants";
.newvertexContainer {
height:100%;
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
.flex-display();
.flex-direction();
}

View File

@@ -0,0 +1,59 @@
<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 }">
<!-- Graph Styling form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Graph Styling header - Start -->
<div class="firstdivbg headerline">
<span>Graph Styling</span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Graph Styling header - End -->
<!-- Graph Styling errors - Start -->
<div
aria-live="assertive"
class="warningErrorContainer"
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"
>
More details
</a>
</span>
</div>
</div>
<!-- Graph Styling errors - End -->
<!-- Add graph configuration - Start -->
<div class="paneMainContent">
<graph-style
id="graphStyleComponent"
params="{ config:graphConfigUIData, firstFieldHasFocus: firstFieldHasFocus }"
></graph-style>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
<!-- Add Graph configuration - End -->
</form>
</div>
<!-- Graph Styling form - End -->
</div>
</div>

View File

@@ -0,0 +1,68 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class GraphStylingPane extends ContextualPaneBase implements ViewModels.GraphStylingPane {
public graphConfigUIData: ViewModels.GraphConfigUiData;
private remoteConfig: ViewModels.GraphConfigUiData;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.graphConfigUIData = {
showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY),
nodeProperties: ko.observableArray([]),
nodePropertiesWithNone: ko.observableArray([]),
nodeCaptionChoice: ko.observable(null),
nodeColorKeyChoice: ko.observable(null),
nodeIconChoice: ko.observable(null),
nodeIconSet: ko.observable(null)
};
this.graphConfigUIData.nodeCaptionChoice.subscribe(val => {
if (this.remoteConfig) {
this.remoteConfig.nodeCaptionChoice(val);
}
});
this.graphConfigUIData.nodeColorKeyChoice.subscribe(val => {
if (this.remoteConfig) {
this.remoteConfig.nodeColorKeyChoice(val);
}
});
this.graphConfigUIData.nodeIconChoice.subscribe(val => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconChoice(val);
}
});
this.graphConfigUIData.nodeIconSet.subscribe(val => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconSet(val);
}
});
this.graphConfigUIData.showNeighborType.subscribe(val => {
if (this.remoteConfig) {
this.remoteConfig.showNeighborType(val);
}
});
}
public setData(config: ViewModels.GraphConfigUiData): void {
// Update pane ko's with config's ko
this.graphConfigUIData.nodeIconChoice(config.nodeIconChoice());
this.graphConfigUIData.nodeIconSet(config.nodeIconSet());
this.graphConfigUIData.nodeProperties(config.nodeProperties());
this.graphConfigUIData.nodePropertiesWithNone(config.nodePropertiesWithNone());
this.graphConfigUIData.showNeighborType(config.showNeighborType());
// Make sure these two happen *after* setting the options of the dropdown,
// otherwise, the ko will not get set if the choice is not part of the options
this.graphConfigUIData.nodeCaptionChoice(config.nodeCaptionChoice());
this.graphConfigUIData.nodeColorKeyChoice(config.nodeColorKeyChoice());
this.remoteConfig = config;
}
public close() {
this.remoteConfig = null;
super.close();
}
}

View File

@@ -0,0 +1,55 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="libraryManagePane">
<!-- Library Manage -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Library Manage header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Library Manage header - End -->
<!-- Library Manage 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"
>More details</a
>
</span>
</div>
</div>
<!-- Library Manage errors - End -->
<!-- Library Manage inputs - Start -->
<div class="paneMainContent"><div data-bind="react: libraryManageComponentAdapter"></div></div>
<!-- Library Manage inputs - End -->
</form>
</div>
<!-- Library Manage - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,372 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { LibraryManageComponentAdapter } from "../Controls/LibraryManagement/LibraryManageComponentAdapter";
import {
LibraryManageComponentProps,
LibraryAddNameTextFieldProps,
LibraryAddUrlTextFieldProps,
LibraryAddButtonProps,
LibraryManageGridProps
} from "../Controls/LibraryManagement/LibraryManage";
import { Library } from "../../Contracts/DataModels";
import { Logger } from "../../Common/Logger";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class LibraryManagePane extends ContextualPaneBase {
public libraryManageComponentAdapter: LibraryManageComponentAdapter;
private _libraryManageProps: ko.Observable<LibraryManageComponentProps>;
private _libraryManageStates: { isNameValid: boolean; isUrlValid: boolean };
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Libraries");
this._libraryManageStates = {
isNameValid: true,
isUrlValid: true
};
this._libraryManageProps = ko.observable<LibraryManageComponentProps>({
addProps: {
nameProps: {
libraryName: "",
onLibraryNameChange: this._onLibraryNameChange,
onLibraryNameValidated: this._onLibraryNameValidated
},
urlProps: {
libraryAddress: "",
onLibraryAddressChange: this._onLibraryAddressChange,
onLibraryAddressValidated: this._onLibraryAddressValidated
},
buttonProps: {
disabled: false,
onLibraryAddClick: this._onLibraryAddClick
}
},
gridProps: {
items: [],
onLibraryDeleteClick: this._onLibraryDeleteClick
}
});
this.libraryManageComponentAdapter = new LibraryManageComponentAdapter();
this.libraryManageComponentAdapter.parameters = this._libraryManageProps;
this.resetData();
}
public open(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this._getLibraries(resourceId).then(
(libraries: Library[]) => {
this._updateLibraryManageComponentProps(null, null, null, {
items: libraries
});
},
reason => {
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
super.open();
}
public submit(): void {
// override default behavior because this is not a form
}
private _updateLibraryManageComponentProps(
newNameProps?: Partial<LibraryAddNameTextFieldProps>,
newUrlProps?: Partial<LibraryAddUrlTextFieldProps>,
newButtonProps?: Partial<LibraryAddButtonProps>,
newGridProps?: Partial<LibraryManageGridProps>
): void {
let {
addProps: { buttonProps, nameProps, urlProps },
gridProps
} = this._libraryManageProps();
if (newNameProps) {
nameProps = { ...nameProps, ...newNameProps };
}
if (newUrlProps) {
urlProps = { ...urlProps, ...newUrlProps };
}
if (newButtonProps) {
buttonProps = { ...buttonProps, ...newButtonProps };
}
if (newGridProps) {
gridProps = { ...gridProps, ...newGridProps };
}
this._libraryManageProps({
addProps: {
nameProps,
urlProps,
buttonProps
},
gridProps
});
this._libraryManageProps.valueHasMutated();
}
private _onLibraryNameChange = (libraryName: string): void => {
this._updateLibraryManageComponentProps({ libraryName });
};
private _onLibraryNameValidated = (errorMessage: string): void => {
this._libraryManageStates.isNameValid = !errorMessage;
this._validateAddButton();
};
private _onLibraryAddressChange = (libraryAddress: string): void => {
this._updateLibraryManageComponentProps(null, {
libraryAddress
});
if (!this._libraryManageProps().addProps.nameProps.libraryName) {
const parsedLibraryAddress = this._parseLibraryUrl(libraryAddress);
if (!parsedLibraryAddress) {
return;
}
let libraryName = this._sanitizeLibraryName(parsedLibraryAddress[2]);
this._updateLibraryManageComponentProps({ libraryName });
}
};
private _sanitizeLibraryName = (libraryName: string): string => {
const invalidCharRegex = /[^a-zA-Z0-9-]/gm;
return libraryName
.replace(invalidCharRegex, "-")
.substring(0, Math.min(Constants.SparkLibrary.nameMaxLength, libraryName.length));
};
private _onLibraryAddressValidated = (errorMessage: string): void => {
this._libraryManageStates.isUrlValid = !errorMessage;
this._validateAddButton();
};
private _validateAddButton = (): void => {
const isValid = this._libraryManageStates.isNameValid && this._libraryManageStates.isUrlValid;
const isUploadDisabled = this._libraryManageProps().addProps.buttonProps.disabled;
if (isValid === isUploadDisabled) {
this._updateLibraryManageComponentProps(null, null, {
disabled: !isUploadDisabled
});
}
};
private _onLibraryDeleteClick = (libraryName: string): void => {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
this._deleteLibrary(resourceId, libraryName).then(
() => {
this.isExecuting(false);
const items = this._libraryManageProps().gridProps.items.filter(lib => lib.name !== libraryName);
this._updateLibraryManageComponentProps(null, null, null, {
items
});
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
};
private _onLibraryAddClick = (): void => {
const libraryAddress = this._libraryManageProps().addProps.urlProps.libraryAddress;
if (!libraryAddress) {
this.formErrors("Library Url cannot be null");
return;
}
const libraryName = this._libraryManageProps().addProps.nameProps.libraryName || this._generateLibraryName();
if (!libraryName) {
this.formErrors("Library Name cannot be null");
return;
}
const parsedLibraryAddress = this._parseLibraryUrl(libraryAddress);
if (!parsedLibraryAddress) {
return;
}
const library: Library = {
name: libraryName,
properties: {
kind: "Jar",
source: {
kind: "HttpsUri",
libraryFileName: `${libraryName}.${parsedLibraryAddress[3]}`,
uri: libraryAddress
}
}
};
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
this._updateLibraryManageComponentProps(null, null, { disabled: true });
this._addLibrary(resourceId, library).then(
() => {
this.isExecuting(false);
this._updateLibraryManageComponentProps(
{
libraryName: ""
},
{
libraryAddress: ""
},
{
disabled: false
},
{
items: [...this._libraryManageProps().gridProps.items, library]
}
);
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
};
private _parseLibraryUrl = (url: string): RegExpExecArray => {
const libraryUrlRegex = /^(https:\/\/.+\/)(.+)\.(jar)$/gi;
return libraryUrlRegex.exec(url);
};
private _generateLibraryName = (): string => {
return `library-${Math.random()
.toString(32)
.substring(2)}`;
};
private async _getLibraries(resourceId: string): Promise<Library[]> {
if (!resourceId) {
return Promise.reject("Invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("Cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Fetching libraries...`
);
try {
const libraries = await this.container.sparkClusterManager.getLibrariesAsync(resourceId);
return libraries;
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch libraries. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _addLibrary(resourceId: string, library: Library): Promise<void> {
if (!library || !resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.LibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name
});
const libraryName = library.name;
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading ${libraryName}...`
);
try {
await this.container.sparkClusterManager.addLibraryAsync(resourceId, libraryName, library);
TelemetryProcessor.traceSuccess(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${libraryName}. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name,
error: e
});
Logger.logError(e, "Explorer/_uploadLibrary");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _deleteLibrary(resourceId: string, libraryName: string): Promise<void> {
if (!libraryName || !resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.LibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "LibraryManagePane/_deleteLibrary",
libraryName
});
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting ${libraryName}...`
);
try {
await this.container.sparkClusterManager.deleteLibraryAsync(resourceId, libraryName);
TelemetryProcessor.traceSuccess(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete ${libraryName}. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName,
error: e
});
Logger.logError(e, "Explorer/_deleteLibrary");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
}

View File

@@ -0,0 +1,88 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="loadQueryPane">
<!-- Load Query form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Load Query header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Load Query header - End -->
<!-- Load Query 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"
>More details</a
>
</span>
</div>
</div>
<!-- Load Query errors - End -->
<!-- Load Query inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader">Select a query document</div>
<input
class="importFilesTitle"
type="text"
role="textbox"
disabled
data-bind="value: selectedFilesTitle"
aria-label="Select a query document"
autofocus
/>
<input
type="file"
id="importQueryInput"
accept="text/plain"
style="display: none"
data-bind="event: { change: updateSelectedFiles }"
/>
<a
href="#"
id="queryFileImportLink"
aria-label="Upload files"
tabindex="0"
role="button"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="upload files" />
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Load" class="btncreatecoll1" /></div>
</div>
<!-- Load Query inputs - End -->
</form>
</div>
<!-- Load Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,150 @@
import * as ko from "knockout";
import * as Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { Logger } from "../../Common/Logger";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class LoadQueryPane extends ContextualPaneBase implements ViewModels.LoadQueryPane {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Load Query");
this.resetData();
this.selectedFilesTitle = ko.observable<string>("");
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this.updateSelectedFilesTitle(newFiles));
const focusElement = document.getElementById("queryFileImportLink");
focusElement && focusElement.focus();
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No file specified");
this.formErrorsDetails("No file specified. Please input a file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not load query -- No file specified. Please input a file."
);
return;
}
const file: File = this.files().item(0);
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Loading query from file ${file.name}`
);
this.isExecuting(true);
this.loadQueryFromFile(this.files().item(0))
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully loaded query from file ${file.name}`
);
this.close();
},
(error: any) => {
this.formErrors("Failed to load query");
this.formErrorsDetails(`Failed to load query: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to load query from file ${file.name}: ${error}`
);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public open() {
super.open();
const focusElement = document.getElementById("queryFileImportLink");
focusElement && focusElement.focus();
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.resetFileInput();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importQueryInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
public loadQueryFromFile(file: File): Q.Promise<void> {
const selectedCollection: ViewModels.Collection = this.container && this.container.findSelectedCollection();
if (!selectedCollection) {
// should never get into this state
Logger.logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
return Q.reject("No collection was selected");
} else if (this.container.isPreferredApiMongoDB()) {
selectedCollection.onNewMongoQueryClick(selectedCollection, null);
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const deferred: Q.Deferred<void> = Q.defer<void>();
const reader = new FileReader();
reader.onload = (evt: any): void => {
const fileData: string = evt.target.result;
const queryTab: ViewModels.QueryTab = this.container.findActiveTab() as ViewModels.QueryTab;
queryTab.initialEditorContent(fileData);
queryTab.sqlQueryEditorContent(fileData);
deferred.resolve();
};
reader.onerror = (evt: ProgressEvent): void => {
deferred.reject((evt as any).error.message);
};
reader.readAsText(file);
return deferred.promise;
}
private updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private resetFileInput(): void {
const inputElement = $("#importQueryInput");
inputElement
.wrap("<form>")
.closest("form")
.get(0)
.reset();
inputElement.unwrap();
}
}

View File

@@ -0,0 +1,37 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="managesparkclusterpane">
<!-- Setup spark form -- Start -->
<div class="contextual-pane-in">
<div class="paneContentContainer">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Setup spark header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Setup spark header - End -->
<div class="paneMainContent"><div data-bind="react: clusterSettingsComponentAdapter"></div></div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Save" class="btncreatecoll1" /></div>
</div>
</form>
</div>
</div>
<!-- Setup spark form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" alt="loading indicator image" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,153 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ClusterSettingsComponentAdapter } from "../Controls/Spark/ClusterSettingsComponentAdapter";
import { ClusterSettingsComponentProps } from "../Controls/Spark/ClusterSettingsComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { Spark } from "../../Common/Constants";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
const MAX_NUM_WORKERS = 10;
export class ManageSparkClusterPane extends ContextualPaneBase {
public readonly maxWorkerCount = Spark.MaxWorkerCount;
public workerCount: ko.Observable<number>;
public clusterSettingsComponentAdapter: ClusterSettingsComponentAdapter;
private _settingsComponentAdapterProps: ko.Observable<ClusterSettingsComponentProps>;
private _defaultCluster: ko.Observable<DataModels.SparkCluster>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Manage spark cluster");
this.workerCount = ko.observable<number>();
this._defaultCluster = ko.observable<DataModels.SparkCluster>({
id: undefined,
name: undefined,
type: undefined,
properties: {
kind: undefined,
creationTime: undefined,
driverSize: undefined,
status: undefined,
workerInstanceCount: undefined,
workerSize: undefined
}
});
this._settingsComponentAdapterProps = ko.observable<ClusterSettingsComponentProps>({
cluster: this._defaultCluster(),
onClusterSettingsChanged: this.onClusterSettingsChange
});
this._defaultCluster.subscribe(cluster => {
this._settingsComponentAdapterProps().cluster = cluster;
this._settingsComponentAdapterProps.valueHasMutated(); // trigger component re-render
});
this.clusterSettingsComponentAdapter = new ClusterSettingsComponentAdapter();
this.clusterSettingsComponentAdapter.parameters = this._settingsComponentAdapterProps;
this.resetData();
}
public async submit() {
if (!this.workerCount() || this.workerCount() > MAX_NUM_WORKERS) {
this.formErrors("Invalid worker count specified");
this.formErrorsDetails(`The number of workers should be between 0 and ${MAX_NUM_WORKERS}`);
return;
}
if (!this._defaultCluster()) {
this.formErrors("No default cluster found");
this.formErrorsDetails("No default cluster found to be associated with this account");
return;
}
const startKey = TelemetryProcessor.traceStart(Action.UpdateSparkCluster, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
});
const workerCount = this.workerCount();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating default cluster worker count to ${workerCount} nodes`
);
this.isExecuting(true);
try {
const databaseAccount = this.container && this.container.databaseAccount();
const cluster = this._defaultCluster();
cluster.properties.workerInstanceCount = workerCount;
const updatedCluster =
this.container &&
(await this.container.sparkClusterManager.updateClusterAsync(
databaseAccount && databaseAccount.id,
cluster.name,
cluster
));
this._defaultCluster(updatedCluster);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated default cluster worker count to ${workerCount} nodes`
);
TelemetryProcessor.traceSuccess(
Action.UpdateSparkCluster,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
} catch (error) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to update default cluster worker count to ${workerCount} nodes: ${JSON.stringify(error)}`
);
TelemetryProcessor.traceFailure(
Action.UpdateSparkCluster,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: JSON.stringify(error)
},
startKey
);
} finally {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}
}
public onClusterSettingsChange = (cluster: DataModels.SparkCluster) => {
this._defaultCluster(cluster);
this.workerCount(
(cluster &&
cluster.properties &&
cluster.properties.workerInstanceCount !== undefined &&
cluster.properties.workerInstanceCount) ||
0
);
};
public async open() {
const defaultCluster = await this.container.sparkClusterManager.getClusterAsync(
this.container.databaseAccount().id,
"default"
);
this._defaultCluster(defaultCluster);
this.workerCount(
(defaultCluster &&
defaultCluster.properties &&
defaultCluster.properties.workerInstanceCount !== undefined &&
defaultCluster.properties.workerInstanceCount) ||
0
);
super.open();
}
}

View File

@@ -0,0 +1,64 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { KeyCodes } from "../../Common/Constants";
export default class NewVertexPane extends ContextualPaneBase implements ViewModels.NewVertexPane {
public container: ViewModels.Explorer;
public visible: ko.Observable<boolean>;
public formErrors: ko.Observable<string>;
public formErrorsDetails: ko.Observable<string>;
// Graph style stuff
public tempVertexData: ko.Observable<ViewModels.NewVertexData>; // vertex data being edited
private onSubmitCreateCallback: (newVertexData: ViewModels.NewVertexData) => void;
private partitionKeyProperty: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.tempVertexData = ko.observable<ViewModels.NewVertexData>(null);
this.partitionKeyProperty = ko.observable(null);
this.resetData();
}
public submit() {
// Commit edited changes
if (this.onSubmitCreateCallback != null) {
this.onSubmitCreateCallback(this.tempVertexData());
}
// this.close();
}
public resetData() {
super.resetData();
this.onSubmitCreateCallback = null;
this.tempVertexData({
label: "",
properties: <ViewModels.InputProperty[]>[]
});
this.partitionKeyProperty(null);
}
public subscribeOnSubmitCreate(callback: (newVertexData: ViewModels.NewVertexData) => void): void {
this.onSubmitCreateCallback = callback;
}
public setPartitionKeyProperty(pKeyProp: string): void {
this.partitionKeyProperty(pKeyProp);
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public buildString = (prefix: string, index: number): string => {
return `${prefix}${index}`;
};
}

View File

@@ -0,0 +1,266 @@
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import DeleteDatabaseConfirmationPaneTemplate from "./DeleteDatabaseConfirmationPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import SettingsPaneTemplate from "./SettingsPane.html";
import ExecuteSprocParamsPaneTemplate from "./ExecuteSprocParamsPane.html";
import RenewAdHocAccessPaneTemplate from "./RenewAdHocAccessPane.html";
import UploadItemsPaneTemplate from "./UploadItemsPane.html";
import LoadQueryPaneTemplate from "./LoadQueryPane.html";
import SaveQueryPaneTemplate from "./SaveQueryPane.html";
import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import UploadFilePaneTemplate from "./UploadFilePane.html";
import StringInputPaneTemplate from "./StringInputPane.html";
import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import SetupSparkClusterPaneTemplate from "./SetupSparkClusterPane.html";
import ManageSparkClusterPaneTemplate from "./ManageSparkClusterPane.html";
import LibraryManagePaneTemplate from "./LibraryManagePane.html";
import ClusterLibraryPaneTemplate from "./ClusterLibraryPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
export class PaneComponent {
constructor(data: any) {
return data.data;
}
}
export class AddDatabasePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: AddDatabasePaneTemplate
};
}
}
export class AddCollectionPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: AddCollectionPaneTemplate
};
}
}
export class DeleteCollectionConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteCollectionConfirmationPaneTemplate
};
}
}
export class DeleteDatabaseConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteDatabaseConfirmationPaneTemplate
};
}
}
export class GraphNewVertexPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GraphNewVertexPaneTemplate
};
}
}
export class GraphStylingPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GraphStylingPaneTemplate
};
}
}
export class TableAddEntityPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableAddEntityPaneTemplate
};
}
}
export class TableEditEntityPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableEditEntityPaneTemplate
};
}
}
export class TableColumnOptionsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableColumnOptionsPaneTemplate
};
}
}
export class TableQuerySelectPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableQuerySelectPaneTemplate
};
}
}
export class CassandraAddCollectionPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: CassandraAddCollectionPaneTemplate
};
}
}
export class SettingsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SettingsPaneTemplate
};
}
}
export class ExecuteSprocParamsComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ExecuteSprocParamsPaneTemplate
};
}
}
export class RenewAdHocAccessPane {
constructor() {
return {
viewModel: PaneComponent,
template: RenewAdHocAccessPaneTemplate
};
}
}
export class UploadItemsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadItemsPaneTemplate
};
}
}
export class LoadQueryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: LoadQueryPaneTemplate
};
}
}
export class SaveQueryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SaveQueryPaneTemplate
};
}
}
export class BrowseQueriesPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: BrowseQueriesPaneTemplate
};
}
}
export class UploadFilePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadFilePaneTemplate
};
}
}
export class StringInputPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: StringInputPaneTemplate
};
}
}
export class SetupNotebooksPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SetupNotebooksPaneTemplate
};
}
}
export class SetupSparkClusterPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SetupSparkClusterPaneTemplate
};
}
}
export class ManageSparkClusterPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ManageSparkClusterPaneTemplate
};
}
}
export class LibraryManagePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: LibraryManagePaneTemplate
};
}
}
export class ClusterLibraryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ClusterLibraryPaneTemplate
};
}
}
export class GitHubReposPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GitHubReposPaneTemplate
};
}
}

View File

@@ -0,0 +1,90 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="renewadhocaccesspane">
<!-- Renew ad-hoc access form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Renew ad-hoc access header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Renew ad-hoc access header - End -->
<!-- Renew ad-hoc access 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"
>More details</a
>
</span>
</div>
</div>
<!-- Renew ad-hoc access errors - End -->
<!-- Renew ad-hoc access inputs - Start -->
<div class="paneMainContent">
<div class="renewUploadItemsHeader">Provide a valid account connection string</div>
<input
class="accessKeyInput"
type="text"
placeholder="Enter a connection string"
required
data-bind="value: accessKey"
/>
<div
class="renewAccessExpandCollapse"
data-bind="click: onShowHelperImageClick, event: { keypress: onShowHelperImageKeyPress }"
>
<img src="/Triangle-right.svg" alt="Show renew access image" data-bind="visible: !isHelperImageVisible()" />
<img src="/Triangle-down.svg" alt="Hide renew access image" data-bind="visible: isHelperImageVisible()" />
<span class="AccountNavigationText">Where do I find the Connection String?</span>
</div>
<div class="renewAccessImg" data-bind="visible: isHelperImageVisible()">
<span class="AccountNavigationText"
>To get the connection string, navigate to your Azure Cosmos DB account in Azure Portal, select Keys and
copy the connection string.</span
>
<img src="/ConnectionString_Artwork.png" />
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Connect" class="btncreatecoll1" /></div>
</div>
<!-- Renew ad-hoc access - End -->
</form>
</div>
<!-- Renew ad-hoc access form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,102 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class RenewAdHocAccessPane extends ContextualPaneBase implements ViewModels.RenewAdHocAccessPane {
public accessKey: ko.Observable<string>;
public isHelperImageVisible: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Connect to Azure Cosmos DB");
this.accessKey = ko.observable<string>();
this.isHelperImageVisible = ko.observable<boolean>(false);
}
public submit(): void {
this.formErrors("");
this.formErrorsDetails("");
if (this._shouldShowContextSwitchPrompt()) {
this.container.displayContextSwitchPromptForConnectionString(this.accessKey());
} else if (!!this.formErrors()) {
return;
} else {
this.isExecuting(true);
this._renewShareAccess();
}
}
public onShowHelperImageClick = (src: any, event: MouseEvent): void => {
this.isHelperImageVisible(!this.isHelperImageVisible());
};
public onShowHelperImageKeyPress = (src: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onShowHelperImageClick(src, null);
return false;
}
return true;
};
private _shouldShowContextSwitchPrompt(): boolean {
const inputMetadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
this.accessKey()
);
const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean =
(this.container && this.container.openedTabs() && this.container.openedTabs().length > 0) || false;
if (!inputMetadata || inputMetadata.apiKind == null || !inputMetadata.accountName) {
this.formErrors("Invalid connection string input");
this.formErrorsDetails("Please enter a valid connection string");
}
if (
!inputMetadata ||
this.formErrors() ||
!this.container ||
apiKind == null ||
!this.container.databaseAccount ||
!this.container.defaultExperience ||
!hasOpenedTabs ||
(this.container.databaseAccount().name === inputMetadata.accountName &&
apiKind === inputMetadata.apiKind &&
!hasOpenedTabs)
) {
return false;
}
return true;
}
private _renewShareAccess = (): void => {
this.container
.renewShareAccess(this.accessKey())
.fail((error: any) => {
const errorMessage: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${errorMessage}`);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
})
.finally(() => {
this.isExecuting(false);
});
};
public close(): void {
super.close();
this.isHelperImageVisible(false);
this.formErrors("");
this.formErrorsDetails("");
this.accessKey("");
}
}

View File

@@ -0,0 +1,63 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="savequerypane">
<!-- Save Query form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Save Query header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Save Query header - End -->
<!-- Save Query 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"
>More details</a
>
</span>
</div>
</div>
<!-- Save Query errors - End -->
<!-- Save Query inputs - Start -->
<div class="paneMainContent">
<div class="pkPadding" data-bind="visible: !canSaveQueries()">
<div data-bind="text: setupSaveQueriesText"></div>
<button class="btncreatecoll1 btnSetupQueries" type="button" data-bind="click: setupQueries">
Complete setup
</button>
</div>
<div class="pkPadding" data-bind="visible: canSaveQueries">
<p><span class="mandatoryStar">*</span> <span>Name</span></p>
<input class="textfontclr collid" required type="text" data-bind="value: queryName" />
</div>
</div>
<div class="paneFooter" data-bind="visible: canSaveQueries">
<div class="leftpanel-okbut"><input type="submit" value="Save" class="btncreatecoll1" /></div>
</div>
<!-- Save Query inputs - End -->
</form>
</div>
<!-- Save Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,160 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
export class SaveQueryPane extends ContextualPaneBase {
public queryName: ko.Observable<string>;
public canSaveQueries: ko.Computed<boolean>;
public setupSaveQueriesText: string = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${Constants.SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Save Query");
this.queryName = ko.observable<string>();
this.canSaveQueries = this.container && this.container.canSaveQueries;
this.resetData();
}
public submit = (): void => {
this.formErrors("");
this.formErrorsDetails("");
if (!this.canSaveQueries()) {
this.formErrors("Cannot save query");
this.formErrorsDetails("Failed to save query: account not set up to save queries");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Failed to save query: account not setup to save queries"
);
}
const queryName: string = this.queryName();
const queryTab: ViewModels.QueryTab = this.container && (this.container.findActiveTab() as ViewModels.QueryTab);
const query: string = queryTab && queryTab.sqlQueryEditorContent();
if (!queryName || queryName.length === 0) {
this.formErrors("No query name specified");
this.formErrorsDetails("No query name specified. Please specify a query name.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not save query -- No query name specified. Please specify a query name."
);
return;
} else if (!query || query.length === 0) {
this.formErrors("Invalid query content specified");
this.formErrorsDetails("Invalid query content specified. Please enter query content.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not save query -- Invalid query content specified. Please enter query content."
);
return;
}
const queryParam: DataModels.Query = {
id: queryName,
resourceId: this.container.queriesClient.getResourceId(),
queryName: queryName,
query: query
};
const startKey: number = TelemetryProcessor.traceStart(Action.SaveQuery, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
this.isExecuting(true);
this.container.queriesClient.saveQuery(queryParam).then(
() => {
this.isExecuting(false);
queryTab.tabTitle(queryParam.queryName);
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
TelemetryProcessor.traceSuccess(
Action.SaveQuery,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
this.close();
},
(error: any) => {
this.isExecuting(false);
if (typeof error != "string") {
error = JSON.stringify(error);
}
this.formErrors("Failed to save query");
this.formErrorsDetails(`Failed to save query: ${error}`);
TelemetryProcessor.traceFailure(
Action.SaveQuery,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
}
);
};
public setupQueries = async (src: any, event: MouseEvent): Promise<void> => {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
});
try {
this.isExecuting(true);
await this.container.queriesClient.setupQueriesCollection();
this.container.refreshAllDatabases();
TelemetryProcessor.traceSuccess(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
this.formErrors("Failed to setup a container for saved queries");
this.formErrors(`Failed to setup a container for saved queries: ${JSON.stringify(error)}`);
} finally {
this.isExecuting(false);
}
};
public close() {
super.close();
this.resetData();
}
public resetData() {
super.resetData();
this.queryName("");
}
}

View File

@@ -0,0 +1,268 @@
<!-- TODO: Move Pane to REACT -->
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="settingspane">
<!-- Settings Confirmation form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Settings Confirmation header - Start -->
<div class="firstdivbg headerline">
<span 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>
<!-- Settings Confirmation header - End -->
<!-- Settings Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Settings Confirmation errors - End -->
<!-- Settings Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<div class="settingsSection" data-bind="visible: shouldShowQueryPageOptions">
<div class="settingsSectionPart pageOptionsPart">
<div class="settingsSectionLabel">
Page options
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
many query results per page.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Page options">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="customItemPerPage"
name="pageOption"
value="custom"
data-bind="checked: pageOption"
/>
<label
for="customItemPerPage"
id="custom-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'custom' ? 'true' : 'false' },
event: { keydown: onCustomPageOptionsKeyDown
}"
>Custom</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="unlimitedItemPerPage"
name="pageOption"
value="unlimited"
data-bind="checked: pageOption"
/>
<label
for="unlimitedItemPerPage"
id="unlimited-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'unlimited' ? 'true' : 'false' },
event: { keydown: onUnlimitedPageOptionKeyDown
}"
>Unlimited</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
<div class="tabs settingsSectionPart">
<div class="tabcontent" data-bind="visible: isCustomPageOptionSelected()">
<div class="settingsSectionLabel">
Query results per page
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Enter the number of query results that should be shown per page.
</span>
</span>
</div>
<input
type="number"
required
min="1"
step="1"
class="textfontclr collid"
aria-label="Custom query items per page"
data-bind="textInput: customItemPerPage, enable: isCustomPageOptionSelected()"
/>
</div>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowCrossPartitionOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Enable cross-partition query
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
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.
</span>
</span>
</div>
<input
type="checkbox"
tabindex="0"
aria-label="Enable cross partition query"
data-bind="checked: crossPartitionQueryEnabled"
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowParallelismOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Max degree of parallelism
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
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.
</span>
</span>
</div>
<input
type="number"
required
min="-1"
step="1"
class="textfontclr collid"
role="textbox"
tabindex="0"
id="max-degree"
aria-label="Max degree of parallelism"
data-bind="value: maxDegreeOfParallelism"
autofocus
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowGraphAutoVizOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Graph Auto-visualization">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOn"
name="graphAutoVizOption"
value="false"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOn"
id="graph-display"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'false' ? 'true' : 'false' },
event: { keypress: onGraphDisplayResultsKeyDown
}"
>Graph</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOff"
name="graphAutoVizOption"
value="true"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOff"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'true' ? 'true' : 'false' },
event: { keypress: onJsonDisplayResultsKeyDown
}"
>JSON</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
</div>
<div class="settingsSection">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">Explorer Version</div>
<div data-bind="text: explorerVersion"></div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Apply" class="btncreatecoll1" /></div>
</div>
<!-- Settings Confirmation inputs - End -->
</form>
</div>
<!-- Settings Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,40 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
jest.mock("../Tabs/NotebookTab");
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: ViewModels.Explorer;
beforeEach(() => {
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
});
it("should be true for SQL API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(true);
});
it("should be false for Cassandra API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Cassandra.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Tables API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Table.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Graph API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Mongo API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
});
});

View File

@@ -0,0 +1,185 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { StringUtility } from "../../Shared/StringUtility";
import { config } from "../../Config";
export class SettingsPane extends ContextualPaneBase implements ViewModels.SettingsPane {
public pageOption: ko.Observable<string>;
public customItemPerPage: ko.Observable<number>;
public crossPartitionQueryEnabled: ko.Observable<boolean>;
public maxDegreeOfParallelism: ko.Observable<number>;
public explorerVersion: string;
public shouldShowQueryPageOptions: ko.Computed<boolean>;
public shouldShowGraphAutoVizOption: ko.Computed<boolean>;
private graphAutoVizDisabled: ko.Observable<string>;
private shouldShowCrossPartitionOption: ko.Computed<boolean>;
private shouldShowParallelismOption: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Settings");
this.resetData();
this.pageOption = ko.observable<string>();
this.customItemPerPage = ko.observable<number>();
const crossPartitionQueryEnabledState: boolean = LocalStorageUtility.hasItem(
StorageKey.IsCrossPartitionQueryEnabled
)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true;
this.crossPartitionQueryEnabled = ko.observable<boolean>(crossPartitionQueryEnabledState);
const maxDegreeOfParallelismState: number = LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism;
this.maxDegreeOfParallelism = ko.observable<number>(maxDegreeOfParallelismState);
const isGraphAutoVizDisabled: boolean = LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
? LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled)
: false;
this.graphAutoVizDisabled = ko.observable<string>(`${isGraphAutoVizDisabled}`);
this.explorerVersion = config.gitSha;
this.shouldShowQueryPageOptions = ko.computed<boolean>(() => this.container.isPreferredApiDocumentDB());
this.shouldShowCrossPartitionOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowParallelismOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowGraphAutoVizOption = ko.computed<boolean>(() => this.container.isPreferredApiGraph());
}
public open() {
this._loadSettings();
super.open();
const pageOptionsFocus = document.getElementById("custom-selection");
const displayQueryFocus = document.getElementById("graph-display");
const maxDegreeFocus = document.getElementById("max-degree");
if (this.container.isPreferredApiGraph()) {
displayQueryFocus && displayQueryFocus.focus();
} else if (this.container.isPreferredApiTable()) {
maxDegreeFocus && maxDegreeFocus.focus();
}
pageOptionsFocus && pageOptionsFocus.focus();
}
public submit() {
this.formErrors("");
this.isExecuting(true);
LocalStorageUtility.setEntryNumber(
StorageKey.ActualItemPerPage,
this.isCustomPageOptionSelected() ? this.customItemPerPage() : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, this.customItemPerPage());
LocalStorageUtility.setEntryString(
StorageKey.IsCrossPartitionQueryEnabled,
this.crossPartitionQueryEnabled().toString()
);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, this.maxDegreeOfParallelism());
if (this.shouldShowGraphAutoVizOption()) {
LocalStorageUtility.setEntryBoolean(
StorageKey.IsGraphAutoVizDisabled,
StringUtility.toBoolean(this.graphAutoVizDisabled())
);
}
this.isExecuting(false);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.crossPartitionQueryEnabled() ? "Enabled" : "Disabled"} cross-partition query feed option`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism
)}`
);
if (this.shouldShowGraphAutoVizOption()) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Graph result will be displayed as ${
LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled) ? "JSON" : "Graph"
}`
);
}
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`
);
this.close();
}
public isCustomPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.CustomPageOption;
};
public isUnlimitedPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.UnlimitedPageOption;
};
public onUnlimitedPageOptionKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.UnlimitedPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onCustomPageOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.CustomPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onJsonDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("true");
event.stopPropagation();
return false;
}
return true;
}
public onGraphDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("false");
event.stopPropagation();
return false;
}
return true;
}
private _loadSettings() {
this.isExecuting(true);
try {
this.pageOption(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) == Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption
);
this.customItemPerPage(LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage));
} catch (exception) {
this.formErrors("Unable to load your settings");
this.formErrorsDetails(exception);
} finally {
this.isExecuting(false);
}
}
}

View File

@@ -0,0 +1,45 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="setupnotebookspane">
<!-- Setup notebooks form -- Start -->
<div class="contextual-pane-in">
<div class="paneContentContainer">
<!-- Setup notebooks header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Setup notebooks header - End -->
<div class="paneMainContent">
<div class="pkPadding">
<div data-bind="text: description"></div>
<button
id="completeSetupBtn"
class="btncreatecoll1 btnSetupQueries"
type="button"
aria-label="Complete setup"
data-bind="click: onCompleteSetupClick, event: { keypress: onCompleteSetupKeyPress }"
>
Complete setup
</button>
</div>
</div>
</div>
</div>
<!-- Setup notebooks form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" alt="loading indicator image" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,111 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas, KeyCodes } from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ko from "knockout";
export class SetupNotebooksPane extends ContextualPaneBase {
private description: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.description = ko.observable<string>();
this.resetData();
}
public openWithTitleAndDescription(title: string, description: string) {
this.title(title);
this.description(description);
this.open();
}
public open() {
super.open();
const completeSetupBtn = document.getElementById("completeSetupBtn");
completeSetupBtn && completeSetupBtn.focus();
}
public submit() {
// override default behavior because this is not a form
}
public onCompleteSetupClick = async (src: any, event: MouseEvent) => {
await this.setupNotebookWorkspace();
};
public onCompleteSetupKeyPress = async (src: any, event: KeyboardEvent) => {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
await this.setupNotebookWorkspace();
event.stopPropagation();
return false;
}
return true;
};
public async setupNotebookWorkspace(): Promise<void> {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
});
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Creating a new default notebook workspace"
);
try {
this.isExecuting(true);
await this.container.notebookWorkspaceManager.createNotebookWorkspaceAsync(
this.container.databaseAccount() && this.container.databaseAccount().id,
"default"
);
this.container.isAccountReady.valueHasMutated(); // re-trigger init notebooks
this.close();
TelemetryProcessor.traceSuccess(
Action.CreateNotebookWorkspace,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Successfully created a default notebook workspace for the account"
);
} catch (error) {
const errorMessage = typeof error == "string" ? error : JSON.stringify(error);
TelemetryProcessor.traceFailure(
Action.CreateNotebookWorkspace,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage
},
startKey
);
this.formErrors("Failed to setup a default notebook workspace");
this.formErrorsDetails(`Failed to setup a default notebook workspace: ${errorMessage}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to create a default notebook workspace: ${errorMessage}`
);
} finally {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}
}
}

View File

@@ -0,0 +1,52 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="setupsparkclusterpane">
<!-- Setup spark form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<div class="paneContentContainer">
<!-- Setup spark header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Setup spark header - End -->
<div class="paneMainContent">
<div class="pkPadding">
<div data-bind="text: setupSparkClusterText"></div>
<div class="seconddivpadding">
<div>Worker Count</div>
<input
class="sparkWorkerCountInput"
type="number"
min="1"
aria-label="worker count input"
required
data-bind="textInput: workerCount, attr: { max: maxWorkerNodes }"
/>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Complete Setup" class="btncreatecoll1" /></div>
</div>
</div>
</form>
</div>
<!-- Setup spark form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" alt="loading indicator image" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,99 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { Spark } from "../../Common/Constants";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
export class SetupSparkClusterPane extends ContextualPaneBase {
public setupSparkClusterText: string =
"Looks like you have not yet created a default spark cluster for this account. To proceed and start using notebooks with spark, we'll need to create a default spark cluster for this account.";
public readonly maxWorkerNodes: number = Spark.MaxWorkerCount;
public workerCount: ko.Observable<number>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Enable spark");
this.workerCount = ko.observable<number>(1);
this.resetData();
}
public async submit() {
await this.setupSparkCluster();
}
public async setupSparkCluster(): Promise<void> {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.CreateSparkCluster, {
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
});
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Creating a new default spark cluster"
);
try {
this.isExecuting(true);
await this.container.sparkClusterManager.createClusterAsync(
this.container.databaseAccount() && this.container.databaseAccount().id,
{
name: "default",
properties: {
kind: "Spark",
workerInstanceCount: this.workerCount(),
driverSize: "Cosmos.Spark.D4s",
workerSize: "Cosmos.Spark.D4s",
creationTime: undefined,
status: undefined
}
}
);
this.container.isAccountReady.valueHasMutated(); // refresh internal state
this.close();
TelemetryProcessor.traceSuccess(
Action.CreateSparkCluster,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title()
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Successfully created a default spark cluster for the account"
);
} catch (error) {
const errorMessage = typeof error == "string" ? error : JSON.stringify(error);
TelemetryProcessor.traceFailure(
Action.CreateSparkCluster,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage
},
startKey
);
this.formErrors("Failed to setup a default spark cluster");
this.formErrorsDetails(`Failed to setup a default spark cluster: ${errorMessage}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to create a default spark cluster: ${errorMessage}`
);
} finally {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}
}
}

View File

@@ -0,0 +1,66 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="stringInputPane">
<!-- String Input form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- String Input header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"></div>
</div>
<!-- String Input header - End -->
<!-- String Input 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"
>More details</a
>
</span>
</div>
</div>
<!-- String Input errors - End -->
<!-- String Input inputs - Start -->
<div class="paneMainContent">
<div>
<p data-bind="text: inputLabel"></p>
<p>
<input
type="text"
name="collectionIdConfirmation"
required
class="collid"
data-bind="textInput: stringInput, hasFocus: firstFieldHasFocus"
aria-label="inputLabel"
/>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
</div>
</div>
<!-- String Input inputs - End -->
</form>
</div>
<!-- String Input form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,98 @@
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
/**
* Generic pane to get a single string input from user
*/
export class StringInputPane extends ContextualPaneBase {
private openOptions: ViewModels.StringInputPaneOpenOptions;
private submitButtonLabel: ko.Observable<string>;
private inputLabel: ko.Observable<string>;
private stringInput: ko.Observable<string>;
private paneDeferred: Q.Deferred<any>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.resetData();
this.inputLabel = ko.observable("");
this.submitButtonLabel = ko.observable("Load");
this.stringInput = ko.observable();
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`${this.openOptions.inProgressMessage} ${this.stringInput()}`
);
this.isExecuting(true);
this.openOptions
.onSubmit(this.stringInput())
.then(
(value: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.openOptions.successMessage}: ${this.stringInput()}`
);
this.close();
this.paneDeferred.resolve(value);
},
reason => {
let error = reason;
if (reason instanceof Error) {
error = reason.message;
} else if (typeof reason === "object") {
error = JSON.stringify(reason);
}
// If it's an AjaxError (AjaxObservable), add more error
if (reason.response && reason.response.message) {
error += `. ${reason.response.message}`;
}
this.formErrors(this.openOptions.errorMessage);
this.formErrorsDetails(`${this.openOptions.errorMessage}: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} ${this.stringInput()}: ${error}`
);
this.paneDeferred.reject(error);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public close() {
super.close();
this.resetData();
this.resetFileInput();
}
public openWithOptions<T>(options: ViewModels.StringInputPaneOpenOptions): Q.Promise<T> {
this.openOptions = options;
this.title(this.openOptions.paneTitle);
if (this.openOptions.submitButtonLabel) {
this.submitButtonLabel(this.openOptions.submitButtonLabel);
}
this.inputLabel(this.openOptions.inputLabel);
this.stringInput(this.openOptions.defaultInput);
super.open();
this.paneDeferred = Q.defer<T>();
return this.paneDeferred.promise;
}
private resetFileInput(): void {
this.stringInput("");
}
}

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 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 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,146 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import * as TableConstants from "../../Tables/Constants";
import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane";
export default class AddTableEntityPane extends TableEntityPane implements ViewModels.AddTableEntityPane {
private static _excludedFields: string[] = [TableConstants.EntityKeyNames.Timestamp];
private static _readonlyFields: string[] = [
TableConstants.EntityKeyNames.PartitionKey,
TableConstants.EntityKeyNames.RowKey,
TableConstants.EntityKeyNames.Timestamp
];
public enterRequiredValueLabel = "Enter identifier value."; // localize
public enterValueLabel = "Enter value to keep property."; // localize
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Add Entity");
this.container.isPreferredApiCassandra.subscribe(isCassandra => {
if (isCassandra) {
this.submitButtonText("Add Row");
}
});
this.scrollId = ko.observable<string>("addEntityScroll");
}
public submit() {
if (!this.canApply()) {
return;
}
let entity: Entities.ITableEntity = this.entityFromAttributes(this.displayedAttributes());
this.container.tableDataClient
.createDocument(this.tableViewModel.queryTablesTab.collection, entity)
.then((newEntity: Entities.ITableEntity) => {
this.tableViewModel.addEntityToCache(newEntity).then(() => {
if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) {
this.tableViewModel.redrawTableThrottled();
}
});
this.close();
});
}
public open() {
var headers = this.tableViewModel.headers;
if (DataTableUtilities.checkForDefaultHeader(headers)) {
headers = [];
if (this.container.isPreferredApiTable()) {
headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey];
}
}
if (this.container.isPreferredApiCassandra()) {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((columns: CassandraTableKey[]) => {
this.displayedAttributes(
this.constructDisplayedAttributes(
columns.map(col => col.property),
Utilities.getDataTypesFromCassandraSchema(columns)
)
);
this.updateIsActionEnabled();
super.open();
});
} else {
this.displayedAttributes(
this.constructDisplayedAttributes(
headers,
Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items())
)
);
this.updateIsActionEnabled();
super.open();
}
const focusElement = document.getElementById("addTableEntityValue");
focusElement && focusElement.focus();
}
private constructDisplayedAttributes(headers: string[], dataTypes: any): EntityPropertyViewModel[] {
var displayedAttributes: EntityPropertyViewModel[] = [];
headers &&
headers.forEach((key: string) => {
if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) {
if (this.container.isPreferredApiCassandra()) {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map(key => key.property);
var isRequired: boolean = _.contains<string>(cassandraKeys, key);
var editable: boolean = false;
var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel;
var entityAttributeType: string = dataTypes[key] || TableConstants.CassandraType.Text; // Default to String if there is no type specified.
// 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;
var entity: EntityPropertyViewModel = new EntityPropertyViewModel(
this,
key,
entityAttributeType,
"", // default to empty string
/* namePlaceholder */ undefined,
nonEditableType ? "Type is not editable via DataExplorer." : placeholderLabel,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* required value */ isRequired,
/* removable */ false,
/* valueEditable */ !nonEditableType,
/* ignoreEmptyValue */ true
);
} else {
var isRequired: boolean = _.contains<string>(AddTableEntityPane.requiredFieldsForTablesAPI, key);
var editable: boolean = !_.contains<string>(AddTableEntityPane._readonlyFields, key);
var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel;
var entityAttributeType: string = dataTypes[key] || TableConstants.TableType.String; // Default to String if there is no type specified.
var entity: EntityPropertyViewModel = new EntityPropertyViewModel(
this,
key,
entityAttributeType,
"", // default to empty string
/* namePlaceholder */ undefined,
placeholderLabel,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* required value */ isRequired,
/* removable */ editable,
/* valueEditable */ true,
/* ignoreEmptyValue */ true
);
}
displayedAttributes.push(entity);
}
});
return displayedAttributes;
}
}

View File

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

View File

@@ -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
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,78 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="tablecolumnoptionspane">
<!-- Table Column Options form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Table Column Options header - Start -->
<div class="firstdivbg headerline">
Column Options
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Table Column Options header - End -->
<div class="paneMainContent paneContentContainer">
<div><span>Choose the columns and the order in which you want to display them in the table.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight:700;"
id="availableColumnsLabel"
data-bind="text: availableColumnsLabel"
></label>
<span class="column-arrows-svg" data-bind="click: moveDown, enable: canMoveDown">
<img class="column-opt-arrow-Img" src="/Down.svg" alt="Move down" />
</span>
<span class="column-arrows-svg" data-bind="click: moveUp, enable: canMoveUp">
<img class="column-opt-arrow-Img" src="/Up.svg" alt="Move up" />
</span>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsLabel" tabindex="0">
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input
type="checkbox"
for="columnName"
data-bind="attr: { title: columnName, 'aria-selected': (selected()? 'true': 'false') }, checked: selected"
/>
<label id="columnName" data-bind="text: columnName"></label>
</li>
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Table Column Options form - End -->
</div>
</div>

View File

@@ -0,0 +1,195 @@
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableOperations from "../../Tables/DataTable/DataTableOperations";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import { ContextualPaneBase } from "../ContextualPaneBase";
import _ from "underscore";
/**
* Represents an item shown in the available columns.
* columnName: the name of the column.
* selected: indicate whether user wants to display the column in the table.
* order: the order in the initial table. E.g.,
* Order array of initial table: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Order array of current table: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* if order = 6, then this column will be the one with column name prop6
* index: index in the observable array, this used for selection and moving up/down.
*/
interface IColumnOption {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
order: number;
index: number;
}
export interface IColumnSetting {
columnNames: string[];
visible?: boolean[];
order?: number[];
}
export class TableColumnOptionsPane extends ContextualPaneBase implements ViewModels.TableColumnOptionsPane {
public titleLabel: string = "Column Options";
public instructionLabel: string = "Choose the columns and the order in which you want to display them in the table.";
public availableColumnsLabel: string = "Available Columns";
public moveUpLabel: string = "Move Up";
public moveDownLabel: string = "Move Down";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<IColumnOption>;
public allSelected: ko.Computed<boolean>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public canMoveUp: ko.Observable<boolean>;
public canMoveDown: ko.Observable<boolean>;
public tableViewModel: TableEntityListViewModel;
public parameters: IColumnSetting;
private selectedColumnOption: IColumnOption = null;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<IColumnOption>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return !value.selected();
});
});
this.canMoveUp = ko.observable<boolean>(false);
this.canMoveDown = ko.observable<boolean>(false);
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: value => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this
});
}
public submit() {
var newColumnSetting = this.getParameters();
DataTableOperations.reorderColumns(this.tableViewModel.table, newColumnSetting.order).then(() => {
DataTableOperations.filterColumns(this.tableViewModel.table, newColumnSetting.visible);
this.visible(false);
});
}
public open() {
this.setDisplayedColumns(this.parameters.columnNames, this.parameters.order, this.parameters.visible);
super.open();
}
private getParameters(): IColumnSetting {
var newColumnSettings: IColumnSetting = <IColumnSetting>{
columnNames: [],
order: [],
visible: []
};
this.columnOptions().map((value: IColumnOption) => {
newColumnSettings.columnNames.push(value.columnName());
newColumnSettings.order.push(value.order);
newColumnSettings.visible.push(value.selected());
});
return newColumnSettings;
}
public setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void {
var options: IColumnOption[] = order.map((value: number, index: number) => {
var columnOption: IColumnOption = {
columnName: ko.observable<string>(columnNames[index]),
order: value,
selected: ko.observable<boolean>(visible[index]),
index: index
};
return columnOption;
});
this.columnOptions(options);
}
public selectAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(true);
});
}
public clearAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(false);
});
if (columnOptions && columnOptions.length > 0) {
columnOptions[0].selected(true);
}
}
public moveUp(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex - 1;
//Debug.assert(currentSelectedIndex > 0);
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public moveDown(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex + 1;
//Debug.assert(currentSelectedIndex < (this.columnOptions().length - 1));
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public handleClick = (data: IColumnOption, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: IColumnOption): void {
this.selectedColumnOption = targetColumn;
this.canMoveUp(targetColumn.index !== 0);
this.canMoveDown(targetColumn.index !== this.columnOptions().length - 1);
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private swapColumnOption(options: IColumnOption[], indexA: number, indexB: number): void {
var tempColumnName: string = options[indexA].columnName();
var tempSelected: boolean = options[indexA].selected();
var tempOrder: number = options[indexA].order;
options[indexA].columnName(options[indexB].columnName());
options[indexB].columnName(tempColumnName);
options[indexA].selected(options[indexB].selected());
options[indexB].selected(tempSelected);
options[indexA].order = options[indexB].order;
options[indexB].order = tempOrder;
}
}

View File

@@ -0,0 +1,188 @@
<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 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,281 @@
import * as ko from "knockout";
import _ from "underscore";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableConstants from "../../Tables/Constants";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import * as ViewModels from "../../../Contracts/ViewModels";
import { KeyCodes } from "../../../Common/Constants";
import { ContextualPaneBase } from "../ContextualPaneBase";
// 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);
this.container.isPreferredApiCassandra.subscribe(isCassandra => {
if (isCassandra) {
this.edmTypes([
TableConstants.CassandraType.Text,
TableConstants.CassandraType.Ascii,
TableConstants.CassandraType.Bigint,
TableConstants.CassandraType.Blob,
TableConstants.CassandraType.Boolean,
TableConstants.CassandraType.Decimal,
TableConstants.CassandraType.Double,
TableConstants.CassandraType.Float,
TableConstants.CassandraType.Int,
TableConstants.CassandraType.Uuid,
TableConstants.CassandraType.Varchar,
TableConstants.CassandraType.Varint,
TableConstants.CassandraType.Inet,
TableConstants.CassandraType.Smallint,
TableConstants.CassandraType.Tinyint
]);
}
});
this.canAdd = ko.computed<boolean>(() => {
// Cassandra can't add since the schema can't be changed once created
if (this.container.isPreferredApiCassandra()) {
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 && this.container.isPreferredApiCassandra()) {
// 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 &&
(!viewModel.queryTablesTab.container.isPreferredApiCassandra() ||
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

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

View File

@@ -0,0 +1,306 @@
/* Constants */
var MaximumNameLength = 255;
var noHelp = "";
var detailedHelp = "Enter a name up to 255 characters in size. Most valid C# identifiers are allowed."; // localize
export interface IValidationResult {
isInvalid: boolean;
help: string;
}
export function validate(name: string): IValidationResult {
var help: string = noHelp;
// Note: Disabling encoding check to err the side of lax validation.
// A valid property name should also be XML-serializable.
// Hence, only allowing names that don't require special encoding for network transmission.
// var encoded: string = tryEncode(name);
// var success: boolean = (name === encoded);
var success: boolean = true;
if (success) {
success = name.length <= MaximumNameLength;
if (success) {
success = IsValidIdentifier(name);
}
}
if (!success) {
help = detailedHelp;
}
return { isInvalid: !success, help: help };
}
/*
function tryEncode(name: string): string {
var encoded: string = null;
try {
encoded = encodeURIComponent(name);
} catch (error) {
console.error("tryEncode", "Error encoding:", name, error);
encoded = null;
}
return encoded;
}
*/
// Port of http://referencesource.microsoft.com/#System/compmod/microsoft/csharp/csharpcodeprovider.cs,7b5c20ff8d28dfa7
function IsValidIdentifier(value: string): boolean {
// identifiers must be 1 char or longer
if (!value) {
return false;
}
if (value.length > 512) {
return false;
}
// Note: Disabling keyword check to err the side of lax validation.
// identifiers cannot be a keyword, unless they are escaped with an '@'
/*
if (value[0] !== "@") {
if (IsKeyword(value)) {
return false;
}
} else {
value = value.substring(1);
}
*/
return IsValidLanguageIndependentIdentifier(value);
}
/*
var keywords: string[][] = [
// 2 characters
[
"as",
"do",
"if",
"in",
"is",
],
// 3 characters
[
"for",
"int",
"new",
"out",
"ref",
"try",
],
// 4 characters
[
"base",
"bool",
"byte",
"case",
"char",
"else",
"enum",
"goto",
"lock",
"long",
"null",
"this",
"true",
"uint",
"void",
],
// 5 characters
[
"break",
"catch",
"class",
"const",
"event",
"false",
"fixed",
"float",
"sbyte",
"short",
"throw",
"ulong",
"using",
"while",
],
// 6 characters
[
"double",
"extern",
"object",
"params",
"public",
"return",
"sealed",
"sizeof",
"static",
"string",
"struct",
"switch",
"typeof",
"unsafe",
"ushort",
],
// 7 characters
[
"checked",
"decimal",
"default",
"finally",
"foreach",
"private",
"virtual",
],
// 8 characters
[
"abstract",
"continue",
"delegate",
"explicit",
"implicit",
"internal",
"operator",
"override",
"readonly",
"volatile",
],
// 9 characters
[
"__arglist",
"__makeref",
"__reftype",
"interface",
"namespace",
"protected",
"unchecked",
],
// 10 characters
[
"__refvalue",
"stackalloc",
]
];
function IsKeyword(value: string): boolean {
var isKeyword: boolean = false;
var listCount: number = keywords.length;
for (var i = 0; ((i < listCount) && !isKeyword); ++i) {
var list: string[] = keywords[i];
var listKeywordCount: number = list.length;
for (var j = 0; ((j < listKeywordCount) && !isKeyword); ++j) {
var keyword: string = list[j];
isKeyword = (value === keyword);
}
}
return isKeyword;
}
*/
function IsValidLanguageIndependentIdentifier(value: string): boolean {
return IsValidTypeNameOrIdentifier(value, /* isTypeName */ false);
}
var UnicodeCategory = {
// Uppercase Letter
Lu: /[A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-ՖႠ-ჅḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ---ℝℤΩℨK--ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋA-]|\ud801[\udc00-\udc27]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca]/,
// Lowercase Letter
Ll: /[a-zªµºß-öø-ÿāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıijĵķ-ĸĺļľŀłńņň-ʼnŋōŏőœŕŗřśŝşšţťŧũūŭůűųŵŷźżž-ƀƃƅƈƌ-ƍƒƕƙ-ƛƞơƣƥƨƪ-ƫƭưƴƶƹ-ƺƽ-ƿdžljnjǎǐǒǔǖǘǚǜ-ǝǟǡǣǥǧǩǫǭǯ-ǰdzǵǹǻǽǿȁȃȅȇȉȋȍȏȑȓȕȗșțȝȟȡȣȥȧȩȫȭȯȱȳ-ȹȼȿ-ɀɂɇɉɋɍɏ-ʓʕ-ʯͱͳͷͻ-ͽΐά-ώϐ-ϑϕ-ϗϙϛϝϟϡϣϥϧϩϫϭϯ-ϳϵϸϻ-ϼа-џѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿҁҋҍҏґғҕҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎ-ӏӑӓӕӗәӛӝӟӡӣӥӧөӫӭӯӱӳӵӷӹӻӽӿԁԃԅԇԉԋԍԏԑԓԕԗԙԛԝԟԡԣա-ևᴀ-ᴫᵢ-ᵷᵹ-ᶚḁḃḅḇḉḋḍḏḑḓḕḗḙḛḝḟḡḣḥḧḩḫḭḯḱḳḵḷḹḻḽḿṁṃṅṇṉṋṍṏṑṓṕṗṙṛṝṟṡṣṥṧṩṫṭṯṱṳṵṷṹṻṽṿẁẃẅẇẉẋẍẏẑẓẕ-ẝẟạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹỻỽỿ-ἇἐ-ἕἠ-ἧἰ-ἷὀ-ὅὐ-ὗὠ-ὧὰ-ώᾀ-ᾇᾐ-ᾗᾠ-ᾧᾰ-ᾴᾶ-ᾷιῂ-ῄῆ-ῇῐ-ΐῖ-ῗῠ-ῧῲ-ῴῶ-ῷⁱⁿℊℎ-ℏℓℯℴℹℼ--ⅉⅎↄⰰ-ⱞⱡⱥ-ⱦⱨⱪⱬⱱⱳ-ⱴⱶ-ⱼⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣ-ⳤⴀ-ⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯ-ꜱꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝱ-ꝸꝺꝼꝿꞁꞃꞅꞇꞌff-stﬓ-ﬗa-]|\ud801[\udc28-\udc4f]|\ud835[\udc1a-\udc33\udc4e-\udc54\udc56-\udc67\udc82-\udc9b\udcb6-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udccf\udcea-\udd03\udd1e-\udd37\udd52-\udd6b\udd86-\udd9f\uddba-\uddd3\uddee-\ude07\ude22-\ude3b\ude56-\ude6f\ude8a-\udea5\udec2-\udeda\udedc-\udee1\udefc-\udf14\udf16-\udf1b\udf36-\udf4e\udf50-\udf55\udf70-\udf88\udf8a-\udf8f\udfaa-\udfc2\udfc4-\udfc9\udfcb]/,
// Titlecase Letter
Lt: /[DžLjNjDzᾈ-ᾏᾘ-ᾟᾨ-ᾯᾼῌῼ]/,
// Modifier/Number Letter
Lm: /[ʰ-ˁˆ-ˑˠ-ˤˬˮʹͺՙـۥ-ۦߴ-ߵߺॱๆໆჼៗᡃᱸ-ᱽᴬ-ᵡᵸᶛ-ᶿₐ-ₔⱽⵯⸯ々〱-〵〻ゝ-ゞー-ヾꀕꘌꙿꜗ-ꜟꝰꞈー゙-゚]/,
// Other Letter
Lo: /[ƻǀ-ǃʔא-תװ-ײء-ؿف-يٮ-ٯٱ-ۓەۮ-ۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪऄ-हऽॐक़-ॡॲॻ-ॿঅ-ঌএ-ঐও-নপ-রলশ-হঽৎড়-ঢ়য়-ৡৰ-ৱਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હઽૐૠ-ૡଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହଽଡ଼-ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘ-ౙౠ-ౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠ-ೡഅ-ഌഎ-ഐഒ-നപ-ഹഽൠ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะา-ำเ-ๅກ-ຂຄງ-ຈຊຍດ-ທນ-ຟມ-ຣລວສ-ຫອ-ະາ-ຳຽເ-ໄໜ-ໝༀཀ-ཇཉ-ཬྈ-ྋက-ဪဿၐ-ၕၚ-ၝၡၥ-ၦၮ-ၰၵ-ႁႎა-ჺᄀ-ᅙᅟ-ᆢᆨ-ᇹሀ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙶᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៜᠠ-ᡂᡄ-ᡷᢀ-ᢨᢪᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦩᧁ-ᧇᨀ-ᨖᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮ-ᮯᰀ-ᰣᱍ-ᱏᱚ-ᱷℵ-ℸⴰ-ⵥⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ〆〼ぁ-ゖゟァ-ヺヿㄅ-ㄭㄱ-ㆎㆠ-ㆷㇰ-ㇿ㐀-䶵一-鿃ꀀ-ꀔꀖ-ꒌꔀ-ꘋꘐ-ꘟꘪ-ꘫꙮꟻ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꤊ-ꤥꤰ-ꥆꨀ-ꨨꩀ-ꩂꩄ-ꩋ가-힣豈-鶴侮-頻並-龎יִײַ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼヲ-ッア-ンᅠ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]|[\ud840-\ud868][\udc00-\udfff]|\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c-\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\ude80-\ude9c\udea0-\uded0\udf00-\udf1e\udf30-\udf40\udf42-\udf49\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc50-\udc9d]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37-\udc38\udc3c\udc3f\udd00-\udd15\udd20-\udd39\ude00\ude10-\ude13\ude15-\ude17\ude19-\ude33]|\ud808[\udc00-\udf6e]|\ud869[\udc00-\uded6]|\ud87e[\udc00-\ude1d]/,
// Non Spacing Mark
Mn: /[\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0901-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0954\u0962-\u0963\u0981\u09bc\u09c1-\u09c4\u09cd\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b62-\u0b63\u0b82\u0bc0\u0bcd\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0cbc\u0cbf\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d41-\u0d44\u0d4d\u0d62-\u0d63\u0dca\u0dd2-\u0dd4\u0dd6\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1c2c-\u1c33\u1c36-\u1c37\u1dc0-\u1de6\u1dfe-\u1dff\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2de0-\u2dff\u302a-\u302f\u3099-\u309a\ua66f\ua67c-\ua67d\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua926-\ua92d\ua947-\ua951\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\ufb1e\ufe00-\ufe0f\ufe20-\ufe26]|\ud800\uddfd|\ud802[\ude01-\ude03\ude05-\ude06\ude0c-\ude0f\ude38-\ude3a\ude3f]|\ud834[\udd67-\udd69\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\udb40[\udd00-\uddef]/,
// Spacing Combining Mark
Mc: /[\u0903\u093e-\u0940\u0949-\u094c\u0982-\u0983\u09be-\u09c0\u09c7-\u09c8\u09cb-\u09cc\u09d7\u0a03\u0a3e-\u0a40\u0a83\u0abe-\u0ac0\u0ac9\u0acb-\u0acc\u0b02-\u0b03\u0b3e\u0b40\u0b47-\u0b48\u0b4b-\u0b4c\u0b57\u0bbe-\u0bbf\u0bc1-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd7\u0c01-\u0c03\u0c41-\u0c44\u0c82-\u0c83\u0cbe\u0cc0-\u0cc4\u0cc7-\u0cc8\u0cca-\u0ccb\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d57\u0d82-\u0d83\u0dcf-\u0dd1\u0dd8-\u0ddf\u0df2-\u0df3\u0f3e-\u0f3f\u0f7f\u102b-\u102c\u1031\u1038\u103b-\u103c\u1056-\u1057\u1062-\u1064\u1067-\u106d\u1083-\u1084\u1087-\u108c\u108f\u17b6\u17be-\u17c5\u17c7-\u17c8\u1923-\u1926\u1929-\u192b\u1930-\u1931\u1933-\u1938\u19b0-\u19c0\u19c8-\u19c9\u1a19-\u1a1b\u1b04\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b44\u1b82\u1ba1\u1ba6-\u1ba7\u1baa\u1c24-\u1c2b\u1c34-\u1c35\ua823-\ua824\ua827\ua880-\ua881\ua8b4-\ua8c3\ua952-\ua953\uaa2f-\uaa30\uaa33-\uaa34\uaa4d]|\ud834[\udd65-\udd66\udd6d-\udd72]/,
// Connector Punctuation
Pc: /[_‿-⁀⁔︳-︴﹍-_]/,
// Decimal Digit Number
Nd: /[0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꩐-꩙0-]|\ud801[\udca0-\udca9]|\ud835[\udfce-\udfff]/
};
function IsValidTypeNameOrIdentifier(value: string, isTypeName: boolean): boolean {
var nextMustBeStartChar: boolean = true;
if (!value.length) {
return false;
}
// each char must be Lu, Ll, Lt, Lm, Lo, Nd, Mn, Mc, Pc
for (var i = 0; i < value.length; i++) {
var ch: string = value[i];
if (
UnicodeCategory.Lu.test(ch) ||
UnicodeCategory.Ll.test(ch) ||
UnicodeCategory.Lt.test(ch) ||
UnicodeCategory.Lm.test(ch) ||
UnicodeCategory.Lo.test(ch)
) {
nextMustBeStartChar = false;
} else if (
UnicodeCategory.Mn.test(ch) ||
UnicodeCategory.Mc.test(ch) ||
UnicodeCategory.Pc.test(ch) ||
UnicodeCategory.Nd.test(ch)
) {
// Underscore is a valid starting character, even though it is a ConnectorPunctuation.
if (nextMustBeStartChar && ch !== "_") {
return false;
}
nextMustBeStartChar = false;
} else {
// We only check the special Type chars for type names.
if (!isTypeName) {
return false;
} else {
var ref: { nextMustBeStartChar: boolean } = { nextMustBeStartChar: nextMustBeStartChar };
var isSpecialTypeChar = IsSpecialTypeChar(ch, ref);
nextMustBeStartChar = ref.nextMustBeStartChar;
if (!isSpecialTypeChar) {
return false;
}
}
}
}
return true;
}
// This can be a special character like a separator that shows up in a type name
// This is an odd set of characters. Some come from characters that are allowed by C++, like < and >.
// Others are characters that are specified in the type and assembly name grammer.
function IsSpecialTypeChar(ch: string, ref: { nextMustBeStartChar: boolean }): boolean {
switch (ch) {
case ":":
case ".":
case "$":
case "+":
case "<":
case ">":
case "-":
case "[":
case "]":
case ",":
case "&":
case "*":
ref.nextMustBeStartChar = true;
return true;
case "`":
return true;
}
return false;
}

View File

@@ -0,0 +1,23 @@
export var Int32 = {
Min: -2147483648,
Max: 2147483647
};
export var Int64 = {
Min: -9223372036854775808,
Max: 9223372036854775807
};
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
export var ValidationRegExp = {
Guid: /^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$/i,
Float: /^[+-]?\d+(\.\d+)?(e[+-]?\d+)?$/i,
// OData seems to require an "L" suffix for Int64 values, yet Azure Storage errors out with it. See http://www.odata.org/documentation/odata-version-2-0/overview/
Integer: /^[+-]?\d+$/i, // Used for both Int32 and Int64 values
Boolean: /^"?(true|false)"?$/i,
DateTime: new RegExp(`^${yearMonthDay}${timeOfDay}${timeZone}$`),
PrimaryKey: /^[^/\\#?\u0000-\u001F\u007F-\u009F]*$/
};

View File

@@ -0,0 +1,341 @@
import * as Utilities from "../../../Tables/Utilities";
import * as StorageExplorerConstants from "../../../Tables/Constants";
import * as EntityPropertyValidationCommon from "./EntityPropertyValidationCommon";
interface IValidationResult {
isInvalid: boolean;
help: string;
}
interface IValueValidator {
validate: (value: string) => IValidationResult;
parseValue: (value: string) => any;
}
/* Constants */
var noHelp: string = "";
var MaximumStringLength = 64 * 1024; // 64 KB
var MaximumRequiredStringLength = 1 * 1024; // 1 KB
class ValueValidator implements IValueValidator {
public validate(value: string): IValidationResult {
// throw new Errors.NotImplementedFunctionError("ValueValidator.validate");
return null;
}
public parseValue(value: string): any {
return value; // default pass-thru implementation
}
}
class KeyValidator implements ValueValidator {
private static detailedHelp = "Enter a string ('/', '\\', '#', '?' and control characters not allowed)."; // Localize
public validate(value: string): IValidationResult {
if (
value == null ||
value.trim().length === 0 ||
EntityPropertyValidationCommon.ValidationRegExp.PrimaryKey.test(value)
) {
return { isInvalid: false, help: noHelp };
} else {
return { isInvalid: true, help: KeyValidator.detailedHelp };
}
}
public parseValue(value: string): string {
return value;
}
}
class BooleanValueValidator extends ValueValidator {
private detailedHelp = "Enter true or false."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Boolean.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): boolean {
// OData seems to require lowercase boolean values, see http://www.odata.org/documentation/odata-version-2-0/overview/
return value.toString().toLowerCase() === "true";
}
}
class DateTimeValueValidator extends ValueValidator {
private detailedHelp = "Enter a date and time."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
// Try to parse the value to see if it is a valid date string
var parsed: number = Date.parse(value);
success = !isNaN(parsed);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): Date {
var millisecondTime = Date.parse(value);
var parsed: Date = new Date(millisecondTime);
return parsed;
}
}
class DoubleValueValidator extends ValueValidator {
private detailedHelp = "Enter a 64-bit floating point value."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Float.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): number {
return parseFloat(value);
}
}
class GuidValueValidator extends ValueValidator {
private detailedHelp = "Enter a 16-byte (128-bit) GUID value."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Guid.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
}
class IntegerValueValidator extends ValueValidator {
private detailedInt32Help = "Enter a signed 32-bit integer."; // localize
private detailedInt64Help = "Enter a signed 64-bit integer, in the range (-2^53 - 1, 2^53 - 1)."; // localize
private isInt64: boolean;
constructor(isInt64: boolean = true) {
super();
this.isInt64 = isInt64;
}
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Integer.test(value) && Utilities.isSafeInteger(value);
if (success) {
var intValue = parseInt(value, 10);
success = !isNaN(intValue);
if (success && !this.isInt64) {
success =
EntityPropertyValidationCommon.Int32.Min <= intValue &&
intValue <= EntityPropertyValidationCommon.Int32.Max;
}
}
}
if (!success) {
help = this.isInt64 ? this.detailedInt64Help : this.detailedInt32Help;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): number {
return parseInt(value, 10);
}
}
// Allow all values for string type, unless the property is required, in which case an empty string is invalid.
class StringValidator extends ValueValidator {
private detailedHelp = "Enter a value up to 64 KB in size."; // localize
private isRequiredHelp = "Enter a value up to 1 KB in size."; // localize
private emptyStringHelp = "Empty string."; // localize
private isRequired: boolean;
constructor(isRequired: boolean) {
super();
this.isRequired = isRequired;
}
public validate(value: string): IValidationResult {
var help: string = this.isRequired ? this.isRequiredHelp : this.detailedHelp;
if (value === null) {
return { isInvalid: false, help: help };
}
// Ensure we validate the string projection of value.
value = String(value);
var success = true;
if (success) {
success = value.length <= (this.isRequired ? MaximumRequiredStringLength : MaximumStringLength);
}
if (success && this.isRequired) {
help = value ? noHelp : this.emptyStringHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): string {
return String(value); // Ensure value is converted to string.
}
}
class NotSupportedValidator extends ValueValidator {
private type: string;
constructor(type: string) {
super();
this.type = type;
}
public validate(ignoredValue: string): IValidationResult {
//throw new Errors.NotSupportedError(this.getMessage());
return null;
}
public parseValue(ignoredValue: string): any {
//throw new Errors.NotSupportedError(this.getMessage());
return null;
}
private getMessage(): string {
return "Properties of type " + this.type + " are not supported.";
}
}
class PropertyValidatorFactory {
public getValidator(type: string, isRequired: boolean) {
var validator: IValueValidator = null;
// TODO classify rest of Cassandra types/create validators for them
switch (type) {
case StorageExplorerConstants.TableType.Boolean:
case StorageExplorerConstants.CassandraType.Boolean:
validator = new BooleanValueValidator();
break;
case StorageExplorerConstants.TableType.DateTime:
validator = new DateTimeValueValidator();
break;
case StorageExplorerConstants.TableType.Double:
case StorageExplorerConstants.CassandraType.Decimal:
case StorageExplorerConstants.CassandraType.Double:
case StorageExplorerConstants.CassandraType.Float:
validator = new DoubleValueValidator();
break;
case StorageExplorerConstants.TableType.Guid:
case StorageExplorerConstants.CassandraType.Uuid:
validator = new GuidValueValidator();
break;
case StorageExplorerConstants.TableType.Int32:
case StorageExplorerConstants.CassandraType.Int:
// TODO create separate validators for smallint and tinyint
case StorageExplorerConstants.CassandraType.Smallint:
case StorageExplorerConstants.CassandraType.Tinyint:
validator = new IntegerValueValidator(/* isInt64 */ false);
break;
case StorageExplorerConstants.TableType.Int64:
case StorageExplorerConstants.CassandraType.Bigint:
case StorageExplorerConstants.CassandraType.Varint:
validator = new IntegerValueValidator(/* isInt64 */ true);
break;
case StorageExplorerConstants.TableType.String:
case StorageExplorerConstants.CassandraType.Text:
case StorageExplorerConstants.CassandraType.Ascii:
case StorageExplorerConstants.CassandraType.Varchar:
validator = new StringValidator(isRequired);
break;
case "Key":
validator = new KeyValidator();
break;
default:
validator = new NotSupportedValidator(type);
break;
}
return validator;
}
}
interface ITypeValidatorMap {
[type: string]: IValueValidator;
}
export default class EntityPropertyValueValidator {
private validators: ITypeValidatorMap;
private validatorFactory: PropertyValidatorFactory;
private isRequired: boolean;
constructor(isRequired: boolean) {
this.validators = {};
this.validatorFactory = new PropertyValidatorFactory();
this.isRequired = isRequired;
}
public validate(value: string, type: string): IValidationResult {
var validator: IValueValidator = this.getValidator(type);
return validator ? validator.validate(value) : null; // Should not happen.
}
public parseValue(value: string, type: string): any {
var validator: IValueValidator = this.getValidator(type);
return validator ? validator.parseValue(value) : null; // Should not happen.
}
private getValidator(type: string): IValueValidator {
var validator: IValueValidator = this.validators[type];
if (!validator) {
validator = this.validatorFactory.getValidator(type, this.isRequired);
this.validators[type] = validator;
}
return validator;
}
}

View File

@@ -0,0 +1,72 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="uploadFilePane">
<!-- Upload File form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload File header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Upload File header - End -->
<!-- Upload File 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"
>More details</a
>
</span>
</div>
</div>
<!-- Upload File errors - End -->
<!-- Upload File inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader" data-bind="text: selectFileInputLabel"></div>
<input class="importFilesTitle" type="text" disabled data-bind="value: selectedFilesTitle" />
<input
type="file"
id="importFileInput"
style="display: none"
data-bind="event: { change: updateSelectedFiles }, attr: { accept: extensions }"
/>
<a
href="#"
id="fileImportLinkNotebook"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
</div>
</div>
<!-- Upload File inputs - End -->
</form>
</div>
<!-- Upload File form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,130 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class UploadFilePane extends ContextualPaneBase implements ViewModels.UploadFilePane {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
private openOptions: ViewModels.UploadFilePaneOpenOptions;
private submitButtonLabel: ko.Observable<string>;
private selectFileInputLabel: ko.Observable<string>;
private extensions: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.resetData();
this.selectFileInputLabel = ko.observable("");
this.selectedFilesTitle = ko.observable<string>("");
this.extensions = ko.observable(null);
this.submitButtonLabel = ko.observable("Load");
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this.updateSelectedFilesTitle(newFiles));
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No file specified");
this.formErrorsDetails("No file specified. Please input a file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} -- No file specified. Please input a file.`
);
return;
}
const file: File = this.files().item(0);
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`${this.openOptions.inProgressMessage}: ${file.name}`
);
this.isExecuting(true);
this.openOptions
.onSubmit(this.files().item(0))
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.openOptions.successMessage} ${file.name}`
);
this.close();
},
(error: any) => {
this.formErrors(this.openOptions.errorMessage);
this.formErrorsDetails(`${this.openOptions.errorMessage}: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} ${file.name}: ${error}`
);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.resetFileInput();
}
public openWithOptions(options: ViewModels.UploadFilePaneOpenOptions): void {
this.openOptions = options;
this.title(this.openOptions.paneTitle);
if (this.openOptions.submitButtonLabel) {
this.submitButtonLabel(this.openOptions.submitButtonLabel);
}
this.selectFileInputLabel(this.openOptions.selectFileInputLabel);
if (this.openOptions.extensions) {
this.extensions(this.openOptions.extensions);
}
super.open();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importFileInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
private updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private resetFileInput(): void {
const inputElement = $("#importFileInput");
inputElement
.wrap("<form>")
.closest("form")
.get(0)
.reset();
inputElement.unwrap();
}
}

View File

@@ -0,0 +1,130 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="uploaditemspane">
<!-- Upload items form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload items header - Start -->
<div class="firstdivbg headerline">
<span 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>
<!-- Upload items header - End -->
<!-- Upload items 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"
>More details</a
>
</span>
</div>
</div>
<!-- Upload items errors - End -->
<!-- Upload item inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader">
<span> Select JSON Files </span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>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.</span
>
</span>
</div>
<input
class="importFilesTitle"
type="text"
disabled
data-bind="value: selectedFilesTitle"
aria-label="Select JSON Files"
/>
<input
type="file"
id="importDocsInput"
title="Upload Icon"
multiple
accept="application/json"
role="button"
tabindex="0"
style="display: none"
data-bind="event: { change: updateSelectedFiles }"
/>
<a
href="#"
id="fileImportLink"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
autofocus
>
<img
class="fileImportImg"
src="/folder_16x16.svg"
alt="Select JSON files to upload"
title="Select JSON files to upload"
/>
</a>
</div>
<div class="fileUploadSummaryContainer" data-bind="visible: uploadFileDataVisible">
<b>File upload status</b>
<table class="fileUploadSummary">
<thead>
<tr class="fileUploadSummaryHeader fileUploadSummaryTuple">
<th>FILE NAME</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
<!-- ko foreach: uploadFileData -->
<tr class="fileUploadSummaryTuple">
<td data-bind="text: $data.fileName"></td>
<td data-bind="text: $parent.fileUploadSummaryText($data.numSucceeded, $data.numFailed)"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Upload" class="btncreatecoll1" /></div>
</div>
<!-- Upload item inputs - End -->
</form>
</div>
<!-- Upload items form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,147 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
export class UploadItemsPane extends ContextualPaneBase {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
public uploadFileDataVisible: ko.Computed<boolean>;
public uploadFileData: ko.ObservableArray<UploadDetailsRecord>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this._initTitle();
this.resetData();
this.selectedFilesTitle = ko.observable<string>("");
this.uploadFileData = ko.observableArray<UploadDetailsRecord>();
this.uploadFileDataVisible = ko.computed<boolean>(
() => !!this.uploadFileData() && this.uploadFileData().length > 0
);
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this._updateSelectedFilesTitle(newFiles));
}
public submit() {
this.formErrors("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No files specified");
this.formErrorsDetails("No files were specified. Please input at least one file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- No files were specified. Please input at least one file."
);
return;
} else if (this._totalFileSizeForFileList(this.files()) > UPLOAD_FILE_SIZE_LIMIT) {
this.formErrors("Upload file size limit exceeded");
this.formErrorsDetails("Total file upload size exceeds the 2 MB file size limit.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- Total file upload size exceeds the 2 MB file size limit."
);
return;
}
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.isExecuting(true);
selectedCollection &&
selectedCollection
.uploadFiles(this.files())
.then(
(uploadDetails: UploadDetails) => {
this.uploadFileData(uploadDetails.data);
this.files(undefined);
this._resetFileInput();
},
(error: any) => {
const message = ErrorParserUtility.parse(error);
this.formErrors(message[0].message);
this.formErrorsDetails(message[0].message);
}
)
.finally(() => {
this.isExecuting(false);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.uploadFileData([]);
this._resetFileInput();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importDocsInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
public fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
return `${numSucceeded} items created, ${numFailed} errors`;
};
private _totalFileSizeForFileList(fileList: FileList): number {
let totalFileSize: number = 0;
if (!fileList) {
return totalFileSize;
}
for (let i = 0; i < fileList.length; i++) {
totalFileSize = totalFileSize + fileList.item(i).size;
}
return totalFileSize;
}
private _updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private _initTitle(): void {
if (this.container.isPreferredApiCassandra() || this.container.isPreferredApiTable()) {
this.title("Upload Tables");
} else if (this.container.isPreferredApiGraph()) {
this.title("Upload Graph");
} else {
this.title("Upload Items");
}
}
private _resetFileInput(): void {
const inputElement = $("#importDocsInput");
inputElement
.wrap("<form>")
.closest("form")
.get(0)
.reset();
inputElement.unwrap();
}
}