Prettier 2.0 (#393)

This commit is contained in:
Steve Faulkner
2021-01-20 09:15:01 -06:00
committed by GitHub
parent c1937ca464
commit 4be53284b5
500 changed files with 41927 additions and 41838 deletions

View File

@@ -22,114 +22,176 @@
</div>
<script type="text/html" id="add-collection-inputs">
<!-- Add collection header - Start -->
<!-- Add collection header - Start -->
<div class="firstdivbg headerline">
<span id="containerTitle" role="heading" aria-level="2" data-bind="text: title" ></span>
<div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="Add collection close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
<span id="containerTitle" role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
id="closeBtnAddCollection"
role="button"
aria-label="Add collection close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
tabindex="0"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add collection header - End -->
<!-- Add collection errors - Start -->
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: formErrors() && formErrors() !== ''">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '' , click: showErrorDetails, event: { keypress: onMoreDetailsKeyPress }"
tabindex="0">
More details</a>
</span>
</div>
<div 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 class="warningErrorContent">
<span><img class="paneErrorIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formWarnings, attr: { title: formWarnings }"></span>
</span>
</div>
</div>
<!-- Add collection errors - End -->
<!-- upsell message - start -->
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a class="underlinedLink" id="linkAddCollection" data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="" tabindex="0"></a>
</span>
</div>
<div
class="infoBoxContainer"
aria-live="assertive"
data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()"
>
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo" /></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a
class="underlinedLink"
id="linkAddCollection"
data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank"
href=""
tabindex="0"
></a>
</span>
</div>
</div>
<!-- upsell message - end -->
<!-- Add collection inputs - Start -->
<div class="paneMainContent" data-bind="visible: !maxCollectionsReached()">
<div data-bind="visible: !isPreferredApiTable()">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Database id</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth">A database is analogous to a namespace. It is the unit of
management for a set of containers.</span>
</span>
</p>
<div 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>
<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
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="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">
<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>
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
<option data-bind="value: $data"></option>
</datalist>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="Provision database throughput"
data-bind="visible: databaseCreateNew">
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput" title="Provision database throughput"
data-bind="checked: databaseCreateNewShared" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext provisionDatabaseThroughput">Provisioned throughput at the database level will
be shared across all containers within the database.</span>
</span>
</div>
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input-autopilot-v3 params="{
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="Provision database throughput" data-bind="visible: databaseCreateNew">
<input
tabindex="0"
type="checkbox"
data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput"
title="Provision database throughput"
data-bind="checked: databaseCreateNewShared"
/>
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext provisionDatabaseThroughput"
>Provisioned throughput at the database level will be shared across all containers within the
database.</span
>
</span>
</div>
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input-autopilot-v3
params="{
testId: 'databaseThroughputValue',
value: throughputDatabase,
minimum: minThroughputRU,
@@ -154,164 +216,203 @@
canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}">
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel" data-bind="text: collectionIdTitle"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth">Unique identifier for the container and used for
id-based
routing through REST and all SDKs</span>
</span>
</p>
<input name="collectionId" id="containerId" data-test="addCollection-collectionId" type="text"
aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="e.g., Container1"
size="40" class="textfontclr collid"
data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }">
</div>
<!-- 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>
<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>
<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 (20 GB)</label>
</div>
<!-- Fixed option button - End -->
<!-- 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 -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input type="radio" id="tab2" name="storage" value="100" class="radio" data-bind="checked: storage">
<label for="tab2">Unlimited</label>
</div>
<!-- Unlimited option button - End -->
</div>
<!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: partitionKeyVisible">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel" data-bind="text: partitionKeyName"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth">The <span data-bind="text: partitionKeyName"></span>
is used to automatically partition
data among multiple servers for scalability. Choose a JSON property name that has a
wide
range of values and is likely to have evenly distributed access patterns.</span>
</span>
</p>
<input type="text" id="addCollection-partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey,
<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 (20 GB)</label>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input type="radio" id="tab2" name="storage" value="100" class="radio" data-bind="checked: storage" />
<label for="tab2">Unlimited</label>
</div>
<!-- Unlimited option button - End -->
</div>
<!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: partitionKeyVisible">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel" data-bind="text: partitionKeyName"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>The <span data-bind="text: partitionKeyName"></span> is used to automatically partition data among
multiple servers for scalability. Choose a JSON property name that has a wide range of values and is
likely to have evenly distributed access patterns.</span
>
</span>
</p>
<input
type="text"
id="addCollection-partitionKeyValue"
data-test="addCollection-partitionKeyValue"
aria-required="true"
size="40"
class="textfontclr collid"
data-bind="textInput: partitionKey,
attr: {
placeholder: partitionKeyPlaceholder,
required: partitionKeyVisible(),
'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 -->
}"
/>
</div>
<!-- large parition key - start -->
<div class="largePartitionKey" aria-label="Large Partition Key" data-bind="visible: partitionKeyVisible">
<input
tabindex="0"
type="checkbox"
id="largePartitionKey"
data-test="addCollection-largePartitionKey"
title="Large Partition Key"
data-bind="checked: largePartitionKey"
/>
<span for="largePartitionKey"
>My <span data-bind="text: lowerCasePartitionKeyName"></span> is larger than 100 bytes</span
>
<p
data-bind="visible: largePartitionKey"
class="largePartitionKeyDescription"
data-test="addCollection-largePartitionKeyDescription"
>
Old SDKs do not work with containers that support large
<span data-bind="text: lowerCasePartitionKeyName"></span>s, ensure you are using the right SDK version.
<a class="errorLink" href="https://aka.ms/cosmosdb/pkv2" target="_blank">Learn more</a>
</p>
</div>
<!-- large parition key - end -->
<!-- ko if: canConfigureThroughput -->
<!-- Provision collection throughput checkbox - start -->
<div class="pkPadding" data-bind="visible: databaseHasSharedOffer() && !databaseCreateNew()">
<input type="checkbox" id="collectionSharedThroughput"
data-bind="checked: collectionWithThroughputInShared, attr: {title:collectionWithThroughputInSharedTitle}" />
<span for="collectionSharedThroughput" data-bind="text: collectionWithThroughputInSharedTitle"></span>
<span class="leftAlignInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext sharedCollectionThroughputTooltipWidth">You can optionally
provision
dedicated throughput for a container within a database that has throughput provisioned.
This
dedicated throughput amount will not be shared with other containers in the database and
does
not count towards the throughput you provisioned for the database. This throughput
amount
will
be billed in addition to the throughput amount you provisioned at the database
level.</span>
</span>
</div>
<!-- Provision collection throughput checkbox - end -->
<!-- ko if: canConfigureThroughput -->
<!-- Provision collection throughput checkbox - start -->
<div class="pkPadding" data-bind="visible: databaseHasSharedOffer() && !databaseCreateNew()">
<input
type="checkbox"
id="collectionSharedThroughput"
data-bind="checked: collectionWithThroughputInShared, attr: {title:collectionWithThroughputInSharedTitle}"
/>
<span for="collectionSharedThroughput" data-bind="text: collectionWithThroughputInSharedTitle"></span>
<span class="leftAlignInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext sharedCollectionThroughputTooltipWidth"
>You can optionally provision dedicated throughput for a container within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other containers in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.</span
>
</span>
</div>
<!-- Provision collection throughput checkbox - end -->
<!-- Provision collection throughput spinner - start -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input-autopilot-v3 params="{
<!-- Provision collection throughput spinner - start -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input-autopilot-v3
params="{
testId: 'collectionThroughputValue',
value: throughputMultiPartition,
minimum: minThroughputRU,
@@ -336,130 +437,174 @@
canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFixedStorageSelected(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}">
</throughput-input-autopilot-v3>
</div>
<!-- Provision collection throughput spinner - end -->
<!-- /ko -->
<!-- Provision collection throughput - end -->
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- Provision collection throughput spinner - end -->
<!-- /ko -->
<!-- Provision collection throughput - end -->
<!-- Custom indexes for mongo checkbox - start -->
<div class="pkPadding" data-bind="visible: container.isEnableMongoCapabilityPresent()">
<p>
<span class="addCollectionLabel">Indexing</span>
</p>
<input type="checkbox" id="mongoWildcardIndex" title="mongoWildcardIndex"
data-bind="checked: shouldCreateMongoWildcardIndex" />
<span>Create a Wildcard Index on all fields</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext mongoWildcardIndexTooltipWidth">
By default, only the field _id is indexed. Creating a wildcard index on all fields will quickly optimize query performance and is recommended during development.
</span>
</span>
</div>
<!-- Custom indexes for mongo checkbox - end -->
<!-- Custom indexes for mongo checkbox - start -->
<div class="pkPadding" data-bind="visible: container.isEnableMongoCapabilityPresent()">
<p>
<span class="addCollectionLabel">Indexing</span>
</p>
<input
type="checkbox"
id="mongoWildcardIndex"
title="mongoWildcardIndex"
data-bind="checked: shouldCreateMongoWildcardIndex"
/>
<span>Create a Wildcard Index on all fields</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext mongoWildcardIndexTooltipWidth">
By default, only the field _id is indexed. Creating a wildcard index on all fields will quickly optimize
query performance and is recommended during development.
</span>
</span>
</div>
<!-- Custom indexes for mongo checkbox - end -->
<!-- Enable analytical storage - start -->
<div class="enableAnalyticalStorage pkPadding" aria-label="Enable Analytical Store"
data-bind="visible: isSynapseLinkSupported">
<div>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span>
<span class="infoTooltip" role="tooltip" tabindex="0" data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }">
Enable analytical store capability to perform near real-time analytics on your operational
data, without impacting the performance of transactional workloads.
Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a>
</span>
</span>
</div>
<!-- Enable analytical storage - start -->
<div
class="enableAnalyticalStorage pkPadding"
aria-label="Enable Analytical Store"
data-bind="visible: isSynapseLinkSupported"
>
<div>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span>
<span
class="infoTooltip"
role="tooltip"
tabindex="0"
data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }"
>
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }">
Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads. Learn more
<a
id="link1"
class="errorLink"
href="https://aka.ms/analytical-store-overview"
target="_blank"
data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }"
>here</a
>
</span>
</span>
</div>
<div class="paragraph">
<input class="enableAnalyticalStorageRadio" id="enableAnalyticalStorageRadioOn"
name="analyticalStore" type="radio" role="radio" tabindex="0" data-bind="
<div class="paragraph">
<input
class="enableAnalyticalStorageRadio"
id="enableAnalyticalStorageRadioOn"
name="analyticalStore"
type="radio"
role="radio"
tabindex="0"
data-bind="
disable: showEnableSynapseLink,
checked: isAnalyticalStorageOn,
checkedValue: true,
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'true' : 'false'
}" />
<label for="enableAnalyticalStorageRadioOn" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
On
</span>
</label>
}"
/>
<label for="enableAnalyticalStorageRadioOn" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink"> On </span>
</label>
<input class="enableAnalyticalStorageRadio" id="enableAnalyticalStorageRadioOff"
name="analyticalStore" type="radio" role="radio" tabindex="0" data-bind="
<input
class="enableAnalyticalStorageRadio"
id="enableAnalyticalStorageRadioOff"
name="analyticalStore"
type="radio"
role="radio"
tabindex="0"
data-bind="
disable: showEnableSynapseLink,
checked: isAnalyticalStorageOn,
checkedValue: false,
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'false' : 'true'
}" />
<label for="enableAnalyticalStorageRadioOff" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
Off
</span>
</label>
</div>
}"
/>
<label for="enableAnalyticalStorageRadioOff" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink"> Off </span>
</label>
</div>
<div class="paragraph italic" data-bind="visible: ttl90DaysEnabled() && isAnalyticalStorageOn()">
By default, Analytical Time-to-Live will be configured to retain 90 days of data in the analytical
store. You can configure a custom retention policy in the 'Settings' tab.
<span><a class="errorLink" href="https://aka.ms/cosmosdb-analytical-ttl" target="_blank">Learn
more</a></span>
</div>
<div class="paragraph 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">
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="
<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 -->
"
>
Enable
</button>
</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>
<!-- 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 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>
<error-display params="{ errorMsg: maxCollectionsReachedMessage }"></error-display>
</div>
<!-- Add collection inputs - End -->
</script>

