Compare commits

...

45 Commits

Author SHA1 Message Date
Steve Faulkner
a266fdc89c Create codeql-analysis.yml 2020-10-14 14:58:44 -05:00
victor-meng
39f7ef331a Use RP to read databases for all API (#280)
Since I've cleaned up all the places that use `_rid` and `_self` of a database in my previous commit (d525afa142), we are safe to use RP to read databases for all APIs except Tables.

For Tables API, since RP doesn't support any database level operations, I have to hardcode the read database response. The only property we need in the Tables database is the id which is the string that we display in the resource tree.
2020-10-14 18:10:14 +00:00
victor-meng
9933a4988a Use SDK calls to read and update shared throughput for Tables API (#278)
RP does not supporting reading or updating database level throughput for Tables API so we have to switch back to using SDK calls for these operations.
2020-10-13 22:49:30 +00:00
victor-meng
d525afa142 Refactor code that uses the _rid and _self of a database or collection (#267) 2020-10-13 13:29:39 -07:00
Steve Faulkner
cfb9a0b321 Refactor NotificationsClient (#270) 2020-10-12 22:10:28 -05:00
Tanuj Mittal
3b64d75322 Add Report Abuse dialog for public gallery notebooks (#265)
![image](https://user-images.githubusercontent.com/693092/95408825-3975a680-08d5-11eb-812b-80f922ab9fc8.png)
2020-10-12 23:48:05 +00:00
Steve Faulkner
daba1c4ed4 Remove AutoMerge Github Action (#273)
Instead, we will be trying out the official Microsoft bot https://docs.opensource.microsoft.com/tools/fabricbot/index.html
2020-10-12 21:19:02 +00:00
victor-meng
a698e08638 Remove database offers cache and get offer directly from database (#268)
Currently we maintain a cache of all database offers which can be stale since we have moved to lazy loading the offers. Instead of reading the offer from the cache, we should just find the selected database and get the offer directly by calling `database.offer()`.
2020-10-12 21:00:47 +00:00
Garrett Ausfeldt
88d630fef4 Add summary to each table of the DataTable for narrator context (#238)
The DataTable control creates 3 tables in the DOM, one for the header, one for the body and one for the footer. Because of this when navigating through the tablem it says "leaving table", when it is really the same table. It seems this is not the default and because of the option **dom: "RZlfrtip"**, the DataTable is created this way. 

If I remove that setting, it will only create one table, BUT other things break, because there is a lot of custom DOM manipulation assuming the DOM was the way it was before (gross). This make me question if we wanted this on purpose to maybe solve a cross browser scrolling issue.

Instead I decided to leave it as is, until migrating to Microsoft's Fluent UI is prioritized. However I did add a summary attribute to each table, so that when listening to the narrator, it make more sense when leaving one part of the table into another part of the table. While not optimal, it should at least satisfy accessibility concern of it being confusing.
2020-10-12 20:30:37 +00:00
Armando Trejo Oliver
5ffa746adb Escape quotes in identifiers in CQL queries 2020-10-12 13:00:11 -05:00
Srinath Narayanan
a9a57f4ba9 Added telelmetry for settings v2 and v1 (#269)
* Added telelmetry for settings v2 and v1

* format errors fixed

* renamed actions
2020-10-12 09:01:00 -07:00
Steve Faulkner
47cc6fd7a8 Set default backend endpoint (#271) 2020-10-12 09:23:57 -05:00
Steve Faulkner
14cdf19efb Remove Explorer.isEmulator (#256) 2020-10-09 11:18:50 -05:00
Steve Faulkner
5191ae3f3a All events should trigger and ADO build 2020-10-09 09:47:52 -05:00
Steve Faulkner
ba862a8106 Remove jquery.contextMenu (#248) 2020-10-08 18:19:24 -05:00
Steve Faulkner
fe085b3e5a Restore AppInsights fetch telemetry (#263) 2020-10-08 17:55:02 -05:00
Srinath Narayanan
8028734cb0 Fixed settingsV2 bugs and added experimentation (#264)
* inital commit for flight tests

- FIxed bugs with settingstab v2

* minor edits

* removed console log

* fixed bug with autoscale throughput step increase

* resolved PR comments

* fixed compile error

* Added comment
2020-10-08 14:32:54 -07:00
Armando Trejo Oliver
444f663733 Update README.md (#255) 2020-10-08 15:57:38 -05:00
Laurent Nguyen
8c1ca35420 Fix double-scrollbar bug 2020-10-08 14:04:02 -05:00
Laurent Nguyen
b69174788d Add more Telemetry to Data Explorer (#242)
* Add Telemetry to command bar buttons

* Count and report # of files/notebooks/directories in myNotebook to telemetry

* Add resource tree clicks to Telemetry

* Log to Telemetry: opened notebook cell counts by type, kernelspec name

* Fix unit test

* Move Telemetry processor call in notebook traceNotebookTelemetry action from reducer to epic. Use action to trace other info.

* Fix react duplicate key error

* Log notebook cell context menu actions

* Reformat and cleanup

* Move resource tree tracing code out of render(). Only call once when tree is updated

* Fix build issues
2020-10-08 10:53:01 +02:00
Laurent Nguyen
ff03c79399 Fix horizontal scrollbar in notebook cell input issue (#260)
* Fix horizontal scrollbar in notebook cell input issue

* Cell input overflow visible
2020-10-08 09:17:46 +02:00
victor-meng
0382628249 Move update offers call to RP 2020-10-07 17:25:21 -05:00
Steve Faulkner
d346ebe054 Fix Blackforest Origin check (#261) 2020-10-07 15:11:11 -05:00
Zachary Foster
f5ecb8a04f Fixes e2e test input focus swapping (#262)
* Remove redundant E2E tests

* Remove deps

* Fixes e2e tests hopefully

Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-10-07 15:41:38 -04:00
Laurent Nguyen
7dd046a15d Fix notebook kernel selection and auto-kernel-start (#254)
* Fix bug: cannot select kernel. Properly plug-in kernelspecs middleware to redux store configuration

* Properly auto-launch kernel with nteract's epic

* Keep kernel lazy start for notebook viewer

* Add unit tets
2020-10-07 08:39:04 +02:00
victor-meng
86d3f0d35d Fix database offers not loaded when add collection pane is opened 2020-10-06 22:22:54 -05:00
Tanuj Mittal
c08a1872cc Add correct content-type header to terminals calls (#258) 2020-10-06 16:55:13 -07:00
Jordi Bunster
0cc38868a6 Remove one indirection for ConfigContext
Things like ConfigContext.BACKEND_ENDPOINT are dynamically changed, and assigning them statically like this only bypasses that mechanism :(

Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2020-10-06 15:30:24 -07:00
Steve Faulkner
f5bbd52311 Test Output Cleanup 2020-10-05 23:21:58 -05:00
Steve Faulkner
b83f59ac31 Remove unused external files 2020-10-05 17:25:44 -05:00
Chris-MS-896
6b8c9f6961 [Azure][Screen Reader-CosmosDB – Data Explorer] Alt is not correctly defined for the console image on the data explorer page (#247)
* Update NotificationConsoleComponent.tsx

'Accessibiliy for alt image'

* Update NotificationConsoleComponent.tsx

'meet Unit test problems'

* Update NotificationConsoleComponent.test.tsx.snap

Update for snapshot

Co-authored-by: Chris896 <caoyiqun2017@gmail.com>
2020-10-05 16:14:51 -05:00
Steve Faulkner
2f978bac69 Use Github PAT for automerge 2020-10-05 14:34:17 -05:00
Steve Faulkner
d9d772f29c Add Automerge Action 2020-10-02 23:23:45 +00:00
victor-meng
0f82ed3749 Add checks for serverless account (#244) 2020-10-02 14:44:12 -07:00
Steve Faulkner
3112cf5573 Allow setting of ARM API version via query param (#241) 2020-10-02 10:45:32 -05:00
Laurent Nguyen
0ad5fb465b Switch notebook editors to monaco (#239)
* Upgrade nteract packages and related dependencies to make new stateful-component work

* Switch to new monacoEditor

* Configure store using nteract mythic configuration

* Replace CodeMirror with Monaco editor in NotebookReadOnlyRenderer

* Reformat

* Upgrade d3 to latest to resolve d3-selection conflicts with nteract/data-explorer that broke D3ForceGraph

* Upgrade jupyterlab terminal widget to work with latest version of react. Upgrade jupyterlab services to include latest fix for websocket auth

* Update jest test snapshots

* Upgrade packages to fix build issues

* Remove comment

* Fix unit tests

* Fix unit test snapshot

* Remove useless @types/node-fetch
2020-10-01 14:00:46 +02:00
Zachary Foster
4fe2098730 Adds mongo e2e spec (#207)
* Adds mongo e2e spec

* Adds mongo connection string

* Constantize sql spec

* Use shared login function

* Remove comment

* Remove login lines from cassandra

* Adds frame return tyoe

* test updates

* format

* adds unique name

* remove trivial type annotation
2020-09-30 16:42:33 -04:00
Srinath Narayanan
fc722e87be Refactored Settings Tab (#161)
* added  SettingsV2 Tab

* lint changes

* foxed failing test

* Addressed PR comments

- removed dangerouslySetInnerHtml
- removed underscore dependency
- added AccessibleElement
- removed unnecesary exceptions to linting

* split render into separate functions

- removed sinon in test
- Added some enums to replace constant strings
- removed dangerously set inner html
- made autopilot input as StatefulValue

* add settingscomponent snapshot

* fixed linting errors

* fixed errors

* addressed PR comments

- Moved StatefulValue to new class
- Split render to more functions for throughputInputComponents

* Added sub components

- Added tests for SettingsRenderUtls
- Added empty test files for adding tests later

* Moved all inputs to fluent UI

- removed rupm
- added reusable styles

* Added Tabs

- Added ToolTipLabel component
- Removed toggleables for individual components
- Removed accessible elements
- Added IndexingPolicyComponent

* Added more tests

* Addressed PR comments

* Moved Label radio buttons to choicegroup

* fixed lint errors

* Removed StatefulValue

- Moved conflict res tab to the end
- Added styling for autpilot radiobuttons

* fixed linting errors

* fix bugs from merge to master

* fixed formatting issue

* Addressed PR comments

- Added unit tests for smaller methods within each component

* fixed linting errors

* removed redundant snapshots

* removed empty line

* made separate props objects for subcomponents

* Moved dirty checks to sub components

* Made indesing policy component height = 80% of view port

- modified auto pilot v3 messages
- Added Fluent UI tolltip
-

* Moved warning messages inline

* moved conflict res helpers out

* fixed bugs

* added stack style for message

* fixed tests

* Added tests

* fixed linting and format errors

* undid changes

* more edits

* fixed compile errors

* fixed compile errors

* fixed errors

* fixed bug with save and discard buttons

* fixed compile errors

* addressed PR comments
2020-09-30 12:34:39 -07:00
Steve Faulkner
4ecdfe60eb Fix Parent Origin Regex (#237)
* Fix Parent Origin Regex

* Add another test case

* Handle more cases
2020-09-29 18:09:11 -05:00
Steve Faulkner
0c7a73e716 xxxRevert "More test cases"
This reverts commit b2c24fab4f.
2020-09-29 17:46:54 -05:00
Steve Faulkner
b2c24fab4f More test cases 2020-09-29 17:31:58 -05:00
Steve Faulkner
aa369760ad Remove option to delete/create root table database (#236) 2020-09-28 17:03:47 -05:00
victor-meng
23c5d2d7e0 Lazy load collection offer (#234) 2020-09-28 12:54:28 -07:00
Zachary Foster
f582887fd8 Fix Cassandra Endpoint URLs by adding trailing slash in construction (#235) 2020-09-28 15:53:27 -04:00
Srinath Narayanan
4b0b63b56b Users/srnara/mongo index (#229)
* added placeholder

* Added check box

* Added tolltip width constant

* Add telemetry

* formatting error

* formatting error

* support only for mongo v 3.6 accounts

* resolved comment
2020-09-28 01:36:10 -07:00
196 changed files with 19116 additions and 34910 deletions

View File

@@ -27,7 +27,7 @@ module.exports = {
plugins: ["react"]
},
{
files: ["**/*.test.{ts,tsx}"],
files: ["**/*.{test,spec}.{ts,tsx}"],
env: {
jest: true
},

View File

@@ -105,74 +105,6 @@ jobs:
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
endtoendsql:
name: "End To End Tests | SQL"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Restore Cypress Binary Cache
uses: actions/cache@v2
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress-binary-cache
- run: npm ci
- name: End to End Tests
run: |
npm start &
cd cypress
npm ci
node cleanup.js
npm run wait-for-server
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/SQL/*"
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
- uses: actions/upload-artifact@v2
name: videos
if: ${{ failure() }}
with:
path: "**/*.mp4"
endtoendmongo:
name: "End To End Tests | Mongo"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Restore Cypress Binary Cache
uses: actions/cache@v2
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress-binary-cache
- name: End to End Tests
run: |
npm ci
npm start &
cd cypress
npm ci
node cleanup.js
npm run wait-for-server
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/MONGO/*"
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
- uses: actions/upload-artifact@v2
if: ${{ failure() }}
name: videos
with:
path: "**/*.mp4"
accessibility:
name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
@@ -196,10 +128,32 @@ jobs:
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendpuppeteer:
name: "End to end puppeteer tests"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: End to End Puppeteer Tests
run: |
npm ci
npm start &
npm run wait-for-server
npm run test:e2e
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -223,7 +177,7 @@ jobs:
nugetmpac:
name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 5 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,4 +1,4 @@
# CosmosDB Explorer
# Cosmos DB Explorer
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)

View File

@@ -3,11 +3,8 @@
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
vmImage: "ubuntu-latest"
steps:
- task: ComponentGovernanceComponentDetection@0
- task: ComponentGovernanceComponentDetection@0

View File

@@ -1,177 +0,0 @@
/*!
* jQuery contextMenu - Plugin for simple contextMenu handling
*
* Version: 1.6.6
*
* Authors: Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://medialize.github.com/jQuery-contextMenu/
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
* GPL v3 http://opensource.org/licenses/GPL-3.0
*
*/
.context-menu-list {
z-index: 1001;
position: fixed;
background: white;
border: solid 1px gainsboro;
box-shadow: 4px 4px 4px -2px #888888;
padding: 8px 0px 8px 0px;
line-height: 25px;
width: 254px;
list-style: none;
margin-left: -10px;
outline: 0px #fff;
}
.context-menu-item {
padding: 2px 2px 2px 31px;
background-color: #fff;
position: relative;
-webkit-user-select: none;
-moz-user-select: -moz-none;
-ms-user-select: none;
user-select: none;
}
.context-menu-separator {
padding-bottom: 0;
border-bottom: 1px solid #DDD;
}
.context-menu-item>label>input,
.context-menu-item>label>textarea {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
margin-left: -10px;
}
.context-menu-item:hover {
cursor: pointer;
background-color: #eeeeee;
}
.context-menu-item.disabled {
color: #666;
}
.context-menu-input.hover,
.context-menu-item.disabled.hover {
cursor: default;
background-color: #EEE;
}
.context-menu-submenu:after {
content: ">";
color: #666;
position: absolute;
top: 0;
right: 3px;
z-index: 1;
}
/* icons
#protip:
In case you want to use sprites for icons (which I would suggest you do) have a look at
http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement
.context-menu-item.icon:before {}
*/
.context-menu-item.icon {
min-height: 18px;
background-repeat: no-repeat;
background-position: 10px 7px;
}
.context-menu-item.icon:hover {
min-height: 18px;
background-repeat: no-repeat;
background-position: 10px 7px;
}
/*.context-menu-item.icon-edit {
background-image: url(images/page_white_edit.png);
}
.context-menu-item.icon-cut {
background-image: url(images/cut.png);
}
.context-menu-item.icon-copy {
background-image: url(images/page_white_copy.png);
}
.context-menu-item.icon-paste {
background-image: url(images/page_white_paste.png);
}
.context-menu-item.icon-delete {
background-image: url(images/page_white_delete.png);
}
.context-menu-item.icon-add {
background-image: url(images/page_white_add.png);
}
.context-menu-item.icon-quit {
background-image: url(images/door.png);
}*/
/* vertically align inside labels */
.context-menu-input>label>* {
vertical-align: top;
}
/* position checkboxes and radios as icons */
.context-menu-input>label>input[type="checkbox"],
.context-menu-input>label>input[type="radio"] {
margin-left: -17px;
}
.context-menu-input>label>span {
margin-left: 5px;
}
.context-menu-input>label,
.context-menu-input>label>input[type="text"],
.context-menu-input>label>textarea,
.context-menu-input>label>select {
display: block;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
}
.context-menu-input>label>textarea {
height: 100px;
}
.context-menu-item>.context-menu-list {
display: none;
/* re-positioned by js */
right: -5px;
top: 5px;
}
/*.context-menu-item.hover>.context-menu-list {
display: block;
padding-left: 5px;
}*/
.context-menu-accesskey {
text-decoration: underline;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,163 +0,0 @@
/*!
DataTables 1.10.9
©2008-2015 SpryMedia Ltd - datatables.net/license
*/
(function(Fa,T,k){var S=function(h){function X(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),d[c]=e,"o"===b[1]&&X(a[e])});a._hungarianMap=d}function I(a,b,c){a._hungarianMap||X(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),I(a[d],b[d],c)):b[d]=b[e]})}function S(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;
!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&cb(a)}function db(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");
A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&I(m.models.oSearch,a[b])}function eb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;b&&!h.isArray(b)&&(a.aDataSort=[b])}function fb(a){if(!m.__browser){var b={};m.__browser=b;var c=
h("<div/>").css({position:"fixed",top:0,left:0,height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,
m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function gb(a,b,c,d,e,f){var g,i=!1;c!==k&&(g=c,i=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=i?b(g,a[d],d,a):a[d],i=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:T.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);
la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(eb(c),I(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));
var g=b.mData,i=P(g),j=b.mRender?P(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b.fnGetData=function(a,b,c){var d=i(a,b,k,c);return j&&b?j(d,b,a,c):d};b.fnSetData=function(a,b,c){return Q(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?
(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Y(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&Z(a);w(a,null,"column-sizing",[a])}function $(a,b){var c=
aa(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function ba(a,b){var c=aa(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function ca(a){return aa(a,"bVisible").length}function aa(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ia(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,i,j,h,l,r,q;e=0;for(f=b.length;e<f;e++)if(l=b[e],q=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(i=d.length;g<i;g++){j=0;for(h=c.length;j<
h;j++){q[j]===k&&(q[j]=B(a,j,e,"type"));r=d[g](q[j],a);if(!r&&g!==d.length-1)break;if("html"===r)break}if(r){l.sType=r;break}}l.sType||(l.sType="string")}}function hb(a,b,c,d){var e,f,g,i,j,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var r=n.targets!==k?n.targets:n.aTargets;h.isArray(r)||(r=[r]);f=0;for(g=r.length;f<g;f++)if("number"===typeof r[f]&&0<=r[f]){for(;l.length<=r[f];)Ga(a);d(r[f],n)}else if("number"===typeof r[f]&&0>r[f])d(l.length+r[f],n);else if("string"===typeof r[f]){i=0;
for(j=l.length;i<j;i++)("_all"==r[f]||h(l[i].nTh).hasClass(r[f]))&&d(i,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function L(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,i=0,j=g.length;i<j;i++)g[i].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ja(a,e,c,d);return e}function ma(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=
Ka(a,e);return L(a,c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,f=a.aoColumns[c],g=a.aoData[b]._aData,i=f.sDefaultContent,c=f.fnGetData(g,d,{settings:a,row:b,col:c});if(c===k)return a.iDrawError!=e&&null===i&&(J(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b,4),a.iDrawError=e),i;if((c===g||null===c)&&null!==i)c=i;else if("function"===typeof c)return c.call(g);return null===c&&"display"==d?"":c}function ib(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,
d,{settings:a,row:b,col:c})}function La(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\./g,".")})}function P(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=P(c))});return function(a,c,f,g){var i=b[c]||b._;return i!==k?i(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,b,
f){var g,i;if(""!==f){i=La(f);for(var j=0,n=i.length;j<n;j++){f=i[j].match(da);g=i[j].match(U);if(f){i[j]=i[j].replace(da,"");""!==i[j]&&(a=a[i[j]]);g=[];i.splice(0,j+1);i=i.join(".");if(h.isArray(a)){j=0;for(n=a.length;j<n;j++)g.push(c(a[j],b,i))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){i[j]=i[j].replace(U,"");a=a[i[j]]();continue}if(null===a||a[i[j]]===k)return k;a=a[i[j]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function Q(a){if(h.isPlainObject(a))return Q(a._);
if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=La(e),f;f=e[e.length-1];for(var g,i,j=0,n=e.length-1;j<n;j++){g=e[j].match(da);i=e[j].match(U);if(g){e[j]=e[j].replace(da,"");a[e[j]]=[];f=e.slice();f.splice(0,j+1);g=f.join(".");if(h.isArray(d)){i=0;for(n=d.length;i<n;i++)f={},b(f,d[i],g),a[e[j]].push(f)}else a[e[j]]=d;return}i&&(e[j]=e[j].replace(U,
""),a=a[e[j]](d));if(null===a[e[j]]||a[e[j]]===k)a[e[j]]={};a=a[e[j]]}if(f.match(U))a[f.replace(U,"")](d);else a[f.replace(da,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ma(a){return D(a.aoData,"_aData")}function na(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function oa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ea(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var i=e.anCells;if(i)if(d!==k)g(i[d],d);else{c=0;for(f=i.length;c<f;c++)g(i[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Na(a,e)}}function Ka(a,b,c,d){var e=[],f=b.firstChild,g,i,j=0,n,l=a.aoColumns,r=a._rowReadObject,d=d!==k?d:r?{}:[],q=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
-1!==c&&(c=a.substring(c+1),Q(a)(d,b.getAttribute(c)))}},jb=function(a){if(c===k||c===j)i=l[j],n=h.trim(a.innerHTML),i&&i._bAttrSrc?(Q(i.mData._)(d,n),q(i.mData.sort,a),q(i.mData.type,a),q(i.mData.filter,a)):r?(i._setter||(i._setter=Q(i.mData)),i._setter(d,n)):d[j]=n;j++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)jb(f),e.push(f);f=f.nextSibling}else{e=b.anCells;g=0;for(var o=e.length;g<o;g++)jb(e[g])}if(b=f?b:b.nTr)(b=b.getAttribute("id"))&&Q(a.rowId)(d,b);return{data:d,cells:e}}
function Ja(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],i,j,h,l,r;if(null===e.nTr){i=c||T.createElement("tr");e.nTr=i;e.anCells=g;i._DT_RowIndex=b;Na(a,e);l=0;for(r=a.aoColumns.length;l<r;l++){h=a.aoColumns[l];j=c?d[l]:T.createElement(h.sCellType);g.push(j);if(!c||h.mRender||h.mData!==l)j.innerHTML=B(a,b,l,"display");h.sClass&&(j.className+=" "+h.sClass);h.bVisible&&!c?i.appendChild(j):!h.bVisible&&c&&j.parentNode.removeChild(j);h.fnCreatedCell&&h.fnCreatedCell.call(a.oInstance,j,B(a,b,l),f,b,l)}w(a,
"aoRowCreatedCallback",null,[i,f,b])}e.nTr.setAttribute("role","row")}function Na(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?pa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function kb(a){var b,c,d,e,f,g=a.nTHead,i=a.nTFoot,j=0===h("th, td",g).length,n=a.oClasses,l=a.aoColumns;j&&(e=h("<tr/>").appendTo(g));
b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),j&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),Oa(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Pa(a,"header")(a,d,f,n);j&&fa(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(i).find(">tr>th, >tr>td").addClass(n.sFooterTH);if(null!==i){a=a.aoFooter[0];b=0;for(c=a.length;b<
c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function ga(a,b,c){var d,e,f,g=[],i=[],j=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=j-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);i.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=j=1,i[d][f]===k){a.appendChild(g[d][f].cell);for(i[d][f]=1;g[d+j]!==k&&g[d][f].cell==g[d+j][f].cell;)i[d+
j][f]=1,j++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<j;c++)i[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",j).attr("colspan",n)}}}}function M(a){var b=w(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,i="ssp"==y(a),j=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=i?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();
if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(i){if(!a.bDestroying&&!lb(a))return}else a.iDraw++;if(0!==j.length){f=i?a.aoData.length:n;for(i=i?0:g;i<f;i++){var l=j[i],r=a.aoData[l];null===r.nTr&&Ja(a,l);l=r.nTr;if(0!==e){var q=d[c%e];r._sRowStripe!=q&&(h(l).removeClass(r._sRowStripe).addClass(q),r._sRowStripe=q)}w(a,"aoRowCallback",null,[l,r._aData,c,i]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),
b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:ca(a),"class":a.oClasses.sRowEmpty}).html(c))[0];w(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,j]);w(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,j]);d=h(a.nTBody);d.children().detach();d.append(h(b));w(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function R(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&mb(a);d?ha(a,a.oPreviousSearch):a.aiDisplay=
a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;M(a);a._drawHold=!1}function nb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,i,j,n,l,r,q=0;q<f.length;q++){g=null;i=f[q];if("<"==i){j=h("<div/>")[0];n=f[q+1];if("'"==n||'"'==n){l="";for(r=2;f[q+r]!=n;)l+=
f[q+r],r++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),j.id=n[0].substr(1,n[0].length-1),j.className=n[1]):"#"==l.charAt(0)?j.id=l.substr(1,l.length-1):j.className=l;q+=r}e.append(j);e=h(j)}else if(">"==i)e=e.parent();else if("l"==i&&d.bPaginate&&d.bLengthChange)g=ob(a);else if("f"==i&&d.bFilter)g=pb(a);else if("r"==i&&d.bProcessing)g=qb(a);else if("t"==i)g=rb(a);else if("i"==i&&d.bInfo)g=sb(a);else if("p"==i&&d.bPaginate)g=tb(a);else if(0!==m.ext.feature.length){j=
m.ext.feature;r=0;for(n=j.length;r<n;r++)if(i==j[r].cFeature){g=j[r].fnInit(a);break}}g&&(j=a.aanFeatures,j[i]||(j[i]=[]),j[i].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function fa(a,b){var c=h(b).children("tr"),d,e,f,g,i,j,n,l,r,q;a.splice(0,a.length);f=0;for(j=c.length;f<j;f++)a.push([]);f=0;for(j=c.length;f<j;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");r=1*e.getAttribute("rowspan");l=!l||0===l||
1===l?1:l;r=!r||0===r||1===r?1:r;g=0;for(i=a[f];i[g];)g++;n=g;q=1===l?!0:!1;for(i=0;i<l;i++)for(g=0;g<r;g++)a[f+g][n+i]={cell:e,unique:q},a[f+g].nTr=d}e=e.nextSibling}}}function qa(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],fa(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function ra(a,b,c){w(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=
b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,i=a.oInstance,j=function(b){w(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&J(a,0,c);a.json=b;j(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=w(a,null,"xhr",[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==
c?J(a,0,"Invalid JSON response",1):4===b.readyState&&J(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;w(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(i,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),j,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(i,b,j,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function lb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,!0),ra(a,ub(a),function(b){vb(a,b)}),!1):!0}function ub(a){var b=
a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,i=[],j,n,l,r=V(a);g=a._iDisplayStart;j=!1!==d.bPaginate?a._iDisplayLength:-1;var q=function(a,b){i.push({name:a,value:b})};q("sEcho",a.iDraw);q("iColumns",c);q("sColumns",D(b,"sName").join(","));q("iDisplayStart",g);q("iDisplayLength",j);var k={draw:a.iDraw,columns:[],order:[],start:g,length:j,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],l=f[g],j="function"==typeof n.mData?"function":n.mData,k.columns.push({data:j,
name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),q("mDataProp_"+g,j),d.bFilter&&(q("sSearch_"+g,l.sSearch),q("bRegex_"+g,l.bRegex),q("bSearchable_"+g,n.bSearchable)),d.bSort&&q("bSortable_"+g,n.bSortable);d.bFilter&&(q("sSearch",e.sSearch),q("bRegex",e.bRegex));d.bSort&&(h.each(r,function(a,b){k.order.push({column:b.col,dir:b.dir});q("iSortCol_"+a,b.col);q("sSortDir_"+a,b.dir)}),q("iSortingCols",r.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?
i:k:b?i:k}function vb(a,b){var c=sa(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}na(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)L(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;M(a);a._bInitComplete||ta(a,b);a.bAjaxDataGet=!0;C(a,!1)}function sa(a,b){var c=h.isPlainObject(a.ajax)&&
a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?P(c)(b):b}function pb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',i=d.sSearch,i=i.match(/_INPUT_/)?i.replace("_INPUT_",g):i+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(i)),f=function(){var b=!this.value?"":this.value;b!=e.sSearch&&(ha(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,
bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,M(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,j=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).bind("keyup.DT search.DT input.DT paste.DT cut.DT",g?ua(f,g):f).bind("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{j[0]!==T.activeElement&&j.val(e.sSearch)}catch(d){}});return b[0]}function ha(a,b,c){var d=a.oPreviousSearch,
e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){wb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)xb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);yb(a)}else f(b);a.bFiltered=!0;w(a,null,"search",[a])}function yb(a){for(var b=m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<
g;f++){for(var i=[],j=0,n=c.length;j<n;j++)e=c[j],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,j)&&i.push(e);c.length=0;h.merge(c,i)}}function xb(a,b,c,d,e,f){if(""!==b)for(var g=a.aiDisplay,d=Qa(b,d,e,f),e=g.length-1;0<=e;e--)b=a.aoData[g[e]]._aFilterData[c],d.test(b)||g.splice(e,1)}function wb(a,b,c,d,e,f){var d=Qa(b,d,e,f),e=a.oPreviousSearch.sSearch,f=a.aiDisplayMaster,g;0!==m.ext.search.length&&(c=!0);g=zb(a);if(0>=b.length)a.aiDisplay=f.slice();else{if(g||c||e.length>b.length||0!==b.indexOf(e)||
a.bSorted)a.aiDisplay=f.slice();b=a.aiDisplay;for(c=b.length-1;0<=c;c--)d.test(a.aoData[b[c]]._sFilterRow)||b.splice(c,1)}}function Qa(a,b,c,d){a=b?a:va(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function va(a){return a.replace(Yb,"\\$1")}function zb(a){var b=a.aoColumns,c,d,e,f,g,i,j,h,l=m.ext.type.search;c=!1;d=0;for(f=a.aoData.length;d<
f;d++)if(h=a.aoData[d],!h._aFilterData){i=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(j=B(a,d,e,"filter"),l[c.sType]&&(j=l[c.sType](j)),null===j&&(j=""),"string"!==typeof j&&j.toString&&(j=j.toString())):j="",j.indexOf&&-1!==j.indexOf("&")&&(wa.innerHTML=j,j=Zb?wa.textContent:wa.innerText),j.replace&&(j=j.replace(/[\r\n]/g,"")),i.push(j);h._aFilterData=i;h._sFilterRow=i.join(" ");c=!0}return c}function Ab(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,caseInsensitive:a.bCaseInsensitive}}
function Bb(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function sb(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Cb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Cb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),
g=a.fnRecordsDisplay(),i=g?c.sInfo:c.sInfoEmpty;g!==f&&(i+=" "+c.sInfoFiltered);i+=c.sInfoPostFix;i=Db(a,i);c=c.fnInfoCallback;null!==c&&(i=c.call(a.oInstance,a,d,e,f,g,i));h(b).html(i)}}function Db(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/
e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ia(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){nb(a);kb(a);ga(a,a.aoHeader);ga(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=u(f.sWidth));w(a,null,"preInit",[a]);R(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ra(a,[],function(c){var f=sa(a,c);for(b=0;b<f.length;b++)L(a,f[b]);a.iInitDisplayStart=d;R(a);C(a,!1);ta(a,c)},a):(C(a,!1),
ta(a))}else setTimeout(function(){ia(a)},200)}function ta(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Y(a);w(a,"aoInitComplete","init",[a,b])}function Ra(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Sa(a);w(a,null,"length",[a,c])}function ob(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,i=f.length;g<i;g++)e[0][g]=new Option(d[g],f[g]);var j=h("<div><label/></div>").addClass(b.sLength);
a.aanFeatures.l||(j[0].id=c+"_length");j.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",j).val(a._iDisplayLength).bind("change.DT",function(){Ra(a,h(this).val());M(a)});h(a.nTable).bind("length.dt.DT",function(b,c,d){a===c&&h("select",j).val(d)});return j[0]}function tb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){M(a)},b=h("<div/>").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+
"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,j=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===j,b=l?0:Math.ceil(b/j),j=l?1:Math.ceil(h/j),h=c(b,j),k,l=0;for(k=f.p.length;l<k;l++)Pa(a,"pageButton")(a,f.p[l],l,h,b,j)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Ta(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===e?d=0:"number"===typeof b?(d=b*e,d>f&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==
b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:J(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(w(a,null,"page",[a]),c&&M(a));return b}function qb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");w(a,null,"processing",[a,b])}function rb(a){var b=h(a.nTable);b.attr("role",
"grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),i=g.length?g[0]._captionSide:null,j=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");c.sX&&"100%"===b.attr("width")&&b.removeAttr("width");l.length||(l=null);j=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:u(d):"100%"}).append(h("<div/>",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",
width:c.sXInner||"100%"}).append(j.removeAttr("id").css("margin-left",0).append("top"===i?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:u(d)}).append(b));l&&j.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:u(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",0).append("bottom"===i?g:null).append(b.children("tfoot")))));
var b=j.children(),k=b[0],f=b[1],q=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(q.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=q;a.aoDrawCallback.push({fn:Z,sName:"scrolling"});return j[0]}function Z(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,i=f.children("div"),j=i[0].style,n=i.children("table"),i=a.nScrollBody,l=h(i),k=i.style,q=h(a.nScrollFoot).children("div"),
m=q.children("table"),o=h(a.nTHead),E=h(a.nTable),p=E[0],t=p.style,N=a.nTFoot?h(a.nTFoot):null,Eb=a.oBrowser,w=Eb.bScrollOversize,s,v,O,x,y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};E.children("thead, tfoot").remove();x=o.clone().prependTo(E);o=o.find("tr");v=x.find("tr");x.find("th, td").removeAttr("tabindex");N&&(O=N.clone().prependTo(E),s=N.find("tr"),O=O.find("tr"));c||(k.width="100%",f[0].style.width="100%");
h.each(qa(a,x),function(b,c){B=$(a,b);c.style.width=a.aoColumns[B].sWidth});N&&H(function(a){a.style.width=""},O);f=E.outerWidth();if(""===c){t.width="100%";if(w&&(E.find("tbody").height()>i.offsetHeight||"scroll"==l.css("overflow-y")))t.width=u(E.outerWidth()-b);f=E.outerWidth()}else""!==d&&(t.width=u(d),f=E.outerWidth());H(C,v);H(function(a){A.push(a.innerHTML);y.push(u(h(a).css("width")))},v);H(function(a,b){a.style.width=y[b]},o);h(v).height(0);N&&(H(C,O),H(function(a){z.push(u(h(a).css("width")))},
O),H(function(a,b){a.style.width=z[b]},s),h(O).height(0));H(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+A[b]+"</div>";a.style.width=y[b]},v);N&&H(function(a,b){a.innerHTML="";a.style.width=z[b]},O);if(E.outerWidth()<f){s=i.scrollHeight>i.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(w&&(i.scrollHeight>i.offsetHeight||"scroll"==l.css("overflow-y")))t.width=u(s-b);(""===c||""!==d)&&J(a,1,"Possible column misalignment",6)}else s="100%";k.width=
u(s);g.width=u(s);N&&(a.nScrollFoot.style.width=u(s));!e&&w&&(k.height=u(p.offsetHeight+b));c=E.outerWidth();n[0].style.width=u(c);j.width=u(c);d=E.height()>i.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(Eb.bScrollbarLeft?"Left":"Right");j[e]=d?b+"px":"0px";N&&(m[0].style.width=u(c),q[0].style.width=u(c),q[0].style[e]=d?b+"px":"0px");l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)i.scrollTop=0}function H(a,b,c){for(var d=0,e=0,f=b.length,g,i;e<f;){g=b[e].firstChild;for(i=c?c[e].firstChild:
null;g;)1===g.nodeType&&(c?a(g,i,d):a(g,d),d++),g=g.nextSibling,i=c?i.nextSibling:null;e++}}function Ha(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,e=d.sY,f=d.sX,g=d.sXInner,i=c.length,j=aa(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,q=!1,m,o,p;p=a.oBrowser;d=p.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<j.length;m++)o=c[j[m]],null!==o.sWidth&&(o.sWidth=Fb(o.sWidthOrig,k),q=!0);if(d||!q&&!f&&!e&&i==ca(a)&&i==n.length)for(m=0;m<i;m++){if(j=
$(a,m))c[j].sWidth=u(n.eq(m).width())}else{i=h(b).clone().css("visibility","hidden").removeAttr("id");i.find("tbody tr").remove();var t=h("<tr/>").appendTo(i.find("tbody"));i.find("thead, tfoot").remove();i.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());i.find("tfoot th, tfoot td").css("width","");n=qa(a,i.find("thead")[0]);for(m=0;m<j.length;m++)o=c[j[m]],n[m].style.width=null!==o.sWidthOrig&&""!==o.sWidthOrig?u(o.sWidthOrig):"";if(a.aoData.length)for(m=0;m<j.length;m++)q=j[m],o=c[q],h(Gb(a,
q)).clone(!1).append(o.sContentPadding).appendTo(t);q=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(i).appendTo(k);f&&g?i.width(g):f?(i.css("width","auto"),i.width()<k.clientWidth&&i.width(k.clientWidth)):e?i.width(k.clientWidth):l&&i.width(l);if(f){for(m=g=0;m<j.length;m++)o=c[j[m]],e=p.bBounding?n[m].getBoundingClientRect().width:h(n[m]).outerWidth(),g+=null===o.sWidthOrig?e:parseInt(o.sWidth,10)+e-h(n[m]).width();i.width(u(g));b.style.width=
u(g)}for(m=0;m<j.length;m++)if(o=c[j[m]],p=h(n[m]).width())o.sWidth=u(p);b.style.width=u(i.css("width"));q.remove()}l&&(b.style.width=u(l));if((l||f)&&!a._reszEvt)b=function(){h(Fa).bind("resize.DT-"+a.sInstance,ua(function(){Y(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function ua(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,i=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,i)},c)):(d=g,a.apply(b,i))}}function Fb(a,b){if(!a)return 0;var c=h("<div/>").css("width",
u(a)).appendTo(b||T.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a,b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace($b,""),c.length>d&&(d=c.length,e=f);return e}function u(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,i,j;b=a.aaSortingFixed;
c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){j=n[a][0];f=e[j].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],i=e[g].sType||"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:j,col:g,dir:n[a][1],index:n[a]._idx,type:i,formatter:m.ext.type.order[i+"-pre"]})}return d}function mb(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=
0,i,j=a.aiDisplayMaster,h;Ia(a);h=V(a);b=0;for(c=h.length;b<c;b++)i=h[b],i.formatter&&g++,Ib(a,i.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=j.length;b<c;b++)d[j[b]]=b;g===h.length?j.sort(function(a,b){var c,e,g,i,j=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=0;g<j;g++)if(i=h[g],c=k[i.col],e=m[i.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===i.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):j.sort(function(a,b){var c,g,i,j,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(i=0;i<k;i++)if(j=h[i],
c=m[j.col],g=p[j.col],j=e[j.type+"-"+j.dir]||e["string-"+j.dir],c=j(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var i=c.asSorting;b=c.sTitle.replace(/<.*?>/g,"");var j=c.nTh;j.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(j.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=i[e[0].index+1]||i[0]):c=i[0],b+="asc"===c?a.sSortAscending:
a.sSortDescending);j.setAttribute("aria-label",b)}}function Ua(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],
e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);R(a);"function"==typeof d&&d(a)}function Oa(a,b,c,d){var e=a.aoColumns[c];Va(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Ua(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Ua(a,c,b.shiftKey,d))})}function xa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=V(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;
for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ba(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],i=0,h=a.aoData.length;i<h;i++)if(c=a.aoData[i],c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[i]:B(a,i,b,"sort"),c._aSortData[b]=g?g(f):f}function ya(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,
length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:Ab(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Ab(a.aoPreSearchCols[d])}})};w(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,b)}}function Kb(a){var b,c,d=a.aoColumns;if(a.oFeatures.bStateSave){var e=a.fnStateLoadCallback.call(a.oInstance,a);if(e&&e.time&&(b=w(a,"aoStateLoadParams","stateLoadParams",[a,e]),-1===h.inArray(!1,b)&&(b=
a.iStateDuration,!(0<b&&e.time<+new Date-1E3*b)&&d.length===e.columns.length))){a.oLoadedState=h.extend(!0,{},e);e.start!==k&&(a._iDisplayStart=e.start,a.iInitDisplayStart=e.start);e.length!==k&&(a._iDisplayLength=e.length);e.order!==k&&(a.aaSorting=[],h.each(e.order,function(b,c){a.aaSorting.push(c[0]>=d.length?[0,c[1]]:c)}));e.search!==k&&h.extend(a.oPreviousSearch,Bb(e.search));b=0;for(c=e.columns.length;b<c;b++){var f=e.columns[b];f.visible!==k&&(d[b].bVisible=f.visible);f.search!==k&&h.extend(a.aoPreSearchCols[b],
Bb(f.search))}w(a,"aoStateLoaded","stateLoaded",[a,e])}}}function za(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function J(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)Fa.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&w(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&
b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Lb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Va(a,b,c){h(a).bind("click.DT",b,function(b){a.blur();c(b)}).bind("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).bind("selectstart.DT",
function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function w(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Sa(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===
typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Aa(a,b){var c=[],c=Mb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Wa)},"html-num":function(b){return Ba(b,
a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Wa)}},function(b,c){v.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(v.type.search[b+a]=v.type.search.html)})}function Nb(a){return function(){var b=[za(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m,v,t,p,s,Xa={},Ob=/[\r\n]/g,Ca=/<.*?>/g,ac=/^[\w\+\-]/,bc=/[\w\+\-]$/,Yb=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Wa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,
K=function(a){return!a||!0===a||"-"===a?!0:!1},Pb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Qb=function(a,b){Xa[b]||(Xa[b]=RegExp(va(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Xa[b],"."):a},Ya=function(a,b,c){var d="string"===typeof a;if(K(a))return!0;b&&d&&(a=Qb(a,b));c&&d&&(a=a.replace(Wa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Rb=function(a,b,c){return K(a)?!0:!(K(a)||"string"===typeof a)?null:Ya(a.replace(Ca,""),b,c)?!0:null},D=function(a,
b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<f;e++)a[e]&&d.push(a[e][b]);return d},ja=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},W=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Sb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},pa=function(a){var b=[],c,d,e=a.length,f,g=0;
d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b},A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},da=/\[.*?\]$/,U=/\(\)$/,wa=h("<div>")[0],Zb=wa.textContent!==k,$b=/<.*?>/g;m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new t(za(this[v.iApiIndex])):new t(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?
c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&Z(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);
(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);
return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=
function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return za(this[v.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=v.fnVersionCheck;var b=this,c=a===k,d=this.length;
c&&(a={});this.oApi=this.internal=v.internal;for(var e in m.ext.internal)e&&(this[e]=Nb(e));this.each(function(){var e={},e=1<d?Lb(e,a,!0):a,g=0,i,j=this.getAttribute("id"),n=!1,l=m.defaults,r=h(this);if("table"!=this.nodeName.toLowerCase())J(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{db(l);eb(l.column);I(l,l,!0);I(l.column,l.column,!0);I(l,h.extend(e,r.data()));var q=m.settings,g=0;for(i=q.length;g<i;g++){var p=q[g];if(p.nTable==this||p.nTHead.parentNode==this||p.nTFoot&&
p.nTFoot.parentNode==this){g=e.bRetrieve!==k?e.bRetrieve:l.bRetrieve;if(c||g)return p.oInstance;if(e.bDestroy!==k?e.bDestroy:l.bDestroy){p.oInstance.fnDestroy();break}else{J(p,0,"Cannot reinitialise DataTable",3);return}}if(p.sTableId==this.id){q.splice(g,1);break}}if(null===j||""===j)this.id=j="DataTables_Table_"+m.ext._unique++;var o=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:r[0].style.width,sInstance:j,sTableId:j});o.nTable=this;o.oApi=b.internal;o.oInit=e;q.push(o);o.oInstance=1===b.length?
b:r.dataTable();db(e);e.oLanguage&&S(e.oLanguage);e.aLengthMenu&&!e.iDisplayLength&&(e.iDisplayLength=h.isArray(e.aLengthMenu[0])?e.aLengthMenu[0][0]:e.aLengthMenu[0]);e=Lb(h.extend(!0,{},l),e);F(o.oFeatures,e,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(o,e,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp",
"iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"],["bJQueryUI","bJUI"]]);F(o.oScroll,e,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(o.oLanguage,e,"fnInfoCallback");z(o,"aoDrawCallback",e.fnDrawCallback,"user");z(o,"aoServerParams",
e.fnServerParams,"user");z(o,"aoStateSaveParams",e.fnStateSaveParams,"user");z(o,"aoStateLoadParams",e.fnStateLoadParams,"user");z(o,"aoStateLoaded",e.fnStateLoaded,"user");z(o,"aoRowCallback",e.fnRowCallback,"user");z(o,"aoRowCreatedCallback",e.fnCreatedRow,"user");z(o,"aoHeaderCallback",e.fnHeaderCallback,"user");z(o,"aoFooterCallback",e.fnFooterCallback,"user");z(o,"aoInitComplete",e.fnInitComplete,"user");z(o,"aoPreDrawCallback",e.fnPreDrawCallback,"user");o.rowIdFn=P(e.rowId);fb(o);j=o.oClasses;
e.bJQueryUI?(h.extend(j,m.ext.oJUIClasses,e.oClasses),e.sDom===l.sDom&&"lfrtip"===l.sDom&&(o.sDom='<"H"lfr>t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&&!o.renderer.header&&(o.renderer.header="jqueryui"):o.renderer="jqueryui":h.extend(j,m.ext.classes,e.oClasses);r.addClass(j.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=e.iDisplayStart,o._iDisplayStart=e.iDisplayStart);null!==e.iDeferLoading&&(o.bDeferLoading=!0,g=h.isArray(e.iDeferLoading),o._iRecordsDisplay=g?e.iDeferLoading[0]:e.iDeferLoading,
o._iRecordsTotal=g?e.iDeferLoading[1]:e.iDeferLoading);var t=o.oLanguage;h.extend(!0,t,e.oLanguage);""!==t.sUrl&&(h.ajax({dataType:"json",url:t.sUrl,success:function(a){S(a);I(l.oLanguage,a);h.extend(true,t,a);ia(o)},error:function(){ia(o)}}),n=!0);null===e.asStripeClasses&&(o.asStripeClasses=[j.sStripeOdd,j.sStripeEven]);var g=o.asStripeClasses,s=r.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(g,function(a){return s.hasClass(a)}))&&(h("tbody tr",this).removeClass(g.join(" ")),o.asDestroyStripes=
g.slice());q=[];g=this.getElementsByTagName("thead");0!==g.length&&(fa(o.aoHeader,g[0]),q=qa(o));if(null===e.aoColumns){p=[];g=0;for(i=q.length;g<i;g++)p.push(null)}else p=e.aoColumns;g=0;for(i=p.length;g<i;g++)Ga(o,q?q[g]:null);hb(o,e.aoColumnDefs,p,function(a,b){la(o,a,b)});if(s.length){var u=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(s[0]).children("th, td").each(function(a,b){var c=o.aoColumns[a];if(c.mData===a){var d=u(b,"sort")||u(b,"order"),e=u(b,"filter")||u(b,"search");
if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};la(o,a)}}})}var v=o.oFeatures;e.bStateSave&&(v.bStateSave=!0,Kb(o,e),z(o,"aoDrawCallback",ya,"state_save"));if(e.aaSorting===k){q=o.aaSorting;g=0;for(i=q.length;g<i;g++)q[g][1]=o.aoColumns[g].asSorting[0]}xa(o);v.bSort&&z(o,"aoDrawCallback",function(){if(o.bSorted){var a=V(o),b={};h.each(a,function(a,c){b[c.src]=c.dir});w(o,null,"order",[o,a,b]);Jb(o)}});z(o,
"aoDrawCallback",function(){(o.bSorted||y(o)==="ssp"||v.bDeferRender)&&xa(o)},"sc");g=r.children("caption").each(function(){this._captionSide=r.css("caption-side")});i=r.children("thead");0===i.length&&(i=h("<thead/>").appendTo(this));o.nTHead=i[0];i=r.children("tbody");0===i.length&&(i=h("<tbody/>").appendTo(this));o.nTBody=i[0];i=r.children("tfoot");if(0===i.length&&0<g.length&&(""!==o.oScroll.sX||""!==o.oScroll.sY))i=h("<tfoot/>").appendTo(this);0===i.length||0===i.children().length?r.addClass(j.sNoFooter):
0<i.length&&(o.nTFoot=i[0],fa(o.aoFooter,o.nTFoot));if(e.aaData)for(g=0;g<e.aaData.length;g++)L(o,e.aaData[g]);else(o.bDeferLoading||"dom"==y(o))&&ma(o,h(o.nTBody).children("tr"));o.aiDisplay=o.aiDisplayMaster.slice();o.bInitialised=!0;!1===n&&ia(o)}});b=null;return this};var Tb=[],x=Array.prototype,cc=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:
null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};t=function(a,b){if(!(this instanceof t))return new t(a,b);var c=[],d=function(a){(a=cc(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=pa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};t.extend(this,this,Tb)};
m.Api=t;h.extend(t.prototype,{any:function(){return 0!==this.count()},concat:x.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new t(b[a],this[a]):null},filter:function(a){var b=[];if(x.filter)b=x.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new t(this.context,b)},flatten:function(){var a=
[];return new t(this.context,a.concat.apply(a,this.toArray()))},join:x.join,indexOf:x.indexOf||function(a,b){for(var c=b||0,d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,h,j,n,l=this.context,m,q,p=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(h=l.length;g<h;g++){var o=new t(l[g]);if("table"===b)f=c.call(o,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(o,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===
b||"row"===b||"cell"===b){q=this[g];"column-rows"===b&&(m=Da(l[g],p.opts));j=0;for(n=q.length;j<n;j++)f=q[j],f="cell"===b?c.call(o,l[g],f.row,f.column,g,j):c.call(o,l[g],f,g,j,m),f!==k&&e.push(f)}}return e.length||d?(a=new t(l,a?e.concat.apply([],e):e),b=a.selector,b.rows=p.rows,b.cols=p.cols,b.opts=p.opts,a):this},lastIndexOf:x.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(x.map)b=x.map.call(this,a,this);else for(var c=
0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new t(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:x.pop,push:x.push,reduce:x.reduce||function(a,b){return gb(this,a,b,0,this.length,1)},reduceRight:x.reduceRight||function(a,b){return gb(this,a,b,this.length-1,-1,-1)},reverse:x.reverse,selector:null,shift:x.shift,sort:x.sort,splice:x.splice,toArray:function(){return x.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},
unique:function(){return new t(this.context,pa(this))},unshift:x.unshift});t.extend=function(a,b,c){if(c.length&&b&&(b instanceof t||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=b.apply(a,arguments);t.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,t.extend(a,b[f.name],f.propExt)}};t.register=p=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<
d;c++)t.register(a[c],b);else for(var e=a.split("."),f=Tb,g,i,c=0,d=e.length;c<d;c++){g=(i=-1!==e[c].indexOf("()"))?e[c].replace("()",""):e[c];var j;a:{j=0;for(var n=f.length;j<n;j++)if(f[j].name===g){j=f[j];break a}j=null}j||(j={name:g,val:{},methodExt:[],propExt:[]},f.push(j));c===d-1?j.val=b:f=i?j.methodExt:j.propExt}};t.registerPlural=s=function(a,b,c){t.register(a,c);t.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof t?a.length?h.isArray(a[0])?new t(a.context,
a[0]):a[0]:k:a})};p("tables()",function(a){var b;if(a){b=t;var c=this.context;if("number"===typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,d);return c[a]}).toArray();b=new b(a)}else b=this;return b});p("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new t(b[0]):a});s("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});s("tables().body()","table().body()",
function(){return this.iterator("table",function(a){return a.nTBody},1)});s("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});s("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});s("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});p("draw()",function(a){return this.iterator("table",function(b){"page"===
a?M(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),R(b,!1===a))})});p("page()",function(a){return a===k?this.page.info().page:this.iterator("table",function(b){Ta(b,a)})});p("page.info()",function(){if(0===this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a._iDisplayLength,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});
p("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:k:this.iterator("table",function(b){Ra(b,a)})});var Ub=function(a,b,c){if(c){var d=new t(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))R(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();ra(a,[],function(c){na(a);for(var c=sa(a,c),d=0,e=c.length;d<e;d++)L(a,c[d]);R(a,b);C(a,!1)})}};p("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});p("ajax.params()",
function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});p("ajax.reload()",function(a,b){return this.iterator("table",function(c){Ub(c,!1===b,a)})});p("ajax.url()",function(a){var b=this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});p("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Ub(c,!1===b,a)})});
var Za=function(a,b,c,d,e){var f=[],g,i,j,n,l,m;j=typeof b;if(!b||"string"===j||"function"===j||b.length===k)b=[b];j=0;for(n=b.length;j<n;j++){i=b[j]&&b[j].split?b[j].split(","):[b[j]];l=0;for(m=i.length;l<m;l++)(g=c("string"===typeof i[l]?h.trim(i[l]):i[l]))&&g.length&&(f=f.concat(g))}a=v.selector[a];if(a.length){j=0;for(n=a.length;j<n;j++)f=a[j](d,e,f)}return pa(f)},$a=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},
ab=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Da=function(a,b){var c,d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var i=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===i?[]:W(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==i?c.slice():"applied"==i?g.slice():h.map(c,function(a){return-1===h.inArray(a,
g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<d;c++)"none"==i?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==i||0<=e&&"applied"==i)&&f.push(c))}return f};p("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=b;return Za("row",a,function(a){var b=Pb(a);if(b!==null&&!e)return[b];var i=Da(c,e);if(b!==null&&h.inArray(b,i)!==-1)return[b];if(!a)return i;if(typeof a==="function")return h.map(i,function(b){var e=
c.aoData[b];return a(b,e._aData,e.nTr)?b:null});b=Sb(ja(c.aoData,i,"nTr"));if(a.nodeName&&h.inArray(a,b)!==-1)return[a._DT_RowIndex];if(typeof a==="string"&&a.charAt(0)==="#"){i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});p("rows().nodes()",function(){return this.iterator("row",function(a,b){return a.aoData[b].nTr||k},1)});p("rows().data()",function(){return this.iterator(!0,
"rows",function(a,b){return ja(a.aoData,b,"_aData")},1)});s("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});s("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){ea(b,c,a)})});s("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,b){return b},1)});s("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,
d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new t(c,b)});s("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c];e.splice(c,1);for(var g=0,h=e.length;g<h;g++)null!==e[g].nTr&&(e[g].nTr._DT_RowIndex=g);oa(b.aiDisplayMaster,c);oa(b.aiDisplay,c);oa(a[d],c,!1);Sa(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});this.iterator("table",function(a){for(var c=
0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});p("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(ma(b,c)[0]):h.push(L(b,c));return h},1),c=this.rows(-1);c.pop();h.merge(c,b);return c});p("row()",function(a,b){return ab(this.rows(a,b))});p("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:k;b[0].aoData[this[0]]._aData=
a;ea(b[0],this[0],"data");return this});p("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});p("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?ma(b,a)[0]:L(b,a)});return this.row(b[0])});var bb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=k,c._details=k},Vb=function(a,
b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new t(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<D(g,"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",function(a,b){if(e===
b)for(var c,d=ca(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&bb(f,c)}))}}};p("row().child()",function(a,b){var c=this.context;if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)bb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,b){if(h.isArray(a)||
a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=ca(d),e.push(c[0]))};f(a,b);c._details&&c._details.remove();c._details=h(e);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});p(["row().child.show()","row().child().show()"],function(){Vb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Vb(this,!1);return this});p(["row().child.remove()",
"row().child().remove()"],function(){bb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var dc=/^(.+):(name|visIdx|visible)$/,Wb=function(a,b,c,d,e){for(var c=[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};p("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,i=D(g,"sName"),j=D(g,"nTh");return Za("column",
e,function(a){var b=Pb(a);if(a==="")return W(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Wb(c,f,0,0,e),j[f])?f:null})}var k=typeof a==="string"?a.match(dc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[$(c,b)];case "name":return h.map(i,function(a,b){return a===k[1]?b:null})}else return h(j).filter(a).map(function(){return h.inArray(this,
j)}).toArray()},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});s("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});s("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});s("columns().data()","column().data()",function(){return this.iterator("column-rows",Wb,1)});s("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",
function(a,b){return a.aoColumns[b].mData},1)});s("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});s("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});s("columns().visible()","column().visible()",function(a,b){return this.iterator("column",function(c,d){if(a===k)return c.aoColumns[d].bVisible;
var e=c.aoColumns,f=e[d],g=c.aoData,i,j,m;if(a!==k&&f.bVisible!==a){if(a){var l=h.inArray(!0,D(e,"bVisible"),d+1);i=0;for(j=g.length;i<j;i++)m=g[i].nTr,e=g[i].anCells,m&&m.insertBefore(e[d],e[l]||null)}else h(D(c.aoData,"anCells",d)).detach();f.bVisible=a;ga(c,c.aoHeader);ga(c,c.aoFooter);if(b===k||b)Y(c),(c.oScroll.sX||c.oScroll.sY)&&Z(c);w(c,null,"column-visibility",[c,d,a]);ya(c)}})});s("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===
a?ba(b,c):c},1)});p("columns.adjust()",function(){return this.iterator("table",function(a){Y(a)},1)});p("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return $(c,b);if("fromData"===a||"toVisible"===a)return ba(c,b)}});p("column()",function(a,b){return ab(this.columns(a,b))});p("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",
function(b){var d=a,e=$a(c),f=b.aoData,g=Da(b,e),i=Sb(ja(f,g,"anCells")),j=h([].concat.apply([],i)),l,m=b.aoColumns.length,n,p,t,s,u,v;return Za("cell",d,function(a){var c=typeof a==="function";if(a===null||a===k||c){n=[];p=0;for(t=g.length;p<t;p++){l=g[p];for(s=0;s<m;s++){u={row:l,column:s};if(c){v=f[l];a(u,B(b,l,s),v.anCells?v.anCells[s]:null)&&n.push(u)}else n.push(u)}}return n}return h.isPlainObject(a)?[a]:j.filter(a).map(function(a,b){if(b.parentNode)l=b.parentNode._DT_RowIndex;else{a=0;for(t=
f.length;a<t;a++)if(h.inArray(b,f[a].anCells)!==-1){l=a;break}}return{row:l,column:h.inArray(b,f[l].anCells)}}).toArray()},b,e)});var d=this.columns(b,c),e=this.rows(a,c),f,g,i,j,m,l=this.iterator("table",function(a,b){f=[];g=0;for(i=e[b].length;g<i;g++){j=0;for(m=d[b].length;j<m;j++)f.push({row:e[b][g],column:d[b][j]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});s("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b].anCells)?
a[c]:k},1)});p("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});s("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});s("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});s("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,
column:c,columnVisible:ba(a,c)}},1)});s("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){ea(b,c,a,d)})});p("cell()",function(a,b,c){return ab(this.cells(a,b,c))});p("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;ib(b[0],c[0].row,c[0].column,a);ea(b[0],c[0].row,"data",c[0].column);return this});p("order()",function(a,b){var c=this.context;if(a===k)return 0!==c.length?c[0].aaSorting:
k;"number"===typeof a?a=[[a,b]]:h.isArray(a[0])||(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});p("order.listener()",function(a,b,c){return this.iterator("table",function(d){Oa(d,a,b,c)})});p(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});p("search()",function(a,b,c,d){var e=this.context;return a===k?0!==e.length?
e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&ha(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});s("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),ha(e,
e.oPreviousSearch,1))})});p("state()",function(){return this.context.length?this.context[0].oSavedState:null});p("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});p("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});p("state.save()",function(){return this.iterator("table",function(a){ya(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),a=a.split("."),c,d,e=0,f=
a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});
return b?new t(c):c};m.util={throttle:ua,escapeRegex:va};m.camelToHungarian=I;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0].match(/\.dt\b/)||(a[0]+=".dt");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",function(a){na(a)})});p("settings()",function(){return new t(this.context,
this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,i=b.nTFoot,j=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),p;b.bDestroying=!0;w(b,"aoDestroyCallback","destroy",[b]);a||
(new t(b)).columns().visible(!0);k.unbind(".DT").find(":not(tbody *)").unbind(".DT");h(Fa).unbind(".DT-"+b.sInstance);e!=g.parentNode&&(j.children("thead").detach(),j.append(g));i&&e!=i.parentNode&&(j.children("tfoot").detach(),j.append(i));b.aaSorting=[];b.aaSortingFixed=[];xa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+", td span."+d.sSortIcon,g).detach(),h("th, td",
g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";j[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),j.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){p(b+"s().every()",
function(a){return this.iterator(b,function(d,e,f,g,h){a.call((new t(d))[b](e,"cell"===b?f:k),e,f,g,h)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=P(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.9";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,
idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,
25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,
fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},
fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",
sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};X(m.defaults);m.defaults.column={aDataSort:null,
iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,
iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],
aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,
iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=
this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=v={buttons:{},classes:{},errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,
iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(v,{afnFiltering:v.search,aTypes:v.type.detect,ofnSearch:v.type.search,oSort:v.type.order,afnSortData:v.order,aoFeatures:v.feature,oApi:v.internal,oStdClasses:v.classes,oPagination:v.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",
sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",
sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ea="",Ea="",G=Ea+"ui-state-default",ka=Ea+"css_right ui-icon ui-icon-",Xb=Ea+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,m.ext.classes,{sPageButton:"fg-button ui-button "+G,sPageButtonActive:"ui-state-disabled",
sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:G+" sorting_asc",sSortDesc:G+" sorting_desc",sSortable:G+" sorting",sSortableAsc:G+" sorting_asc_disabled",sSortableDesc:G+" sorting_desc_disabled",sSortableNone:G+" sorting_disabled",sSortJUIAsc:ka+"triangle-1-n",sSortJUIDesc:ka+"triangle-1-s",sSortJUI:ka+"carat-2-n-s",sSortJUIAscAllowed:ka+"carat-1-n",sSortJUIDescAllowed:ka+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",
sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+G,sScrollFoot:"dataTables_scrollFoot "+G,sHeaderTH:G,sFooterTH:G,sJUIHeader:Xb+" ui-corner-tl ui-corner-tr",sJUIFooter:Xb+" ui-corner-bl ui-corner-br"});var Mb=m.ext.pager;h.extend(Mb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[Aa(a,b)]},simple_numbers:function(a,b){return["previous",Aa(a,b),"next"]},full_numbers:function(a,b){return["first",
"previous",Aa(a,b),"next","last"]},_numbers:Aa,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,i=a.oLanguage.oPaginate,j,k,l=0,m=function(b,d){var p,q,t,s,u=function(b){Ta(a,b.data.action,true)};p=0;for(q=d.length;p<q;p++){s=d[p];if(h.isArray(s)){t=h("<"+(s.DT_el||"div")+"/>").appendTo(b);m(t,s)}else{j=null;k="";switch(s){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":j=i.sFirst;k=s+(e>0?"":" "+g.sPageButtonDisabled);
break;case "previous":j=i.sPrevious;k=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":j=i.sNext;k=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":j=i.sLast;k=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:j=s+1;k=e===s?g.sPageButtonActive:""}if(j!==null){t=h("<a>",{"class":g.sPageButton+" "+k,"aria-controls":a.sTableId,"data-dt-idx":l,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(j).appendTo(b);Va(t,{action:s},u);l++}}}},p;try{p=h(b).find(T.activeElement).data("dt-idx")}catch(t){}m(h(b).empty(),
d);p&&h(b).find("[data-dt-idx="+p+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&(!ac.test(a)||!bc.test(a)))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||K(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c,
!0)?"html-num-fmt"+c:null},function(a){return K(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return K(a)?a:"string"===typeof a?a.replace(Ob," ").replace(Ca,""):""},string:function(a){return K(a)?a:"string"===typeof a?a.replace(Ob," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Qb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(v.type.order,{"date-pre":function(a){return Date.parse(a)||
0},"html-pre":function(a){return K(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return K(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});cb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]==
"asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+
d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",f=Math.abs(parseFloat(f)),h=parseInt(f,10),f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}}};h.extend(m.ext.internal,{_fnExternApiFunc:Nb,_fnBuildAjax:ra,_fnAjaxUpdate:lb,
_fnAjaxParameters:ub,_fnAjaxUpdateDraw:vb,_fnAjaxDataSrc:sa,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:$,_fnColumnIndexToVisible:ba,_fnVisbleColumns:ca,_fnGetColumns:aa,_fnColumnTypes:Ia,_fnApplyColumnDefs:hb,_fnHungarianMap:X,_fnCamelToHungarian:I,_fnLanguageCompat:S,_fnBrowserDetect:fb,_fnAddData:L,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},
_fnGetCellData:B,_fnSetCellData:ib,_fnSplitObjNotation:La,_fnGetObjectDataFn:P,_fnSetObjectDataFn:Q,_fnGetDataMaster:Ma,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ea,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:kb,_fnDrawHead:ga,_fnDraw:M,_fnReDraw:R,_fnAddOptionsHtml:nb,_fnDetectHeader:fa,_fnGetUniqueThs:qa,_fnFeatureHtmlFilter:pb,_fnFilterComplete:ha,_fnFilterCustom:yb,_fnFilterColumn:xb,_fnFilter:wb,_fnFilterCreateSearch:Qa,_fnEscapeRegex:va,_fnFilterData:zb,_fnFeatureHtmlInfo:sb,_fnUpdateInfo:Cb,
_fnInfoMacros:Db,_fnInitialise:ia,_fnInitComplete:ta,_fnLengthChange:Ra,_fnFeatureHtmlLength:ob,_fnFeatureHtmlPaginate:tb,_fnPageChange:Ta,_fnFeatureHtmlProcessing:qb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:rb,_fnScrollDraw:Z,_fnApplyToChildren:H,_fnCalculateColumnWidths:Ha,_fnThrottle:ua,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:u,_fnSortFlatten:V,_fnSort:mb,_fnSortAria:Jb,_fnSortListener:Ua,_fnSortAttachListener:Oa,_fnSortingClasses:xa,_fnSortData:Ib,_fnSaveState:ya,
_fnLoadState:Kb,_fnSettingsFromNode:za,_fnLog:J,_fnMap:F,_fnBindAction:Va,_fnCallbackReg:z,_fnCallbackFire:w,_fnLengthOverflow:Sa,_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnCalculateEnd:function(){}});h.fn.dataTable=m;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable};"function"===typeof define&&define.amd?define("datatables",["jquery"],S):"object"===
typeof exports?module.exports=S(require("jquery")):jQuery&&!jQuery.fn.dataTable&&S(jQuery)})(window,document);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,544 +0,0 @@
.typeahead__container {
/**
* Restore the font weight unset by the previous rule.
*/
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
/**
* Remove the inner border and padding in Firefox.
*/
/**
* Restore the focus styles unset by the previous rule.
*/
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
/**
* Remove the default vertical scrollbar in IE.
*/
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on OS X.
*/
/**
* Correct the text style of placeholders in Chrome, Edge, and Safari.
*/
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
}
.typeahead__container button,
.typeahead__container input,
.typeahead__container optgroup,
.typeahead__container select,
.typeahead__container textarea {
font: inherit;
/* 1 */
margin: 0;
/* 2 */
}
.typeahead__container optgroup {
font-weight: bold;
}
.typeahead__container button,
.typeahead__container input {
/* 1 */
overflow: visible;
}
.typeahead__container button,
.typeahead__container select {
/* 1 */
text-transform: none;
}
.typeahead__container button,
.typeahead__container html [type="button"],
.typeahead__container [type="reset"],
.typeahead__container [type="submit"] {
-webkit-appearance: button;
/* 2 */
}
.typeahead__container button::-moz-focus-inner,
.typeahead__container [type="button"]::-moz-focus-inner,
.typeahead__container [type="reset"]::-moz-focus-inner,
.typeahead__container [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.typeahead__container button:-moz-focusring,
.typeahead__container [type="button"]:-moz-focusring,
.typeahead__container [type="reset"]:-moz-focusring,
.typeahead__container [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.typeahead__container fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
.typeahead__container legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
.typeahead__container textarea {
overflow: auto;
}
.typeahead__container [type="checkbox"],
.typeahead__container [type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
.typeahead__container [type="number"]::-webkit-inner-spin-button,
.typeahead__container [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.typeahead__container [type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
.typeahead__container [type="search"]::-webkit-search-cancel-button,
.typeahead__container [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.typeahead__container ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.typeahead__container ::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
.typeahead__container {
position: relative;
font: 14px Lato, "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.typeahead__container * {
box-sizing: border-box;
outline: 0;
}
.typeahead__query {
position: relative;
z-index: 2;
width: 100%;
}
.typeahead__filter {
position: relative;
}
.typeahead__filter button {
min-width: 100%;
white-space: nowrap;
}
.typeahead__filter button:after {
display: inline-block;
margin-left: 4px;
width: 0;
height: 0;
vertical-align: -2px;
content: "";
border: 4px solid;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
.typeahead__field {
font-size: 0;
position: relative;
display: table;
border-collapse: collapse;
width: 100%;
}
.typeahead__field > * {
display: table-cell;
vertical-align: top;
}
.typeahead__query, .typeahead__filter, .typeahead__button {
font-size: 14px;
}
.typeahead__button {
position: relative;
font-size: 0;
width: 1%;
vertical-align: middle;
}
.typeahead__button button {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.typeahead__field {
color: #555;
}
.typeahead__field input {
display: block;
width: 100%;
height: 32px;
padding: 6px 12px;
background: #fff;
border: 1px solid #ccc;
border-radius: 2px 0 0 2px;
transition: all ease-in-out .15s;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
box-sizing: border-box;
}
.typeahead__field input:focus, .typeahead__field input:active {
border-color: #66afe9;
}
.typeahead__field input[type="search"],
.typeahead__field input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
.typeahead__field input[type="search"]::-ms-clear {
display: none;
width: 0;
height: 0;
}
.typeahead__container.hint .typeahead__field input {
background: transparent;
}
.typeahead__container.hint .typeahead__field input:last-child, .typeahead__hint {
background: #fff;
}
.typeahead__container button {
display: inline-block;
margin-bottom: 0;
text-align: center;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
background-color: #fff;
border: 1px solid #ccc;
height: 32px;
padding: 6px 12px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
color: #555;
}
.typeahead__container button:hover, .typeahead__container button:focus {
color: #3c3c3c;
background-color: #f5f5f5;
border-color: #b3b3b3;
}
.typeahead__container button:active, .typeahead__container button.active {
background-image: none;
}
.typeahead__container button:focus, .typeahead__container button:active {
border-color: #66afe9;
}
.typeahead__container input.disabled,
.typeahead__container input[disabled],
.typeahead__container button.disabled,
.typeahead__container button[disabled] {
cursor: not-allowed;
pointer-events: none;
opacity: 0.65;
box-shadow: none;
background-color: #fff;
border-color: #ccc;
}
.typeahead__filter, .typeahead__button {
z-index: 1;
}
.typeahead__filter button, .typeahead__button button {
margin-left: -1px;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.typeahead__filter:hover, .typeahead__filter:active, .typeahead__filter:focus, .typeahead__button:hover, .typeahead__button:active, .typeahead__button:focus {
z-index: 1001;
}
.typeahead__filter:hover button:focus, .typeahead__filter:hover button:active, .typeahead__filter:active button:focus, .typeahead__filter:active button:active, .typeahead__filter:focus button:focus, .typeahead__filter:focus button:active, .typeahead__button:hover button:focus, .typeahead__button:hover button:active, .typeahead__button:active button:focus, .typeahead__button:active button:active, .typeahead__button:focus button:focus, .typeahead__button:focus button:active {
z-index: 1001;
}
.typeahead__filter + .typeahead__button button {
margin-left: -2px;
}
.typeahead__container.filter .typeahead__filter {
z-index: 1001;
}
.typeahead__list, .typeahead__dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
width: 100%;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
text-align: left;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 2px;
background-clip: padding-box;
}
.typeahead__result.detached .typeahead__list {
position: relative;
z-index: 1041;
top: initial;
left: initial;
}
.typeahead__dropdown {
right: 0;
left: initial;
z-index: 1001;
}
.typeahead__list > li {
position: relative;
border-top: solid 1px #ccc;
}
.typeahead__list > li:first-child {
border-top: none;
}
.typeahead__list > li > a,
.typeahead__dropdown > li > a {
display: block;
padding: 6px 12px;
clear: both;
color: #333333;
text-decoration: none;
}
.typeahead__list > li > a:hover,
.typeahead__list > li > a:focus,
.typeahead__list > li.active > a,
.typeahead__dropdown > li > a:hover,
.typeahead__dropdown > li > a:focus,
.typeahead__dropdown > li.active > a {
background-color: #f5f5f5;
color: #3c3c3c;
}
.typeahead__list.empty > li > a {
cursor: default;
}
.typeahead__list.empty > li > a:hover,
.typeahead__list.empty > li > a:focus,
.typeahead__list.empty > li.active > a {
background-color: transparent;
}
.typeahead__list > li.typeahead__group {
border-color: #bfdef6;
font-weight: bold;
}
.typeahead__list > li.typeahead__group:first-child {
border-top: solid 1px #bfdef6;
}
.typeahead__list > li.typeahead__group > a,
.typeahead__list > li.typeahead__group > a:hover,
.typeahead__list > li.typeahead__group > a:focus,
.typeahead__list > li.typeahead__group.active > a {
cursor: default;
color: #17639f;
background: #ecf5fc;
}
.typeahead__list > li.typeahead__group + li.typeahead__item {
border-color: #bfdef6;
}
.typeahead__container.result .typeahead__list,
.typeahead__container.filter .typeahead__dropdown,
.typeahead__container.hint .typeahead__hint,
.typeahead__container.backdrop + .typeahead__backdrop {
display: block !important;
}
.typeahead__container .typeahead__list,
.typeahead__container .typeahead__dropdown,
.typeahead__container .typeahead__hint,
.typeahead__container + .typeahead__backdrop {
display: none !important;
}
.typeahead__dropdown li:last-child {
margin-top: 5px;
padding-top: 5px;
border-top: solid 1px #ccc;
}
.typeahead__cancel-button {
visibility: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border-radius: 50%;
width: 16px;
height: 16px;
position: absolute;
top: 8px;
right: .8em;
cursor: pointer;
background: url(data:image/svg+xml;charset=utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDQzOC41MzMgNDM4LjUzMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDM4LjUzMyA0MzguNTMzOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPHBhdGggZD0iTTQwOS4xMzMsMTA5LjIwM2MtMTkuNjA4LTMzLjU5Mi00Ni4yMDUtNjAuMTg5LTc5Ljc5OC03OS43OTZDMjk1LjczNiw5LjgwMSwyNTkuMDU4LDAsMjE5LjI3MywwICAgYy0zOS43ODEsMC03Ni40Nyw5LjgwMS0xMTAuMDYzLDI5LjQwN2MtMzMuNTk1LDE5LjYwNC02MC4xOTIsNDYuMjAxLTc5LjgsNzkuNzk2QzkuODAxLDE0Mi44LDAsMTc5LjQ4OSwwLDIxOS4yNjcgICBjMCwzOS43OCw5LjgwNCw3Ni40NjMsMjkuNDA3LDExMC4wNjJjMTkuNjA3LDMzLjU5Miw0Ni4yMDQsNjAuMTg5LDc5Ljc5OSw3OS43OThjMzMuNTk3LDE5LjYwNSw3MC4yODMsMjkuNDA3LDExMC4wNjMsMjkuNDA3ICAgczc2LjQ3LTkuODAyLDExMC4wNjUtMjkuNDA3YzMzLjU5My0xOS42MDIsNjAuMTg5LTQ2LjIwNiw3OS43OTUtNzkuNzk4YzE5LjYwMy0zMy41OTYsMjkuNDAzLTcwLjI4NCwyOS40MDMtMTEwLjA2MiAgIEM0MzguNTMzLDE3OS40ODUsNDI4LjczMiwxNDIuNzk1LDQwOS4xMzMsMTA5LjIwM3ogTTMyMi42MjEsMjcwLjkzOWMzLjYxNywzLjYxMyw1LjQyOCw3LjkwNSw1LjQyOCwxMi44NTQgICBjMCw1LjEzMy0xLjgxMSw5LjUxNC01LjQyOCwxMy4xMjdsLTI1LjY5MywyNS43MDFjLTMuNjE0LDMuNjEzLTcuOTk0LDUuNDItMTMuMTM1LDUuNDJjLTQuOTQ4LDAtOS4yMzYtMS44MDctMTIuODQ3LTUuNDIgICBsLTUxLjY3Ni01MS42ODJsLTUxLjY3OCw1MS42ODJjLTMuNjE2LDMuNjEzLTcuODk4LDUuNDItMTIuODQ3LDUuNDJjLTUuMTQsMC05LjUxNy0xLjgwNy0xMy4xMzQtNS40MmwtMjUuNjk3LTI1LjcwMSAgIGMtMy42MTYtMy42MTMtNS40MjQtNy45OTQtNS40MjQtMTMuMTI3YzAtNC45NDgsMS44MDktOS4yNCw1LjQyNC0xMi44NTRsNTEuNjc4LTUxLjY3M2wtNTEuNjc4LTUxLjY3OCAgIGMtMy42MTYtMy42MTItNS40MjQtNy44OTgtNS40MjQtMTIuODQ3YzAtNS4xNCwxLjgwOS05LjUxNyw1LjQyNC0xMy4xMzRsMjUuNjk3LTI1LjY5M2MzLjYxNy0zLjYxNiw3Ljk5NC01LjQyNCwxMy4xMzQtNS40MjQgICBjNC45NDksMCw5LjIzMSwxLjgwOSwxMi44NDcsNS40MjRsNTEuNjc4LDUxLjY3NGw1MS42NzYtNTEuNjc0YzMuNjEtMy42MTYsNy44OTgtNS40MjQsMTIuODQ3LTUuNDI0ICAgYzUuMTQxLDAsOS41MjEsMS44MDksMTMuMTM1LDUuNDI0bDI1LjY5MywyNS42OTNjMy42MTcsMy42MTcsNS40MjgsNy45OTQsNS40MjgsMTMuMTM0YzAsNC45NDgtMS44MTEsOS4yMzUtNS40MjgsMTIuODQ3ICAgbC01MS42NzUsNTEuNjc4TDMyMi42MjEsMjcwLjkzOXoiIGZpbGw9IiM1NTU1NTUiLz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K) no-repeat scroll center center transparent;
}
.typeahead__container.cancel:not(.loading) .typeahead__cancel-button {
visibility: visible;
opacity: .25;
}
.typeahead__container.cancel:not(.loading) .typeahead__cancel-button:hover {
opacity: .4;
}
.typeahead__search-icon {
padding: 0 1.25rem;
width: 16px;
height: 16px;
display: block;
background: url(data:image/svg+xml;charset=utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDI1MC4zMTMgMjUwLjMxMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjUwLjMxMyAyNTAuMzEzOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCI+CjxnIGlkPSJTZWFyY2giPgoJPHBhdGggc3R5bGU9ImZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkOyIgZD0iTTI0NC4xODYsMjE0LjYwNGwtNTQuMzc5LTU0LjM3OGMtMC4yODktMC4yODktMC42MjgtMC40OTEtMC45My0wLjc2ICAgYzEwLjctMTYuMjMxLDE2Ljk0NS0zNS42NiwxNi45NDUtNTYuNTU0QzIwNS44MjIsNDYuMDc1LDE1OS43NDcsMCwxMDIuOTExLDBTMCw0Ni4wNzUsMCwxMDIuOTExICAgYzAsNTYuODM1LDQ2LjA3NCwxMDIuOTExLDEwMi45MSwxMDIuOTExYzIwLjg5NSwwLDQwLjMyMy02LjI0NSw1Ni41NTQtMTYuOTQ1YzAuMjY5LDAuMzAxLDAuNDcsMC42NCwwLjc1OSwwLjkyOWw1NC4zOCw1NC4zOCAgIGM4LjE2OSw4LjE2OCwyMS40MTMsOC4xNjgsMjkuNTgzLDBDMjUyLjM1NCwyMzYuMDE3LDI1Mi4zNTQsMjIyLjc3MywyNDQuMTg2LDIxNC42MDR6IE0xMDIuOTExLDE3MC4xNDYgICBjLTM3LjEzNCwwLTY3LjIzNi0zMC4xMDItNjcuMjM2LTY3LjIzNWMwLTM3LjEzNCwzMC4xMDMtNjcuMjM2LDY3LjIzNi02Ny4yMzZjMzcuMTMyLDAsNjcuMjM1LDMwLjEwMyw2Ny4yMzUsNjcuMjM2ICAgQzE3MC4xNDYsMTQwLjA0NCwxNDAuMDQzLDE3MC4xNDYsMTAyLjkxMSwxNzAuMTQ2eiIgZmlsbD0iIzU1NTU1NSIvPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=) no-repeat scroll center center transparent;
}
.typeahead__container.loading .typeahead__query:before, .typeahead__container.loading .typeahead__query:after {
transition: all 0s linear, opacity 0.2s ease;
position: absolute;
z-index: 3;
content: '';
top: 50%;
right: .55em;
margin-top: -10.5px;
width: 21px;
height: 21px;
box-sizing: border-box;
border-radius: 500rem;
border-style: solid;
border-width: .1em;
}
.typeahead__container.loading .typeahead__query:before {
border-color: rgba(0, 0, 0, 0.35);
}
.typeahead__container.loading .typeahead__query:after {
-webkit-animation: button-spin 0.6s linear;
animation: button-spin 0.6s linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
border-color: #fff transparent transparent;
box-shadow: 0 0 0 1px transparent;
}
@-webkit-keyframes button-spin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes button-spin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,497 +0,0 @@
/*!
* jQuery UI Position v1.10.0
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/position/
*/
(function( $, undefined ) {
$.ui = $.ui || {};
var cachedScrollbarWidth,
max = Math.max,
abs = Math.abs,
round = Math.round,
rhorizontal = /left|center|right/,
rvertical = /top|center|bottom/,
roffset = /[\+\-]\d+%?/,
rposition = /^\w+/,
rpercent = /%$/,
_position = $.fn.position;
function getOffsets( offsets, width, height ) {
return [
parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
];
}
function parseCss( element, property ) {
return parseInt( $.css( element, property ), 10 ) || 0;
}
function getDimensions( elem ) {
var raw = elem[0];
if ( raw.nodeType === 9 ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: 0, left: 0 }
};
}
if ( $.isWindow( raw ) ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
};
}
if ( raw.preventDefault ) {
return {
width: 0,
height: 0,
offset: { top: raw.pageY, left: raw.pageX }
};
}
return {
width: elem.outerWidth(),
height: elem.outerHeight(),
offset: elem.offset()
};
}
$.position = {
scrollbarWidth: function() {
if ( cachedScrollbarWidth !== undefined ) {
return cachedScrollbarWidth;
}
var w1, w2,
div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
innerDiv = div.children()[0];
$( "body" ).append( div );
w1 = innerDiv.offsetWidth;
div.css( "overflow", "scroll" );
w2 = innerDiv.offsetWidth;
if ( w1 === w2 ) {
w2 = div[0].clientWidth;
}
div.remove();
return (cachedScrollbarWidth = w1 - w2);
},
getScrollInfo: function( within ) {
var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ),
overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ),
hasOverflowX = overflowX === "scroll" ||
( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
hasOverflowY = overflowY === "scroll" ||
( overflowY === "auto" && within.height < within.element[0].scrollHeight );
return {
width: hasOverflowX ? $.position.scrollbarWidth() : 0,
height: hasOverflowY ? $.position.scrollbarWidth() : 0
};
},
getWithinInfo: function( element ) {
var withinElement = $( element || window ),
isWindow = $.isWindow( withinElement[0] );
return {
element: withinElement,
isWindow: isWindow,
offset: withinElement.offset() || { left: 0, top: 0 },
scrollLeft: withinElement.scrollLeft(),
scrollTop: withinElement.scrollTop(),
width: isWindow ? withinElement.width() : withinElement.outerWidth(),
height: isWindow ? withinElement.height() : withinElement.outerHeight()
};
}
};
$.fn.position = function( options ) {
if ( !options || !options.of ) {
return _position.apply( this, arguments );
}
// make a copy, we don't want to modify arguments
options = $.extend( {}, options );
var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
target = $( options.of ),
within = $.position.getWithinInfo( options.within ),
scrollInfo = $.position.getScrollInfo( within ),
collision = ( options.collision || "flip" ).split( " " ),
offsets = {};
dimensions = getDimensions( target );
if ( target[0].preventDefault ) {
// force left top to allow flipping
options.at = "left top";
}
targetWidth = dimensions.width;
targetHeight = dimensions.height;
targetOffset = dimensions.offset;
// clone to reuse original targetOffset later
basePosition = $.extend( {}, targetOffset );
// force my and at to have valid horizontal and vertical positions
// if a value is missing or invalid, it will be converted to center
$.each( [ "my", "at" ], function() {
var pos = ( options[ this ] || "" ).split( " " ),
horizontalOffset,
verticalOffset;
if ( pos.length === 1) {
pos = rhorizontal.test( pos[ 0 ] ) ?
pos.concat( [ "center" ] ) :
rvertical.test( pos[ 0 ] ) ?
[ "center" ].concat( pos ) :
[ "center", "center" ];
}
pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
// calculate offsets
horizontalOffset = roffset.exec( pos[ 0 ] );
verticalOffset = roffset.exec( pos[ 1 ] );
offsets[ this ] = [
horizontalOffset ? horizontalOffset[ 0 ] : 0,
verticalOffset ? verticalOffset[ 0 ] : 0
];
// reduce to just the positions without the offsets
options[ this ] = [
rposition.exec( pos[ 0 ] )[ 0 ],
rposition.exec( pos[ 1 ] )[ 0 ]
];
});
// normalize collision option
if ( collision.length === 1 ) {
collision[ 1 ] = collision[ 0 ];
}
if ( options.at[ 0 ] === "right" ) {
basePosition.left += targetWidth;
} else if ( options.at[ 0 ] === "center" ) {
basePosition.left += targetWidth / 2;
}
if ( options.at[ 1 ] === "bottom" ) {
basePosition.top += targetHeight;
} else if ( options.at[ 1 ] === "center" ) {
basePosition.top += targetHeight / 2;
}
atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
basePosition.left += atOffset[ 0 ];
basePosition.top += atOffset[ 1 ];
return this.each(function() {
var collisionPosition, using,
elem = $( this ),
elemWidth = elem.outerWidth(),
elemHeight = elem.outerHeight(),
marginLeft = parseCss( this, "marginLeft" ),
marginTop = parseCss( this, "marginTop" ),
collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
position = $.extend( {}, basePosition ),
myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
if ( options.my[ 0 ] === "right" ) {
position.left -= elemWidth;
} else if ( options.my[ 0 ] === "center" ) {
position.left -= elemWidth / 2;
}
if ( options.my[ 1 ] === "bottom" ) {
position.top -= elemHeight;
} else if ( options.my[ 1 ] === "center" ) {
position.top -= elemHeight / 2;
}
position.left += myOffset[ 0 ];
position.top += myOffset[ 1 ];
// if the browser doesn't support fractions, then round for consistent results
if ( !$.support.offsetFractions ) {
position.left = round( position.left );
position.top = round( position.top );
}
collisionPosition = {
marginLeft: marginLeft,
marginTop: marginTop
};
$.each( [ "left", "top" ], function( i, dir ) {
if ( $.ui.position[ collision[ i ] ] ) {
$.ui.position[ collision[ i ] ][ dir ]( position, {
targetWidth: targetWidth,
targetHeight: targetHeight,
elemWidth: elemWidth,
elemHeight: elemHeight,
collisionPosition: collisionPosition,
collisionWidth: collisionWidth,
collisionHeight: collisionHeight,
offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
my: options.my,
at: options.at,
within: within,
elem : elem
});
}
});
if ( options.using ) {
// adds feedback as second argument to using callback, if present
using = function( props ) {
var left = targetOffset.left - position.left,
right = left + targetWidth - elemWidth,
top = targetOffset.top - position.top,
bottom = top + targetHeight - elemHeight,
feedback = {
target: {
element: target,
left: targetOffset.left,
top: targetOffset.top,
width: targetWidth,
height: targetHeight
},
element: {
element: elem,
left: position.left,
top: position.top,
width: elemWidth,
height: elemHeight
},
horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
};
if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
feedback.horizontal = "center";
}
if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
feedback.vertical = "middle";
}
if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
feedback.important = "horizontal";
} else {
feedback.important = "vertical";
}
options.using.call( this, props, feedback );
};
}
elem.offset( $.extend( position, { using: using } ) );
});
};
$.ui.position = {
fit: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
outerWidth = within.width,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = withinOffset - collisionPosLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
newOverRight;
// element is wider than within
if ( data.collisionWidth > outerWidth ) {
// element is initially over the left side of within
if ( overLeft > 0 && overRight <= 0 ) {
newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
position.left += overLeft - newOverRight;
// element is initially over right side of within
} else if ( overRight > 0 && overLeft <= 0 ) {
position.left = withinOffset;
// element is initially over both left and right sides of within
} else {
if ( overLeft > overRight ) {
position.left = withinOffset + outerWidth - data.collisionWidth;
} else {
position.left = withinOffset;
}
}
// too far left -> align with left edge
} else if ( overLeft > 0 ) {
position.left += overLeft;
// too far right -> align with right edge
} else if ( overRight > 0 ) {
position.left -= overRight;
// adjust based on position and margin
} else {
position.left = max( position.left - collisionPosLeft, position.left );
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
outerHeight = data.within.height,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = withinOffset - collisionPosTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
newOverBottom;
// element is taller than within
if ( data.collisionHeight > outerHeight ) {
// element is initially over the top of within
if ( overTop > 0 && overBottom <= 0 ) {
newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
position.top += overTop - newOverBottom;
// element is initially over bottom of within
} else if ( overBottom > 0 && overTop <= 0 ) {
position.top = withinOffset;
// element is initially over both top and bottom of within
} else {
if ( overTop > overBottom ) {
position.top = withinOffset + outerHeight - data.collisionHeight;
} else {
position.top = withinOffset;
}
}
// too far up -> align with top
} else if ( overTop > 0 ) {
position.top += overTop;
// too far down -> align with bottom edge
} else if ( overBottom > 0 ) {
position.top -= overBottom;
// adjust based on position and margin
} else {
position.top = max( position.top - collisionPosTop, position.top );
}
}
},
flip: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.offset.left + within.scrollLeft,
outerWidth = within.width,
offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = collisionPosLeft - offsetLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
myOffset = data.my[ 0 ] === "left" ?
-data.elemWidth :
data.my[ 0 ] === "right" ?
data.elemWidth :
0,
atOffset = data.at[ 0 ] === "left" ?
data.targetWidth :
data.at[ 0 ] === "right" ?
-data.targetWidth :
0,
offset = -2 * data.offset[ 0 ],
newOverRight,
newOverLeft;
if ( overLeft < 0 ) {
newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
position.left += myOffset + atOffset + offset;
}
}
else if ( overRight > 0 ) {
newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
position.left += myOffset + atOffset + offset;
}
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.offset.top + within.scrollTop,
outerHeight = within.height,
offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = collisionPosTop - offsetTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
top = data.my[ 1 ] === "top",
myOffset = top ?
-data.elemHeight :
data.my[ 1 ] === "bottom" ?
data.elemHeight :
0,
atOffset = data.at[ 1 ] === "top" ?
data.targetHeight :
data.at[ 1 ] === "bottom" ?
-data.targetHeight :
0,
offset = -2 * data.offset[ 1 ],
newOverTop,
newOverBottom;
if ( overTop < 0 ) {
newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
position.top += myOffset + atOffset + offset;
}
}
else if ( overBottom > 0 ) {
newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
position.top += myOffset + atOffset + offset;
}
}
}
},
flipfit: {
left: function() {
$.ui.position.flip.left.apply( this, arguments );
$.ui.position.fit.left.apply( this, arguments );
},
top: function() {
$.ui.position.flip.top.apply( this, arguments );
$.ui.position.fit.top.apply( this, arguments );
}
}
};
// fraction support test
(function () {
var testElement, testElementParent, testElementStyle, offsetLeft, i,
body = document.getElementsByTagName( "body" )[ 0 ],
div = document.createElement( "div" );
//Create a "fake body" for testing based on method used in jQuery.support
testElement = document.createElement( body ? "div" : "body" );
testElementStyle = {
visibility: "hidden",
width: 0,
height: 0,
border: 0,
margin: 0,
background: "none"
};
if ( body ) {
$.extend( testElementStyle, {
position: "absolute",
left: "-1000px",
top: "-1000px"
});
}
for ( i in testElementStyle ) {
testElement.style[ i ] = testElementStyle[ i ];
}
testElement.appendChild( div );
testElementParent = body || document.documentElement;
testElementParent.insertBefore( testElement, testElementParent.firstChild );
div.style.cssText = "position: absolute; left: 10.7432222px;";
offsetLeft = $( div ).offset().left;
$.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
testElement.innerHTML = "";
testElementParent.removeChild( testElement );
})();
}( jQuery ) );

View File

@@ -3,7 +3,7 @@ const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: 50,
slowMo: 55,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"]

View File

@@ -35,6 +35,7 @@
@NotificationLow: #FFF4CE;
@NotificationHigh: #F9E9B0;
@Purple1: #8A2DA5;
@Dirty: #9b4f96;
@BaseLow: #F2F2F2;
@BaseMediumLow: #E6E6E6;
@@ -104,6 +105,7 @@
@newCollectionPaneInputWidth: 300px;
@tooltipTextWidth: 280px;
@sharedCollectionThroughputTooltipTextWidth: 150px;
@mongoWildcardIndexTooltipWidth: 150px;
@addContainerPaneThroughputInfoWidth: 370px;
@optionsInfoWidth: 210px;
@noFixedCollectionsTooltipWidth: 196px;

View File

@@ -1565,6 +1565,10 @@ p {
min-width: @tooltipTextWidth;
}
.mongoWildcardIndexTooltipWidth {
min-width: @mongoWildcardIndexTooltipWidth;
}
.sharedCollectionThroughputTooltipWidth {
min-width: @sharedCollectionThroughputTooltipTextWidth;
}
@@ -2078,7 +2082,8 @@ a:link {
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
height: 100%;
}

16463
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,26 +6,26 @@
"dependencies": {
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",
"@microsoft/applicationinsights-web": "2.5.8",
"@nteract/commutable": "7.1.4",
"@nteract/connected-components": "6.7.8",
"@nteract/core": "13.0.0",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "9.6.6",
"@nteract/editor": "10.1.2",
"@nteract/fixtures": "2.3.0",
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0",
"@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.0.3",
"@nteract/monaco-editor": "3.2.0",
"@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7",
"@nteract/stateful-components": "1.4.0",
"@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8",
"@nteract/transform-model-debug": "5.0.1",
@@ -47,6 +47,7 @@
"copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
"datatables.net-colreorder-dt": "1.5.1",
"datatables.net-dt": "1.10.19",
"date-fns": "1.29.0",
@@ -55,6 +56,7 @@
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0",
"hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5",
"immutable": "4.0.0-rc.12",
@@ -72,7 +74,7 @@
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
"q": "1.5.1",
"react": "16.9.0",
"react": "16.13.1",
"react-animate-height": "2.0.8",
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "9.4.0",
@@ -81,12 +83,13 @@
"react-notification-system": "0.2.17",
"react-redux": "7.1.3",
"redux": "4.0.4",
"rx-jupyter": "5.5.2",
"rxjs": "6.5.3",
"rx-jupyter": "5.5.12",
"rxjs": "6.6.3",
"styled-components": "4.3.2",
"text-encoding": "0.7.0",
"underscore": "1.9.1",
"url-polyfill": "1.1.7",
"utility-types": "3.10.0",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"
@@ -99,9 +102,9 @@
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30",
"@types/d3": "4.13.2",
"@types/enzyme": "3.10.3",
"@types/enzyme-adapter-react-16": "1.0.5",
"@types/d3": "5.9.2",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/expect-puppeteer": "4.4.3",
"@types/hasher": "0.0.31",
"@types/jest": "23.3.10",
@@ -112,7 +115,7 @@
"@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1",
"@types/q": "1.5.1",
"@types/react": "16.8.25",
"@types/react": "16.9.49",
"@types/react-dom": "16.0.7",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
@@ -131,15 +134,13 @@
"case-sensitive-paths-webpack-plugin": "2.3.0",
"create-file-webpack": "1.0.2",
"css-loader": "1.0.0",
"d3": "4.13.0",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5",
"enzyme-to-json": "3.6.1",
"eslint": "7.8.1",
"eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react": "7.20.0",
"expose-loader": "0.7.5",
"file-loader": "2.0.0",
"fs-extra": "7.0.0",
@@ -156,7 +157,7 @@
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.0",
"node-fetch": "2.6.1",
"prettier": "1.19.1",
"puppeteer": "4.0.0",
"raw-loader": "0.5.1",
@@ -193,8 +194,8 @@
"compile": "tsc",
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
"compile:strict": "tsc -p ./tsconfig.strict.json",
"format": "prettier --write \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js",

View File

@@ -1,5 +1,4 @@
import { AutopilotTier } from "../Contracts/DataModels";
import { configContext } from "../ConfigContext";
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
@@ -13,12 +12,6 @@ export class CodeOfConductEndpoints {
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class BackendEndpoints {
public static localhost: string = "https://localhost:12900";
public static dev: string = "https://ext.documents-dev.windows-int.net";
public static productionPortal: string = configContext.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
}
export class EndpointsRegex {
public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
@@ -137,6 +130,11 @@ export class Features {
public static readonly enableSDKoperations = "enablesdkoperations";
}
// flight names returned from the portal are always lowercase
export class Flights {
public static readonly SettingsV2 = "settingsv2";
}
export class AfecFeatures {
public static readonly Spark = "spark-public-preview";
public static readonly Notebooks = "sparknotebooks-public-preview";

View File

@@ -14,7 +14,9 @@ describe("tokenProvider", () => {
};
beforeEach(() => {
window.dataExplorer = { extensionEndpoint: () => "https://main.documentdb.ext.azure.com" } as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(() => {
return {
json: () => "{}",
@@ -58,7 +60,9 @@ describe("getTokenFromAuthService", () => {
});
it("builds the correct URL in production", () => {
window.dataExplorer = { extensionEndpoint: () => "https://main.documentdb.ext.azure.com" } as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
getTokenFromAuthService("GET", "dbs", "foo");
expect(window.fetch).toHaveBeenCalledWith(
"https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",

View File

@@ -52,7 +52,7 @@ export const endpoint = () => {
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
try {
const host = configContext.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
const host = configContext.BACKEND_ENDPOINT;
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
method: "POST",
headers: {
@@ -85,8 +85,7 @@ export function client(): Cosmos.CosmosClient {
userAgentSuffix: "Azure Portal"
};
// In development we proxy requests to the backend via webpack. This is removed in production bundles.
if (process.env.NODE_ENV === "development") {
if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
return new Cosmos.CosmosClient(options);

View File

@@ -72,22 +72,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
return [partitionKeyValue];
}
export function updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options?: RequestOptions
): Q.Promise<DataModels.Offer> {
return Q(
client()
.offer(offer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options)
.then(response => {
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
})
);
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
@@ -184,86 +168,6 @@ export function deleteConflict(
);
}
export function readCollectionQuotaInfo(
collection: ViewModels.Collection,
options: any
): Q.Promise<DataModels.CollectionQuotaInfo> {
options = options || {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.read(options)
// TODO any needed because SDK does not properly type response.resource.statistics
.then((response: any) => {
let quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
quota["usageSizeInKB"] = response.resource.statistics.reduce(
(
previousValue: number,
currentValue: DataModels.Statistic,
currentIndex: number,
array: DataModels.Statistic[]
) => {
return previousValue + currentValue.sizeInKB;
},
0
);
quota["numPartitions"] = response.resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
})
);
}
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
if (options.isServerless) {
return Q([]); // Reading offers is not supported for serverless accounts
}
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
(<any>window).dataExplorer.databaseAccount().id,
Constants.ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
return Q(
client()
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
})
);
}
export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
options = options || {};
options.initialHeaders = options.initialHeaders || {};
if (!OfferUtils.isOfferV1(requestedResource)) {
options.initialHeaders[Constants.HttpHeaders.populateCollectionThroughputInfo] = true;
}
return Q(
client()
.offer(requestedResource.id)
.read(options)
.then(response => ({ ...response.resource, headers: response.headers }))
);
}
export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);

View File

@@ -157,41 +157,6 @@ export function updateDocument(
return deferred.promise;
}
export function updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options: RequestOptions
): Q.Promise<DataModels.Offer> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
.then(
(replacedOffer: DataModels.Offer) => {
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
deferred.resolve(replacedOffer);
},
(error: any) => {
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
Logger.logError(
JSON.stringify({
oldOffer: offer,
newOffer: newOffer,
error: error
}),
"UpdateOffer",
error.code
);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
@@ -277,78 +242,3 @@ export function refreshCachedResources(options: any = {}): Q.Promise<void> {
export function refreshCachedOffers(): Q.Promise<void> {
return DataAccessUtilityBase.refreshCachedOffers();
}
export function readCollectionQuotaInfo(
collection: ViewModels.Collection,
options?: any
): Q.Promise<DataModels.CollectionQuotaInfo> {
var deferred = Q.defer<DataModels.CollectionQuotaInfo>();
const clearMessage = logConsoleProgress(`Querying quota info for container ${collection.id}`);
DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
.then(
(quota: DataModels.CollectionQuotaInfo) => {
deferred.resolve(quota);
},
(error: any) => {
logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
var deferred = Q.defer<DataModels.Offer[]>();
const clearMessage = logConsoleProgress("Querying offers");
DataAccessUtilityBase.readOffers(options)
.then(
(offers: DataModels.Offer[]) => {
deferred.resolve(offers);
},
(error: any) => {
logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function readOffer(
requestedResource: DataModels.Offer,
options: any = {}
): Q.Promise<DataModels.OfferWithHeaders> {
var deferred = Q.defer<DataModels.OfferWithHeaders>();
const clearMessage = logConsoleProgress("Querying offer");
DataAccessUtilityBase.readOffer(requestedResource, options)
.then(
(offer: DataModels.OfferWithHeaders) => {
deferred.resolve(offer);
},
(error: any) => {
logConsoleError(`Error while querying offer:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}

View File

@@ -63,10 +63,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
afterEach(() => {
@@ -96,10 +95,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
afterEach(() => {
@@ -129,10 +127,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
afterEach(() => {
@@ -162,10 +159,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
afterEach(() => {
@@ -195,10 +191,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
afterEach(() => {
@@ -229,10 +224,9 @@ describe("MongoProxyClient", () => {
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
} as any;
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
});
});
it("returns a production endpoint", () => {

View File

@@ -327,8 +327,7 @@ export function createMongoCollectionWithProxy(
}
export function getEndpoint(): string {
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
let url = (configContext.MONGO_BACKEND_ENDPOINT || extensionEndpoint) + "/api/mongo/explorer";
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
if (window.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,46 +0,0 @@
import "jquery";
import * as Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { userContext } from "../UserContext";
export class NotificationsClientBase {
private _extensionEndpoint: string;
private _notificationsApiSuffix: string;
protected constructor(notificationsApiSuffix: string) {
this._notificationsApiSuffix = notificationsApiSuffix;
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token;
$.ajax({
url: url,
type: "GET",
headers: headers,
cache: false
}).then(
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve(notifications);
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise;
}
public setExtensionEndpoint(extensionEndpoint: string): void {
this._extensionEndpoint = extensionEndpoint;
}
}

View File

@@ -0,0 +1,41 @@
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { userContext } from "../UserContext";
import { configContext, Platform } from "../ConfigContext";
const notificationsPath = () => {
switch (configContext.platform) {
case Platform.Hosted:
return "/api/guest/notifications";
case Platform.Portal:
return "/api/notifications";
default:
throw new Error(`Unknown platform: ${configContext.platform}`);
}
};
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
if (configContext.platform === Platform.Emulator) {
return [];
}
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
databaseAccount.name
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const response = await window.fetch(url, {
headers
});
if (!response.ok) {
throw new Error(await response.text());
}
return (await response.json()) as DataModels.Notification[];
};

View File

@@ -29,6 +29,8 @@ import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { createDatabase } from "./createDatabase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
let collection: DataModels.Collection;
@@ -138,6 +140,7 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
};
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
try {
const getResponse = await getMongoDBCollection(
userContext.subscriptionId,
@@ -166,6 +169,9 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
const partitionKeyPath: string = params.partitionKey.paths[0];
resource.shardKey = { [partitionKeyPath]: "Hash" };
}
if (params.createMongoWildcardIndex) {
resource.indexes = mongoWildcardIndexOnAllFields;
}
const rpPayload: ARMTypes.MongoDBCollectionCreateUpdateParameters = {
properties: {
@@ -182,6 +188,13 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
params.collectionId,
rpPayload
);
if (params.createMongoWildcardIndex) {
TelemetryProcessor.trace(Action.CreateMongoCollectionWithWildcardIndex, ActionModifiers.Mark, {
message: "Mongo Collection created with wildcard index on all fields."
});
}
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};

View File

@@ -34,11 +34,10 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
let database: DataModels.Database;
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
throw new Error("Creating database resources is not allowed for tables accounts");
}
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
database = await createDatabaseWithARM(params);
} else {
database = await createDatabaseWithSDK(params);

View File

@@ -15,11 +15,10 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
!userContext.useSDKOperations
) {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
throw new Error("Deleting database resources is not allowed for tables accounts");
}
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteDatabaseWithARM(databaseId);
} else {
await client()

View File

@@ -0,0 +1,126 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { readOffers } from "./readOffers";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export const readCollectionOffer = async (
params: DataModels.ReadCollectionOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
let offerId = params.offerId;
if (!offerId) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
} catch (error) {
clearMessage();
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
} else {
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
if (!offerId) {
clearMessage();
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try {
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
} catch (error) {
logConsoleError(`Error while querying offer for collection ${params.collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollectionOffer", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
};
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === collectionResourceId);
return offer?.id;
};

View File

@@ -0,0 +1,48 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -8,32 +8,26 @@ import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { readOffers } from "./readOffers";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
let offerId = params.offerId;
if (!offerId) {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
try {
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
} catch (error) {
if (error.code !== "NotFound") {
throw new Error(error);
}
return undefined;
}
} else {
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId);
if (!offerId) {
return undefined;
}
offerId = await (window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
? getDatabaseOfferIdWithARM(params.databaseId)
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
if (!offerId) {
clearMessage();
return undefined;
}
}
@@ -43,15 +37,24 @@ export const readDatabaseOffer = async (
}
};
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
try {
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
} catch (error) {
logConsoleError(`Error while querying offer for database ${params.databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadDatabaseOffer", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
@@ -60,24 +63,32 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
try {
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
};
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {

View File

@@ -12,16 +12,14 @@ import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readDatabases(): Promise<DataModels.Database[]> {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
return [{ id: "TablesDB" } as DataModels.Database];
}
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
databases = await readDatabasesWithARM();
} else {
const sdkResponse = await client()

View File

@@ -3,10 +3,14 @@ import { ClientDefaults } from "../Constants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { Platform, configContext } from "../../ConfigContext";
import { client } from "../CosmosClient";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendCachedDataMessage } from "../MessageHandler";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
@@ -18,15 +22,22 @@ export const readOffers = async (): Promise<Offer[]> => {
// If error getting cached Offers, continue on and read via SDK
}
return client()
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
});
try {
const response = await client()
.offers.readAll()
.fetchAll();
return response?.resources;
} catch (error) {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -41,6 +41,7 @@ export async function updateCollection(
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
@@ -52,16 +53,18 @@ export async function updateCollection(
.replace(newCollection as ContainerDefinition, options);
collection = sdkResponse.resource as Collection;
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
await refreshCachedResources();
return collection;
} catch (error) {
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
clearMessage();
await refreshCachedResources();
return collection;
}
async function updateCollectionWithARM(

View File

@@ -0,0 +1,421 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
import { OfferDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { readCollectionOffer } from "./readCollectionOffer";
import { readDatabaseOffer } from "./readDatabaseOffer";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import {
updateSqlDatabaseThroughput,
migrateSqlDatabaseToAutoscale,
migrateSqlDatabaseToManualThroughput,
migrateSqlContainerToAutoscale,
migrateSqlContainerToManualThroughput,
updateSqlContainerThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
updateCassandraKeyspaceThroughput,
migrateCassandraKeyspaceToAutoscale,
migrateCassandraKeyspaceToManualThroughput,
migrateCassandraTableToAutoscale,
migrateCassandraTableToManualThroughput,
updateCassandraTableThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
updateMongoDBDatabaseThroughput,
migrateMongoDBDatabaseToAutoscale,
migrateMongoDBDatabaseToManualThroughput,
migrateMongoDBCollectionToAutoscale,
migrateMongoDBCollectionToManualThroughput,
updateMongoDBCollectionThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
updateGremlinDatabaseThroughput,
migrateGremlinDatabaseToAutoscale,
migrateGremlinDatabaseToManualThroughput,
migrateGremlinGraphToAutoscale,
migrateGremlinGraphToManualThroughput,
updateGremlinGraphThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { userContext } from "../../UserContext";
import {
migrateTableToAutoscale,
migrateTableToManualThroughput,
updateTableThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> => {
let updatedOffer: Offer;
const offerResourceText: string = params.collectionId
? `collection ${params.collectionId}`
: `database ${params.databaseId}`;
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
// update table's database offer with SDK since RP doesn't support it
updatedOffer = await updateOfferWithSDK(params);
} else {
updatedOffer = await updateDatabaseOfferWithARM(params);
}
} else {
updatedOffer = await updateOfferWithSDK(params);
}
await refreshCachedOffers();
await refreshCachedResources();
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
return updatedOffer;
} catch (error) {
logConsoleError(`Error updating offer for ${offerResourceText}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};
const updateCollectionOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
try {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
await updateSqlContainerOffer(params);
break;
case DefaultAccountExperienceType.MongoDB:
await updateMongoCollectionOffer(params);
break;
case DefaultAccountExperienceType.Cassandra:
await updateCassandraTableOffer(params);
break;
case DefaultAccountExperienceType.Graph:
await updateGremlinGraphOffer(params);
break;
case DefaultAccountExperienceType.Table:
await updateTableOffer(params);
break;
default:
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
}
} catch (error) {
if (error.code !== "MethodNotAllowed") {
throw error;
}
}
return await readCollectionOffer({
collectionId: params.collectionId,
databaseId: params.databaseId,
offerId: params.currentOffer.id
});
};
const updateDatabaseOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
try {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
await updateSqlDatabaseOffer(params);
break;
case DefaultAccountExperienceType.MongoDB:
await updateMongoDatabaseOffer(params);
break;
case DefaultAccountExperienceType.Cassandra:
await updateCassandraKeyspaceOffer(params);
break;
case DefaultAccountExperienceType.Graph:
await updateGremlinDatabaseOffer(params);
break;
default:
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
}
} catch (error) {
if (error.code !== "MethodNotAllowed") {
throw error;
}
}
return await readDatabaseOffer({
databaseId: params.databaseId,
offerId: params.currentOffer.id
});
};
const updateSqlContainerOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateSqlContainerToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateSqlContainerToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateMongoCollectionOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateMongoDBCollectionToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateMongoDBCollectionToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateCassandraTableOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateCassandraTableToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateCassandraTableToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateGremlinGraphOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateGremlinGraphToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateGremlinGraphToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateTableOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateTableToAutoscale(subscriptionId, resourceGroup, accountName, params.collectionId);
} else if (params.migrateToManual) {
await migrateTableToManualThroughput(subscriptionId, resourceGroup, accountName, params.collectionId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateTableThroughput(subscriptionId, resourceGroup, accountName, params.collectionId, body);
}
};
const updateSqlDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateSqlDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateSqlDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateMongoDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateMongoDBDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateMongoDBDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateCassandraKeyspaceOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateCassandraKeyspaceToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateCassandraKeyspaceToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateGremlinDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateGremlinDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateGremlinDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpdateParameters => {
const body: ThroughputSettingsUpdateParameters = {
properties: {
resource: {}
}
};
if (params.autopilotThroughput) {
body.properties.resource.autoscaleSettings = {
maxThroughput: params.autopilotThroughput
};
} else {
body.properties.resource.throughput = params.manualThroughput;
}
return body;
};
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
const currentOffer = params.currentOffer;
const newOffer: Offer = {
content: {
offerThroughput: undefined,
offerIsRUPerMinuteThroughputEnabled: false
},
_etag: undefined,
_ts: undefined,
_rid: currentOffer._rid,
_self: currentOffer._self,
id: currentOffer.id,
offerResourceId: currentOffer.offerResourceId,
offerVersion: currentOffer.offerVersion,
offerType: currentOffer.offerType,
resource: currentOffer.resource
};
if (params.autopilotThroughput) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: params.autopilotThroughput
};
} else {
newOffer.content.offerThroughput = params.manualThroughput;
}
const options: RequestOptions = {};
if (params.migrateToAutoPilot) {
options.initialHeaders[HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else if (params.migrateToManual) {
options.initialHeaders[HttpHeaders.migrateOfferToManualThroughput] = "true";
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
}
const sdkResponse = await client()
.offer(params.currentOffer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options);
return sdkResponse?.resource;
};

View File

@@ -9,8 +9,7 @@ describe("updateOfferThroughputBeyondLimit", () => {
});
window.dataExplorer = {
logConsoleData: jest.fn(),
deleteInProgressConsoleDataWithId: jest.fn(),
extensionEndpoint: jest.fn()
deleteInProgressConsoleDataWithId: jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await updateOfferThroughputBeyondLimit({

View File

@@ -28,8 +28,7 @@ export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThrou
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
const explorer = window.dataExplorer;
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const authorizationHeader = getAuthorizationHeader();
const response = await fetch(url, {

View File

@@ -25,16 +25,19 @@ interface ConfigContext {
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
hostedExplorerURL: string;
armAPIVersion?: string;
}
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.portal.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.ext.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+microsoftazure.de$`
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
],
// Webpack injects this at build time
gitSha: process.env.GIT_SHA,
@@ -48,7 +51,8 @@ let configContext: Readonly<ConfigContext> = {
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
};
export function resetConfigContext(): void {
@@ -92,6 +96,10 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}
// Allow override of platform value with URL query parameter
const params = new URLSearchParams(window.location.search);
if (params.has("armAPIVersion")) {
const armAPIVersion = params.get("armAPIVersion") || "";
updateConfigContext({ armAPIVersion });
}
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {

View File

@@ -287,6 +287,7 @@ export interface CreateCollectionParams {
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean;
}
export interface ReadDatabaseOfferParams {
@@ -295,6 +296,23 @@ export interface ReadDatabaseOfferParams {
offerId?: string;
}
export interface ReadCollectionOfferParams {
collectionId: string;
databaseId: string;
collectionResourceId?: string;
offerId?: string;
}
export interface UpdateOfferParams {
currentOffer: Offer;
databaseId: string;
autopilotThroughput: number;
manualThroughput: number;
collectionId?: string;
migrateToAutoPilot?: boolean;
migrateToManual?: boolean;
}
export interface Notification {
id: string;
kind: string;

View File

@@ -85,7 +85,7 @@ export interface Database extends TreeNode {
collapseDatabase(): void;
loadCollections(): Promise<void>;
findCollectionWithId(collectionRid: string): Collection;
findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void;
@@ -133,8 +133,7 @@ export interface Collection extends CollectionBase {
onMongoDBDocumentsClick(): void;
openTab(): void;
onSettingsClick: () => void;
readSettings(): Q.Promise<void>;
onSettingsClick: () => Promise<void>;
onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void;
onNewGraphClick(): void;
@@ -162,6 +161,7 @@ export interface Collection extends CollectionBase {
loadUserDefinedFunctions(): Promise<any>;
loadStoredProcedures(): Promise<any>;
loadTriggers(): Promise<any>;
loadOffer(): Promise<void>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
@@ -266,7 +266,6 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
selfLink: string;
isActive: ko.Observable<boolean>;
hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
@@ -305,14 +304,9 @@ export interface QueryTabOptions extends TabOptions {
export interface ScriptTabOption extends TabOptions {
resource: any;
isNew: boolean;
collectionSelfLink?: string;
partitionKey?: DataModels.PartitionKey;
}
export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>;
}
export interface EditorPosition {
line: number;
column: number;
@@ -359,7 +353,8 @@ export enum CollectionTabKind {
NotebookV2 = 15,
SparkMasterTab = 16,
Gallery = 17,
NotebookViewer = 18
NotebookViewer = 18,
SettingsV2 = 19
}
export enum TerminalKind {
@@ -391,6 +386,7 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string;
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[];
}
export interface CollectionCreationDefaults {

View File

@@ -1,42 +0,0 @@
// Type definitions for jQuery contextMenu 1.7.0
// Project: http://medialize.github.com/jQuery-contextMenu/
// Definitions by: Natan Vivo <https://github.com/nvivo/>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference path="jquery.d.ts" />
interface JQueryContextMenuOptions {
selector: string;
appendTo?: string;
trigger?: string;
autoHide?: boolean;
delay?: number;
determinePosition?: (menu: JQuery) => void;
position?: (opt: JQuery, x: number, y: number) => void;
positionSubmenu?: (menu: JQuery) => void;
zIndex?: number;
animation?: {
duration?: number;
show?: string;
hide?: string;
};
events?: {
show?: () => void;
hide?: () => void;
};
callback?: (key: any, options: any) => any;
items?: any;
build?: (triggerElement: JQuery, e: Event) => any;
reposition?: boolean;
className?: string;
itemClickEvent?: string;
}
interface JQueryStatic {
contextMenu(options?: JQueryContextMenuOptions): JQuery;
contextMenu(type: string, selector?: any): JQuery;
}
interface JQuery {
contextMenu(options?: any): JQuery;
}

View File

@@ -48,6 +48,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("settings-tab")).toBe(true);
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
});
it("should register query-tab component", () => {
expect(ko.components.isRegistered("query-tab")).toBe(true);
});

View File

@@ -34,6 +34,7 @@ ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedure
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("settings-tab", new TabComponents.SettingsTab());
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("graph-tab", new TabComponents.GraphTab());

View File

@@ -16,6 +16,8 @@ import Explorer from "./Explorer";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import { userContext } from "../UserContext";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@@ -29,23 +31,24 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS)
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(
container: Explorer,
selectedDatabase: ViewModels.Database
): TreeNodeMenuItem[] {
const newCollectionMenuItem: TreeNodeMenuItem = {
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(),
label: container.addCollectionText()
};
public static createDatabaseContextMenu(container: Explorer): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(),
label: container.addCollectionText()
}
];
const deleteDatabaseMenuItem = {
iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem"
};
return [newCollectionMenuItem, deleteDatabaseMenuItem];
if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem"
});
}
return items;
}
public static createCollectionContextMenuButton(

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { FontIcon } from "office-ui-fabric-react";
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps {
label: string;
@@ -24,6 +24,7 @@ export interface DialogProps {
subText: string;
isModal: boolean;
visible: boolean;
choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps;
linkProps?: LinkProps;
primaryButtonText: string;
@@ -65,6 +66,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH
};
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps;
const primaryButtonProps: IButtonProps = {
@@ -82,6 +84,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
return (
<Dialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
{textFieldProps && <TextField {...textFieldProps} />}
{linkProps && (
<Link href={linkProps.linkUrl} target="_blank">

View File

@@ -26,7 +26,7 @@ exports[`GalleryCardComponent renders 1`] = `
/>
</CardItem>
<CardItem>
<Memo(StyledImageBase)
<StyledImageBase
alt="name cover image"
height={144}
imageFit={2}
@@ -95,7 +95,7 @@ exports[`GalleryCardComponent renders 1`] = `
}
variant="tiny"
>
<Memo(StyledIconBase)
<StyledIconBase
iconName="RedEye"
styles={
Object {
@@ -119,7 +119,7 @@ exports[`GalleryCardComponent renders 1`] = `
}
variant="tiny"
>
<Memo(StyledIconBase)
<StyledIconBase
iconName="Download"
styles={
Object {
@@ -143,7 +143,7 @@ exports[`GalleryCardComponent renders 1`] = `
}
variant="tiny"
>
<Memo(StyledIconBase)
<StyledIconBase
iconName="Heart"
styles={
Object {

View File

@@ -1,31 +1,25 @@
jest.mock("../../../Juno/JunoClient");
import { shallow } from "enzyme";
import * as sinon from "sinon";
import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes } from "../../../Common/Constants";
describe("CodeOfConductComponent", () => {
let sandbox: sinon.SinonSandbox;
let codeOfConductProps: CodeOfConductComponentProps;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
const junoClient = new JunoClient(undefined);
junoClient.acceptCodeOfConduct = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
data: true
} as IJunoResponse<boolean>);
const junoClient = new JunoClient(undefined);
});
codeOfConductProps = {
junoClient: junoClient,
onAcceptCodeOfConduct: jest.fn()
};
});
afterEach(() => {
sandbox.restore();
});
it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot();

View File

@@ -227,7 +227,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item>
{this.props.container?.isGalleryPublishEnabled() && (
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
<Stack.Item>
<InfoComponent />
</Stack.Item>

View File

@@ -3,10 +3,14 @@ import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fa
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
export class InfoComponent extends React.Component {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
export interface InfoComponentProps {
onReportAbuseClick?: () => void;
}
export class InfoComponent extends React.Component<InfoComponentProps> {
private getInfoPanel = (iconName: string, labelText: string, url?: string, onClick?: () => void): JSX.Element => {
return (
<Link href={url} target="_blank">
<Link href={url} target={url && "_blank"} onClick={onClick}>
<div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label>
@@ -25,6 +29,11 @@ export class InfoComponent extends React.Component {
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
{this.props.onReportAbuseClick !== undefined && (
<Stack.Item>
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
</Stack.Item>
)}
</Stack>
);
};

View File

@@ -13,7 +13,7 @@ exports[`InfoComponent renders 1`] = `
<div
className="infoPanelMain"
>
<Memo(StyledIconBase)
<StyledIconBase
className="infoIconMain"
iconName="Help"
styles={

View File

@@ -77,6 +77,9 @@ exports[`GalleryViewerComponent renders 1`] = `
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
</Stack>
</PivotItem>

View File

@@ -25,7 +25,8 @@ describe("NotebookMetadataComponent", () => {
onTagClick: undefined,
onDownloadClick: undefined,
onFavoriteClick: undefined,
onUnfavoriteClick: undefined
onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
@@ -54,7 +55,8 @@ describe("NotebookMetadataComponent", () => {
onTagClick: undefined,
onDownloadClick: undefined,
onFavoriteClick: undefined,
onUnfavoriteClick: undefined
onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);

View File

@@ -17,6 +17,7 @@ import { IGalleryItem } from "../../../Juno/JunoClient";
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
export interface NotebookMetadataComponentProps {
data: IGalleryItem;
@@ -26,6 +27,7 @@ export interface NotebookMetadataComponentProps {
onFavoriteClick: () => void;
onUnfavoriteClick: () => void;
onDownloadClick: () => void;
onReportAbuseClick: () => void;
}
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
@@ -41,24 +43,39 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
return (
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
<Text variant="xxLarge" nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
</Text>
<Text>
{this.props.isFavorite !== undefined && (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
)}
</Text>
<Stack.Item>
<Text variant="xxLarge" nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.props.isFavorite !== undefined && (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
)}
</Text>
</Stack.Item>
{this.props.downloadButtonText && (
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
<Stack.Item>
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
</Stack.Item>
)}
<Stack.Item grow>
<></>
</Stack.Item>
<Stack.Item>
<InfoComponent onReportAbuseClick={this.props.onReportAbuseClick} />
</Stack.Item>
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>

View File

@@ -3,11 +3,10 @@
*/
import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core";
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react";
import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
@@ -15,12 +14,13 @@ import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationCon
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
export interface NotebookViewerComponentProps {
container?: Explorer;
@@ -43,10 +43,8 @@ interface NotebookViewerComponentState {
showProgressBar: boolean;
}
export class NotebookViewerComponent extends React.Component<
NotebookViewerComponentProps,
NotebookViewerComponentState
> {
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
implements DialogHost {
private clientManager: NotebookClientV2;
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
@@ -58,7 +56,7 @@ export class NotebookViewerComponent extends React.Component<
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
cellEditorType: "codemirror",
cellEditorType: "monaco",
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
contentProvider: contents.JupyterContentProvider // NotebookViewer only knows how to talk to Jupyter contents API
});
@@ -140,6 +138,7 @@ export class NotebookViewerComponent extends React.Component<
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}
onDownloadClick={this.downloadItem}
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
/>
</div>
) : (
@@ -179,6 +178,39 @@ export class NotebookViewerComponent extends React.Component<
};
}
// DialogHost
showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
): void {
this.setState({
dialogProps: {
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onOk && onOk();
},
onSecondaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps
}
});
}
private favoriteItem = async (): Promise<void> => {
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
this.setState({ galleryItem: item, isFavorite: true })
@@ -196,4 +228,8 @@ export class NotebookViewerComponent extends React.Component<
this.setState({ galleryItem: item })
);
};
private reportAbuse = (): void => {
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
};
}

View File

@@ -17,26 +17,38 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
}
verticalAlign="center"
>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "HeartFill",
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "HeartFill",
}
}
}
/>
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
/>
0
likes
</Text>
<CustomizedPrimaryButton
text="Download"
</StackItem>
<StackItem
grow={true}
/>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<Stack
horizontal={true}
@@ -56,14 +68,14 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
Invalid Date
</Text>
<Text>
<Memo(StyledIconBase)
<StyledIconBase
iconName="RedEye"
/>
0
</Text>
<Text>
<Memo(StyledIconBase)
<StyledIconBase
iconName="Download"
/>
0
@@ -117,26 +129,38 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
}
verticalAlign="center"
>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Heart",
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Heart",
}
}
}
/>
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
/>
0
likes
</Text>
<CustomizedPrimaryButton
text="Download"
</StackItem>
<StackItem
grow={true}
/>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<Stack
horizontal={true}
@@ -156,14 +180,14 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
Invalid Date
</Text>
<Text>
<Memo(StyledIconBase)
<StyledIconBase
iconName="RedEye"
/>
0
</Text>
<Text>
<Memo(StyledIconBase)
<StyledIconBase
iconName="Download"
/>
0

View File

@@ -0,0 +1,31 @@
@import "../../../../less/Common/Constants";
.settingsV2MainContainer {
height: 100%;
overflow-y: auto;
width: 100%;
}
.settingsV2ToolTip {
padding: 10px;
font: 12px @DataExplorerFont;
max-width: 300px;
}
.autoPilotSelector span {
height: 25px;
font: 14px @DataExplorerFont;
}
.settingsV2TabsContainer {
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
height: 100%;
overflow-y: auto;
width: 100%;
font-family: @DataExplorerFont;
}
.settingsV2IndexingPolicyEditor {
width: 100%;
height: 60vh;
}

View File

@@ -0,0 +1,255 @@
import { shallow } from "enzyme";
import React from "react";
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import SettingsTabV2 from "../../Tabs/SettingsTabV2";
import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils";
import Explorer from "../../Explorer";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
updateCollection: jest.fn().mockReturnValue({
id: undefined,
defaultTtl: undefined,
indexingPolicy: undefined,
conflictResolutionPolicy: undefined,
changeFeedPolicy: undefined,
analyticalStorageTtl: undefined,
geospatialConfig: undefined
} as DataModels.Collection)
}));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
}));
describe("SettingsComponent", () => {
const baseProps: SettingsComponentProps = {
settingsTab: new SettingsTabV2({
collection: collection,
tabKind: ViewModels.CollectionTabKind.SettingsV2,
title: "Scale & Settings",
tabPath: "",
node: undefined,
hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined
})
};
it("renders", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
});
it("dirty value enables save button and discard button", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(false);
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(false);
wrapper.setState({ isScaleSaveable: true, isScaleDiscardable: true });
wrapper.update();
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
wrapper.setState({
isScaleSaveable: false,
isScaleDiscardable: false,
isSubSettingsSaveable: true,
isSubSettingsDiscardable: true
});
wrapper.update();
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
wrapper.setState({ isSubSettingsSaveable: false, isSubSettingsDiscardable: false, isIndexingPolicyDirty: true });
wrapper.update();
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
wrapper.setState({ isIndexingPolicyDirty: false, isConflictResolutionDirty: true });
wrapper.update();
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
});
it("auto pilot helper functions pass on correct value", () => {
const newCollection = { ...collection };
newCollection.offer = ko.observable<DataModels.Offer>({
content: {
offerAutopilotSettings: {
maxThroughput: 10000
}
}
} as DataModels.Offer);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
const wrapper = shallow(<SettingsComponent {...props} />);
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(false);
wrapper.setState({
userCanChangeProvisioningTypes: true,
isAutoPilotSelected: true,
wasAutopilotOriginallySet: false,
autoPilotThroughput: 1000
});
wrapper.update();
expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(true);
});
it("shouldShowKeyspaceSharedThroughputMessage", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true);
const newCollection = { ...collection };
newCollection.container = newContainer;
const newDatabase = {
nodeKind: undefined,
rid: undefined,
container: newContainer,
self: undefined,
id: undefined,
collections: undefined,
offer: undefined,
isDatabaseExpanded: undefined,
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: undefined,
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,
findCollectionWithId: undefined,
openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined,
onSettingsClick: undefined,
loadOffer: undefined
} as ViewModels.Database;
newCollection.getDatabase = () => newDatabase;
newCollection.offer = ko.observable(undefined);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
settingsComponentInstance = new SettingsComponent(props);
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(true);
});
it("hasConflictResolution", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.hasConflictResolution()).toEqual(undefined);
const newContainer = new Explorer();
newContainer.databaseAccount = ko.observable({
id: undefined,
name: undefined,
location: undefined,
type: undefined,
kind: undefined,
tags: undefined,
properties: {
documentEndpoint: undefined,
tableEndpoint: undefined,
gremlinEndpoint: undefined,
cassandraEndpoint: undefined,
enableMultipleWriteLocations: true
}
});
const newCollection = { ...collection };
newCollection.container = newContainer;
newCollection.conflictResolutionPolicy = ko.observable({
mode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionProcedure: undefined
} as DataModels.ConflictResolutionPolicy);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
settingsComponentInstance = new SettingsComponent(props);
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
});
it("isOfferReplacePending", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
const newCollection = { ...collection };
newCollection.offer = ko.observable({
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
settingsComponentInstance = new SettingsComponent(props);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
});
it("save calls updateCollection and updateOffer", async () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true });
wrapper.update();
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled();
expect(updateOffer).toBeCalled();
});
it("revert resets state values", async () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
wrapper.setState({ timeToLive: TtlType.OnNoDefault, throughput: 10 });
wrapper.update();
let state = wrapper.state() as SettingsComponentState;
expect(isDirty(state.timeToLive, state.timeToLiveBaseline)).toEqual(true);
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(true);
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
settingsComponentInstance.onRevertClick();
state = wrapper.state() as SettingsComponentState;
expect(isDirty(state.timeToLive, state.timeToLiveBaseline)).toEqual(false);
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(false);
});
it("getAnalyticalStorageTtl", () => {
const newCollection = { ...collection };
newCollection.analyticalStorageTtl = ko.observable(10);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
const wrapper = shallow(<SettingsComponent {...props} />);
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
expect(settingsComponentInstance.getAnalyticalStorageTtl()).toEqual(10);
wrapper.setState({ analyticalStorageTtlSelection: TtlType.Off });
wrapper.update();
expect(settingsComponentInstance.getAnalyticalStorageTtl()).toEqual(-1);
});
it("getUpdatedConflictResolutionPolicy", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const conflictResolutionPolicyPath = "_ts";
const conflictResolutionPolicyProcedure = "sample_sproc";
const expectSprocPath =
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
wrapper.setState({
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPolicyPath: conflictResolutionPolicyPath
});
wrapper.update();
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
let conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy();
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.LastWriterWins);
expect(conflictResolutionPolicy.conflictResolutionPath).toEqual(conflictResolutionPolicyPath);
wrapper.setState({
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure
});
wrapper.update();
conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy();
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
});
});

View File

@@ -0,0 +1,875 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as SharedConstants from "../../../Shared/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../../UserContext";
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
import SettingsTab from "../../Tabs/SettingsTabV2";
import { throughputUnit } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
getMaxRUs,
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty,
parseConflictResolutionMode,
parseConflictResolutionProcedure
} from "./SettingsUtils";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
content: JSX.Element;
}
interface ButtonV2 {
isVisible: () => boolean;
isEnabled: () => boolean;
isSelected?: () => boolean;
}
export interface SettingsComponentProps {
settingsTab: SettingsTab;
}
export interface SettingsComponentState {
throughput: number;
throughputBaseline: number;
autoPilotThroughput: number;
autoPilotThroughputBaseline: number;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean;
isScaleDiscardable: boolean;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
analyticalStorageTtlSelection: TtlType;
analyticalStorageTtlSelectionBaseline: TtlType;
analyticalStorageTtlSeconds: number;
analyticalStorageTtlSecondsBaseline: number;
changeFeedPolicy: ChangeFeedPolicyState;
changeFeedPolicyBaseline: ChangeFeedPolicyState;
isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
shouldDiscardIndexingPolicy: boolean;
isIndexingPolicyDirty: boolean;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
conflictResolutionPolicyPath: string;
conflictResolutionPolicyPathBaseline: string;
conflictResolutionPolicyProcedure: string;
conflictResolutionPolicyProcedureBaseline: string;
isConflictResolutionDirty: boolean;
initialNotification: DataModels.Notification;
selectedTab: SettingsV2TabTypes;
}
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
private static readonly sixMonthsInSeconds = 15768000;
public saveSettingsButton: ButtonV2;
public discardSettingsChangesButton: ButtonV2;
public isAnalyticalStorageEnabled: boolean;
private collection: ViewModels.Collection;
private container: Explorer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
private shouldShowIndexingPolicyEditor: boolean;
constructor(props: SettingsComponentProps) {
super(props);
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
!this.collection.partitionKey ||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
this.state = {
throughput: undefined,
throughputBaseline: undefined,
autoPilotThroughput: undefined,
autoPilotThroughputBaseline: undefined,
isAutoPilotSelected: false,
wasAutopilotOriginallySet: false,
isScaleSaveable: false,
isScaleDiscardable: false,
timeToLive: undefined,
timeToLiveBaseline: undefined,
timeToLiveSeconds: undefined,
timeToLiveSecondsBaseline: undefined,
geospatialConfigType: undefined,
geospatialConfigTypeBaseline: undefined,
analyticalStorageTtlSelection: undefined,
analyticalStorageTtlSelectionBaseline: undefined,
analyticalStorageTtlSeconds: undefined,
analyticalStorageTtlSecondsBaseline: undefined,
changeFeedPolicy: undefined,
changeFeedPolicyBaseline: undefined,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
indexingPolicyContent: undefined,
indexingPolicyContentBaseline: undefined,
shouldDiscardIndexingPolicy: false,
isIndexingPolicyDirty: false,
conflictResolutionPolicyMode: undefined,
conflictResolutionPolicyModeBaseline: undefined,
conflictResolutionPolicyPath: undefined,
conflictResolutionPolicyPathBaseline: undefined,
conflictResolutionPolicyProcedure: undefined,
conflictResolutionPolicyProcedureBaseline: undefined,
isConflictResolutionDirty: false,
initialNotification: undefined,
selectedTab: SettingsV2TabTypes.ScaleTab
};
this.saveSettingsButton = {
isEnabled: this.isSaveSettingsButtonEnabled,
isVisible: () => {
return true;
}
};
this.discardSettingsChangesButton = {
isEnabled: this.isDiscardSettingsButtonEnabled,
isVisible: () => {
return true;
}
};
}
componentDidMount(): void {
this.setAutoPilotStates();
this.setBaseline();
if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
}
}
componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
}
}
public isSaveSettingsButtonEnabled = (): boolean => {
if (this.isOfferReplacePending()) {
return false;
}
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty
);
};
public isDiscardSettingsButtonEnabled = (): boolean => {
return (
this.state.isScaleDiscardable ||
this.state.isSubSettingsDiscardable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty
);
};
private setAutoPilotStates = (): void => {
const offer = this.collection?.offer && this.collection.offer();
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
if (
offerAutopilotSettings &&
offerAutopilotSettings.maxThroughput &&
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
) {
this.setState({
isAutoPilotSelected: true,
wasAutopilotOriginallySet: true,
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
});
}
};
public hasProvisioningTypeChanged = (): boolean =>
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean =>
this.container?.databaseAccount &&
this.container.databaseAccount()?.properties?.enableMultipleWriteLocations &&
this.collection.conflictResolutionPolicy &&
!!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => {
const offer = this.collection?.offer && this.collection.offer();
return (
offer &&
Object.keys(offer).find(value => value === "headers") &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
);
};
public onSaveClick = async (): Promise<void> => {
this.props.settingsTab.isExecutionError(false);
this.props.settingsTab.isExecuting(true);
const startKey: number = traceStart(Action.SettingsV2Updated, {
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
});
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
try {
if (
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty
) {
let defaultTtl: number;
switch (this.state.timeToLive) {
case TtlType.On:
defaultTtl = Number(this.state.timeToLiveSeconds);
break;
case TtlType.OnNoDefault:
defaultTtl = -1;
break;
case TtlType.Off:
default:
defaultTtl = undefined;
break;
}
newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy =
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
? {
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
}
: undefined;
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
newCollection.geospatialConfig = {
type: this.state.geospatialConfigType
};
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (conflictResolutionChanges) {
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
}
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
this.collection.id(updatedCollection.id);
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false
});
}
if (this.state.isScaleSaveable) {
const newThroughput = this.state.throughput;
const newOffer: DataModels.Offer = { ...this.collection.offer() };
const originalThroughputValue: number = this.state.throughput;
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
} else {
newOffer.content = {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: false
};
}
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.state.isAutoPilotSelected) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.state.autoPilotThroughput
};
// user has changed from provisioned --> autoscale
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.setState({
isAutoPilotSelected: false
});
// user has changed from autoscale --> provisioned
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
if (
getMaxRUs(this.collection, this.container) <=
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: false
};
try {
await updateOfferThroughputBeyondLimit(requestPayload);
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.setState({
throughput: originalThroughputValue,
throughputBaseline: originalThroughputValue,
initialNotification: {
description: `Throughput update for ${newThroughput} ${throughputUnit}`
} as DataModels.Notification
});
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
} catch (error) {
traceFailure(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: error
},
startKey
);
throw error;
}
} else {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
});
} else {
this.setState({
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
}
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
startKey
);
} catch (reason) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true);
console.error(reason);
traceFailure(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
startKey
);
}
this.props.settingsTab.isExecuting(false);
};
public onRevertClick = (): void => {
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
message: "Settings Discarded"
});
this.setState({
throughput: this.state.throughputBaseline,
timeToLive: this.state.timeToLiveBaseline,
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyModeBaseline,
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPathBaseline,
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedureBaseline,
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelectionBaseline,
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSecondsBaseline,
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
shouldDiscardIndexingPolicy: true,
isScaleSaveable: false,
isScaleDiscardable: false,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false
});
};
private onScaleSaveableChange = (isScaleSaveable: boolean): void =>
this.setState({ isScaleSaveable: isScaleSaveable });
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
this.setState({ isScaleDiscardable: isScaleDiscardable });
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
private logIndexingPolicySuccessMessage = (): void => {
if (this.props.settingsTab.onLoadStartKey) {
traceSuccess(
Action.Tab,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
this.props.settingsTab.onLoadStartKey
);
this.props.settingsTab.onLoadStartKey = undefined;
}
};
private onConflictResolutionPolicyModeChange = (newMode: DataModels.ConflictResolutionMode): void =>
this.setState({ conflictResolutionPolicyMode: newMode });
private onConflictResolutionPolicyPathChange = (newPath: string): void =>
this.setState({ conflictResolutionPolicyPath: newPath });
private onConflictResolutionPolicyProcedureChange = (newProcedure: string): void =>
this.setState({ conflictResolutionPolicyProcedure: newProcedure });
private onConflictResolutionDirtyChange = (isConflictResolutionDirty: boolean): void =>
this.setState({ isConflictResolutionDirty: isConflictResolutionDirty });
private onTtlChange = (newTtl: TtlType): void => this.setState({ timeToLive: newTtl });
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
private onAnalyticalStorageTtlSelectionChange = (newAnalyticalStorageTtlSelection: TtlType): void =>
this.setState({ analyticalStorageTtlSelection: newAnalyticalStorageTtlSelection });
private onAnalyticalStorageTtlSecondsChange = (newAnalyticalStorageTtlSeconds: number): void =>
this.setState({ analyticalStorageTtlSeconds: newAnalyticalStorageTtlSeconds });
private onChangeFeedPolicyChange = (newChangeFeedPolicy: ChangeFeedPolicyState): void =>
this.setState({ changeFeedPolicy: newChangeFeedPolicy });
private onSubSettingsSaveableChange = (isSubSettingsSaveable: boolean): void =>
this.setState({ isSubSettingsSaveable: isSubSettingsSaveable });
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
public getAnalyticalStorageTtl = (): number => {
if (this.isAnalyticalStorageEnabled) {
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
return Number(this.state.analyticalStorageTtlSeconds);
} else {
return Constants.AnalyticalStorageTtl.Infinite;
}
}
return undefined;
};
public getUpdatedConflictResolutionPolicy = (): DataModels.ConflictResolutionPolicy => {
if (
!isDirty(this.state.conflictResolutionPolicyMode, this.state.conflictResolutionPolicyModeBaseline) &&
!isDirty(this.state.conflictResolutionPolicyPath, this.state.conflictResolutionPolicyPathBaseline) &&
!isDirty(this.state.conflictResolutionPolicyProcedure, this.state.conflictResolutionPolicyProcedureBaseline)
) {
return undefined;
}
const policy: DataModels.ConflictResolutionPolicy = {
mode: parseConflictResolutionMode(this.state.conflictResolutionPolicyMode)
};
if (
policy.mode === DataModels.ConflictResolutionMode.Custom &&
this.state.conflictResolutionPolicyProcedure?.length > 0
) {
policy.conflictResolutionProcedure = Constants.HashRoutePrefixes.sprocWithIds(
this.collection.databaseId,
this.collection.id(),
this.state.conflictResolutionPolicyProcedure,
false
);
}
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
if (policy.conflictResolutionPath?.startsWith("/")) {
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
}
}
return policy;
};
public setBaseline = (): void => {
const defaultTtl = this.collection.defaultTtl();
let timeToLive: TtlType = this.state.timeToLive;
let timeToLiveSeconds = this.state.timeToLiveSeconds;
switch (defaultTtl) {
case undefined:
case 0:
timeToLive = TtlType.Off;
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
break;
case -1:
timeToLive = TtlType.OnNoDefault;
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
break;
default:
timeToLive = TtlType.On;
timeToLiveSeconds = defaultTtl;
break;
}
let analyticalStorageTtlSelection: TtlType;
let analyticalStorageTtlSeconds: number;
if (this.isAnalyticalStorageEnabled) {
const analyticalStorageTtl: number = this.collection.analyticalStorageTtl();
if (analyticalStorageTtl === Constants.AnalyticalStorageTtl.Infinite) {
analyticalStorageTtlSelection = TtlType.OnNoDefault;
} else {
analyticalStorageTtlSelection = TtlType.On;
analyticalStorageTtlSeconds = analyticalStorageTtl;
}
}
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
const indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
conflictResolutionPolicy?.conflictResolutionProcedure
);
const geospatialConfigTypeString: string =
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
changeFeedPolicy: changeFeedPolicy,
changeFeedPolicyBaseline: changeFeedPolicy,
timeToLive: timeToLive,
timeToLiveBaseline: timeToLive,
timeToLiveSeconds: timeToLiveSeconds,
timeToLiveSecondsBaseline: timeToLiveSeconds,
analyticalStorageTtlSelection: analyticalStorageTtlSelection,
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
indexingPolicyContent: indexingPolicyContent,
indexingPolicyContentBaseline: indexingPolicyContent,
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: conflictResolutionPolicyMode,
conflictResolutionPolicyPath: conflictResolutionPolicyPath,
conflictResolutionPolicyPathBaseline: conflictResolutionPolicyPath,
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure,
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
geospatialConfigType: geoSpatialConfigType,
geospatialConfigTypeBaseline: geoSpatialConfigType
});
};
private getTabsButtons = (): CommandButtonComponentProps[] => {
const buttons: CommandButtonComponentProps[] = [];
if (this.saveSettingsButton.isVisible()) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: async () => await this.onSaveClick(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveSettingsButton.isEnabled()
});
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardSettingsChangesButton.isEnabled()
});
}
return buttons;
};
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
this.setState({ autoPilotThroughput: newThroughput });
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
private onPivotChange = (item: PivotItem): void => {
const selectedTab = SettingsV2TabTypes[item.props.itemKey as keyof typeof SettingsV2TabTypes];
this.setState({ selectedTab: selectedTab });
};
public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
container: this.container,
isFixedContainer: this.isFixedContainer,
autoPilotTiersList: this.autoPilotTiersList,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
throughputBaseline: this.state.throughputBaseline,
autoPilotThroughput: this.state.autoPilotThroughput,
autoPilotThroughputBaseline: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.isAutoPilotSelected,
wasAutopilotOriginallySet: this.state.wasAutopilotOriginallySet,
onAutoPilotSelected: this.onAutoPilotSelected,
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
onScaleSaveableChange: this.onScaleSaveableChange,
onScaleDiscardableChange: this.onScaleDiscardableChange,
initialNotification: this.props.settingsTab.pendingNotification()
};
const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection,
container: this.container,
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
timeToLive: this.state.timeToLive,
timeToLiveBaseline: this.state.timeToLiveBaseline,
onTtlChange: this.onTtlChange,
timeToLiveSeconds: this.state.timeToLiveSeconds,
timeToLiveSecondsBaseline: this.state.timeToLiveSecondsBaseline,
onTimeToLiveSecondsChange: this.onTimeToLiveSecondsChange,
geospatialConfigType: this.state.geospatialConfigType,
geospatialConfigTypeBaseline: this.state.geospatialConfigTypeBaseline,
onGeoSpatialConfigTypeChange: this.onGeoSpatialConfigTypeChange,
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelection,
analyticalStorageTtlSelectionBaseline: this.state.analyticalStorageTtlSelectionBaseline,
onAnalyticalStorageTtlSelectionChange: this.onAnalyticalStorageTtlSelectionChange,
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: this.state.analyticalStorageTtlSecondsBaseline,
onAnalyticalStorageTtlSecondsChange: this.onAnalyticalStorageTtlSecondsChange,
changeFeedPolicy: this.state.changeFeedPolicy,
changeFeedPolicyBaseline: this.state.changeFeedPolicyBaseline,
onChangeFeedPolicyChange: this.onChangeFeedPolicyChange,
onSubSettingsSaveableChange: this.onSubSettingsSaveableChange,
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange
};
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
indexingPolicyContent: this.state.indexingPolicyContent,
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
};
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection,
container: this.container,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPath,
conflictResolutionPolicyPathBaseline: this.state.conflictResolutionPolicyPathBaseline,
onConflictResolutionPolicyPathChange: this.onConflictResolutionPolicyPathChange,
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedure,
conflictResolutionPolicyProcedureBaseline: this.state.conflictResolutionPolicyProcedureBaseline,
onConflictResolutionPolicyProcedureChange: this.onConflictResolutionPolicyProcedureChange,
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection)) {
tabs.push({
tab: SettingsV2TabTypes.ScaleTab,
content: <ScaleComponent {...scaleComponentProps} />
});
}
tabs.push({
tab: SettingsV2TabTypes.SubSettingsTab,
content: <SubSettingsComponent {...subSettingsComponentProps} />
});
if (this.shouldShowIndexingPolicyEditor) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
});
}
if (this.hasConflictResolution()) {
tabs.push({
tab: SettingsV2TabTypes.ConflictResolutionTab,
content: <ConflictResolutionComponent {...conflictResolutionPolicyComponentProps} />
});
}
const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab]
};
const pivotItems = tabs.map(tab => {
const pivotItemProps: IPivotItemProps = {
itemKey: SettingsV2TabTypes[tab.tab],
style: { marginTop: 20 },
headerText: getTabTitle(tab.tab)
};
return (
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
{tab.content}
</PivotItem>
);
});
return (
<div className="settingsV2MainContainer">
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div>
)}
<div className="settingsV2TabsContainer">
<Pivot {...pivotProps}>{pivotItems}</Pivot>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,20 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent";
export class SettingsComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private props: SettingsComponentProps) {
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <SettingsComponent {...this.props} />;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -0,0 +1,58 @@
import { shallow } from "enzyme";
import React from "react";
import {
getAutoPilotV3SpendElement,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
manualToAutoscaleDisclaimerElement,
ttlWarning,
indexingPolicyTTLWarningMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage,
getThroughputApplyDelayedMessage,
getThroughputApplyShortDelayMessage,
getThroughputApplyLongDelayMessage,
getToolTipContainer,
conflictResolutionCustomToolTip,
changeFeedPolicyToolTip,
conflictResolutionLwwTooltip
} from "./SettingsRenderUtils";
class SettingsRenderUtilsTestComponent extends React.Component {
public render(): JSX.Element {
return (
<>
{getAutoPilotV3SpendElement(1000, false)}
{getAutoPilotV3SpendElement(undefined, false)}
{getAutoPilotV3SpendElement(1000, true)}
{getAutoPilotV3SpendElement(undefined, true)}
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
{manualToAutoscaleDisclaimerElement}
{ttlWarning}
{indexingPolicyTTLWarningMessage}
{updateThroughputBeyondLimitWarningMessage}
{updateThroughputDelayedApplyWarningMessage}
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getToolTipContainer(<span>Sample Text</span>)}
{conflictResolutionLwwTooltip}
{conflictResolutionCustomToolTip}
{changeFeedPolicyToolTip}
</>
);
}
}
describe("SettingsUtils functions", () => {
it("render", () => {
const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,349 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants";
import {
computeAutoscaleUsagePriceHourly,
getPriceCurrency,
getCurrencySign,
getAutoscalePricePerRu,
getMultimasterMultiplier,
computeRUUsagePriceHourly,
getPricePerRu,
calculateEstimateNumber
} from "../../../Utils/PricingUtils";
import {
ITextFieldStyles,
ICheckboxStyles,
IStackProps,
IStackTokens,
IChoiceGroupStyles,
Link,
Text,
IMessageBarStyles,
ITextStyles
} from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils";
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const spendAckCheckBoxStyle: ICheckboxStyles = {
label: {
margin: 0,
padding: "2 0 2 0"
},
text: {
fontSize: 12
}
};
export const subComponentStackProps: Partial<IStackProps> = {
tokens: { childrenGap: 20 }
};
export const titleAndInputStackProps: Partial<IStackProps> = {
tokens: { childrenGap: 5 }
};
export const checkBoxAndInputStackProps: Partial<IStackProps> = {
tokens: { childrenGap: 10 }
};
export const toolTipLabelStackTokens: IStackTokens = {
childrenGap: 6
};
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
export const throughputUnit = "RU/s";
export const getAutoPilotV3SpendElement = (
maxAutoPilotThroughputSet: number,
isDatabaseThroughput: boolean,
requestUnitsUsageCostElement?: JSX.Element
): JSX.Element => {
if (!maxAutoPilotThroughputSet) {
return <></>;
}
const resource: string = isDatabaseThroughput ? "database" : "container";
return (
<>
<Text>
Your {resource} throughput will automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(maxAutoPilotThroughputSet)} RU/s (10% of max RU/s) -{" "}
{maxAutoPilotThroughputSet} RU/s
</b>{" "}
based on usage.
<br />
</Text>
{requestUnitsUsageCostElement}
<Text>
After the first {AutoPilotUtils.getStorageBasedOnUserInput(maxAutoPilotThroughputSet)} GB of data stored, the
max RU/s will be automatically upgraded based on the new storage value.
<Link href={AutopilotDocumentation.Url} target="_blank">
{" "}
Learn more
</Link>
.
</Text>
</>
);
};
export const getEstimatedAutoscaleSpendElement = (
throughput: number,
serverId: string,
regions: number,
multimaster: boolean
): JSX.Element => {
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const pricePerRu =
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
getMultimasterMultiplier(regions, multimaster);
return (
<Text id="autoscaleSpendElement">
Estimated monthly cost ({currency}) is{" "}
<b>
{currencySign}
{calculateEstimateNumber(monthlyPrice / 10)}
{` - `}
{currencySign}
{calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
{pricePerRu}/RU)
</Text>
);
};
export const getEstimatedSpendElement = (
throughput: number,
serverId: string,
regions: number,
multimaster: boolean,
rupmEnabled: boolean
): JSX.Element => {
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
return (
<Text id="throughputSpendElement">
Estimated cost ({currency}):{" "}
<b>
{currencySign}
{calculateEstimateNumber(hourlyPrice)} hourly {` / `}
{currencySign}
{calculateEstimateNumber(dailyPrice)} daily {` / `}
{currencySign}
{calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({"regions: "} {regions}, {throughput}RU/s, {currencySign}
{pricePerRu}/RU)
</Text>
);
};
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
<a href={Urls.autoscaleMigration}>Learn more</a>
</Text>
);
export const ttlWarning: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
operation explicitly issued by a client application. For more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
Time to Live (TTL) in Azure Cosmos DB
</Link>
.
</Text>
);
export const indexingPolicyTTLWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and
the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For
more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
Modifying Indexing Policies
</Link>
.
</Text>
);
export const updateThroughputBeyondLimitWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputBeyondLimitWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and
increase throughput for the selected container. This operation will take 1-3 business days to complete. You can
track the status of this request in Notifications.
</Text>
);
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
time to complete.
</Text>
);
const getCurrentThroughput = (
isAutoscale: boolean,
throughput: number,
throughputUnit: string,
targetThroughput?: number
): string => {
if (targetThroughput) {
if (throughput) {
return isAutoscale
? `, Current autoscale throughput: ${Math.round(
throughput / 10
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
targetThroughput / 10
)} - ${targetThroughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
} else {
return isAutoscale
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
}
}
if (!targetThroughput && throughput) {
return isAutoscale
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
}
return "";
};
export const getThroughputApplyDelayedMessage = (
isAutoscale: boolean,
throughput: number,
throughputUnit: string,
databaseName: string,
collectionName: string,
requestedThroughput: number
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days
to complete. View the latest status in Notifications.
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
export const getThroughputApplyShortDelayMessage = (
isAutoscale: boolean,
throughput: number,
throughputUnit: string,
databaseName: string,
collectionName: string,
targetThroughput: number
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
</Text>
);
export const getThroughputApplyLongDelayMessage = (
isAutoscale: boolean,
throughput: number,
throughputUnit: string,
databaseName: string,
collectionName: string,
requestedThroughput: number
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
complete. View the latest status in Notifications.
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
export const getToolTipContainer = (content: string | JSX.Element): JSX.Element =>
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
export const conflictResolutionLwwTooltip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
default timestamp based conflict resolution.
</Text>
);
export const conflictResolutionCustomToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
conflicts will be populated in the
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
{` conflicts feed`}
</Link>
. You can update/re-register the stored procedure at any time.
</Text>
);
export const changeFeedPolicyToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
Reads are unaffected.
</Text>
);
export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<ITextFieldStyles> => ({
fieldGroup: {
height: 25,
width: 300,
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
selectors: {
":disabled": {
backgroundColor: StyleConstants.BaseMedium,
borderColor: StyleConstants.BaseMediumHigh
}
}
}
});
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
flexContainer: [
{
selectors: {
".ms-ChoiceField-field.is-checked::before": {
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
},
".ms-ChoiceField-field.is-checked::after": {
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
},
".ms-ChoiceField-wrapper label": {
whiteSpace: "nowrap",
fontSize: 14,
fontFamily: StyleConstants.DataExplorerFont,
padding: "2px 5px"
}
}
}
]
});

View File

@@ -0,0 +1,54 @@
import { shallow } from "enzyme";
import React from "react";
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
import { container, collection } from "../TestUtils";
import * as DataModels from "../../../../Contracts/DataModels";
describe("ConflictResolutionComponent", () => {
const baseProps: ConflictResolutionComponentProps = {
collection: collection,
container: container,
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
onConflictResolutionPolicyModeChange: () => {
return;
},
conflictResolutionPolicyPath: "",
conflictResolutionPolicyPathBaseline: "",
onConflictResolutionPolicyPathChange: () => {
return;
},
conflictResolutionPolicyProcedure: "",
conflictResolutionPolicyProcedureBaseline: "",
onConflictResolutionPolicyProcedureChange: () => {
return;
},
onConflictResolutionDirtyChange: () => {
return;
}
};
it("Sproc text field displayed", () => {
const wrapper = shallow(<ConflictResolutionComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(true);
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(false);
});
it("Path text field displayed", () => {
const props = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
const wrapper = shallow(<ConflictResolutionComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(false);
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(true);
});
it("conflict resolution policy dirty is set", () => {
let conflictRsolutionComponent = new ConflictResolutionComponent(baseProps);
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
conflictRsolutionComponent = new ConflictResolutionComponent(newProps);
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@@ -0,0 +1,151 @@
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import {
getTextFieldStyles,
conflictResolutionLwwTooltip,
conflictResolutionCustomToolTip,
subComponentStackProps,
getChoiceGroupStyles
} from "../SettingsRenderUtils";
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
import { isDirty } from "../SettingsUtils";
export interface ConflictResolutionComponentProps {
collection: ViewModels.Collection;
container: Explorer;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
conflictResolutionPolicyPath: string;
conflictResolutionPolicyPathBaseline: string;
onConflictResolutionPolicyPathChange: (newPath: string) => void;
conflictResolutionPolicyProcedure: string;
conflictResolutionPolicyProcedureBaseline: string;
onConflictResolutionPolicyProcedureChange: (newProcedure: string) => void;
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
}
export class ConflictResolutionComponent extends React.Component<ConflictResolutionComponentProps> {
private shouldCheckComponentIsDirty = true;
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: DataModels.ConflictResolutionMode.LastWriterWins,
text: "Last Write Wins (default)"
},
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" }
];
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onConflictResolutionDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.conflictResolutionPolicyMode, this.props.conflictResolutionPolicyModeBaseline) ||
isDirty(this.props.conflictResolutionPolicyPath, this.props.conflictResolutionPolicyPathBaseline) ||
isDirty(this.props.conflictResolutionPolicyProcedure, this.props.conflictResolutionPolicyProcedureBaseline)
) {
return true;
}
return false;
};
private onConflictResolutionPolicyModeChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onConflictResolutionPolicyModeChange(
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
);
private onConflictResolutionPolicyPathChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.props.onConflictResolutionPolicyPathChange(newValue);
private onConflictResolutionPolicyProcedureChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.props.onConflictResolutionPolicyProcedureChange(newValue);
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label="Mode"
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange}
styles={getChoiceGroupStyles(
this.props.conflictResolutionPolicyMode,
this.props.conflictResolutionPolicyModeBaseline
)}
/>
);
private onRenderLwwComponentTextField = (props: ITextFieldProps) => (
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionLwwTooltip} />
);
private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField
id="conflictResolutionLwwTextField"
label={"Conflict Resolver Property"}
onRenderLabel={this.onRenderLwwComponentTextField}
styles={getTextFieldStyles(
this.props.conflictResolutionPolicyPath,
this.props.conflictResolutionPolicyPathBaseline
)}
value={this.props.conflictResolutionPolicyPath}
onChange={this.onConflictResolutionPolicyPathChange}
/>
);
private onRenderCustomComponentTextField = (props: ITextFieldProps) => (
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionCustomToolTip} />
);
private getConflictResolutionCustomComponent = (): JSX.Element => (
<TextField
id="conflictResolutionCustomTextField"
label="Stored procedure"
onRenderLabel={this.onRenderCustomComponentTextField}
styles={getTextFieldStyles(
this.props.conflictResolutionPolicyProcedure,
this.props.conflictResolutionPolicyProcedureBaseline
)}
value={this.props.conflictResolutionPolicyProcedure}
onChange={this.onConflictResolutionPolicyProcedureChange}
/>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.getConflictResolutionModeComponent()}
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.LastWriterWins &&
this.getConflictResolutionLWWComponent()}
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.Custom &&
this.getConflictResolutionCustomComponent()}
</Stack>
);
}
}

View File

@@ -0,0 +1,56 @@
import { shallow } from "enzyme";
import React from "react";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./IndexingPolicyComponent";
import * as DataModels from "../../../../Contracts/DataModels";
describe("IndexingPolicyComponent", () => {
const initialIndexingPolicyContent: DataModels.IndexingPolicy = {
automatic: false,
indexingMode: "",
includedPaths: [],
excludedPaths: []
};
const baseProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: false,
resetShouldDiscardIndexingPolicy: () => {
return;
},
indexingPolicyContent: initialIndexingPolicyContent,
indexingPolicyContentBaseline: initialIndexingPolicyContent,
onIndexingPolicyContentChange: () => {
return;
},
logIndexingPolicySuccessMessage: () => {
return;
},
onIndexingPolicyDirtyChange: () => {
return;
}
};
it("renders", () => {
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
});
it("indexing policy is reset", () => {
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
const indexingPolicyComponentInstance = wrapper.instance() as IndexingPolicyComponent;
const resetIndexingPolicyEditorMockFn = jest.fn();
indexingPolicyComponentInstance.resetIndexingPolicyEditor = resetIndexingPolicyEditorMockFn;
wrapper.setProps({ shouldDiscardIndexingPolicy: true });
wrapper.update();
expect(resetIndexingPolicyEditorMockFn.mock.calls.length).toEqual(1);
});
it("conflict resolution policy dirty is set", () => {
let indexingPolicyComponent = new IndexingPolicyComponent(baseProps);
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, indexingPolicyContent: undefined as DataModels.IndexingPolicy };
indexingPolicyComponent = new IndexingPolicyComponent(newProps);
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@@ -0,0 +1,118 @@
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import * as monaco from "monaco-editor";
import { isDirty } from "../SettingsUtils";
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
export interface IndexingPolicyComponentProps {
shouldDiscardIndexingPolicy: boolean;
resetShouldDiscardIndexingPolicy: () => void;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
logIndexingPolicySuccessMessage: () => void;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
}
interface IndexingPolicyComponentState {
indexingPolicyContentIsValid: boolean;
}
export class IndexingPolicyComponent extends React.Component<
IndexingPolicyComponentProps,
IndexingPolicyComponentState
> {
private shouldCheckComponentIsDirty = true;
private indexingPolicyDiv = React.createRef<HTMLDivElement>();
private indexingPolicyEditor: monaco.editor.IStandaloneCodeEditor;
constructor(props: IndexingPolicyComponentProps) {
super(props);
this.state = {
indexingPolicyContentIsValid: true
};
}
componentDidUpdate(): void {
if (this.props.shouldDiscardIndexingPolicy) {
this.resetIndexingPolicyEditor();
this.props.resetShouldDiscardIndexingPolicy();
}
this.onComponentUpdate();
}
componentDidMount(): void {
this.resetIndexingPolicyEditor();
this.onComponentUpdate();
}
public resetIndexingPolicyEditor = (): void => {
if (!this.indexingPolicyEditor) {
this.createIndexingPolicyEditor();
} else {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
indexingPolicyEditorModel.setValue(value);
}
this.onComponentUpdate();
};
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onIndexingPolicyDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) &&
this.state.indexingPolicyContentIsValid
) {
return true;
}
return false;
};
private createIndexingPolicyEditor = (): void => {
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value,
language: "json",
readOnly: false,
ariaLabel: "Indexing Policy"
});
if (this.indexingPolicyEditor) {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logIndexingPolicySuccessMessage();
}
};
private onEditorContentChange = (): void => {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
try {
const newIndexingPolicyContent = JSON.parse(indexingPolicyEditorModel.getValue()) as DataModels.IndexingPolicy;
this.props.onIndexingPolicyContentChange(newIndexingPolicyContent);
this.setState({ indexingPolicyContentIsValid: true });
} catch (e) {
this.setState({ indexingPolicyContentIsValid: false });
}
};
public render(): JSX.Element {
return (
<Stack {...titleAndInputStackProps}>
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}
}

View File

@@ -0,0 +1,154 @@
import { shallow } from "enzyme";
import React from "react";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { container, collection } from "../TestUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import Explorer from "../../../Explorer";
import * as Constants from "../../../../Common/Constants";
import { PlatformType } from "../../../../PlatformType";
import * as DataModels from "../../../../Contracts/DataModels";
import { throughputUnit } from "../SettingsRenderUtils";
import * as SharedConstants from "../../../../Shared/Constants";
import ko from "knockout";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = {
collection: collection,
container: container,
isFixedContainer: false,
autoPilotTiersList: [],
onThroughputChange: () => {
return;
},
throughput: 1000,
throughputBaseline: 1000,
autoPilotThroughput: 4000,
autoPilotThroughputBaseline: 4000,
isAutoPilotSelected: false,
wasAutopilotOriginallySet: true,
onAutoPilotSelected: () => false,
onMaxAutoPilotThroughputChange: () => {
return;
},
onScaleSaveableChange: () => {
return;
},
onScaleDiscardableChange: () => {
return;
},
initialNotification: {
description: `Throughput update for ${targetThroughput} ${throughputUnit}`
} as DataModels.Notification
};
it("renders with correct intiial notification", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
const newCollection = { ...collection };
const maxThroughput = 5000;
const targetMaxThroughput = 50000;
newCollection.offer = ko.observable({
content: {
offerAutopilotSettings: {
maxThroughput: maxThroughput,
targetMaxThroughput: targetMaxThroughput
}
},
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
const newProps = {
...baseProps,
initialNotification: undefined as DataModels.Notification,
collection: newCollection
};
wrapper = shallow(<ScaleComponent {...newProps} />);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
});
it("autoScale disabled", () => {
const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
});
it("autoScale enabled", () => {
const newContainer = new Explorer();
newContainer.databaseAccount({
id: undefined,
name: undefined,
location: undefined,
type: undefined,
kind: "documentdb",
tags: undefined,
properties: {
documentEndpoint: undefined,
tableEndpoint: undefined,
gremlinEndpoint: undefined,
cassandraEndpoint: undefined,
capabilities: [
{
name: Constants.CapabilityNames.EnableAutoScale.toLowerCase(),
description: undefined
}
]
}
});
const props = { ...baseProps, container: newContainer };
const scaleComponent = new ScaleComponent(props);
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
});
it("getMaxRUThroughputInputLimit", () => {
const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
});
it("getThroughputTitle", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - 40,000 RU/s)");
let newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
newProps = { ...baseProps, isAutoPilotSelected: true };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
});
it("canThroughputExceedMaximumValue", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(false);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
it("getThroughputWarningMessage", () => {
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
let scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
newProps.throughput = throughputBeyondMaxRus;
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
});
});

View File

@@ -0,0 +1,236 @@
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import Explorer from "../../../Explorer";
import { PlatformType } from "../../../../PlatformType";
import {
getTextFieldStyles,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage
} from "../SettingsRenderUtils";
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
container: Explorer;
isFixedContainer: boolean;
autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
onThroughputChange: (newThroughput: number) => void;
throughput: number;
throughputBaseline: number;
autoPilotThroughput: number;
autoPilotThroughputBaseline: number;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
initialNotification: DataModels.Notification;
}
export class ScaleComponent extends React.Component<ScaleComponentProps> {
private isEmulator: boolean;
constructor(props: ScaleComponentProps) {
super(props);
this.isEmulator = configContext.platform === Platform.Emulator;
}
public isAutoScaleEnabled = (): boolean => {
const accountCapabilities: DataModels.Capability[] = this.props.container?.databaseAccount()?.properties
?.capabilities;
const enableAutoScaleCapability =
accountCapabilities &&
accountCapabilities.find((capability: DataModels.Capability) => {
return (
capability &&
capability.name &&
capability.name.toLowerCase() === Constants.CapabilityNames.EnableAutoScale.toLowerCase()
);
});
return !!enableAutoScaleCapability;
};
private getStorageCapacityTitle = (): JSX.Element => {
// Mongo container with system partition key still treat as "Fixed"
const isFixed =
!this.props.collection.partitionKey ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
const capacity: string = isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
<Text>{capacity}</Text>
</Stack>
);
};
public getMaxRUThroughputInputLimit = (): number => {
if (this.props.container?.getPlatformType() === PlatformType.Hosted && this.props.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return getMaxRUs(this.props.collection, this.props.container);
};
public getThroughputTitle = (): string => {
if (this.props.isAutoPilotSelected) {
return AutoPilotUtils.getAutoPilotHeaderText(false);
}
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
const maxThroughput: string =
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
? "unlimited"
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
public canThroughputExceedMaximumValue = (): boolean => {
const isPublicAzurePortal: boolean =
this.props.container.getPlatformType() === PlatformType.Portal &&
!this.props.container.isRunningOnNationalCloud();
const hasPartitionKey = !!this.props.collection.partitionKey;
return isPublicAzurePortal && hasPartitionKey;
};
public getInitialNotificationElement = (): JSX.Element => {
if (this.props.initialNotification) {
return this.getLongDelayMessage();
}
const offer = this.props.collection?.offer && this.props.collection.offer();
if (
offer &&
Object.keys(offer).find(value => {
return value === "headers";
}) &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
) {
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
const targetThroughput =
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return undefined;
};
public getThroughputWarningMessage = (): JSX.Element => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
getMaxRUs(this.props.collection, this.props.container) <=
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
const throughputExceedsMaxValue: boolean =
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputDelayedApplyWarningMessage;
}
return undefined;
};
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`
);
const throughput = this.props.throughputBaseline;
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
if (targetThroughput) {
return getThroughputApplyLongDelayMessage(
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return <></>;
};
private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()}
serverId={this.props.container.serverId()}
throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={getMinRUs(this.props.collection, this.props.container)}
maximum={this.getMaxRUThroughputInputLimit()}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()}
isEmulator={this.isEmulator}
isFixed={this.props.isFixedContainer}
isAutoPilotSelected={this.props.isAutoPilotSelected}
onAutoPilotSelected={this.props.onAutoPilotSelected}
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
maxAutoPilotThroughput={this.props.autoPilotThroughput}
maxAutoPilotThroughputBaseline={this.props.autoPilotThroughputBaseline}
onMaxAutoPilotThroughputChange={this.props.onMaxAutoPilotThroughputChange}
spendAckChecked={false}
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
/>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.getInitialNotificationElement() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
)}
{!this.isAutoScaleEnabled() && (
<Stack {...subComponentStackProps}>
{this.getThroughputInputComponent()}
{this.getStorageCapacityTitle()}
</Stack>
)}
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>Throughput (RU/s)</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text>
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</Text>
</Stack>
)}
</Stack>
);
}
}

View File

@@ -0,0 +1,140 @@
import { shallow } from "enzyme";
import React from "react";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
import { container, collection } from "../TestUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
import ko from "knockout";
import Explorer from "../../../Explorer";
describe("SubSettingsComponent", () => {
container.isPreferredApiDocumentDB = ko.computed(() => true);
const baseProps: SubSettingsComponentProps = {
collection: collection,
container: container,
timeToLive: TtlType.On,
timeToLiveBaseline: TtlType.On,
onTtlChange: () => {
return;
},
timeToLiveSeconds: 1000,
timeToLiveSecondsBaseline: 1000,
onTimeToLiveSecondsChange: () => {
return;
},
geospatialConfigType: GeospatialConfigType.Geography,
geospatialConfigTypeBaseline: GeospatialConfigType.Geography,
onGeoSpatialConfigTypeChange: () => {
return;
},
isAnalyticalStorageEnabled: true,
analyticalStorageTtlSelection: TtlType.On,
analyticalStorageTtlSelectionBaseline: TtlType.On,
onAnalyticalStorageTtlSelectionChange: () => {
return;
},
analyticalStorageTtlSeconds: 2000,
analyticalStorageTtlSecondsBaseline: 2000,
onAnalyticalStorageTtlSecondsChange: () => {
return;
},
changeFeedPolicyVisible: true,
changeFeedPolicy: ChangeFeedPolicyState.On,
changeFeedPolicyBaseline: ChangeFeedPolicyState.On,
onChangeFeedPolicyChange: () => {
return;
},
onSubSettingsSaveableChange: () => {
return;
},
onSubSettingsDiscardableChange: () => {
return;
}
};
it("renders", () => {
const wrapper = shallow(<SubSettingsComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#timeToLive")).toEqual(true);
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(true);
expect(wrapper.exists("#geoSpatialConfig")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(true);
expect(wrapper.exists("#changeFeedPolicy")).toEqual(true);
});
it("timeToLiveSeconds hidden", () => {
const props = { ...baseProps, timeToLive: TtlType.Off };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#timeToLive")).toEqual(true);
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(false);
});
it("analyticalTimeToLive hidden", () => {
const props = { ...baseProps, isAnalyticalStorageEnabled: false };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(false);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
});
it("analyticalTimeToLiveSeconds hidden", () => {
const props = { ...baseProps, analyticalStorageTtlSelection: TtlType.OnNoDefault };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
});
it("changeFeedPolicy hidden", () => {
const props = { ...baseProps, changeFeedPolicyVisible: false };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#changeFeedPolicy")).toEqual(false);
});
it("partitionKey is visible", () => {
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(true);
});
it("partitionKey not visible", () => {
const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true);
const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);
});
it("largePartitionKey is enabled", () => {
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.isLargePartitionKeyEnabled()).toEqual(true);
});
it("sub settings saveable and discardable are set", () => {
let subSettingsComponent = new SubSettingsComponent(baseProps);
let isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(false);
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
const newProps = { ...baseProps, timeToLive: TtlType.OnNoDefault };
subSettingsComponent = new SubSettingsComponent(newProps);
isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(true);
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
});
it("getTtlValue", async () => {
const subSettingsComponentInstance = new SubSettingsComponent(baseProps);
expect(subSettingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
});
});

View File

@@ -0,0 +1,336 @@
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import {
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult,
TtlOn,
TtlOff,
TtlOnNoDefault,
getSanitizedInputValue
} from "../SettingsUtils";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
Label,
Text,
TextField,
Stack,
IChoiceGroupOption,
ChoiceGroup,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import {
getTextFieldStyles,
changeFeedPolicyToolTip,
subComponentStackProps,
titleAndInputStackProps,
getChoiceGroupStyles,
ttlWarning,
messageBarStyles
} from "../SettingsRenderUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps {
collection: ViewModels.Collection;
container: Explorer;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
onTtlChange: (newTtl: TtlType) => void;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
onGeoSpatialConfigTypeChange: (newGeoSpatialConfigType: GeospatialConfigType) => void;
isAnalyticalStorageEnabled: boolean;
analyticalStorageTtlSelection: TtlType;
analyticalStorageTtlSelectionBaseline: TtlType;
onAnalyticalStorageTtlSelectionChange: (newAnalyticalStorageTtlSelection: TtlType) => void;
analyticalStorageTtlSeconds: number;
analyticalStorageTtlSecondsBaseline: number;
onAnalyticalStorageTtlSecondsChange: (newAnalyticalStorageTtlSeconds: number) => void;
changeFeedPolicyVisible: boolean;
changeFeedPolicy: ChangeFeedPolicyState;
changeFeedPolicyBaseline: ChangeFeedPolicyState;
onChangeFeedPolicyChange: (newChangeFeedPolicyState: ChangeFeedPolicyState) => void;
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
}
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
private shouldCheckComponentIsDirty = true;
private ttlVisible: boolean;
private geospatialVisible: boolean;
private partitionKeyValue: string;
private partitionKeyName: string;
constructor(props: SubSettingsComponentProps) {
super(props);
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
}
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
const isComponentDirtyResult = this.IsComponentDirty();
this.props.onSubSettingsSaveableChange(isComponentDirtyResult.isSaveable);
this.props.onSubSettingsDiscardableChange(isComponentDirtyResult.isDiscardable);
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): IsComponentDirtyResult => {
if (
(this.props.timeToLive === TtlType.On && !this.props.timeToLiveSeconds) ||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds)
) {
return { isSaveable: false, isDiscardable: true };
} else if (
isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) ||
(this.props.timeToLive === TtlType.On &&
isDirty(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)) ||
isDirty(this.props.analyticalStorageTtlSelection, this.props.analyticalStorageTtlSelectionBaseline) ||
(this.props.analyticalStorageTtlSelection === TtlType.On &&
isDirty(this.props.analyticalStorageTtlSeconds, this.props.analyticalStorageTtlSecondsBaseline)) ||
isDirty(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline) ||
isDirty(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)
) {
return { isSaveable: true, isDiscardable: true };
}
return { isSaveable: false, isDiscardable: false };
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off" },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" }
];
public getTtlValue = (value: string): TtlType => {
switch (value) {
case TtlOn:
return TtlType.On;
case TtlOff:
return TtlType.Off;
case TtlOnNoDefault:
return TtlType.OnNoDefault;
}
return undefined;
};
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
this.props.onTtlChange(this.getTtlValue(option.key));
private onTimeToLiveSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
};
private onGeoSpatialConfigTypeChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onGeoSpatialConfigTypeChange(GeospatialConfigType[option.key as keyof typeof GeospatialConfigType]);
private onAnalyticalStorageTtlSelectionChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.props.onAnalyticalStorageTtlSelectionChange(this.getTtlValue(option.key));
private onAnalyticalStorageTtlSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newAnalyticalStorageTtlSeconds = getSanitizedInputValue(newValue, Int32.Max);
this.props.onAnalyticalStorageTtlSecondsChange(newAnalyticalStorageTtlSeconds);
};
private onChangeFeedPolicyChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
private getTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
</Stack>
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" }
];
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="analyticalStorageTimeToLive"
label="Analytical Storage Time to Live"
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange}
styles={getChoiceGroupStyles(
this.props.analyticalStorageTtlSelection,
this.props.analyticalStorageTtlSelectionBaseline
)}
/>
{this.props.analyticalStorageTtlSelection === TtlType.On && (
<TextField
id="analyticalStorageTimeToLiveSeconds"
styles={getTextFieldStyles(
this.props.analyticalStorageTtlSeconds,
this.props.analyticalStorageTtlSecondsBaseline
)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix="second(s)"
onChange={this.onAnalyticalStorageTtlSecondsChange}
/>
)}
</Stack>
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography" },
{ key: GeospatialConfigType.Geometry, text: "Geometry" }
];
private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup
id="geoSpatialConfig"
label="Geospatial Configuration"
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange}
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
/>
);
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: "Off" },
{ key: ChangeFeedPolicyState.On, text: "On" }
];
private getChangeFeedComponent = (): JSX.Element => {
const labelId = "settingsV2ChangeFeedLabelId";
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
</Label>
<ChoiceGroup
id="changeFeedPolicy"
selectedKey={this.props.changeFeedPolicy}
options={this.changeFeedChoiceGroupOptions}
onChange={this.onChangeFeedPolicyChange}
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
aria-labelledby={labelId}
/>
</Stack>
);
};
private getPartitionKeyComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
{this.getPartitionKeyVisible() && (
<TextField
label={this.partitionKeyName}
disabled
styles={getTextFieldStyles(undefined, undefined)}
defaultValue={this.partitionKeyValue}
/>
)}
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
</Stack>
);
public getPartitionKeyVisible = (): boolean => {
if (
this.props.container.isPreferredApiCassandra() ||
this.props.container.isPreferredApiTable() ||
!this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
) {
return false;
}
return true;
};
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.ttlVisible && this.getTtlComponent()}
{this.geospatialVisible && this.getGeoSpatialComponent()}
{this.props.isAnalyticalStorageEnabled && this.getAnalyticalStorageTtlComponent()}
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
{this.getPartitionKeyComponent()}
</Stack>
);
}
}

View File

@@ -0,0 +1,97 @@
import { shallow } from "enzyme";
import React from "react";
import {
ThroughputInputAutoPilotV3Component,
ThroughputInputAutoPilotV3Props
} from "./ThroughputInputAutoPilotV3Component";
import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount,
serverId: undefined,
wasAutopilotOriginallySet: false,
throughput: 100,
throughputBaseline: 100,
onThroughputChange: undefined,
minimum: 10000,
maximum: 400,
step: 100,
isEnabled: true,
isEmulator: false,
spendAckChecked: false,
spendAckId: "spendAckId",
spendAckText: "spendAckText",
spendAckVisible: false,
showAsMandatory: true,
isFixed: false,
label: "label",
infoBubbleText: "infoBubbleText",
canExceedMaximumValue: true,
onAutoPilotSelected: undefined,
isAutoPilotSelected: false,
maxAutoPilotThroughput: 4000,
maxAutoPilotThroughputBaseline: 4000,
onMaxAutoPilotThroughputChange: undefined,
onScaleSaveableChange: () => {
return;
},
onScaleDiscardableChange: () => {
return;
},
getThroughputWarningMessage: () => undefined
};
it("throughput input visible", () => {
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...baseProps} />);
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(false);
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(false);
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#throughputInput")).toEqual(true);
expect(wrapper.exists("#autopilotInput")).toEqual(false);
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
});
it("autopilot input visible", () => {
const newProps = { ...baseProps, isAutoPilotSelected: true };
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(true);
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(true);
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#autopilotInput")).toEqual(true);
expect(wrapper.exists("#throughputInput")).toEqual(false);
expect(wrapper.exists("#manualToAutoscaleDisclaimerElement")).toEqual(true);
wrapper.setProps({ wasAutopilotOriginallySet: true });
wrapper.update();
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
});
it("spendAck checkbox visible", () => {
const newProps = { ...baseProps, spendAckVisible: true };
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#spendAckCheckBox")).toEqual(true);
});
it("scale saveable and discardable are set", () => {
let throughputComponent = new ThroughputInputAutoPilotV3Component(baseProps);
let isComponentDirtyResult = throughputComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(false);
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
const newProps = { ...baseProps, throughput: 1000000 };
throughputComponent = new ThroughputInputAutoPilotV3Component(newProps);
isComponentDirtyResult = throughputComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(true);
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
});
});

View File

@@ -0,0 +1,339 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
getTextFieldStyles,
getToolTipContainer,
spendAckCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
ChoiceGroup,
IChoiceGroupOption,
Checkbox,
Stack,
Label,
Link,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
serverId: string;
throughput: number;
throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void;
minimum: number;
maximum: number;
step?: number;
isEnabled?: boolean;
spendAckChecked?: boolean;
spendAckId?: string;
spendAckText?: string;
spendAckVisible?: boolean;
showAsMandatory?: boolean;
isFixed: boolean;
isEmulator: boolean;
label: string;
infoBubbleText?: string;
canExceedMaximumValue?: boolean;
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
maxAutoPilotThroughput: number;
maxAutoPilotThroughputBaseline: number;
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
}
interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean;
}
export class ThroughputInputAutoPilotV3Component extends React.Component<
ThroughputInputAutoPilotV3Props,
ThroughputInputAutoPilotV3State
> {
private shouldCheckComponentIsDirty = true;
private static readonly defaultStep = 100;
private step: number;
private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number;
private options: IChoiceGroupOption[] = [
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" }
];
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
const isComponentDirtyResult = this.IsComponentDirty();
this.props.onScaleSaveableChange(isComponentDirtyResult.isSaveable);
this.props.onScaleDiscardableChange(isComponentDirtyResult.isDiscardable);
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): IsComponentDirtyResult => {
let isSaveable = false;
let isDiscardable = false;
if (this.props.isEnabled) {
if (this.hasProvisioningTypeChanged()) {
isSaveable = true;
isDiscardable = true;
} else if (this.props.isAutoPilotSelected) {
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
isDiscardable = true;
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
isSaveable = true;
}
}
} else {
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
isDiscardable = true;
isSaveable = true;
if (
!this.props.throughput ||
this.props.throughput < this.props.minimum ||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
!this.props.canExceedMaximumValue)
) {
isSaveable = false;
}
}
}
}
return { isSaveable, isDiscardable };
};
public constructor(props: ThroughputInputAutoPilotV3Props) {
super(props);
this.state = {
spendAckChecked: this.props.spendAckChecked
};
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
this.autoPilotInputMaxValue = Int32.Max;
}
public hasProvisioningTypeChanged = (): boolean =>
this.props.wasAutopilotOriginallySet !== this.props.isAutoPilotSelected;
public overrideWithAutoPilotSettings = (): boolean =>
this.hasProvisioningTypeChanged() && this.props.wasAutopilotOriginallySet;
public overrideWithProvisionedThroughputSettings = (): boolean =>
this.hasProvisioningTypeChanged() && !this.props.wasAutopilotOriginallySet;
private getRequestUnitsUsageCost = (): JSX.Element => {
const account = this.props.databaseAccount;
if (!account) {
return <></>;
}
const serverId: string = this.props.serverId;
const offerThroughput: number = this.props.throughput;
const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
let estimatedSpend: JSX.Element;
if (!this.props.isAutoPilotSelected) {
estimatedSpend = getEstimatedSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
serverId,
regions,
multimaster,
false
);
} else {
estimatedSpend = getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughput,
serverId,
regions,
multimaster
);
}
return estimatedSpend;
};
private getAutoPilotUsageCost = (): JSX.Element => {
if (!this.props.maxAutoPilotThroughput) {
return <></>;
}
return getAutoPilotV3SpendElement(
this.props.maxAutoPilotThroughput,
false /* isDatabaseThroughput */,
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
);
};
private onAutoPilotThroughputChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
this.props.onMaxAutoPilotThroughputChange(newThroughput);
};
private onThroughputChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else {
this.props.onThroughputChange(newThroughput);
}
};
private onChoiceGroupChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.props.onAutoPilotSelected(option.key === "true");
private renderThroughputModeChoices = (): JSX.Element => {
const labelId = "settingsV2RadioButtonLabelId";
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.label}
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
/>
</Label>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{manualToAutoscaleDisclaimerElement}
</MessageBar>
)}
<ChoiceGroup
selectedKey={this.props.isAutoPilotSelected.toString()}
options={this.options}
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
/>
</Stack>
);
};
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
this.setState({ spendAckChecked: checked });
private renderAutoPilotInput = (): JSX.Element => (
<>
<Text>
Provision maximum RU/s required by this resource. Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`}
</Link>
</Text>
<TextField
label="Max RU/s"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={spendAckCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
</>
);
private renderThroughputInput = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<TextField
required
type="number"
id="throughputInput"
key="provisioned throughput input"
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
disabled={this.overrideWithAutoPilotSettings()}
step={this.step}
value={
this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline?.toString()
: this.props.throughput?.toString()
}
onChange={this.onThroughputChange}
/>
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={spendAckCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && <p>Choose unlimited storage capacity for more than 10,000 RU/s.</p>}
</Stack>
);
public render(): JSX.Element {
return (
<Stack {...checkBoxAndInputStackProps}>
{!this.props.isFixed && this.renderThroughputModeChoices()}
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
</Stack>
);
}
}

View File

@@ -0,0 +1,431 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledMessageBarBase
messageBarType={5}
styles={
Object {
"root": Object {
"marginTop": "5px",
},
}
}
>
<Text
id="manualToAutoscaleDisclaimerElement"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<a
href="https://aka.ms/cosmos-autoscale-migration"
>
Learn more
</a>
</Text>
</StyledMessageBarBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="true"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": undefined,
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Text>
Provision maximum RU/s required by this resource. Estimate your required RU/s with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
</StyledLinkBase>
</Text>
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
label="Max RU/s"
onChange={[Function]}
required={true}
step={1000}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value=""
/>
</Stack>
`;
exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="false"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
key="provisioned throughput input"
onChange={[Function]}
required={true}
step={100}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
<StyledCheckboxBase
checked={false}
id="spendAckCheckBox"
label="spendAckText"
onChange={[Function]}
styles={
Object {
"label": Object {
"margin": 0,
"padding": "2 0 2 0",
},
"text": Object {
"fontSize": 12,
},
}
}
/>
</Stack>
</Stack>
`;
exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="false"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
key="provisioned throughput input"
onChange={[Function]}
required={true}
step={100}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
</Stack>
</Stack>
`;

View File

@@ -0,0 +1,15 @@
import { shallow } from "enzyme";
import React from "react";
import { ToolTipLabelComponent, ToolTipLabelComponentProps } from "./ToolTipLabelComponent";
describe("ToolTipLabelComponent", () => {
const props: ToolTipLabelComponentProps = {
label: "sample tool tip label",
toolTipElement: <span>sample tool tip text</span>
};
it("renders", () => {
const wrapper = shallow(<ToolTipLabelComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,32 @@
import * as React from "react";
import { Stack, Text, IIconStyles, Icon, TooltipHost, DirectionalHint } from "office-ui-fabric-react";
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
export interface ToolTipLabelComponentProps {
label: string;
toolTipElement: JSX.Element;
}
const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } };
export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponentProps> {
public render(): JSX.Element {
return (
<>
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
{this.props.label && <Text style={{ fontWeight: 600 }}>{this.props.label}</Text>}
{this.props.toolTipElement && (
<TooltipHost
content={this.props.toolTipElement}
directionalHint={DirectionalHint.rightCenter}
calloutProps={{ gapSpace: 0 }}
styles={{ root: { display: "inline-block", float: "right" } }}
>
<Icon iconName="Info" ariaLabel="Info" styles={iconButtonStyles} />
</TooltipHost>
)}
</Stack>
</>
);
}
}

View File

@@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConflictResolutionComponent Path text field displayed 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledChoiceGroupBase
label="Mode"
onChange={[Function]}
options={
Array [
Object {
"key": "LastWriterWins",
"text": "Last Write Wins (default)",
},
Object {
"key": "Custom",
"text": "Merge Procedure (custom)",
},
]
}
selectedKey="LastWriterWins"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": undefined,
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
<StyledTextFieldBase
id="conflictResolutionLwwTextField"
label="Conflict Resolver Property"
onChange={[Function]}
onRenderLabel={[Function]}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
value=""
/>
</Stack>
`;
exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledChoiceGroupBase
label="Mode"
onChange={[Function]}
options={
Array [
Object {
"key": "LastWriterWins",
"text": "Last Write Wins (default)",
},
Object {
"key": "Custom",
"text": "Merge Procedure (custom)",
},
]
}
selectedKey="Custom"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
<StyledTextFieldBase
id="conflictResolutionCustomTextField"
label="Stored procedure"
onChange={[Function]}
onRenderLabel={[Function]}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
value=""
/>
</Stack>
`;

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndexingPolicyComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<div
className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/>
</Stack>
`;

View File

@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScaleComponent renders with correct intiial notification 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledMessageBarBase
messageBarType={5}
>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBarBase>
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={false}
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
isFixed={false}
label="Throughput (6,000 - 40,000 RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={40000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
onScaleDiscardableChange={[Function]}
onScaleSaveableChange={[Function]}
onThroughputChange={[Function]}
spendAckChecked={false}
throughput={1000}
throughputBaseline={1000}
wasAutopilotOriginallySet={true}
/>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledLabelBase>
Storage capacity
</StyledLabelBase>
<Text>
Unlimited
</Text>
</Stack>
</Stack>
</Stack>
`;

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ToolTipLabelComponent renders 1`] = `
<Fragment>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 6,
}
}
verticalAlign="center"
>
<Text
style={
Object {
"fontWeight": 600,
}
}
>
sample tool tip label
</Text>
<StyledTooltipHostBase
calloutProps={
Object {
"gapSpace": 0,
}
}
content={
<span>
sample tool tip text
</span>
}
directionalHint={12}
styles={
Object {
"root": Object {
"display": "inline-block",
"float": "right",
},
}
}
>
<StyledIconBase
ariaLabel="Info"
iconName="Info"
styles={
Object {
"root": Object {
"marginBottom": -3,
},
}
}
/>
</StyledTooltipHostBase>
</Stack>
</Fragment>
`;

View File

@@ -0,0 +1,97 @@
import { collection, container } from "./TestUtils";
import {
getMaxRUs,
getMinRUs,
getSanitizedInputValue,
hasDatabaseSharedThroughput,
isDirty,
isDirtyTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure
} from "./SettingsUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import ko from "knockout";
describe("SettingsUtils", () => {
it("getMaxRUs", () => {
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
expect(getMaxRUs(collection, container)).toEqual(40000);
});
it("getMinRUs", () => {
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
expect(getMinRUs(collection, container)).toEqual(6000);
});
it("hasDatabaseSharedThroughput", () => {
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
const newCollection = { ...collection };
newCollection.getDatabase = () => {
return {
nodeKind: undefined,
rid: undefined,
container: undefined,
self: "",
id: ko.observable(""),
collections: ko.observableArray(undefined),
offer: ko.observable(undefined),
isDatabaseExpanded: ko.observable(false),
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: ko.observable(undefined),
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,
findCollectionWithId: undefined,
openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined,
onSettingsClick: undefined,
loadOffer: undefined
} as ViewModels.Database;
};
newCollection.offer(undefined);
expect(hasDatabaseSharedThroughput(newCollection)).toEqual(true);
});
it("parseConflictResolutionMode", () => {
expect(parseConflictResolutionMode("custom")).toEqual(DataModels.ConflictResolutionMode.Custom);
expect(parseConflictResolutionMode("lastwriterwins")).toEqual(DataModels.ConflictResolutionMode.LastWriterWins);
});
it("parseConflictResolutionProcedure", () => {
expect(parseConflictResolutionProcedure("/dbs/db/colls/coll/sprocs/conflictResSproc")).toEqual("conflictResSproc");
expect(parseConflictResolutionProcedure("conflictResSproc")).toEqual("conflictResSproc");
});
describe("isDirty", () => {
const indexingPolicy = {
automatic: true,
indexingMode: "consistent",
includedPaths: [],
excludedPaths: []
} as DataModels.IndexingPolicy;
const cases = [
["baseline", "current"],
[0, 1],
[true, false],
[undefined, indexingPolicy],
[indexingPolicy, { ...indexingPolicy, automatic: false }]
];
test.each(cases)("", (baseline: isDirtyTypes, current: isDirtyTypes) => {
expect(isDirty(baseline, baseline)).toEqual(false);
expect(isDirty(baseline, current)).toEqual(true);
});
});
it("getSanitizedInputValue", () => {
const max = 100;
expect(getSanitizedInputValue("", max)).toEqual(0);
expect(getSanitizedInputValue("999", max)).toEqual(99);
expect(getSanitizedInputValue("10", max)).toEqual(10);
});
});

View File

@@ -0,0 +1,182 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels";
import * as Constants from "../../../Common/Constants";
import * as SharedConstants from "../../../Shared/Constants";
import * as PricingUtils from "../../../Utils/PricingUtils";
import Explorer from "../../Explorer";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
export const TtlOff = "off";
export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault";
export enum ChangeFeedPolicyState {
Off = "Off",
On = "On"
}
export enum TtlType {
Off = "off",
On = "on",
OnNoDefault = "on-nodefault"
}
export enum GeospatialConfigType {
Geography = "Geography",
Geometry = "Geometry"
}
export enum SettingsV2TabTypes {
ScaleTab,
ConflictResolutionTab,
SubSettingsTab,
IndexingPolicyTab
}
export interface IsComponentDirtyResult {
isSaveable: boolean;
isDiscardable: boolean;
}
export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => {
const database: ViewModels.Database = collection.getDatabase();
return database?.isDatabaseShared() && !collection.offer();
};
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
if (isTryCosmosDBSubscription) {
return Constants.TryCosmosExperience.maxRU;
}
const numPartitionsFromOffer: number =
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo().numPartitions;
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
};
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
if (isTryCosmosDBSubscription) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const offerContent = collection?.offer && collection.offer()?.content;
if (offerContent?.offerAutopilotSettings) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
return collectionThroughputInfo.minimumRUForCollection;
}
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo().numPartitions;
if (!numPartitions || numPartitions === 1) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
const quotaInKb = collection.quotaInfo().collectionSize;
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
return Math.max(baseRU, baseRUbyPartitions);
};
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
// Backend can contain different casing as it does case-insensitive comparisson
if (!modeFromBackend) {
return undefined;
}
const modeAsLowerCase: string = modeFromBackend.toLowerCase();
if (modeAsLowerCase === DataModels.ConflictResolutionMode.Custom.toLowerCase()) {
return DataModels.ConflictResolutionMode.Custom;
}
// Default is LWW
return DataModels.ConflictResolutionMode.LastWriterWins;
};
export const parseConflictResolutionProcedure = (procedureFromBackEnd: string): string => {
// Backend data comes in /dbs/xxxx/colls/xxxx/sprocs/{name}, to make it easier for users, we just use the name
if (!procedureFromBackEnd) {
return undefined;
}
if (procedureFromBackEnd.indexOf("/") >= 0) {
const sprocsIndex: number = procedureFromBackEnd.indexOf(Constants.HashRoutePrefixes.sprocHash);
if (sprocsIndex === -1) {
return undefined;
}
return procedureFromBackEnd.substr(sprocsIndex + Constants.HashRoutePrefixes.sprocHash.length);
}
// No path, just a name, in case backend returns just the name
return procedureFromBackEnd;
};
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
let newValue = parseInt(newValueString);
if (isNaN(newValue)) {
newValue = zeroValue;
} else if (newValue > max) {
newValue = Math.floor(newValue / 10);
}
return newValue;
};
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
const currentType = typeof current;
const baselineType = typeof baseline;
if (currentType !== "undefined" && baselineType !== "undefined" && currentType !== baselineType) {
throw new Error("current and baseline values are not of the same type.");
}
const currentStringValue = getStringValue(current, currentType);
const baselineStringValue = getStringValue(baseline, baselineType);
return currentStringValue !== baselineStringValue;
};
const getStringValue = (value: isDirtyTypes, type: string): string => {
switch (type) {
case "string":
case "undefined":
case "number":
case "boolean":
return value?.toString();
default:
return JSON.stringify(value);
}
};
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
switch (tab) {
case SettingsV2TabTypes.ScaleTab:
return "Scale";
case SettingsV2TabTypes.ConflictResolutionTab:
return "Conflict Resolution";
case SettingsV2TabTypes.SubSettingsTab:
return "Settings";
case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy";
default:
throw new Error(`Unknown tab ${tab}`);
}
};

View File

@@ -0,0 +1,49 @@
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import ko from "knockout";
export const container = new Explorer();
export const collection = ({
container: container,
databaseId: "test",
id: ko.observable<string>("test"),
defaultTtl: ko.observable<number>(5),
analyticalStorageTtl: ko.observable<number>(undefined),
indexingPolicy: ko.observable<DataModels.IndexingPolicy>({
automatic: true,
indexingMode: "default",
includedPaths: [],
excludedPaths: []
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
offer: ko.observable<DataModels.Offer>({
content: {
offerThroughput: 10000,
offerIsRUPerMinuteThroughputEnabled: false,
collectionThroughputInfo: {
minimumRUForCollection: 6000,
numPhysicalPartitions: 4
} as DataModels.OfferThroughputInfo
}
} as DataModels.Offer),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy
),
changeFeedPolicy: ko.observable<DataModels.ChangeFeedPolicy>({} as DataModels.ChangeFeedPolicy),
geospatialConfig: ko.observable<DataModels.GeospatialConfig>({} as DataModels.GeospatialConfig),
getDatabase: () => {
return;
},
partitionKey: {
paths: [],
kind: "hash",
version: 2
},
partitionKeyProperty: "partitionKey",
readSettings: () => {
return;
}
} as unknown) as ViewModels.Collection;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,314 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsUtils functions render 1`] = `
<Fragment>
<Text>
Your
container
throughput will automatically scale from
<b>
100
RU/s (10% of max RU/s) -
1000
RU/s
</b>
based on usage.
<br />
</Text>
<Text>
After the first
10
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
<StyledLinkBase
href="https://aka.ms/cosmos-autoscale-info"
target="_blank"
>
Learn more
</StyledLinkBase>
.
</Text>
<Text>
Your
database
throughput will automatically scale from
<b>
100
RU/s (10% of max RU/s) -
1000
RU/s
</b>
based on usage.
<br />
</Text>
<Text>
After the first
10
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
<StyledLinkBase
href="https://aka.ms/cosmos-autoscale-info"
target="_blank"
>
Learn more
</StyledLinkBase>
.
</Text>
<Text
id="throughputSpendElement"
>
Estimated cost (
RMB
):
<b>
¥
1.29
hourly
/
¥
31.06
daily
/
¥
944.60
monthly
</b>
(
regions:
2
,
1000
RU/s,
¥
0.00051
/RU)
</Text>
<Text
id="autoscaleSpendElement"
>
Estimated monthly cost (
RMB
) is
<b>
¥
111.69
-
¥
1116.90
</b>
(
regions:
2
,
100
-
1000
RU/s,
¥
0.000765
/RU)
</Text>
<Text
id="manualToAutoscaleDisclaimerElement"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<a
href="https://aka.ms/cosmos-autoscale-migration"
>
Learn more
</a>
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application. For more information see,
<StyledLinkBase
href="https://aka.ms/cosmos-db-ttl"
target="_blank"
>
Time to Live (TTL) in Azure Cosmos DB
</StyledLinkBase>
.
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For more information see,
<StyledLinkBase
href="https://aka.ms/cosmosdb/modify-index-policy"
target="_blank"
>
Modifying Indexing Policies
</StyledLinkBase>
.
</Text>
<Text
id="updateThroughputBeyondLimitWarningMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected container. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.
</Text>
<Text
id="updateThroughputDelayedApplyWarningMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some time to complete.
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text>
<Text
id="throughputApplyShortDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
Database:
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
<span>
Sample Text
</span>
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the winner for the conflicting versions of the document. Specify your own integer property if you want to override the default timestamp based conflict resolution.
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write application defined logic to determine the winner of the conflicting versions of a document. The stored procedure will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the conflicts will be populated in the
<StyledLinkBase
className="linkDarkBackground"
href="https://aka.ms/dataexplorerconflics"
target="_blank"
>
conflicts feed
</StyledLinkBase>
. You can update/re-register the stored procedure at any time.
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default. To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes. Reads are unaffected.
</Text>
</Fragment>
`;

View File

@@ -18,6 +18,8 @@ import {
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface TreeNodeMenuItem {
label: string;
@@ -276,7 +278,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
text: menuItem.label,
disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: menuItem.onClick,
onClick: () => {
menuItem.onClick();
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
label: menuItem.label
});
},
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
}))
}}

View File

@@ -191,7 +191,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"className": undefined,
"disabled": true,
"key": "menuLabel",
"onClick": undefined,
"onClick": [Function],
"onRenderIcon": [Function],
"text": "menuLabel",
},

View File

@@ -1,12 +1,11 @@
jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection");
import * as ko from "knockout";
import * as sinon from "sinon";
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
@@ -26,6 +25,10 @@ describe("ContainerSampleGenerator", () => {
return explorerStub;
};
beforeEach(() => {
(createDocument as jest.Mock).mockResolvedValue(undefined);
});
it("should insert documents for sql API account", async () => {
const sampleCollectionId = "SampleCollection";
const sampleDatabaseId = "SampleDB";
@@ -69,13 +72,10 @@ describe("ContainerSampleGenerator", () => {
await generator.createSampleContainerAsync();
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(createDocument).toHaveBeenCalled();
});
it("should send gremlin queries for Graph API account", async () => {
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
updateUserContext({
databaseAccount: {
id: "foo",
@@ -121,9 +121,6 @@ describe("ContainerSampleGenerator", () => {
generator.setData(sampleData);
await generator.createSampleContainerAsync();
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(executeStub.called).toBe(true);
});
it("should not create any sample for Mongo API account", async () => {
@@ -133,7 +130,7 @@ describe("ContainerSampleGenerator", () => {
explorerStub.defaultExperience = ko.observable<string>(experience);
// Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});
it("should not create any sample for Table API account", async () => {

View File

@@ -1,7 +1,6 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
@@ -96,9 +95,9 @@ export class ContainerSampleGenerator {
.reduce((previous, current) => previous.then(current), Promise.resolve());
} else {
// For SQL all queries are executed at the same time
this.sampleDataFile.data.forEach(doc => {
this.sampleDataFile.data.map(doc => {
const subPromise = createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason));
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
promises.push(subPromise);
});
await Promise.all(promises);

View File

@@ -82,12 +82,12 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -98,10 +98,6 @@ enum ShareAccessToggleState {
Read
}
interface ExplorerOptions {
notificationsClient: NotificationsClientBase;
isEmulator: boolean;
}
interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
@@ -133,16 +129,14 @@ export default class Explorer {
public isPreferredApiGraph: ko.Computed<boolean>;
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isEmulator: boolean;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public extensionEndpoint: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public notificationsClient: NotificationsClientBase;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
@@ -212,6 +206,7 @@ export default class Explorer {
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isSettingsV2Enabled: ko.Observable<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
@@ -270,7 +265,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(options: ExplorerOptions) {
constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree
});
@@ -376,12 +371,9 @@ export default class Explorer {
}
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.notificationsClient = options.notificationsClient;
this.isEmulator = options.isEmulator;
this.features = ko.observable();
this.serverId = ko.observable<string>();
this.extensionEndpoint = ko.observable<string>(undefined);
this.armEndpoint = ko.observable<string>(undefined);
this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
@@ -421,6 +413,8 @@ export default class Explorer {
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
//this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
this.isSettingsV2Enabled = ko.observable(false);
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@@ -522,22 +516,7 @@ export default class Explorer {
return false;
}
const capabilities = this.databaseAccount().properties && this.databaseAccount().properties.capabilities;
if (!capabilities) {
return false;
}
for (let i = 0; i < capabilities.length; i++) {
if (typeof capabilities[i] === "object") {
if (capabilities[i].name === Constants.CapabilityNames.EnableMongo) {
// version 3.6
return true;
}
}
}
return false;
return this.isEnableMongoCapabilityPresent();
});
this.isServerlessEnabled = ko.computed(
@@ -569,6 +548,21 @@ export default class Explorer {
return false;
});
this.isEnableMongoCapabilityPresent = ko.computed(() => {
const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities;
if (!capabilities) {
return false;
}
for (let i = 0; i < capabilities.length; i++) {
if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) {
return true;
}
}
return false;
});
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() =>
this.getPlatformType() === PlatformType.Portal &&
@@ -1867,7 +1861,7 @@ export default class Explorer {
return null;
}
if (this.selectedNode().nodeKind === "Database") {
return _.find(this.databases(), (database: ViewModels.Database) => database.rid === this.selectedNode().rid);
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id());
}
return this.findSelectedCollection().database;
}
@@ -1909,9 +1903,7 @@ export default class Explorer {
}
this.features(inputs.features);
this.serverId(inputs.serverId);
this.extensionEndpoint(inputs.extensionEndpoint || "");
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
@@ -1919,6 +1911,7 @@ export default class Explorer {
this.flight(inputs.addCollectionDefaultFlight);
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights);
if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
@@ -1927,6 +1920,7 @@ export default class Explorer {
this._importExplorerConfigComplete = true;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
ARM_ENDPOINT: this.armEndpoint()
});
@@ -1952,12 +1946,20 @@ export default class Explorer {
return Q();
}
public findSelectedCollection(): ViewModels.Collection {
if (this.selectedNode().nodeKind === "Collection") {
return this.findSelectedCollectionForSelectedNode();
} else {
return this.findSelectedCollectionForSubNode();
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
if (!flights) {
return;
}
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
this.isSettingsV2Enabled(true);
}
}
public findSelectedCollection(): ViewModels.Collection {
return (this.selectedNode().nodeKind === "Collection"
? this.selectedNode()
: this.selectedNode().collection) as ViewModels.Collection;
}
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
@@ -2074,11 +2076,11 @@ export default class Explorer {
});
databasesToLoad.forEach(async (database: ViewModels.Database) => {
await database.loadCollections();
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().id() === database.id());
});
Q.all(loadCollectionPromises).done(
@@ -2190,21 +2192,11 @@ export default class Explorer {
}
}
private findSelectedCollectionForSelectedNode(): ViewModels.Collection {
return this.findCollection(this.selectedNode().rid);
}
public findCollection(rid: string): ViewModels.Collection {
for (let i = 0; i < this.databases().length; i++) {
const database = this.databases()[i];
for (let j = 0; j < database.collections().length; j++) {
const collection = database.collections()[j];
if (collection.rid === rid) {
return collection;
}
}
}
return null;
public findCollection(databaseId: string, collectionId: string): ViewModels.Collection {
const database: ViewModels.Database = this.databases().find(
(database: ViewModels.Database) => database.id() === databaseId
);
return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId);
}
public isLastCollection(): boolean {
@@ -2228,7 +2220,7 @@ export default class Explorer {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
this.databases(),
(existingDatabase: ViewModels.Database) => existingDatabase.rid === database._rid
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
);
return !databaseExists;
});
@@ -2240,7 +2232,7 @@ export default class Explorer {
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
const databasePresentInUpdatedList = _.some(
updatedDatabaseList,
(db: DataModels.Database) => db._rid === database.rid
(db: DataModels.Database) => db.id === database.id()
);
if (!databasePresentInUpdatedList) {
databasesToDelete.push(database);
@@ -2262,7 +2254,7 @@ export default class Explorer {
const databasesToKeep: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.rid === database.rid);
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id);
if (!shouldRemoveDatabase) {
databasesToKeep.push(database);
}
@@ -2271,19 +2263,6 @@ export default class Explorer {
this.databases(databasesToKeep);
}
private findSelectedCollectionForSubNode(): ViewModels.Collection {
for (let i = 0; i < this.databases().length; i++) {
const database = this.databases()[i];
for (let j = 0; j < database.collections().length; j++) {
const collection = database.collections()[j];
if (this.selectedNode().collection && collection.rid === this.selectedNode().collection.rid) {
return collection;
}
}
}
return null;
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
@@ -2375,42 +2354,16 @@ export default class Explorer {
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void
): void {
this._dialogProps({
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this._closeModalDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
this._closeModalDialog();
onCancel && onCancel();
}
});
}
public showOkCancelTextFieldModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
textFieldProps: TextFieldProps,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
let textFieldValue: string = null;
this._dialogProps({
isModal: true,
visible: true,
@@ -2426,8 +2379,9 @@ export default class Explorer {
this._closeModalDialog();
onCancel && onCancel();
},
primaryButtonDisabled: isPrimaryButtonDisabled,
textFieldProps
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled
});
}
@@ -2468,7 +2422,6 @@ export default class Explorer {
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: null,
selfLink: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
@@ -2630,7 +2583,7 @@ export default class Explorer {
const databaseAccount = this.databaseAccount();
const databaseAccountLocation = databaseAccount && databaseAccount.location.toLowerCase();
const disallowedLocationsUri = `${this.extensionEndpoint()}/api/disallowedLocations`;
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
@@ -2920,7 +2873,6 @@ export default class Explorer {
title: title,
tabPath: title,
collection: null,
selfLink: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -2964,7 +2916,6 @@ export default class Explorer {
title: title,
tabPath: title,
documentClientUtility: null,
selfLink: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -3007,7 +2958,6 @@ export default class Explorer {
tabPath: title,
documentClientUtility: null,
collection: null,
selfLink: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -3127,8 +3077,10 @@ export default class Explorer {
}
public async loadDatabaseOffers(): Promise<void> {
this.databases()?.forEach(async (database: ViewModels.Database) => {
await database.loadOffer();
});
await Promise.all(
this.databases()?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
})
);
}
}

View File

@@ -32,13 +32,11 @@ export class ArraysByKeyCache<T> {
this.cache[key] = elements;
if (index < 0) {
console.error("Inserting with negative index is not allowed by ArraysByCache");
return;
}
// Check that previous index is populated, if not, ignore
if (index > elements.length) {
console.error("Inserting non-contiguous element is not allowed by ArraysByCache");
return;
}

View File

@@ -1,13 +1,13 @@
import * as ko from "knockout";
import Q from "q";
import { schemeCategory20 } from "d3";
import { event as d3Event, selectAll, select } from "d3-selection";
import { schemeCategory10 } from "d3-scale-chromatic";
import { selectAll, select } from "d3-selection";
import { zoom, zoomIdentity } from "d3-zoom";
import { scaleOrdinal } from "d3-scale";
import { forceSimulation, forceLink, forceCollide, forceManyBody } from "d3-force";
import { interpolateNumber, interpolate } from "d3-interpolate";
import { map as d3Map } from "d3-collection";
import { drag } from "d3-drag";
import { drag, D3DragEvent } from "d3-drag";
import _ from "underscore";
import { NeighborType } from "../../../Contracts/ViewModels";
@@ -89,7 +89,7 @@ export class D3ForceGraph implements GraphRenderer {
private static readonly PAGINATION_LINE2_Y_OFFSET_PX = 14;
// We limit the number of different colors to 20
private static readonly COLOR_SCHEME_20 = scaleOrdinal(schemeCategory20);
private static readonly COLOR_SCHEME = scaleOrdinal(schemeCategory10);
private static readonly MAX_COLOR_NB = 20;
// Some state variables
@@ -344,13 +344,13 @@ export class D3ForceGraph implements GraphRenderer {
} while ((el = el && el.parentNode));
}
private zoomed() {
private zoomed(event: any) {
this.zoomTransform = {
x: d3Event.transform.x,
y: d3Event.transform.y,
k: d3Event.transform.k
x: event.transform.x,
y: event.transform.y,
k: event.transform.k
};
this.g.attr("transform", d3Event.transform);
this.g.attr("transform", event.transform);
}
private instantiateSimulation() {
@@ -719,17 +719,17 @@ export class D3ForceGraph implements GraphRenderer {
})
.call(
drag()
.on("start", (d: D3Node) => {
return this.dragstarted(d);
})
.on("drag", (d: D3Node) => {
return this.dragged(d);
})
.on("end", (d: D3Node) => {
return this.dragended(d);
})
.on("start", ((e: D3DragEvent<SVGGElement, D3Node, unknown>, d: D3Node) => {
return this.dragstarted(d, e);
}) as any)
.on("drag", ((e: D3DragEvent<SVGGElement, D3Node, unknown>, d: D3Node) => {
return this.dragged(d, e);
}) as any)
.on("end", ((e: D3DragEvent<SVGGElement, D3Node, unknown>, d: D3Node) => {
return this.dragended(d, e);
}) as any)
)
.on("mouseover", (d: D3Node) => {
.on("mouseover", (_: MouseEvent, d: D3Node) => {
if (this.isHighlightDisabled || this.selectedNode || this.isDragging) {
return;
}
@@ -737,7 +737,7 @@ export class D3ForceGraph implements GraphRenderer {
this.highlightNode(this, d);
this.simulation.stop();
})
.on("mouseout", (d: D3Node) => {
.on("mouseout", (_: MouseEvent, d: D3Node) => {
if (this.isHighlightDisabled || this.selectedNode || this.isDragging) {
return;
}
@@ -765,17 +765,17 @@ export class D3ForceGraph implements GraphRenderer {
.attr("aria-label", (d: D3Node) => {
return this.retrieveNodeCaption(d);
})
.on("dblclick", function(d: D3Node) {
.on("dblclick", function(_: MouseEvent, d: D3Node) {
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("click", function(d: D3Node) {
.on("click", function(_: MouseEvent, d: D3Node) {
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("keypress", function(d: D3Node) {
if (d3Event.charCode === Constants.KeyCodes.Space || d3Event.charCode === Constants.KeyCodes.Enter) {
d3Event.stopPropagation();
.on("keypress", function(event: KeyboardEvent, d: D3Node) {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
}
@@ -850,24 +850,24 @@ export class D3ForceGraph implements GraphRenderer {
return `Next page of nodes for ${this.retrieveNodeCaption(d)}`;
})
.attr("tabindex", 0)
.on("click", function(d: D3Node) {
.on("click", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE);
})
.on("dblclick", function(d: D3Node) {
}) as any)
.on("dblclick", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE);
})
.on("keypress", function(d: D3Node) {
if (d3Event.charCode === Constants.KeyCodes.Space || d3Event.charCode === Constants.KeyCodes.Enter) {
d3Event.stopPropagation();
}) as any)
.on("keypress", ((event: KeyboardEvent, d: D3Node) => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE);
}
})
.on("mouseover", function(d: D3Node) {
select(this).classed("active", true);
})
.on("mouseout", function(d: D3Node) {
select(this).classed("active", false);
})
}) as any)
.on("mouseover", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", true);
}) as any)
.on("mouseout", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", false);
}) as any)
.attr("visibility", (d: D3Node) => (!d._outEAllLoaded || !d._inEAllLoaded ? "visible" : "hidden"));
parent
.append("use")
@@ -879,24 +879,24 @@ export class D3ForceGraph implements GraphRenderer {
return `Previous page of nodes for ${this.retrieveNodeCaption(d)}`;
})
.attr("tabindex", 0)
.on("click", function(d: D3Node) {
.on("click", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE);
})
.on("dblclick", function(d: D3Node) {
}) as any)
.on("dblclick", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE);
})
.on("keypress", function(d: D3Node) {
if (d3Event.charCode === Constants.KeyCodes.Space || d3Event.charCode === Constants.KeyCodes.Enter) {
d3Event.stopPropagation();
}) as any)
.on("keypress", ((event: KeyboardEvent, d: D3Node) => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE);
}
})
.on("mouseover", function(d: D3Node) {
select(this).classed("active", true);
})
.on("mouseout", function(d: D3Node) {
select(this).classed("active", false);
})
}) as any)
.on("mouseover", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", true);
}) as any)
.on("mouseout", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", false);
}) as any)
.attr("visibility", (d: D3Node) =>
!d._pagination || d._pagination.currentPage.start !== 0 ? "visible" : "hidden"
);
@@ -975,24 +975,24 @@ export class D3ForceGraph implements GraphRenderer {
return `Load adjacent nodes for ${this.retrieveNodeCaption(d)}`;
})
.attr("tabindex", 0)
.on("click", function(d: D3Node) {
.on("click", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE);
})
.on("dblclick", function(d: D3Node) {
}) as any)
.on("dblclick", ((_: MouseEvent, d: D3Node) => {
self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE);
})
.on("keypress", function(d: D3Node) {
if (d3Event.charCode === Constants.KeyCodes.Space || d3Event.charCode === Constants.KeyCodes.Enter) {
d3Event.stopPropagation();
}) as any)
.on("keypress", ((event: KeyboardEvent, d: D3Node) => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE);
}
})
.on("mouseover", function(d: D3Node) {
select(this).classed("active", true);
})
.on("mouseout", function(d: D3Node) {
select(this).classed("active", false);
});
}) as any)
.on("mouseover", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", true);
}) as any)
.on("mouseout", ((e: MouseEvent, d: D3Node) => {
select(e.target as any).classed("active", false);
}) as any);
}
/**
@@ -1053,7 +1053,7 @@ export class D3ForceGraph implements GraphRenderer {
if (index < 0 || index >= D3ForceGraph.MAX_COLOR_NB) {
index = D3ForceGraph.MAX_COLOR_NB - 1;
}
return D3ForceGraph.COLOR_SCHEME_20(index.toString());
return D3ForceGraph.COLOR_SCHEME(index.toString());
}
/**
@@ -1071,23 +1071,23 @@ export class D3ForceGraph implements GraphRenderer {
}
}
private dragstarted(d: D3Node) {
private dragstarted(d: D3Node, event: D3DragEvent<SVGGElement, D3Node, unknown>) {
this.isDragging = true;
if (!d3Event.active) {
if (!event.active) {
this.simulation.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
}
private dragged(d: D3Node) {
d.fx = d3Event.x;
d.fy = d3Event.y;
private dragged(d: D3Node, event: D3DragEvent<SVGGElement, D3Node, unknown>) {
d.fx = event.x;
d.fy = event.y;
}
private dragended(d: D3Node) {
private dragended(d: D3Node, event: D3DragEvent<SVGGElement, D3Node, unknown>) {
this.isDragging = false;
if (!d3Event.active) {
if (!event.active) {
this.simulation.alphaTarget(0);
}

View File

@@ -141,8 +141,6 @@ describe("getPkIdFromDocumentId", () => {
});
describe("GraphExplorer", () => {
const COLLECTION_RID = "collectionRid";
const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12;
const createMockProps = (): GraphExplorerProps => {
@@ -160,8 +158,6 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint",
databaseId: "databaseId",
collectionId: "collectionId",

View File

@@ -47,8 +47,6 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
@@ -1761,7 +1759,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return queryDocumentsPage(
this.props.collectionRid,
this.props.collectionId,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{

View File

@@ -17,8 +17,6 @@ interface Parameter {
graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
@@ -49,8 +47,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
collectionRid={this.params.collectionRid}
collectionSelfLink={this.params.collectionSelfLink}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}

View File

@@ -192,7 +192,6 @@ export class GremlinClient {
}
private static reportError(msg: string): void {
console.error(msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
Logger.logError(msg, GremlinClient.LOG_AREA);
}

View File

@@ -136,7 +136,6 @@ export class GremlinSimpleClient {
const data = typeof msg.data === "string" ? msg.data : new TextDecoder("utf-8").decode(msg.data);
return JSON.parse(data);
} catch (e) {
console.error(e, msg);
if (this.params.failureCallback) {
this.params.failureCallback(
null,

View File

@@ -0,0 +1,6 @@
/* eslint-disable */
export class GremlinClient {
constructor() {}
initialize() {}
execute() {}
}

View File

@@ -10,7 +10,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import { CommandBarUtil } from "./CommandBarUtil";
import * as CommandBarUtil from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";

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