View File

@@ -16,10 +16,10 @@ describe("Add Collection Pane", () => {
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false
enableFreeTier: false,
},
type: undefined,
tags: []
tags: [],
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
@@ -32,10 +32,10 @@ describe("Add Collection Pane", () => {
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true
enableFreeTier: true,
},
type: undefined,
tags: []
tags: [],
};
beforeEach(() => {

View File

@@ -106,7 +106,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseCreateNew = ko.observable<boolean>(true);
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.container.subscriptionType &&
this.container.subscriptionType.subscribe(subscriptionType => {
this.container.subscriptionType.subscribe((subscriptionType) => {
this.databaseCreateNewShared(this.getSharedThroughputDefault());
});
this.collectionWithThroughputInShared = ko.observable<boolean>(false);
@@ -132,7 +132,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.partitionKey.extend({ rateLimit: 100 });
this.partitionKeyPattern = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) {
return "^\/[^\/]*";
return "^/[^/]*";
}
return ".*";
});
@@ -620,7 +620,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
// TODO: remove check for capability once all accounts have been migrated
const capabilities = properties.capabilities || ([] as DataModels.Capability[]);
if (capabilities.some(capability => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return true;
}
@@ -648,9 +648,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.isSynapseLinkUpdating = ko.computed(() => this.container.isSynapseLinkUpdating());
this.useIndexingForSharedThroughput.subscribe(value => {
this.useIndexingForSharedThroughput.subscribe((value) => {
TelemetryProcessor.traceMark(Action.ModifyOptionForThroughputWithSharedDatabase, {
changedSelectedValueTo: value ? ActionModifiers.IndexAll : ActionModifiers.NoIndex
changedSelectedValueTo: value ? ActionModifiers.IndexAll : ActionModifiers.NoIndex,
});
});
@@ -696,16 +696,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(),
offerThroughput: this._getThroughput(),
partitionKey: this.partitionKey(),
databaseId: this.databaseId()
databaseId: this.databaseId(),
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: this._getThroughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
await this.container.loadDatabaseOffers();
@@ -778,7 +778,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
? {
paths: [partitionKeyPath],
kind: Constants.BackendDefaults.partitionKeyKind,
version: partitionKeyVersion
version: partitionKeyVersion,
}
: null;
const autoPilot: DataModels.AutoPilotCreationSettings = this._getAutoPilot();
@@ -789,7 +789,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
database: ko.toJS({
id: this.databaseId(),
new: this.databaseCreateNew(),
shared: this.databaseHasSharedOffer()
shared: this.databaseHasSharedOffer(),
}),
offerThroughput: offerThroughput,
offerAutopilot: autoPilot,
@@ -798,17 +798,17 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(),
partitionKey,
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
useIndexingForSharedThroughput: this.useIndexingForSharedThroughput()
useIndexingForSharedThroughput: this.useIndexingForSharedThroughput(),
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
@@ -850,7 +850,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
indexingPolicy,
partitionKey,
uniqueKeyPolicy,
createMongoWildcardIndex
createMongoWildcardIndex,
};
createCollection(createCollectionParams).then(
@@ -864,7 +864,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
database: ko.toJS({
id: this.databaseId(),
new: this.databaseCreateNew(),
shared: this.databaseHasSharedOffer()
shared: this.databaseHasSharedOffer(),
}),
offerThroughput,
collection: ko.toJS({
@@ -872,16 +872,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(),
partitionKey,
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
this.resetData();
@@ -898,7 +898,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
database: ko.toJS({
id: this.databaseId(),
new: this.databaseCreateNew(),
shared: this.databaseHasSharedOffer()
shared: this.databaseHasSharedOffer(),
}),
offerThroughput: offerThroughput,
collection: {
@@ -906,18 +906,18 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(),
partitionKey,
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
},
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error)
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
@@ -1073,12 +1073,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput()) {
return {
maxThroughput: this.sharedAutoPilotThroughput() * 1
maxThroughput: this.sharedAutoPilotThroughput() * 1,
};
}
if (this.isAutoPilotSelected() && this.autoPilotThroughput()) {
return {
maxThroughput: this.autoPilotThroughput() * 1
maxThroughput: this.autoPilotThroughput() * 1,
};
}

View File

@@ -22,75 +22,110 @@
</div>
<script type="text/html" id="add-database-inputs">
<!-- Add database header - Start -->
<!-- Add database header - Start -->
<div class="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level="2" 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>
<span id="databaseTitle" role="heading" aria-level="2" 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 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: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a class="underlinedLink" id="linkAddDatabase" data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="" tabindex="0"></a>
</span>
</div>
<div
class="infoBoxContainer"
aria-live="assertive"
data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()"
>
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo" /></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a
class="underlinedLink"
id="linkAddDatabase"
data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank"
href=""
tabindex="0"
></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>
<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 '\' '/' '#' '?'"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { 'aria-label': databaseIdLabel, 'placeholder': databaseIdPlaceHolder }"
autofocus>
<input
id="database-id"
type="text"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size="40"
class="collid"
data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { 'aria-label': databaseIdLabel, 'placeholder': databaseIdPlaceHolder }"
autofocus
/>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<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>
<div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3 params="{
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<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>
<div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3
params="{
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
@@ -116,21 +151,25 @@
canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}">
</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 -->
<!-- Database provisioned throughput - End -->
}"
>
</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 -->
<!-- Database provisioned throughput - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" value="OK" class="btncreatecoll1">
</div>
<div class="leftpanel-okbut">
<input type="submit" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Add database inputs - End -->
</script>

View File

@@ -17,10 +17,10 @@ describe("Add Database Pane", () => {
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false
enableFreeTier: false,
},
type: undefined,
tags: []
tags: [],
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
@@ -33,10 +33,10 @@ describe("Add Database Pane", () => {
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true
enableFreeTier: true,
},
type: undefined,
tags: []
tags: [],
};
beforeEach(() => {

View File

@@ -62,7 +62,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.container.subscriptionType &&
this.container.subscriptionType.subscribe(subscriptionType => {
this.container.subscriptionType.subscribe((subscriptionType) => {
this.databaseCreateNewShared(this.getSharedThroughputDefault());
});
@@ -278,9 +278,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: this.throughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("database-id");
focusElement && focusElement.focus();
@@ -299,15 +299,15 @@ export default class AddDatabasePane extends ContextualPaneBase {
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
shared: this.databaseCreateNewShared(),
}),
offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
this.formErrors("");
@@ -315,7 +315,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
const createDatabaseParams: DataModels.CreateDatabaseParams = {
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (this.isAutoPilotSelected()) {
@@ -363,15 +363,15 @@ export default class AddDatabasePane extends ContextualPaneBase {
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
this.resetData();
@@ -387,17 +387,17 @@ export default class AddDatabasePane extends ContextualPaneBase {
defaultExperience: this.container.defaultExperience(),
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared()
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error)
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}
@@ -458,7 +458,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) {
return {
maxThroughput: this.maxAutoPilotThroughputSet() * 1
maxThroughput: this.maxAutoPilotThroughputSet() * 1,
};
}
return undefined;

View File

@@ -1,33 +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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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>
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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

@@ -1,108 +1,108 @@
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 * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class BrowseQueriesPane extends ContextualPaneBase {
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) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
this.formErrors(`Failed to setup a collection for saved queries: ${errorMessage}`);
} 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 = this.container.tabsManager.activeTab() as 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();
};
}
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 * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class BrowseQueriesPane extends ContextualPaneBase {
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) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
this.formErrors(`Failed to setup a collection for saved queries: ${errorMessage}`);
} 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 = this.container.tabsManager.activeTab() as 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

@@ -37,7 +37,7 @@
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<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>
@@ -115,7 +115,7 @@
/>
<datalist id="keyspacesList" data-bind="foreach: container.nonSystemDatabases">
<option data-bind="value: $data.id"> </option>
<option data-bind="value: $data.id"></option>
</datalist>
<!-- Database provisioned throughput - Start -->
@@ -179,7 +179,7 @@
<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>
<div data-bind="text: createTableQuery" style="float: left; padding-top: 3px; padding-right: 3px"></div>
<input
type="text"
data-test="addCollection-tableId"
@@ -192,14 +192,14 @@
size="20"
class="textfontclr"
data-bind="value: tableId"
style="margin-bottom: 5px;"
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;"
style="height: 125px; width: calc(100% - 80px); resize: vertical"
></textarea>
</div>

View File

@@ -106,7 +106,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
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.keyspaceId.subscribe((keyspaceId) => {
this.createTableQuery(`CREATE TABLE ${keyspaceId}.`);
});
@@ -297,16 +297,16 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId()
databaseId: this.keyspaceId(),
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("keyspace-id");
focusElement && focusElement.focus();
@@ -350,7 +350,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput()
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
@@ -358,12 +358,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery
createTableQuery: createTableQuery,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
if (toCreateKeyspace) {
@@ -396,7 +396,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput()
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
@@ -404,16 +404,16 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery
createTableQuery: createTableQuery,
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
},
error => {
(error) => {
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.isExecuting(false);
@@ -426,7 +426,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput()
hasDedicatedThroughput: this.dedicateTableThroughput(),
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
@@ -434,14 +434,14 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: this.container.flight()
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery,
error: errorMessage,
errorStack: getErrorStack(error)
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
@@ -536,13 +536,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.sharedAutoPilotThroughput()
) {
return {
maxThroughput: this.sharedAutoPilotThroughput() * 1
maxThroughput: this.sharedAutoPilotThroughput() * 1,
};
}
if (this.selectedAutoPilotThroughput()) {
return {
maxThroughput: this.selectedAutoPilotThroughput() * 1
maxThroughput: this.selectedAutoPilotThroughput() * 1,
};
}

View File

@@ -1,136 +1,136 @@
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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
private initalFocusedElement: HTMLElement | undefined;
public id: string;
public container: 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 isExecuting: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super();
this.id = options.id;
this.container = options.container;
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();
this.resetFocus();
}
public open() {
this.initalFocusedElement = document.activeElement as HTMLElement;
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);
}
private resetFocus(): void {
if (this.initalFocusedElement) {
this.initalFocusedElement.focus();
this.initalFocusedElement = undefined;
}
}
}
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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
private initalFocusedElement: HTMLElement | undefined;
public id: string;
public container: 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 isExecuting: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super();
this.id = options.id;
this.container = options.container;
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();
this.resetFocus();
}
public open() {
this.initalFocusedElement = document.activeElement as HTMLElement;
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);
}
private resetFocus(): void {
if (this.initalFocusedElement) {
this.initalFocusedElement.focus();
this.initalFocusedElement = undefined;
}
}
}

View File

@@ -1,194 +1,194 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export class CopyNotebookPaneAdapter implements ReactAdapter {
private static readonly BranchNameWhiteSpace = " ";
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private content: string;
private pinnedRepos: IPinnedRepo[];
private selectedLocation: Location;
constructor(
private container: Explorer,
private junoClient: JunoClient,
private gitHubOAuthService: GitHubOAuthService
) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const genericPaneProps: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "copynotebookpane",
isExecuting: this.isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: () => this.close(),
onSubmit: () => this.submit()
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name: this.name,
pinnedRepos: this.pinnedRepos,
onDropDownChange: this.onDropDownChange
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(name: string, content: string): Promise<void> {
this.name = name;
this.content = content;
this.isOpened = true;
this.triggerRender();
if (this.gitHubOAuthService.isLoggedIn()) {
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
this.pinnedRepos = response.data;
this.triggerRender();
}
}
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
let destination: string = this.selectedLocation?.type;
let clearMessage: () => void;
this.isExecuting = true;
this.triggerRender();
try {
if (!this.selectedLocation) {
throw new Error(`No location selected`);
}
if (this.selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
this.selectedLocation.owner,
this.selectedLocation.repo
)} - ${this.selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${this.name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to copy ${this.name} to ${destination}`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearMessage && clearMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(
this.selectedLocation.owner,
this.selectedLocation.repo,
this.selectedLocation.branch,
""
),
type: NotebookContentItemType.Directory
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return this.container.uploadFile(this.name, this.content, parent);
};
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
this.selectedLocation = option?.data;
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.content = undefined;
this.pinnedRepos = undefined;
this.selectedLocation = undefined;
};
}
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export class CopyNotebookPaneAdapter implements ReactAdapter {
private static readonly BranchNameWhiteSpace = " ";
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private content: string;
private pinnedRepos: IPinnedRepo[];
private selectedLocation: Location;
constructor(
private container: Explorer,
private junoClient: JunoClient,
private gitHubOAuthService: GitHubOAuthService
) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const genericPaneProps: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "copynotebookpane",
isExecuting: this.isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name: this.name,
pinnedRepos: this.pinnedRepos,
onDropDownChange: this.onDropDownChange,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(name: string, content: string): Promise<void> {
this.name = name;
this.content = content;
this.isOpened = true;
this.triggerRender();
if (this.gitHubOAuthService.isLoggedIn()) {
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
this.pinnedRepos = response.data;
this.triggerRender();
}
}
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
let destination: string = this.selectedLocation?.type;
let clearMessage: () => void;
this.isExecuting = true;
this.triggerRender();
try {
if (!this.selectedLocation) {
throw new Error(`No location selected`);
}
if (this.selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
this.selectedLocation.owner,
this.selectedLocation.repo
)} - ${this.selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${this.name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to copy ${this.name} to ${destination}`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearMessage && clearMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(
this.selectedLocation.owner,
this.selectedLocation.repo,
this.selectedLocation.branch,
""
),
type: NotebookContentItemType.Directory,
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return this.container.uploadFile(this.name, this.content, parent);
};
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
this.selectedLocation = option?.data;
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.content = undefined;
this.pinnedRepos = undefined;
this.selectedLocation = undefined;
};
}

View File

@@ -11,7 +11,7 @@ import {
IDropdownOption,
SelectableOptionMenuItemType,
IRenderFunction,
ISelectableOption
ISelectableOption,
} from "office-ui-fabric-react";
interface Location {
@@ -40,7 +40,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
onRenderTitle: this.onRenderDropDownTitle,
onRenderOption: this.onRenderDropDownOption,
options: this.getDropDownOptions(),
onChange: this.props.onDropDownChange
onChange: this.props.onDropDownChange,
};
return (
@@ -73,32 +73,32 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
text: ResourceTreeAdapter.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle,
data: {
type: "MyNotebooks"
} as Location
type: "MyNotebooks",
} as Location,
});
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
options.push({
key: "GitHub-Header-Divider",
text: undefined,
itemType: SelectableOptionMenuItemType.Divider
itemType: SelectableOptionMenuItemType.Divider,
});
options.push({
key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header
itemType: SelectableOptionMenuItemType.Header,
});
this.props.pinnedRepos.forEach(pinnedRepo => {
this.props.pinnedRepos.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
options.push({
key: `GitHub-Repo-${repoFullName}`,
text: repoFullName,
disabled: true
disabled: true,
});
pinnedRepo.branches.forEach(branch =>
pinnedRepo.branches.forEach((branch) =>
options.push({
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
@@ -107,8 +107,8 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
type: "GitHub",
owner: pinnedRepo.owner,
repo: pinnedRepo.name,
branch: branch.name
} as Location
branch: branch.name,
} as Location,
})
);
});

View File

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

View File

@@ -31,7 +31,7 @@ describe("Delete Collection Confirmation Pane", () => {
let database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([
{} as ViewModels.Collection,
{} as ViewModels.Collection
{} as ViewModels.Collection,
]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastCollection()).toBe(false);
@@ -63,7 +63,7 @@ describe("Delete Collection Confirmation Pane", () => {
let pane = new DeleteCollectionConfirmationPane({
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer
container: fakeExplorer,
});
fakeExplorer.isLastCollection = () => true;
@@ -98,7 +98,7 @@ describe("Delete Collection Confirmation Pane", () => {
fakeExplorer.findSelectedCollection = () => {
return {
id: ko.observable<string>(selectedCollectionId),
rid: "test"
rid: "test",
} as ViewModels.Collection;
};
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
@@ -108,7 +108,7 @@ describe("Delete Collection Confirmation Pane", () => {
const AccountName = "testAccount";
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName
name: AccountName,
} as DataModels.DatabaseAccount);
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
@@ -124,7 +124,7 @@ describe("Delete Collection Confirmation Pane", () => {
let pane = new DeleteCollectionConfirmationPane({
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
container: fakeExplorer as any,
});
pane.collectionIdConfirmation = ko.observable<string>(selectedCollectionId);
const Feedback = "my feedback";
@@ -135,7 +135,7 @@ describe("Delete Collection Confirmation Pane", () => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
})
).toBe(true);
});

View File

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

View File

@@ -1,109 +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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete 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>
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete 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

@@ -60,7 +60,7 @@ describe("Delete Database Confirmation Pane", () => {
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
container: fakeExplorer as any,
});
fakeExplorer.isLastNonEmptyDatabase = () => true;
@@ -88,7 +88,7 @@ describe("Delete Database Confirmation Pane", () => {
return {
id: ko.observable<string>(selectedDatabaseId),
rid: "test",
collections: ko.observableArray<ViewModels.Collection>()
collections: ko.observableArray<ViewModels.Collection>(),
} as ViewModels.Database;
};
fakeExplorer.refreshAllDatabases = () => Q.resolve();
@@ -99,7 +99,7 @@ describe("Delete Database Confirmation Pane", () => {
const AccountName = "testAccount";
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName
name: AccountName,
} as DataModels.DatabaseAccount);
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
@@ -112,7 +112,7 @@ describe("Delete Database Confirmation Pane", () => {
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any
container: fakeExplorer as any,
});
pane.databaseIdConfirmation = ko.observable<string>(selectedDatabaseId);
const Feedback = "my feedback";
@@ -121,7 +121,7 @@ describe("Delete Database Confirmation Pane", () => {
return pane.submit().then(() => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
});
});

View File

@@ -1,159 +1,159 @@
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 { 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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
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()
});
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
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 = Q(deleteDatabase(selectedDatabase.id()));
}
return promise.then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator(tab => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
tab =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
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, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
});
this.databaseDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
}
);
}
public resetData() {
this.databaseIdConfirmation("");
super.resetData();
}
public async open() {
await this.container.loadSelectedDatabaseOffer();
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();
}
}
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 { 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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
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(),
});
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
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 = Q(deleteDatabase(selectedDatabase.id()));
}
return promise.then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
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, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
this.databaseDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
);
}
public resetData() {
this.databaseIdConfirmation("");
super.resetData();
}
public async open() {
await this.container.loadSelectedDatabaseOffer();
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

@@ -1,165 +1,175 @@
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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>
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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

@@ -1,172 +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";
import StoredProcedure from "../Tree/StoredProcedure";
export interface ExecuteSprocParam {
type: ko.Observable<string>;
value: ko.Observable<string>;
}
type UnwrappedExecuteSprocParam = {
type: string;
value: any;
};
export class ExecuteSprocParamsPane extends ContextualPaneBase {
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: 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 = 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();
}
}
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";
import StoredProcedure from "../Tree/StoredProcedure";
export interface ExecuteSprocParam {
type: ko.Observable<string>;
value: ko.Observable<string>;
}
type UnwrappedExecuteSprocParam = {
type: string;
value: any;
};
export class ExecuteSprocParamsPane extends ContextualPaneBase {
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: 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 = 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

@@ -30,7 +30,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
super(props);
this.state = {
panelHeight: this.getPanelHeight()
panelHeight: this.getPanelHeight(),
};
}

View File

@@ -1,14 +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>
<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

@@ -1,353 +1,353 @@
import _ from "underscore";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { handleError } from "../../Common/ErrorHandlingUtils";
interface GitHubReposPaneOptions extends ViewModels.PaneOptions {
gitHubClient: GitHubClient;
junoClient: JunoClient;
}
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 allGitHubReposLastPageInfo?: IGitHubPageInfo;
private pinnedReposUpdated: boolean;
constructor(options: 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.allGitHubReposLastPageInfo = undefined;
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 => JunoUtils.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) {
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
}
}
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 = [];
this.allGitHubReposLastPageInfo = undefined;
// Reset flags
this.pinnedReposUpdated = false;
this.unpinnedReposProps.hasMore = true;
this.unpinnedReposProps.isLoading = true;
this.triggerRender();
super.resetData();
}
private getOAuthScope(): string {
return (
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope ||
AuthorizeAccessComponent.Scopes.Public.key
);
}
private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.container.notebookManager?.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, gitHubRepo.name)
) === -1
);
return unpinnedGitHubRepos.map(gitHubRepo => ({
key: GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name),
repo: gitHubRepo,
branches: []
}));
}
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)];
branchesProps.hasMore = true;
branchesProps.isLoading = true;
this.triggerRender();
try {
const response = await this.gitHubClient.getBranchesAsync(
repo.owner,
repo.name,
GitHubReposPane.PageSize,
branchesProps.lastPageInfo?.endCursor
);
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);
branchesProps.lastPageInfo = response.pageInfo;
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
}
branchesProps.isLoading = false;
branchesProps.hasMore = branchesProps.lastPageInfo?.hasNextPage;
this.triggerRender();
}
private async loadMoreUnpinnedRepos(): Promise<void> {
this.unpinnedReposProps.isLoading = true;
this.unpinnedReposProps.hasMore = true;
this.triggerRender();
try {
const response = await this.gitHubClient.getReposAsync(
GitHubReposPane.PageSize,
this.allGitHubReposLastPageInfo?.endCursor
);
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.allGitHubReposLastPageInfo = response.pageInfo;
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreUnpinnedRepos", "Failed to fetch unpinned repos");
}
this.unpinnedReposProps.isLoading = false;
this.unpinnedReposProps.hasMore = this.allGitHubReposLastPageInfo?.hasNextPage;
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) {
handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo");
return Promise.resolve(undefined);
}
}
private async pinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
const initialReposLength = this.pinnedReposProps.repos.length;
const existingRepo = _.find(this.pinnedReposProps.repos, 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.notebookManager?.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: JunoUtils.toGitHubRepo(pinnedRepo)
} as RepoListItem)
);
this.pinnedReposProps.repos = pinnedRepos;
this.triggerRender();
}
} catch (error) {
handleError(error, "GitHubReposPane/refreshPinnedReposListItems", "Failed to fetch pinned repos");
}
}
private refreshBranchesForPinnedRepos(): void {
this.pinnedReposProps.repos.map(item => {
if (!this.branchesProps[item.key]) {
this.branchesProps[item.key] = {
branches: [],
lastPageInfo: undefined,
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo)
};
this.loadMoreBranches(item.repo);
}
});
}
private async refreshUnpinnedRepoListItems(): Promise<void> {
this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
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.notebookManager?.gitHubOAuthService.startOAuth(scope);
}
private triggerRender(): void {
this.gitHubReposAdapter.triggerRender();
}
}
import _ from "underscore";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { handleError } from "../../Common/ErrorHandlingUtils";
interface GitHubReposPaneOptions extends ViewModels.PaneOptions {
gitHubClient: GitHubClient;
junoClient: JunoClient;
}
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 allGitHubReposLastPageInfo?: IGitHubPageInfo;
private pinnedReposUpdated: boolean;
constructor(options: 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.allGitHubReposLastPageInfo = undefined;
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) => JunoUtils.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) {
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
}
}
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 = [];
this.allGitHubReposLastPageInfo = undefined;
// Reset flags
this.pinnedReposUpdated = false;
this.unpinnedReposProps.hasMore = true;
this.unpinnedReposProps.isLoading = true;
this.triggerRender();
super.resetData();
}
private getOAuthScope(): string {
return (
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope ||
AuthorizeAccessComponent.Scopes.Public.key
);
}
private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.container.notebookManager?.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, gitHubRepo.name)
) === -1
);
return unpinnedGitHubRepos.map((gitHubRepo) => ({
key: GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name),
repo: gitHubRepo,
branches: [],
}));
}
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)];
branchesProps.hasMore = true;
branchesProps.isLoading = true;
this.triggerRender();
try {
const response = await this.gitHubClient.getBranchesAsync(
repo.owner,
repo.name,
GitHubReposPane.PageSize,
branchesProps.lastPageInfo?.endCursor
);
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);
branchesProps.lastPageInfo = response.pageInfo;
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
}
branchesProps.isLoading = false;
branchesProps.hasMore = branchesProps.lastPageInfo?.hasNextPage;
this.triggerRender();
}
private async loadMoreUnpinnedRepos(): Promise<void> {
this.unpinnedReposProps.isLoading = true;
this.unpinnedReposProps.hasMore = true;
this.triggerRender();
try {
const response = await this.gitHubClient.getReposAsync(
GitHubReposPane.PageSize,
this.allGitHubReposLastPageInfo?.endCursor
);
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.allGitHubReposLastPageInfo = response.pageInfo;
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreUnpinnedRepos", "Failed to fetch unpinned repos");
}
this.unpinnedReposProps.isLoading = false;
this.unpinnedReposProps.hasMore = this.allGitHubReposLastPageInfo?.hasNextPage;
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) {
handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo");
return Promise.resolve(undefined);
}
}
private async pinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
const initialReposLength = this.pinnedReposProps.repos.length;
const existingRepo = _.find(this.pinnedReposProps.repos, (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.notebookManager?.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: JunoUtils.toGitHubRepo(pinnedRepo),
} as RepoListItem)
);
this.pinnedReposProps.repos = pinnedRepos;
this.triggerRender();
}
} catch (error) {
handleError(error, "GitHubReposPane/refreshPinnedReposListItems", "Failed to fetch pinned repos");
}
}
private refreshBranchesForPinnedRepos(): void {
this.pinnedReposProps.repos.map((item) => {
if (!this.branchesProps[item.key]) {
this.branchesProps[item.key] = {
branches: [],
lastPageInfo: undefined,
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
};
this.loadMoreBranches(item.repo);
}
});
}
private async refreshUnpinnedRepoListItems(): Promise<void> {
this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
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.notebookManager?.gitHubOAuthService.startOAuth(scope);
}
private triggerRender(): void {
this.gitHubReposAdapter.triggerRender();
}
}

View File

@@ -1,60 +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 role="heading" aria-level="2">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>
<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 role="heading" aria-level="2">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

@@ -1,59 +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 role="heading" aria-level="2">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>
<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 role="heading" aria-level="2">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

@@ -1,68 +1,68 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class GraphStylingPane extends ContextualPaneBase {
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();
}
}
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class GraphStylingPane extends ContextualPaneBase {
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

@@ -1,88 +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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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>
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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

@@ -1,151 +1,147 @@
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 * as Logger from "../../Common/Logger";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import QueryTab from "../Tabs/QueryTab";
export class LoadQueryPane extends ContextualPaneBase {
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 = this.container.tabsManager.activeTab() as 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();
}
}
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 * as Logger from "../../Common/Logger";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import QueryTab from "../Tabs/QueryTab";
export class LoadQueryPane extends ContextualPaneBase {
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 = this.container.tabsManager.activeTab() as 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

@@ -1,65 +1,65 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer";
export default class NewVertexPane extends ContextualPaneBase {
public container: 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}`;
};
}
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer";
export default class NewVertexPane extends ContextualPaneBase {
public container: 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

@@ -1,226 +1,226 @@
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 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 GitHubReposPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GitHubReposPaneTemplate
};
}
}
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 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 GitHubReposPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GitHubReposPaneTemplate,
};
}
}

View File

@@ -1,212 +1,212 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import Explorer from "../Explorer";
import { JunoClient } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private author: string;
private content: string;
private description: string;
private tags: string;
private imageSrc: string;
private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement;
private isCodeOfConductAccepted: boolean;
private isLinkInjectionEnabled: boolean;
constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const props: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "publishnotebookpane",
isExecuting: this.isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish",
onClose: () => this.close(),
onSubmit: () => this.submit(),
isSubmitButtonHidden: !this.isCodeOfConductAccepted
};
const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookName: this.name,
notebookDescription: "",
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeName: (newValue: string) => (this.name = newValue),
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormErrorForLargeImageSelection,
clearFormError: this.clearFormError
};
return (
<GenericRightPaneComponent {...props}>
{!this.isCodeOfConductAccepted ? (
<div style={{ padding: "15px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={this.junoClient}
onAcceptCodeOfConduct={() => {
this.isCodeOfConductAccepted = true;
this.triggerRender();
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)}
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(
name: string,
author: string,
notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement,
isLinkInjectionEnabled: boolean
): Promise<void> {
try {
const response = await this.junoClient.isCodeOfConductAccepted();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.isCodeOfConductAccepted = response.data;
} catch (error) {
handleError(
error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted"
);
}
this.name = name;
this.author = author;
if (typeof notebookContent === "string") {
this.content = notebookContent as string;
} else {
this.content = JSON.stringify(toJS(notebookContent));
this.notebookObject = notebookContent;
}
this.parentDomElement = parentDomElement;
this.isOpened = true;
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
this.triggerRender();
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
this.isExecuting = true;
this.triggerRender();
try {
if (!this.name || !this.description || !this.author) {
throw new Error("Name, description, and author are required");
}
const response = await this.junoClient.publishNotebook(
this.name,
this.description,
this.tags?.split(","),
this.author,
this.imageSrc,
this.content,
this.isLinkInjectionEnabled
);
const data = response.data;
if (data) {
if (data.pendingScanJobIds?.length > 0) {
NotificationConsoleUtils.logConsoleInfo(
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
}
}
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${this.name} to gallery`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearPublishingMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
this.formError = formError;
this.formErrorDetail = formErrorDetail;
handleError(formErrorDetail, area, formError);
this.triggerRender();
};
private clearFormError = (): void => {
this.formError = undefined;
this.formErrorDetail = undefined;
this.triggerRender();
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.author = undefined;
this.content = undefined;
this.description = undefined;
this.tags = undefined;
this.imageSrc = undefined;
this.notebookObject = undefined;
this.parentDomElement = undefined;
this.isCodeOfConductAccepted = undefined;
this.isLinkInjectionEnabled = undefined;
};
}
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import Explorer from "../Explorer";
import { JunoClient } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private author: string;
private content: string;
private description: string;
private tags: string;
private imageSrc: string;
private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement;
private isCodeOfConductAccepted: boolean;
private isLinkInjectionEnabled: boolean;
constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const props: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "publishnotebookpane",
isExecuting: this.isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish",
onClose: () => this.close(),
onSubmit: () => this.submit(),
isSubmitButtonHidden: !this.isCodeOfConductAccepted,
};
const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookName: this.name,
notebookDescription: "",
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeName: (newValue: string) => (this.name = newValue),
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormErrorForLargeImageSelection,
clearFormError: this.clearFormError,
};
return (
<GenericRightPaneComponent {...props}>
{!this.isCodeOfConductAccepted ? (
<div style={{ padding: "15px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={this.junoClient}
onAcceptCodeOfConduct={() => {
this.isCodeOfConductAccepted = true;
this.triggerRender();
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)}
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(
name: string,
author: string,
notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement,
isLinkInjectionEnabled: boolean
): Promise<void> {
try {
const response = await this.junoClient.isCodeOfConductAccepted();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.isCodeOfConductAccepted = response.data;
} catch (error) {
handleError(
error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted"
);
}
this.name = name;
this.author = author;
if (typeof notebookContent === "string") {
this.content = notebookContent as string;
} else {
this.content = JSON.stringify(toJS(notebookContent));
this.notebookObject = notebookContent;
}
this.parentDomElement = parentDomElement;
this.isOpened = true;
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
this.triggerRender();
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
this.isExecuting = true;
this.triggerRender();
try {
if (!this.name || !this.description || !this.author) {
throw new Error("Name, description, and author are required");
}
const response = await this.junoClient.publishNotebook(
this.name,
this.description,
this.tags?.split(","),
this.author,
this.imageSrc,
this.content,
this.isLinkInjectionEnabled
);
const data = response.data;
if (data) {
if (data.pendingScanJobIds?.length > 0) {
NotificationConsoleUtils.logConsoleInfo(
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
}
}
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${this.name} to gallery`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearPublishingMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
this.formError = formError;
this.formErrorDetail = formErrorDetail;
handleError(formErrorDetail, area, formError);
this.triggerRender();
};
private clearFormError = (): void => {
this.formError = undefined;
this.formErrorDetail = undefined;
this.triggerRender();
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.author = undefined;
this.content = undefined;
this.description = undefined;
this.tags = undefined;
this.imageSrc = undefined;
this.notebookObject = undefined;
this.parentDomElement = undefined;
this.isCodeOfConductAccepted = undefined;
this.isLinkInjectionEnabled = undefined;
};
}

View File

@@ -17,7 +17,7 @@ describe("PublishNotebookPaneComponent", () => {
onChangeTags: undefined,
onChangeImageSrc: undefined,
onError: undefined,
clearFormError: undefined
clearFormError: undefined,
};
const wrapper = shallow(<PublishNotebookPaneComponent {...props} />);

View File

@@ -35,7 +35,7 @@ enum ImageTypes {
Url = "URL",
CustomImage = "Custom Image",
TakeScreenshot = "Take Screenshot",
UseFirstDisplayOutput = "Use First Display Output"
UseFirstDisplayOutput = "Use First Display Output",
}
export class PublishNotebookPaneComponent extends React.Component<PublishNotebookPaneProps, PublishNotebookPaneState> {
@@ -58,7 +58,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
notebookName: props.notebookName,
notebookDescription: "",
notebookTags: "",
imageSrc: undefined
imageSrc: undefined,
};
this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
@@ -69,7 +69,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
};
const onError = this.props.onError;
reader.onerror = error => {
reader.onerror = (error) => {
const formError = `Failed to convert ${file.name} to base64 format`;
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/selectImageFile";
@@ -88,9 +88,9 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
useCORS: true,
allowTaint: true,
scale: 1,
logging: true
logging: true,
})
.then(canvas => {
.then((canvas) => {
//redraw canvas to fit Card Cover Image dimensions
const originalImageData = canvas.toDataURL();
const requiredHeight =
@@ -104,7 +104,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
updateImageSrcWithScreenshot(canvas.toDataURL());
};
})
.catch(error => {
.catch((error) => {
onError(error);
});
};
@@ -123,7 +123,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
onChange: (event, newValue) => {
this.props.onChangeImageSrc(newValue);
this.setState({ imageSrc: newValue });
}
},
};
const screenshotErrorHandler = (error: Error) => {
@@ -148,7 +148,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
ImageTypes.Url,
ImageTypes.CustomImage,
ImageTypes.TakeScreenshot,
ImageTypes.UseFirstDisplayOutput
ImageTypes.UseFirstDisplayOutput,
].map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
this.props.clearFormError();
@@ -166,7 +166,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
}
}
this.setState({ type: options.text });
}
},
};
this.nameProps = {
@@ -177,7 +177,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
onChange: (event, newValue) => {
this.props.onChangeName(newValue);
this.setState({ notebookName: newValue });
}
},
};
this.descriptionProps = {
@@ -189,7 +189,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
onChange: (event, newValue) => {
this.props.onChangeDescription(newValue);
this.setState({ notebookDescription: newValue });
}
},
};
this.tagsProps = {
@@ -199,7 +199,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
onChange: (event, newValue) => {
this.props.onChangeTags(newValue);
this.setState({ notebookTags: newValue });
}
},
};
}
@@ -213,7 +213,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
id="selectImageFile"
type="file"
accept="image/*"
onChange={event => {
onChange={(event) => {
const file = event.target.files[0];
if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) {
event.target.value = "";
@@ -298,7 +298,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
pendingScanJobIds: undefined,
}}
isFavorite={false}
showDownload={true}

View File

@@ -1,90 +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 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"
>
<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>
<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 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"
>
<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

@@ -1,101 +1,101 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class RenewAdHocAccessPane extends ContextualPaneBase {
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 = parseConnectionString(this.accessKey());
const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean =
(this.container && this.container.tabsManager && this.container.tabsManager.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 = getErrorMessage(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("");
}
}
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class RenewAdHocAccessPane extends ContextualPaneBase {
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 = parseConnectionString(this.accessKey());
const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean =
(this.container && this.container.tabsManager && this.container.tabsManager.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 = getErrorMessage(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

@@ -1,63 +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 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">
<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>
<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 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">
<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

@@ -1,165 +1,165 @@
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
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 = this.container && (this.container.tabsManager.activeTab() as 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);
const errorMessage = getErrorMessage(error);
this.formErrors("Failed to save query");
this.formErrorsDetails(`Failed to save query: ${errorMessage}`);
TelemetryProcessor.traceFailure(
Action.SaveQuery,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
},
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) {
const errorMessage = getErrorMessage(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(),
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
this.formErrors("Failed to setup a container for saved queries");
this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
this.isExecuting(false);
}
};
public close() {
super.close();
this.resetData();
}
public resetData() {
super.resetData();
this.queryName("");
}
}
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
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 = this.container && (this.container.tabsManager.activeTab() as 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);
const errorMessage = getErrorMessage(error);
this.formErrors("Failed to save query");
this.formErrorsDetails(`Failed to save query: ${errorMessage}`);
TelemetryProcessor.traceFailure(
Action.SaveQuery,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
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) {
const errorMessage = getErrorMessage(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(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
this.formErrors("Failed to setup a container for saved queries");
this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
this.isExecuting(false);
}
};
public close() {
super.close();
this.resetData();
}
public resetData() {
super.resetData();
this.queryName("");
}
}

View File

@@ -33,7 +33,7 @@
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<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

View File

@@ -1,38 +1,38 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
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);
});
});
});
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
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

@@ -1,45 +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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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>
<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 role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- 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

@@ -1,113 +1,113 @@
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ko from "knockout";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
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 = getErrorMessage(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,
errorStack: getErrorStack(error)
},
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);
}
}
}
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ko from "knockout";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
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 = getErrorMessage(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,
errorStack: getErrorStack(error),
},
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

@@ -18,7 +18,7 @@
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<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

View File

@@ -55,7 +55,7 @@ export class StringInputPane extends ContextualPaneBase {
this.close();
this.paneDeferred.resolve(value);
},
reason => {
(reason) => {
let error = reason;
if (reason instanceof Error) {
error = reason.message;

View File

@@ -17,7 +17,7 @@ export class SwitchDirectoryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SwitchDirectoryPaneTemplate
template: SwitchDirectoryPaneTemplate,
};
}
}
@@ -47,7 +47,7 @@ export class SwitchDirectoryPane {
this.firstFieldHasFocus(true);
this.resizePane();
TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Open, {
paneTitle: this.title()
paneTitle: this.title(),
});
this.directoryComponentAdapter.forceRender();

View File

@@ -1,151 +1,151 @@
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 {
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();
this.focusValueElement();
});
} else {
this.displayedAttributes(
this.constructDisplayedAttributes(
headers,
Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items())
)
);
this.updateIsActionEnabled();
super.open();
this.focusValueElement();
}
}
private focusValueElement() {
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;
}
}
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 {
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();
this.focusValueElement();
});
} else {
this.displayedAttributes(
this.constructDisplayedAttributes(
headers,
Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items())
)
);
this.updateIsActionEnabled();
super.open();
this.focusValueElement();
}
}
private focusValueElement() {
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

@@ -1,229 +1,229 @@
import * as ko from "knockout";
import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as Entities from "../../Tables/Entities";
import TableEntityPane from "./TableEntityPane";
import * as Utilities from "../../Tables/Utilities";
import * as TableConstants from "../../Tables/Constants";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import Explorer from "../../Explorer";
export default class EditTableEntityPane extends TableEntityPane {
container: Explorer;
visible: ko.Observable<boolean>;
public originEntity: Entities.ITableEntity;
public originalNumberOfProperties: number;
private originalDocument: any;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Update Entity");
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;
}
}
import * as ko from "knockout";
import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as Entities from "../../Tables/Entities";
import TableEntityPane from "./TableEntityPane";
import * as Utilities from "../../Tables/Utilities";
import * as TableConstants from "../../Tables/Constants";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import Explorer from "../../Explorer";
export default class EditTableEntityPane extends TableEntityPane {
container: Explorer;
visible: ko.Observable<boolean>;
public originEntity: Entities.ITableEntity;
public originalNumberOfProperties: number;
private originalDocument: any;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Update Entity");
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

@@ -1,164 +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);
}
}
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

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

View File

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

View File

@@ -1,78 +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>
<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

@@ -1,195 +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 {
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;
}
}
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 {
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

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

View File

@@ -1,281 +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;
}
}
}
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

@@ -1,79 +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>
<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

@@ -1,306 +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;
}
/* 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

@@ -1,23 +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]*$/
};
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

@@ -1,341 +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;
}
}
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

@@ -20,7 +20,7 @@
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<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

View File

@@ -131,11 +131,7 @@ export class UploadFilePane extends ContextualPaneBase {
private resetFileInput(): void {
const inputElement = $("#importFileInput");
inputElement
.wrap("<form>")
.closest("form")
.get(0)
.reset();
inputElement.wrap("<form>").closest("form").get(0).reset();
inputElement.unwrap();
}
}

View File

@@ -1,130 +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 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>
<!-- 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>
<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 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>
<!-- 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

@@ -1,147 +1,143 @@
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
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 errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
}
)
.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();
}
}
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
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 errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
}
)
.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();
}
}

View File

@@ -42,13 +42,13 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
title: "Upload Items",
submitButtonText: "Upload",
onClose: () => this.close(),
onSubmit: () => this.submit()
onSubmit: () => this.submit(),
};
const uploadItemsPaneProps: UploadItemsPaneProps = {
selectedFilesTitle: this.selectedFilesTitle,
updateSelectedFiles: this.updateSelectedFiles,
uploadFileData: this.uploadFileData
uploadFileData: this.uploadFileData,
};
return (
@@ -106,7 +106,7 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
this.selectedFiles = undefined;
this.selectedFilesTitle = "";
},
error => {
(error) => {
const errorMessage = getErrorMessage(error);
this.formError = errorMessage;
this.formErrorDetail = errorMessage;