Migrate DocumentsTab to React and add bulk delete and column resize (#1770)
* Document page now loads list of docs and displays selection * DocumentsTabV2 now properly loads documents, show partition keys and display first doc with proper selection behavior. Move it to its own folder. * Extract table in a separate component * Resizable columns on the document table * Fix selection behavior and some layout issue * Adding table scrolling * Fix NaN height issue * Fix NaN height issue * Fix column sizing + cell selection * Improvement in width size. Add Load More * Add react editor and pass column headers * Dynamic columns for pk * Fix initial columns size * Add nav buttons * Editing content updates buttons state * Discard and save buttons working * Fix save new document. Implement delete. * Remove debug display * Fix unexpand filter and reformat * Fix compil issues * Add refresh button * Update column header placeholder style * Implement delete multiple docs * Fix multi delete * Fix show/hide delete button * Fix selection behavior * Fix UX with buttons behavior and editor display * Fix UX issue with not discarding edit changes * Add some TODO's * Remove debugging info and reformat * Add mongo support * Fix build issues * Fix table header. Remove debug statement * Restore broken nosql * Fix mongo save new document/update document * Fix bugs with clicking on newly created documents * Fix comment * Fix double fetch issue when clicking on an item * Auto-select last document when saving new document * Fix resourceTokenPartitionKey code * Fix format * Fix isQueryCopilotSampleContainer flag * Fix unused code * Call tab when updating error flag * Destructure props to make useEffect dependencies work * Fix loadStartKey * minor update * Fix format * Add title to table * Fix table coming off its container with unwanted horizontal scrollbar * Increase table width. Fix eslint issue. * Move refresh documents button from table back to DocumentsTabV2 * Fix load more text centering * Don't show Load More if nothing to show * Fix columns min width * Add keyboard shortcuts * Add keyboard handlers to load more and refresh button * Add keyboard support to select table row * Disable eslint issue from fluent library * Connect cancel query button * Add Fluent V9 theme for Fabric (#1821) * Clean up dependencies and memoize internal functions and object. Move methods and object that don't depend on state outside of component. * Fix filter disappearing when clicking Apply Filter * Fix typo and format * Implement bulk delete for nosql * Replace filter ui components with fluent ui * Remove jquery calls * Migrate unit test to DocumentsTabV2 * Remove DocumentsTab and MongoDocumentsTab. Fix build issues. * Properly handle activetab * Remove comments and unused code * Port keyboard shortcuts from commitId1f4d0f2
* Port item editor shortcuts to improved Items tab branch (#1831) * set filter focus on Ctrl+Shift+F * implement filter enter/esc keybinds * remove debugging * Collapse filter when query is executed * Fix monaco editor not happy when parent is null * Fix how bulk delete operation gets called when no partition key * Fix update id list after delete * Fix deleteDocuments * Fix build issue * Fix bug in mongo delete * Fix mongo delete flow * Proper error handling in mongo * Handle >100 bulk delete operations * Add unit tests for DocumentsTableComponent * More improvements to table unit tests * Fix import. Disable selection test for now * Add more DocumentsTab unit react tests * Remove selection test * Add more unit tests. Add lcov coverage report to display in vscode * Move unit tests to correct file * Add unit test on command bar * Fix build issues * Add more unit tests * Remove unneeded call * Add DocumentsTab for Mongo API * Fix linting errors * Update fluent ui v9 dependency. Color columns separation. Fix refresh button placement to not interfere with header cell width. * Revert @fluentui/react-components to a safe version that compiles * Add confirmation window when documents have been deleted * Fix mongo unit tests * Fix format * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com> * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com> * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com> * Fix bug with markup. Simplify code. * Protect against creating React editor without parent node * Replace rendering tests with snapshot match * Add test screenshot to troubleshoot e2e test * Revert "Add test screenshot to troubleshoot e2e test" This reverts commit1b8138ade0
. * Attempt 2 at troubleshooting failing test * Revert "Attempt 2 at troubleshooting failing test" This reverts commit3e51a593bf
. * Delete button now shows if one or more rows are selected --------- Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com> Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>
This commit is contained in:
parent
19d1e0d1df
commit
36736882ee
|
@ -89,10 +89,7 @@ src/Explorer/Tables/TableEntityProcessor.ts
|
||||||
src/Explorer/Tables/Utilities.ts
|
src/Explorer/Tables/Utilities.ts
|
||||||
src/Explorer/Tabs/ConflictsTab.ts
|
src/Explorer/Tabs/ConflictsTab.ts
|
||||||
src/Explorer/Tabs/DatabaseSettingsTab.ts
|
src/Explorer/Tabs/DatabaseSettingsTab.ts
|
||||||
src/Explorer/Tabs/DocumentsTab.test.ts
|
|
||||||
src/Explorer/Tabs/DocumentsTab.ts
|
|
||||||
src/Explorer/Tabs/GraphTab.ts
|
src/Explorer/Tabs/GraphTab.ts
|
||||||
src/Explorer/Tabs/MongoDocumentsTab.ts
|
|
||||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
|
|
|
@ -31,7 +31,7 @@ module.exports = {
|
||||||
coveragePathIgnorePatterns: ["/node_modules/"],
|
coveragePathIgnorePatterns: ["/node_modules/"],
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
coverageReporters: ["json", "text", "cobertura"],
|
coverageReporters: ["json", "text", "cobertura", "lcov"],
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
|
|
|
@ -2264,33 +2264,33 @@ a:link {
|
||||||
width: 82px;
|
width: 82px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabdocuments .scrollable {
|
// .tabdocuments .scrollable {
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
overflow-y: auto;
|
// overflow-y: auto;
|
||||||
overflow-x: hidden;
|
// overflow-x: hidden;
|
||||||
padding-left: 5px;
|
// padding-left: 5px;
|
||||||
padding-right: 5px;
|
// padding-right: 5px;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.tabdocuments > .tabdocumentsGridElement {
|
// .tabdocuments > .tabdocumentsGridElement {
|
||||||
width: 50%;
|
// width: 50%;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.tabdocuments > .evenlySpacedHeader {
|
// .tabdocuments > .evenlySpacedHeader {
|
||||||
width: 30%;
|
// width: 30%;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.tabdocuments.scrollable:focus,
|
// .tabdocuments.scrollable:focus,
|
||||||
.tabdocuments.scrollable:active {
|
// .tabdocuments.scrollable:active {
|
||||||
outline: 1px dotted;
|
// outline: 1px dotted;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.tabdocuments .scrollable table td {
|
// .tabdocuments .scrollable table td {
|
||||||
white-space: nowrap;
|
// white-space: nowrap;
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
// text-overflow: ellipsis;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
|
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
@ -2316,10 +2316,9 @@ td a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMore {
|
.loadMore {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 30%;
|
text-align: center;
|
||||||
padding-top: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMore > a:focus {
|
.loadMore > a:focus {
|
||||||
|
@ -2558,10 +2557,12 @@ a:link {
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterdivs {
|
.filterdivs {
|
||||||
padding-top: 15px;
|
margin: 10px 0px;
|
||||||
height: 45px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
input {
|
||||||
|
line-height: 14px; // To correct vertical position of the down arrow of the input
|
||||||
|
outline: none; // Remove the dotted border on focus, because fluent has its own focus style (underlined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editFilterContainer {
|
.editFilterContainer {
|
||||||
|
@ -2578,6 +2579,18 @@ a:link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.documentsTab {
|
||||||
|
.documentsTable {
|
||||||
|
.documentsTableCell {
|
||||||
|
border-left: 1px solid @BaseMedium;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.documentsTableHeader {
|
||||||
|
border-bottom: 1px solid @BaseMedium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.querydropdown {
|
.querydropdown {
|
||||||
border: 1px solid @BaseMedium;
|
border: 1px solid @BaseMedium;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -46,6 +46,7 @@
|
||||||
"@types/lodash": "4.14.171",
|
"@types/lodash": "4.14.171",
|
||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
|
"@uiw/react-split": "5.9.3",
|
||||||
"@xmldom/xmldom": "0.7.13",
|
"@xmldom/xmldom": "0.7.13",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
|
@ -98,6 +99,7 @@
|
||||||
"react-splitter-layout": "4.0.0",
|
"react-splitter-layout": "4.0.0",
|
||||||
"react-string-format": "1.0.1",
|
"react-string-format": "1.0.1",
|
||||||
"react-youtube": "9.0.1",
|
"react-youtube": "9.0.1",
|
||||||
|
"react-window": "1.8.10",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.12",
|
||||||
"sanitize-html": "2.3.3",
|
"sanitize-html": "2.3.3",
|
||||||
|
@ -123,8 +125,8 @@
|
||||||
"@types/datatables.net": "1.10.28",
|
"@types/datatables.net": "1.10.28",
|
||||||
"@types/datatables.net-colreorder": "1.4.5",
|
"@types/datatables.net-colreorder": "1.4.5",
|
||||||
"@types/dom-to-image": "2.6.2",
|
"@types/dom-to-image": "2.6.2",
|
||||||
"@types/enzyme": "3.10.7",
|
"@types/enzyme": "3.10.12",
|
||||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
"@types/enzyme-adapter-react-16": "1.0.9",
|
||||||
"@types/hasher": "0.0.31",
|
"@types/hasher": "0.0.31",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/jquery": "3.5.29",
|
"@types/jquery": "3.5.29",
|
||||||
|
@ -136,6 +138,7 @@
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
"@types/react-splitter-layout": "3.0.1",
|
"@types/react-splitter-layout": "3.0.1",
|
||||||
|
"@types/react-window": "1.8.8",
|
||||||
"@types/sanitize-html": "1.27.2",
|
"@types/sanitize-html": "1.27.2",
|
||||||
"@types/sinon": "2.3.3",
|
"@types/sinon": "2.3.3",
|
||||||
"@types/styled-components": "5.1.1",
|
"@types/styled-components": "5.1.1",
|
||||||
|
@ -151,8 +154,8 @@
|
||||||
"create-file-webpack": "1.0.2",
|
"create-file-webpack": "1.0.2",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "6.8.1",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.5",
|
"enzyme-adapter-react-16": "1.15.8",
|
||||||
"enzyme-to-json": "3.6.1",
|
"enzyme-to-json": "3.6.2",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
"eslint-cli": "1.1.1",
|
"eslint-cli": "1.1.1",
|
||||||
"eslint-plugin-no-null": "1.0.2",
|
"eslint-plugin-no-null": "1.0.2",
|
||||||
|
@ -243,4 +246,4 @@
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts
|
||||||
|
index 644bcc3..f794760 100644
|
||||||
|
--- a/node_modules/@uiw/react-split/cjs/index.d.ts
|
||||||
|
+++ b/node_modules/@uiw/react-split/cjs/index.d.ts
|
||||||
|
@@ -56,5 +56,5 @@ export default class Split extends React.Component<SplitProps, SplitState> {
|
||||||
|
onMouseDown(paneNumber: number, env: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
|
||||||
|
onDragging(env: Event): void;
|
||||||
|
onDragEnd(): void;
|
||||||
|
- render(): import("react/jsx-runtime").JSX.Element;
|
||||||
|
+ render(): JSX.Element;
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export const getEntityName = (): string => {
|
export const getEntityName = (multiple?: boolean): string => {
|
||||||
if (userContext.apiType === "Mongo") {
|
if (userContext.apiType === "Mongo") {
|
||||||
return "document";
|
return multiple ? "documents" : "document";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "item";
|
return multiple ? "items" : "item";
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,7 @@ import * as _ from "underscore";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
|
||||||
import { useDatabases } from "../Explorer/useDatabases";
|
import { useDatabases } from "../Explorer/useDatabases";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
|
@ -162,10 +161,10 @@ export class QueriesClient {
|
||||||
{
|
{
|
||||||
partitionKey: QueriesClient.PartitionKey,
|
partitionKey: QueriesClient.PartitionKey,
|
||||||
partitionKeyProperties: ["id"],
|
partitionKeyProperties: ["id"],
|
||||||
} as DocumentsTab,
|
} as IDocumentIdContainer,
|
||||||
query,
|
query,
|
||||||
[query.queryName],
|
[query.queryName],
|
||||||
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
);
|
||||||
const options: any = { partitionKey: query.resourceId };
|
const options: any = { partitionKey: query.resourceId };
|
||||||
return deleteDocument(queriesCollection, documentId)
|
return deleteDocument(queriesCollection, documentId)
|
||||||
.then(
|
.then(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { BulkOperationType, OperationInput } from "@azure/cosmos";
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
@ -24,3 +25,58 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk delete documents
|
||||||
|
* @param collection
|
||||||
|
* @param documentId
|
||||||
|
* @returns array of ids that were successfully deleted
|
||||||
|
*/
|
||||||
|
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
|
const nbDocuments = documentIds.length;
|
||||||
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
|
try {
|
||||||
|
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||||
|
|
||||||
|
// Bulk can only delete 100 documents at a time
|
||||||
|
const BULK_DELETE_LIMIT = 100;
|
||||||
|
const promiseArray = [];
|
||||||
|
|
||||||
|
while (documentIds.length > 0) {
|
||||||
|
const documentIdsChunk = documentIds.splice(0, BULK_DELETE_LIMIT);
|
||||||
|
const operations: OperationInput[] = documentIdsChunk.map((documentId) => ({
|
||||||
|
id: documentId.id(),
|
||||||
|
// bulk delete: if not partition key is specified, do not pass empty array, but undefined
|
||||||
|
partitionKey:
|
||||||
|
documentId.partitionKeyValue &&
|
||||||
|
Array.isArray(documentId.partitionKeyValue) &&
|
||||||
|
documentId.partitionKeyValue.length === 0
|
||||||
|
? undefined
|
||||||
|
: documentId.partitionKeyValue,
|
||||||
|
operationType: BulkOperationType.Delete,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
||||||
|
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
||||||
|
});
|
||||||
|
promiseArray.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allResult = await Promise.all(promiseArray);
|
||||||
|
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
|
||||||
|
);
|
||||||
|
// TODO: handle case result.length != nbDocuments
|
||||||
|
return flatAllResult;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(
|
||||||
|
error,
|
||||||
|
"DeleteDocuments",
|
||||||
|
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -137,7 +137,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
|
|
||||||
this.rootNode.innerHTML = "";
|
this.rootNode.innerHTML = "";
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
createCallback(monaco?.editor?.create(this.rootNode, options));
|
try {
|
||||||
|
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||||
|
} catch (error) {
|
||||||
|
// This could happen if the parent node suddenly disappears during create()
|
||||||
|
console.error("Unable to create EditorReact", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.rootNode.innerHTML) {
|
if (this.rootNode.innerHTML) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -1,239 +0,0 @@
|
||||||
<div
|
|
||||||
class="tab-pane active tabdocuments flexContainer"
|
|
||||||
data-bind="
|
|
||||||
setTemplateReady: true,
|
|
||||||
attr:{
|
|
||||||
id: tabId
|
|
||||||
},
|
|
||||||
visible: isActive"
|
|
||||||
role="tabpanel"
|
|
||||||
>
|
|
||||||
<!-- ko if: false -->
|
|
||||||
<!-- Messagebox Ok Cancel- Start -->
|
|
||||||
<div class="messagebox-background">
|
|
||||||
<div class="messagebox">
|
|
||||||
<h2 class="messagebox-title">Title</h2>
|
|
||||||
<div class="messagebox-text" tabindex="0">Text</div>
|
|
||||||
<div class="messagebox-buttons">
|
|
||||||
<div class="messagebox-buttons-container">
|
|
||||||
<button value="ok" class="messagebox-button-primary">Ok</button>
|
|
||||||
<button value="cancel" class="messagebox-button-default">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Messagebox OK Cancel - End -->
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- Filter - Start -->
|
|
||||||
<div class="filterdivs" data-bind="visible: isFilterCreated">
|
|
||||||
<!-- Read-only Filter - Start -->
|
|
||||||
<div class="filterDocCollapsed" data-bind="visible: !isFilterExpanded() && !isPreferredApiMongoDB">
|
|
||||||
<span class="selectQuery">SELECT * FROM c</span>
|
|
||||||
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
|
|
||||||
<button class="filterbtnstyle queryButton" data-bind="click: onShowFilterClick">Edit Filter</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="filterDocCollapsed"
|
|
||||||
data-bind="
|
|
||||||
visible: !isFilterExpanded() && isPreferredApiMongoDB"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="selectQuery"
|
|
||||||
data-bind="
|
|
||||||
visible: appliedFilter().length > 0"
|
|
||||||
>Filter :
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="noFilterApplied"
|
|
||||||
data-bind="
|
|
||||||
visible: !appliedFilter().length > 0"
|
|
||||||
>No filter applied</span
|
|
||||||
>
|
|
||||||
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
|
|
||||||
<button
|
|
||||||
class="filterbtnstyle queryButton"
|
|
||||||
data-bind="
|
|
||||||
click: onShowFilterClick"
|
|
||||||
>
|
|
||||||
Edit Filter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<!-- Read-only Filter - End -->
|
|
||||||
|
|
||||||
<!-- Editable Filter - start -->
|
|
||||||
<div
|
|
||||||
class="filterDocExpanded"
|
|
||||||
data-bind="
|
|
||||||
visible: isFilterExpanded"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div class="editFilterContainer">
|
|
||||||
<span class="filterspan" data-bind="visible: !isPreferredApiMongoDB"> SELECT * FROM c </span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
list="filtersList"
|
|
||||||
class="querydropdown"
|
|
||||||
title="Type a query predicate or choose one from the list."
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
|
|
||||||
},
|
|
||||||
css: { placeholderVisible: filterContent().length === 0 },
|
|
||||||
textInput: filterContent,
|
|
||||||
event: { keydown: onFilterKeyDown }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist
|
|
||||||
id="filtersList"
|
|
||||||
data-bind="
|
|
||||||
foreach: lastFilterContents"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
data-bind="
|
|
||||||
value: $data"
|
|
||||||
></option>
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<span class="filterbuttonpad">
|
|
||||||
<button
|
|
||||||
class="filterbtnstyle queryButton"
|
|
||||||
data-bind="
|
|
||||||
click: refreshDocumentsGrid.bind($data, true),
|
|
||||||
enable: applyFilterButton.enabled"
|
|
||||||
aria-label="Apply filter"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span class="filterbuttonpad">
|
|
||||||
<button
|
|
||||||
class="filterbtnstyle queryButton"
|
|
||||||
data-bind="
|
|
||||||
visible: !isPreferredApiMongoDB && isExecuting,
|
|
||||||
click: onAbortQueryClick"
|
|
||||||
aria-label="Cancel Query"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel Query
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="filterclose"
|
|
||||||
role="button"
|
|
||||||
aria-label="close filter"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="
|
|
||||||
click: onHideFilterClick, event: { keydown: onCloseButtonKeyDown }"
|
|
||||||
>
|
|
||||||
<img src="/close-black.svg" style="height: 14px; width: 14px" alt="Hide filter" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Editable Filter - End -->
|
|
||||||
</div>
|
|
||||||
<!-- Filter - End -->
|
|
||||||
|
|
||||||
<!-- Ids and Editor - Start -->
|
|
||||||
<div class="documentsTabGridAndEditor">
|
|
||||||
<div class="documentsContainerWithSplitter" , data-bind="attr: { id: documentContentsContainerId }">
|
|
||||||
<div class="flexContainer">
|
|
||||||
<!-- Document Ids - Start -->
|
|
||||||
<div
|
|
||||||
class="documentsGridHeaderContainer tabdocuments scrollable"
|
|
||||||
data-bind="
|
|
||||||
attr: {
|
|
||||||
id: documentContentsGridId,
|
|
||||||
tabindex: documentIds().length <= 0 ? -1 : 0
|
|
||||||
},
|
|
||||||
style: { height: dataContentsGridScrollHeight },
|
|
||||||
event: { keydown: accessibleDocumentList.onKeyDown }"
|
|
||||||
>
|
|
||||||
<table id="tabsTable" class="table table-hover can-select dataTable">
|
|
||||||
<thead id="theadcontent">
|
|
||||||
<tr>
|
|
||||||
<th class="documentsGridHeader" data-bind="text: idHeader" tabindex="0"></th>
|
|
||||||
<!-- ko if: showPartitionKey -->
|
|
||||||
<!-- ko foreach: partitionKeyPropertyHeaders -->
|
|
||||||
<th
|
|
||||||
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
|
|
||||||
data-bind="
|
|
||||||
attr: {
|
|
||||||
title: $data
|
|
||||||
},
|
|
||||||
text: $data"
|
|
||||||
tabindex="0"
|
|
||||||
></th>
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- /ko -->
|
|
||||||
<th
|
|
||||||
class="refreshColHeader"
|
|
||||||
role="button"
|
|
||||||
aria-label="Refresh documents"
|
|
||||||
data-bind="event: { keydown: onRefreshButtonKeyDown }"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="refreshcol"
|
|
||||||
src="/refresh-cosmos.svg"
|
|
||||||
data-bind="click: refreshDocumentsGrid.bind($data, false)"
|
|
||||||
alt="Refresh documents"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tbodycontent">
|
|
||||||
<!-- ko foreach: documentIds -->
|
|
||||||
<tr
|
|
||||||
class="pointer accessibleListElement"
|
|
||||||
data-bind="
|
|
||||||
click: $data.click,
|
|
||||||
css: {
|
|
||||||
gridRowSelected: $parent.selectedDocumentId && $parent.selectedDocumentId() && $parent.selectedDocumentId().rid === $data.rid,
|
|
||||||
gridRowHighlighted: $parent.accessibleDocumentList.currentItem() && $parent.accessibleDocumentList.currentItem().rid === $data.rid
|
|
||||||
}"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<td class="tabdocumentsGridElement"><a data-bind="text: $data.id, attr: { title: $data.id }"></a></td>
|
|
||||||
<!-- ko if: $data.partitionKeyProperties -->
|
|
||||||
<!-- ko foreach: $data.stringPartitionKeyValues -->
|
|
||||||
<td class="tabdocumentsGridElement" data-bind="colspan: $parent.stringPartitionKeyValues.length + 1">
|
|
||||||
<a data-bind="text: $data, attr: { title: $data }"></a>
|
|
||||||
</td>
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- /ko -->
|
|
||||||
</tr>
|
|
||||||
<!-- /ko -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="loadMore">
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
|
|
||||||
tabindex="0"
|
|
||||||
>Load more</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<!-- Document Ids - End -->
|
|
||||||
<!-- Splitter -->
|
|
||||||
</div>
|
|
||||||
<div class="splitter ui-resizable-handle ui-resizable-e colResizePointer" id="h_splitter2"></div>
|
|
||||||
</div>
|
|
||||||
<div class="documentWaterMark" data-bind="visible: !shouldShowEditor()">
|
|
||||||
<p><img src="/DocumentWaterMark.svg" alt="Document WaterMark" /></p>
|
|
||||||
<p class="documentWaterMarkText">Create new or work with existing document(s).</p>
|
|
||||||
</div>
|
|
||||||
<!-- Editor - Start -->
|
|
||||||
<json-editor
|
|
||||||
class="editorDivContent"
|
|
||||||
data-bind="visible: shouldShowEditor, css: { mongoDocumentEditor: isPreferredApiMongoDB }"
|
|
||||||
params="{content: initialDocumentContent, isReadOnly: false,lineNumbers: 'on',ariaLabel: 'Document editor',
|
|
||||||
updatedContent: selectedDocumentContent}"
|
|
||||||
></json-editor>
|
|
||||||
<!-- Editor - End -->
|
|
||||||
</div>
|
|
||||||
<!-- Ids and Editor - End -->
|
|
||||||
</div>
|
|
|
@ -1,152 +0,0 @@
|
||||||
import * as ko from "knockout";
|
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { updateUserContext } from "../../UserContext";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import DocumentId from "../Tree/DocumentId";
|
|
||||||
import DocumentsTab from "./DocumentsTab";
|
|
||||||
|
|
||||||
describe("Documents tab", () => {
|
|
||||||
describe("buildQuery", () => {
|
|
||||||
it("should generate the right select query for SQL API", () => {
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.buildQuery("")).toContain("select");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("showPartitionKey", () => {
|
|
||||||
const explorer = new Explorer();
|
|
||||||
const mongoExplorer = new Explorer();
|
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableGremlin" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
database: {
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
},
|
|
||||||
container: explorer,
|
|
||||||
});
|
|
||||||
|
|
||||||
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
database: {
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
},
|
|
||||||
partitionKey: {
|
|
||||||
paths: ["/foo"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
systemKey: true,
|
|
||||||
},
|
|
||||||
container: explorer,
|
|
||||||
});
|
|
||||||
|
|
||||||
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
database: {
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
},
|
|
||||||
partitionKey: {
|
|
||||||
paths: ["/foo"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
systemKey: false,
|
|
||||||
},
|
|
||||||
container: explorer,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
database: {
|
|
||||||
id: ko.observable<string>("foo"),
|
|
||||||
},
|
|
||||||
partitionKey: {
|
|
||||||
paths: ["/foo"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
systemKey: true,
|
|
||||||
},
|
|
||||||
container: mongoExplorer,
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false for null or undefined collection", () => {
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.showPartitionKey).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false for null or undefined partitionKey", () => {
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
collection: collectionWithoutPartitionKey,
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.showPartitionKey).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be true for non-Mongo accounts with system partitionKey", () => {
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
collection: collectionWithSystemPartitionKey,
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.showPartitionKey).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false for Mongo accounts with system partitionKey", () => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "Mongo",
|
|
||||||
});
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
collection: mongoCollectionWithSystemPartitionKey,
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.showPartitionKey).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be true for non-system partitionKey", () => {
|
|
||||||
const documentsTab = new DocumentsTab({
|
|
||||||
collection: collectionWithNonSystemPartitionKey,
|
|
||||||
partitionKey: null,
|
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
|
||||||
title: "",
|
|
||||||
tabPath: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(documentsTab.showPartitionKey).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,476 @@
|
||||||
|
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||||
|
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||||
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import {
|
||||||
|
ButtonsDependencies,
|
||||||
|
DELETE_BUTTON_ID,
|
||||||
|
DISCARD_BUTTON_ID,
|
||||||
|
DocumentsTabComponent,
|
||||||
|
IDocumentsTabComponentProps,
|
||||||
|
NEW_DOCUMENT_BUTTON_ID,
|
||||||
|
SAVE_BUTTON_ID,
|
||||||
|
UPDATE_BUTTON_ID,
|
||||||
|
UPLOAD_BUTTON_ID,
|
||||||
|
buildQuery,
|
||||||
|
getDiscardExistingDocumentChangesButtonState,
|
||||||
|
getDiscardNewDocumentChangesButtonState,
|
||||||
|
getSaveExistingDocumentButtonState,
|
||||||
|
getSaveNewDocumentButtonState,
|
||||||
|
getTabsButtons,
|
||||||
|
showPartitionKey,
|
||||||
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
|
import { ReactWrapper, ShallowWrapper, mount, shallow } from "enzyme";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import React from "react";
|
||||||
|
import { act } from "react-dom/test-utils";
|
||||||
|
import { DatabaseAccount, DocumentId } from "../../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { updateUserContext } from "../../../UserContext";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
|
||||||
|
jest.mock("Common/dataAccess/queryDocuments", () => ({
|
||||||
|
queryDocuments: jest.fn(() => ({
|
||||||
|
// Omit headers, because we can't mock a private field and we don't need to test it
|
||||||
|
fetchNext: (): Promise<Omit<FeedResponse<ItemDefinition & Resource>, "headers">> =>
|
||||||
|
Promise.resolve({
|
||||||
|
resources: [{ id: "id", _rid: "rid", _self: "self", _etag: "etag", _ts: 123 }],
|
||||||
|
hasMoreResults: false,
|
||||||
|
diagnostics: undefined,
|
||||||
|
|
||||||
|
continuation: undefined,
|
||||||
|
continuationToken: undefined,
|
||||||
|
queryMetrics: "queryMetrics",
|
||||||
|
requestCharge: 1,
|
||||||
|
activityId: "activityId",
|
||||||
|
indexMetrics: "indexMetrics",
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
|
||||||
|
jest.mock("Common/dataAccess/readDocument", () => ({
|
||||||
|
readDocument: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
container: undefined,
|
||||||
|
id: "id",
|
||||||
|
property: PROPERTY_VALUE,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
|
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Dialog", () => ({
|
||||||
|
useDialog: {
|
||||||
|
getState: jest.fn(() => ({
|
||||||
|
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
||||||
|
showOkModalDialog: () => {},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/dataAccess/deleteDocument", () => ({
|
||||||
|
deleteDocuments: jest.fn((collection: ViewModels.CollectionBase, documentIds: DocumentId[]) =>
|
||||||
|
Promise.resolve(documentIds),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
|
let newWrapper;
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, amount));
|
||||||
|
newWrapper = wrapper.update();
|
||||||
|
});
|
||||||
|
return newWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Documents tab (noSql API)", () => {
|
||||||
|
describe("buildQuery", () => {
|
||||||
|
it("should generate the right select query for SQL API", () => {
|
||||||
|
expect(buildQuery(false, "")).toContain("select");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showPartitionKey", () => {
|
||||||
|
const explorer = new Explorer();
|
||||||
|
const mongoExplorer = new Explorer();
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableGremlin" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const collectionWithoutPartitionKey: ViewModels.Collection = {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
databaseId: "foo",
|
||||||
|
container: explorer,
|
||||||
|
} as ViewModels.Collection;
|
||||||
|
|
||||||
|
const collectionWithSystemPartitionKey: ViewModels.Collection = {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
databaseId: "foo",
|
||||||
|
partitionKey: {
|
||||||
|
paths: ["/foo"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
systemKey: true,
|
||||||
|
},
|
||||||
|
container: explorer,
|
||||||
|
} as ViewModels.Collection;
|
||||||
|
|
||||||
|
const collectionWithNonSystemPartitionKey: ViewModels.Collection = {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
databaseId: "foo",
|
||||||
|
partitionKey: {
|
||||||
|
paths: ["/foo"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
systemKey: false,
|
||||||
|
},
|
||||||
|
container: explorer,
|
||||||
|
} as ViewModels.Collection;
|
||||||
|
|
||||||
|
const mongoCollectionWithSystemPartitionKey: ViewModels.Collection = {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
databaseId: "foo",
|
||||||
|
partitionKey: {
|
||||||
|
paths: ["/foo"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
systemKey: true,
|
||||||
|
},
|
||||||
|
container: mongoExplorer,
|
||||||
|
} as ViewModels.Collection;
|
||||||
|
|
||||||
|
it("should be false for null or undefined collection", () => {
|
||||||
|
expect(showPartitionKey(undefined, false)).toBe(false);
|
||||||
|
expect(showPartitionKey(null, false)).toBe(false);
|
||||||
|
expect(showPartitionKey(undefined, true)).toBe(false);
|
||||||
|
expect(showPartitionKey(null, true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false for null or undefined partitionKey", () => {
|
||||||
|
expect(showPartitionKey(collectionWithoutPartitionKey, false)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true for non-Mongo accounts with system partitionKey", () => {
|
||||||
|
expect(showPartitionKey(collectionWithSystemPartitionKey, false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false for Mongo accounts with system partitionKey", () => {
|
||||||
|
expect(showPartitionKey(mongoCollectionWithSystemPartitionKey, true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true for non-system partitionKey", () => {
|
||||||
|
expect(showPartitionKey(collectionWithNonSystemPartitionKey, false)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when getting command bar button state", () => {
|
||||||
|
describe("should set Save New Document state", () => {
|
||||||
|
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: true });
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const state = getSaveNewDocumentButtonState(testCase.state);
|
||||||
|
it(`enable for ${testCase.state}`, () => {
|
||||||
|
expect(state.enabled).toBe(testCase.enabled);
|
||||||
|
});
|
||||||
|
it(`visible for ${testCase.state}`, () => {
|
||||||
|
expect(state.visible).toBe(testCase.visible);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should set Discard New Document state", () => {
|
||||||
|
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: true, visible: true });
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid,
|
||||||
|
enabled: false,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const state = getDiscardNewDocumentChangesButtonState(testCase.state);
|
||||||
|
it(`enable for ${testCase.state}`, () => {
|
||||||
|
expect(state.enabled).toBe(testCase.enabled);
|
||||||
|
});
|
||||||
|
it(`visible for ${testCase.state}`, () => {
|
||||||
|
expect(state.visible).toBe(testCase.visible);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should set Save Existing Document state", () => {
|
||||||
|
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits,
|
||||||
|
enabled: false,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid,
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid,
|
||||||
|
enabled: false,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const state = getSaveExistingDocumentButtonState(testCase.state);
|
||||||
|
it(`enable for ${testCase.state}`, () => {
|
||||||
|
expect(state.enabled).toBe(testCase.enabled);
|
||||||
|
});
|
||||||
|
it(`visible for ${testCase.state}`, () => {
|
||||||
|
expect(state.visible).toBe(testCase.visible);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should set Discard Existing Document state", () => {
|
||||||
|
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits,
|
||||||
|
enabled: false,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid,
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid,
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const state = getDiscardExistingDocumentChangesButtonState(testCase.state);
|
||||||
|
it(`enable for ${testCase.state}`, () => {
|
||||||
|
expect(state.enabled).toBe(testCase.enabled);
|
||||||
|
});
|
||||||
|
it(`visible for ${testCase.state}`, () => {
|
||||||
|
expect(state.visible).toBe(testCase.visible);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should set Delete Existing Document state", () => {
|
||||||
|
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
|
||||||
|
testCases.add({ state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, enabled: true, visible: true });
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid,
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
testCases.add({
|
||||||
|
state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid,
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Do not get tabs button for Fabric readonly", () => {
|
||||||
|
updateConfigContext({ platform: Platform.Fabric });
|
||||||
|
updateUserContext({
|
||||||
|
fabricContext: {
|
||||||
|
connectionId: "test",
|
||||||
|
databaseConnectionInfo: undefined,
|
||||||
|
isReadOnly: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = getTabsButtons({} as ButtonsDependencies);
|
||||||
|
expect(buttons.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when rendered", () => {
|
||||||
|
const createMockProps = (): IDocumentsTabComponentProps => ({
|
||||||
|
isPreferredApiMongoDB: false,
|
||||||
|
documentIds: [],
|
||||||
|
collection: undefined,
|
||||||
|
partitionKey: undefined,
|
||||||
|
onLoadStartKey: 0,
|
||||||
|
tabTitle: "",
|
||||||
|
onExecutionErrorChange: (isExecutionError: boolean): void => {
|
||||||
|
isExecutionError;
|
||||||
|
},
|
||||||
|
onIsExecutingChange: (isExecuting: boolean): void => {
|
||||||
|
isExecuting;
|
||||||
|
},
|
||||||
|
isTabActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const props: IDocumentsTabComponentProps = createMockProps();
|
||||||
|
wrapper = shallow(<DocumentsTabComponent {...props} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the page", () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on Edit filter should render the Apply Filter button", () => {
|
||||||
|
wrapper
|
||||||
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
|
.at(0)
|
||||||
|
.simulate("click");
|
||||||
|
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on Edit filter should render input for filter", () => {
|
||||||
|
wrapper
|
||||||
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
|
.at(0)
|
||||||
|
.simulate("click");
|
||||||
|
expect(wrapper.find("#filterInput").exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Command bar buttons", () => {
|
||||||
|
const createMockProps = (): IDocumentsTabComponentProps => ({
|
||||||
|
isPreferredApiMongoDB: false,
|
||||||
|
documentIds: [],
|
||||||
|
collection: {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
container: new Explorer(),
|
||||||
|
partitionKey: {
|
||||||
|
kind: "MultiHash",
|
||||||
|
paths: ["/pkey1", "/pkey2", "/pkey3"],
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
partitionKeyProperties: ["pkey1", "pkey2", "pkey3"],
|
||||||
|
partitionKeyPropertyHeaders: ["/pkey1", "/pkey2", "/pkey3"],
|
||||||
|
} as ViewModels.CollectionBase,
|
||||||
|
partitionKey: undefined,
|
||||||
|
onLoadStartKey: 0,
|
||||||
|
tabTitle: "",
|
||||||
|
onExecutionErrorChange: (isExecutionError: boolean): void => {
|
||||||
|
isExecutionError;
|
||||||
|
},
|
||||||
|
onIsExecutingChange: (isExecuting: boolean): void => {
|
||||||
|
isExecuting;
|
||||||
|
},
|
||||||
|
isTabActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
updateConfigContext({ platform: Platform.Hosted });
|
||||||
|
|
||||||
|
const props: IDocumentsTabComponentProps = createMockProps();
|
||||||
|
|
||||||
|
wrapper = mount(<DocumentsTabComponent {...props} />);
|
||||||
|
wrapper = await waitForComponentToPaint(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders by default the first document", async () => {
|
||||||
|
expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default buttons", async () => {
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPLOAD_BUTTON_ID)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on New Document should show editor with new document", () => {
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on New Document should show Save and Discard buttons", () => {
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,195 @@
|
||||||
|
import { deleteDocument } from "Common/MongoProxyClient";
|
||||||
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import {
|
||||||
|
DELETE_BUTTON_ID,
|
||||||
|
DISCARD_BUTTON_ID,
|
||||||
|
DocumentsTabComponent,
|
||||||
|
IDocumentsTabComponentProps,
|
||||||
|
NEW_DOCUMENT_BUTTON_ID,
|
||||||
|
SAVE_BUTTON_ID,
|
||||||
|
UPDATE_BUTTON_ID,
|
||||||
|
buildQuery,
|
||||||
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
|
import { ReactWrapper, ShallowWrapper, mount } from "enzyme";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import React from "react";
|
||||||
|
import { act } from "react-dom/test-utils";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
|
||||||
|
jest.requireActual("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
|
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
|
||||||
|
|
||||||
|
jest.mock("Common/MongoProxyClient", () => ({
|
||||||
|
queryDocuments: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
continuationToken: "",
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
_rid: "_rid",
|
||||||
|
_self: "_self",
|
||||||
|
_etag: "etag",
|
||||||
|
_ts: 1234,
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
headers: {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
readDocument: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
_rid: "_rid1",
|
||||||
|
_self: "_self1",
|
||||||
|
_etag: "etag1",
|
||||||
|
property: PROPERTY_VALUE,
|
||||||
|
_ts: 5678,
|
||||||
|
id: "id1",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
deleteDocument: jest.fn(() => Promise.resolve()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
|
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Dialog", () => ({
|
||||||
|
useDialog: {
|
||||||
|
getState: jest.fn(() => ({
|
||||||
|
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
||||||
|
showOkModalDialog: () => {},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
|
let newWrapper;
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, amount));
|
||||||
|
newWrapper = wrapper.update();
|
||||||
|
});
|
||||||
|
return newWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Documents tab (Mongo API)", () => {
|
||||||
|
describe("buildQuery", () => {
|
||||||
|
it("should generate the right select query for SQL API", () => {
|
||||||
|
expect(buildQuery(true, "")).toContain("{}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Command bar buttons", () => {
|
||||||
|
const createMockProps = (): IDocumentsTabComponentProps => ({
|
||||||
|
isPreferredApiMongoDB: true,
|
||||||
|
documentIds: [],
|
||||||
|
collection: {
|
||||||
|
id: ko.observable<string>("foo"),
|
||||||
|
container: new Explorer(),
|
||||||
|
partitionKey: {
|
||||||
|
kind: "Hash",
|
||||||
|
paths: ["/pkey"],
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
partitionKeyProperties: ["pkey"],
|
||||||
|
partitionKeyPropertyHeaders: ["/pkey"],
|
||||||
|
databaseId: "databaseId",
|
||||||
|
self: "self",
|
||||||
|
rawDataModel: undefined,
|
||||||
|
selectedSubnodeKind: undefined,
|
||||||
|
children: undefined,
|
||||||
|
isCollectionExpanded: undefined,
|
||||||
|
onDocumentDBDocumentsClick: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
onNewQueryClick: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
expandCollection: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
collapseCollection: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
getDatabase: (): ViewModels.Database => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
nodeKind: "nodeKind",
|
||||||
|
rid: "rid",
|
||||||
|
},
|
||||||
|
partitionKey: undefined,
|
||||||
|
onLoadStartKey: 0,
|
||||||
|
tabTitle: "",
|
||||||
|
onExecutionErrorChange: (isExecutionError: boolean): void => {
|
||||||
|
isExecutionError;
|
||||||
|
},
|
||||||
|
onIsExecutingChange: (isExecuting: boolean): void => {
|
||||||
|
isExecuting;
|
||||||
|
},
|
||||||
|
isTabActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
updateConfigContext({ platform: Platform.Hosted });
|
||||||
|
|
||||||
|
const props: IDocumentsTabComponentProps = createMockProps();
|
||||||
|
|
||||||
|
wrapper = mount(<DocumentsTabComponent {...props} />);
|
||||||
|
wrapper = await waitForComponentToPaint(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders by default the first document", async () => {
|
||||||
|
expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default buttons", async () => {
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on New Document should show editor with new document", () => {
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on New Document should show Save and Discard buttons", () => {
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
|
const mockDeleteDocument = deleteDocument as jest.Mock;
|
||||||
|
mockDeleteDocument.mockClear();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDeleteDocument).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { TableRowId } from "@fluentui/react-components";
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
|
const PARTITION_KEY_HEADER = "partitionKey";
|
||||||
|
const ID_HEADER = "id";
|
||||||
|
|
||||||
|
describe("DocumentsTableComponent", () => {
|
||||||
|
const createMockProps = (): IDocumentsTableComponentProps => ({
|
||||||
|
items: [
|
||||||
|
{ [ID_HEADER]: "1", [PARTITION_KEY_HEADER]: "pk1" },
|
||||||
|
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
||||||
|
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
||||||
|
],
|
||||||
|
onItemClicked: (): void => {},
|
||||||
|
onSelectedRowsChange: (): void => {},
|
||||||
|
selectedRows: new Set<TableRowId>(),
|
||||||
|
size: {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
columnHeaders: {
|
||||||
|
idHeader: ID_HEADER,
|
||||||
|
partitionKeyHeaders: [PARTITION_KEY_HEADER],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render documents and partition keys in header", () => {
|
||||||
|
const props: IDocumentsTableComponentProps = createMockProps();
|
||||||
|
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,271 @@
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
MenuPopover,
|
||||||
|
MenuTrigger,
|
||||||
|
TableRowData as RowStateBase,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableCellLayout,
|
||||||
|
TableColumnDefinition,
|
||||||
|
TableColumnSizingOptions,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
TableRowId,
|
||||||
|
TableSelectionCell,
|
||||||
|
createTableColumn,
|
||||||
|
useArrowNavigationGroup,
|
||||||
|
useTableColumnSizing_unstable,
|
||||||
|
useTableFeatures,
|
||||||
|
useTableSelection,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||||
|
|
||||||
|
export type DocumentsTableComponentItem = {
|
||||||
|
id: string;
|
||||||
|
} & Record<string, string>;
|
||||||
|
|
||||||
|
export type ColumnHeaders = {
|
||||||
|
idHeader: string;
|
||||||
|
partitionKeyHeaders: string[];
|
||||||
|
};
|
||||||
|
export interface IDocumentsTableComponentProps {
|
||||||
|
items: DocumentsTableComponentItem[];
|
||||||
|
onItemClicked: (index: number) => void;
|
||||||
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
|
selectedRows: Set<TableRowId>;
|
||||||
|
size: { height: number; width: number };
|
||||||
|
columnHeaders: ColumnHeaders;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
|
onClick: (e: React.MouseEvent) => void;
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||||
|
selected: boolean;
|
||||||
|
appearance: "brand" | "none";
|
||||||
|
}
|
||||||
|
interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
||||||
|
data: TableRowData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
|
items,
|
||||||
|
onItemClicked,
|
||||||
|
onSelectedRowsChange,
|
||||||
|
selectedRows,
|
||||||
|
style,
|
||||||
|
size,
|
||||||
|
columnHeaders,
|
||||||
|
}: IDocumentsTableComponentProps) => {
|
||||||
|
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(undefined);
|
||||||
|
|
||||||
|
const initialSizingOptions: TableColumnSizingOptions = {
|
||||||
|
id: {
|
||||||
|
idealWidth: 280,
|
||||||
|
minWidth: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
columnHeaders.partitionKeyHeaders.forEach((pkHeader) => {
|
||||||
|
initialSizingOptions[pkHeader] = {
|
||||||
|
idealWidth: 200,
|
||||||
|
minWidth: 50,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
|
||||||
|
|
||||||
|
const onColumnResize = React.useCallback((_, { columnId, width }) => {
|
||||||
|
setColumnSizingOptions((state) => ({
|
||||||
|
...state,
|
||||||
|
[columnId]: {
|
||||||
|
...state[columnId],
|
||||||
|
idealWidth: width,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
|
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
createTableColumn<DocumentsTableComponentItem>({
|
||||||
|
columnId: "id",
|
||||||
|
compare: (a, b) => a.id.localeCompare(b.id),
|
||||||
|
renderHeaderCell: () => columnHeaders.idHeader,
|
||||||
|
renderCell: (item) => (
|
||||||
|
<TableCellLayout truncate title={item.id}>
|
||||||
|
{item.id}
|
||||||
|
</TableCellLayout>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
].concat(
|
||||||
|
columnHeaders.partitionKeyHeaders.map((pkHeader) =>
|
||||||
|
createTableColumn<DocumentsTableComponentItem>({
|
||||||
|
columnId: pkHeader,
|
||||||
|
compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]),
|
||||||
|
// Show Refresh button on last column
|
||||||
|
renderHeaderCell: () => <span title={pkHeader}>{pkHeader}</span>,
|
||||||
|
renderCell: (item) => (
|
||||||
|
<TableCellLayout truncate title={item[pkHeader]}>
|
||||||
|
{item[pkHeader]}
|
||||||
|
</TableCellLayout>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[columnHeaders],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onIdClicked = useCallback((index: number) => onSelectedRowsChange(new Set([index])), [onSelectedRowsChange]);
|
||||||
|
|
||||||
|
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
|
||||||
|
const { item, selected, appearance, onClick, onKeyDown } = data[index];
|
||||||
|
return (
|
||||||
|
<TableRow aria-rowindex={index + 2} style={style} key={item.id} aria-selected={selected} appearance={appearance}>
|
||||||
|
<TableSelectionCell
|
||||||
|
checked={selected}
|
||||||
|
checkboxIndicator={{ "aria-label": "Select row" }}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.columnId}
|
||||||
|
className="documentsTableCell"
|
||||||
|
onClick={(/* e */) => onSelectedRowsChange(new Set<TableRowId>([index]))}
|
||||||
|
onKeyDown={() => onIdClicked(index)}
|
||||||
|
{...columnSizing.getTableCellProps(column.columnId)}
|
||||||
|
tabIndex={column.columnId === "id" ? 0 : -1}
|
||||||
|
>
|
||||||
|
{column.renderCell(item)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRows,
|
||||||
|
columnSizing_unstable: columnSizing,
|
||||||
|
tableRef,
|
||||||
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||||
|
} = useTableFeatures(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
items,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
useTableColumnSizing_unstable({ columnSizingOptions, onColumnResize }),
|
||||||
|
useTableSelection({
|
||||||
|
selectionMode: "multiselect",
|
||||||
|
selectedItems: selectedRows,
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows: TableRowData[] = getRows((row) => {
|
||||||
|
const selected = isRowSelected(row.rowId);
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId),
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleRow(e, row.rowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected,
|
||||||
|
appearance: selected ? ("brand" as const) : ("none" as const),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAllKeydown = React.useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === " ") {
|
||||||
|
toggleAllRows(e);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toggleAllRows],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load document depending on selection
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRows.size === 1 && items.length > 0) {
|
||||||
|
const newActiveItemIndex = selectedRows.values().next().value;
|
||||||
|
if (newActiveItemIndex !== activeItemIndex) {
|
||||||
|
onItemClicked(newActiveItemIndex);
|
||||||
|
setActiveItemIndex(newActiveItemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedRows, items]);
|
||||||
|
|
||||||
|
// Cell keyboard navigation
|
||||||
|
const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" });
|
||||||
|
|
||||||
|
// TODO: Bug in fluent UI typings that requires any here
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const tableProps: any = {
|
||||||
|
"aria-label": "Filtered documents table",
|
||||||
|
role: "grid",
|
||||||
|
...columnSizing.getTableProps(),
|
||||||
|
...keyboardNavAttr,
|
||||||
|
size: "extra-small",
|
||||||
|
ref: tableRef,
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className="documentsTable" noNativeElements {...tableProps}>
|
||||||
|
<TableHeader className="documentsTableHeader">
|
||||||
|
<TableRow style={{ width: size ? size.width - 15 : "100%" }}>
|
||||||
|
<TableSelectionCell
|
||||||
|
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
||||||
|
onClick={toggleAllRows}
|
||||||
|
onKeyDown={toggleAllKeydown}
|
||||||
|
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
||||||
|
/>
|
||||||
|
{columns.map((column /* index */) => (
|
||||||
|
<Menu openOnContext key={column.columnId}>
|
||||||
|
<MenuTrigger>
|
||||||
|
<TableHeaderCell
|
||||||
|
className="documentsTableCell"
|
||||||
|
key={column.columnId}
|
||||||
|
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
||||||
|
>
|
||||||
|
{column.renderHeaderCell()}
|
||||||
|
</TableHeaderCell>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
||||||
|
Keyboard Column Resizing
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<List
|
||||||
|
height={size !== undefined ? size.height - 32 /* table header */ - 21 /* load more */ : 0}
|
||||||
|
itemCount={items.length}
|
||||||
|
itemSize={30}
|
||||||
|
width={size ? size.width : 0}
|
||||||
|
itemData={rows}
|
||||||
|
style={{ overflowY: "scroll" }}
|
||||||
|
>
|
||||||
|
{RenderRow}
|
||||||
|
</List>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,558 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||||
|
<FluentProvider
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme={
|
||||||
|
Object {
|
||||||
|
"borderRadiusCircular": "10000px",
|
||||||
|
"borderRadiusLarge": "6px",
|
||||||
|
"borderRadiusMedium": "4px",
|
||||||
|
"borderRadiusNone": "0",
|
||||||
|
"borderRadiusSmall": "2px",
|
||||||
|
"borderRadiusXLarge": "8px",
|
||||||
|
"colorBackgroundOverlay": "rgba(0, 0, 0, 0.4)",
|
||||||
|
"colorBrandBackground": "#117865",
|
||||||
|
"colorBrandBackground2": "#e3f7ef",
|
||||||
|
"colorBrandBackground2Hover": "#c0ecdd",
|
||||||
|
"colorBrandBackground2Pressed": "#78d3b9",
|
||||||
|
"colorBrandBackgroundHover": "#0c695a",
|
||||||
|
"colorBrandBackgroundInverted": "#ffffff",
|
||||||
|
"colorBrandBackgroundInvertedHover": "#e3f7ef",
|
||||||
|
"colorBrandBackgroundInvertedPressed": "#9ee0cb",
|
||||||
|
"colorBrandBackgroundInvertedSelected": "#c0ecdd",
|
||||||
|
"colorBrandBackgroundPressed": "#033f38",
|
||||||
|
"colorBrandBackgroundSelected": "#0a5c50",
|
||||||
|
"colorBrandBackgroundStatic": "#117865",
|
||||||
|
"colorBrandForeground1": "#117865",
|
||||||
|
"colorBrandForeground2": "#0c695a",
|
||||||
|
"colorBrandForeground2Hover": "#0a5c50",
|
||||||
|
"colorBrandForeground2Pressed": "#01322E",
|
||||||
|
"colorBrandForegroundInverted": "#2aaC94",
|
||||||
|
"colorBrandForegroundInvertedHover": "#3abb9f",
|
||||||
|
"colorBrandForegroundInvertedPressed": "#2aaC94",
|
||||||
|
"colorBrandForegroundLink": "#0c695a",
|
||||||
|
"colorBrandForegroundLinkHover": "#0a5c50",
|
||||||
|
"colorBrandForegroundLinkPressed": "#033f38",
|
||||||
|
"colorBrandForegroundLinkSelected": "#0c695a",
|
||||||
|
"colorBrandForegroundOnLight": "#117865",
|
||||||
|
"colorBrandForegroundOnLightHover": "#0c695a",
|
||||||
|
"colorBrandForegroundOnLightPressed": "#054d43",
|
||||||
|
"colorBrandForegroundOnLightSelected": "#0a5c50",
|
||||||
|
"colorBrandShadowAmbient": "rgba(0,0,0,0.30)",
|
||||||
|
"colorBrandShadowKey": "rgba(0,0,0,0.25)",
|
||||||
|
"colorBrandStroke1": "#117865",
|
||||||
|
"colorBrandStroke2": "#9ee0cb",
|
||||||
|
"colorBrandStroke2Contrast": "#9ee0cb",
|
||||||
|
"colorBrandStroke2Hover": "#52c7aa",
|
||||||
|
"colorBrandStroke2Pressed": "#117865",
|
||||||
|
"colorCompoundBrandBackground": "#117865",
|
||||||
|
"colorCompoundBrandBackgroundHover": "#0c695a",
|
||||||
|
"colorCompoundBrandBackgroundPressed": "#0a5c50",
|
||||||
|
"colorCompoundBrandForeground1": "#117865",
|
||||||
|
"colorCompoundBrandForeground1Hover": "#0c695a",
|
||||||
|
"colorCompoundBrandForeground1Pressed": "#0a5c50",
|
||||||
|
"colorCompoundBrandStroke": "#117865",
|
||||||
|
"colorCompoundBrandStrokeHover": "#0c695a",
|
||||||
|
"colorCompoundBrandStrokePressed": "#0a5c50",
|
||||||
|
"colorNeutralBackground1": "#ffffff",
|
||||||
|
"colorNeutralBackground1Hover": "#f5f5f5",
|
||||||
|
"colorNeutralBackground1Pressed": "#e0e0e0",
|
||||||
|
"colorNeutralBackground1Selected": "#ebebeb",
|
||||||
|
"colorNeutralBackground2": "#fafafa",
|
||||||
|
"colorNeutralBackground2Hover": "#f0f0f0",
|
||||||
|
"colorNeutralBackground2Pressed": "#dbdbdb",
|
||||||
|
"colorNeutralBackground2Selected": "#e6e6e6",
|
||||||
|
"colorNeutralBackground3": "#f5f5f5",
|
||||||
|
"colorNeutralBackground3Hover": "#ebebeb",
|
||||||
|
"colorNeutralBackground3Pressed": "#d6d6d6",
|
||||||
|
"colorNeutralBackground3Selected": "#e0e0e0",
|
||||||
|
"colorNeutralBackground4": "#f0f0f0",
|
||||||
|
"colorNeutralBackground4Hover": "#fafafa",
|
||||||
|
"colorNeutralBackground4Pressed": "#f5f5f5",
|
||||||
|
"colorNeutralBackground4Selected": "#ffffff",
|
||||||
|
"colorNeutralBackground5": "#ebebeb",
|
||||||
|
"colorNeutralBackground5Hover": "#f5f5f5",
|
||||||
|
"colorNeutralBackground5Pressed": "#f0f0f0",
|
||||||
|
"colorNeutralBackground5Selected": "#fafafa",
|
||||||
|
"colorNeutralBackground6": "#e6e6e6",
|
||||||
|
"colorNeutralBackgroundAlpha": "rgba(255, 255, 255, 0.5)",
|
||||||
|
"colorNeutralBackgroundAlpha2": "rgba(255, 255, 255, 0.8)",
|
||||||
|
"colorNeutralBackgroundDisabled": "#f0f0f0",
|
||||||
|
"colorNeutralBackgroundInverted": "#292929",
|
||||||
|
"colorNeutralBackgroundInvertedDisabled": "rgba(255, 255, 255, 0.1)",
|
||||||
|
"colorNeutralBackgroundStatic": "#333333",
|
||||||
|
"colorNeutralForeground1": "#242424",
|
||||||
|
"colorNeutralForeground1Hover": "#242424",
|
||||||
|
"colorNeutralForeground1Pressed": "#242424",
|
||||||
|
"colorNeutralForeground1Selected": "#242424",
|
||||||
|
"colorNeutralForeground1Static": "#242424",
|
||||||
|
"colorNeutralForeground2": "#424242",
|
||||||
|
"colorNeutralForeground2BrandHover": "#117865",
|
||||||
|
"colorNeutralForeground2BrandPressed": "#0c695a",
|
||||||
|
"colorNeutralForeground2BrandSelected": "#117865",
|
||||||
|
"colorNeutralForeground2Hover": "#242424",
|
||||||
|
"colorNeutralForeground2Link": "#424242",
|
||||||
|
"colorNeutralForeground2LinkHover": "#242424",
|
||||||
|
"colorNeutralForeground2LinkPressed": "#242424",
|
||||||
|
"colorNeutralForeground2LinkSelected": "#242424",
|
||||||
|
"colorNeutralForeground2Pressed": "#242424",
|
||||||
|
"colorNeutralForeground2Selected": "#242424",
|
||||||
|
"colorNeutralForeground3": "#616161",
|
||||||
|
"colorNeutralForeground3BrandHover": "#117865",
|
||||||
|
"colorNeutralForeground3BrandPressed": "#0c695a",
|
||||||
|
"colorNeutralForeground3BrandSelected": "#117865",
|
||||||
|
"colorNeutralForeground3Hover": "#424242",
|
||||||
|
"colorNeutralForeground3Pressed": "#424242",
|
||||||
|
"colorNeutralForeground3Selected": "#424242",
|
||||||
|
"colorNeutralForeground4": "#707070",
|
||||||
|
"colorNeutralForegroundDisabled": "#bdbdbd",
|
||||||
|
"colorNeutralForegroundInverted": "#ffffff",
|
||||||
|
"colorNeutralForegroundInverted2": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedDisabled": "rgba(255, 255, 255, 0.4)",
|
||||||
|
"colorNeutralForegroundInvertedHover": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedLink": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedLinkHover": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedLinkPressed": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedLinkSelected": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedPressed": "#ffffff",
|
||||||
|
"colorNeutralForegroundInvertedSelected": "#ffffff",
|
||||||
|
"colorNeutralForegroundOnBrand": "#ffffff",
|
||||||
|
"colorNeutralForegroundStaticInverted": "#ffffff",
|
||||||
|
"colorNeutralShadowAmbient": "rgba(0,0,0,0.12)",
|
||||||
|
"colorNeutralShadowAmbientDarker": "rgba(0,0,0,0.20)",
|
||||||
|
"colorNeutralShadowAmbientLighter": "rgba(0,0,0,0.06)",
|
||||||
|
"colorNeutralShadowKey": "rgba(0,0,0,0.14)",
|
||||||
|
"colorNeutralShadowKeyDarker": "rgba(0,0,0,0.24)",
|
||||||
|
"colorNeutralShadowKeyLighter": "rgba(0,0,0,0.07)",
|
||||||
|
"colorNeutralStencil1": "#e6e6e6",
|
||||||
|
"colorNeutralStencil1Alpha": "rgba(0, 0, 0, 0.1)",
|
||||||
|
"colorNeutralStencil2": "#fafafa",
|
||||||
|
"colorNeutralStencil2Alpha": "rgba(0, 0, 0, 0.05)",
|
||||||
|
"colorNeutralStroke1": "#d1d1d1",
|
||||||
|
"colorNeutralStroke1Hover": "#c7c7c7",
|
||||||
|
"colorNeutralStroke1Pressed": "#b3b3b3",
|
||||||
|
"colorNeutralStroke1Selected": "#bdbdbd",
|
||||||
|
"colorNeutralStroke2": "#e0e0e0",
|
||||||
|
"colorNeutralStroke3": "#f0f0f0",
|
||||||
|
"colorNeutralStrokeAccessible": "#616161",
|
||||||
|
"colorNeutralStrokeAccessibleHover": "#575757",
|
||||||
|
"colorNeutralStrokeAccessiblePressed": "#4d4d4d",
|
||||||
|
"colorNeutralStrokeAccessibleSelected": "#117865",
|
||||||
|
"colorNeutralStrokeAlpha": "rgba(0, 0, 0, 0.05)",
|
||||||
|
"colorNeutralStrokeAlpha2": "rgba(255, 255, 255, 0.2)",
|
||||||
|
"colorNeutralStrokeDisabled": "#e0e0e0",
|
||||||
|
"colorNeutralStrokeInvertedDisabled": "rgba(255, 255, 255, 0.4)",
|
||||||
|
"colorNeutralStrokeOnBrand": "#ffffff",
|
||||||
|
"colorNeutralStrokeOnBrand2": "#ffffff",
|
||||||
|
"colorNeutralStrokeOnBrand2Hover": "#ffffff",
|
||||||
|
"colorNeutralStrokeOnBrand2Pressed": "#ffffff",
|
||||||
|
"colorNeutralStrokeOnBrand2Selected": "#ffffff",
|
||||||
|
"colorNeutralStrokeSubtle": "#e0e0e0",
|
||||||
|
"colorPaletteAnchorBackground2": "#bcc3c7",
|
||||||
|
"colorPaletteAnchorBorderActive": "#394146",
|
||||||
|
"colorPaletteAnchorForeground2": "#202427",
|
||||||
|
"colorPaletteBeigeBackground2": "#d7d4d4",
|
||||||
|
"colorPaletteBeigeBorderActive": "#7a7574",
|
||||||
|
"colorPaletteBeigeForeground2": "#444241",
|
||||||
|
"colorPaletteBerryBackground1": "#fdf5fc",
|
||||||
|
"colorPaletteBerryBackground2": "#edbbe7",
|
||||||
|
"colorPaletteBerryBackground3": "#c239b3",
|
||||||
|
"colorPaletteBerryBorder1": "#edbbe7",
|
||||||
|
"colorPaletteBerryBorder2": "#c239b3",
|
||||||
|
"colorPaletteBerryBorderActive": "#c239b3",
|
||||||
|
"colorPaletteBerryForeground1": "#af33a1",
|
||||||
|
"colorPaletteBerryForeground2": "#6d2064",
|
||||||
|
"colorPaletteBerryForeground3": "#c239b3",
|
||||||
|
"colorPaletteBlueBackground2": "#a9d3f2",
|
||||||
|
"colorPaletteBlueBorderActive": "#0078d4",
|
||||||
|
"colorPaletteBlueForeground2": "#004377",
|
||||||
|
"colorPaletteBrassBackground2": "#e0cea2",
|
||||||
|
"colorPaletteBrassBorderActive": "#986f0b",
|
||||||
|
"colorPaletteBrassForeground2": "#553e06",
|
||||||
|
"colorPaletteBrownBackground2": "#ddc3b0",
|
||||||
|
"colorPaletteBrownBorderActive": "#8e562e",
|
||||||
|
"colorPaletteBrownForeground2": "#50301a",
|
||||||
|
"colorPaletteCornflowerBackground2": "#c8d1fa",
|
||||||
|
"colorPaletteCornflowerBorderActive": "#4f6bed",
|
||||||
|
"colorPaletteCornflowerForeground2": "#2c3c85",
|
||||||
|
"colorPaletteCranberryBackground2": "#eeacb2",
|
||||||
|
"colorPaletteCranberryBorderActive": "#c50f1f",
|
||||||
|
"colorPaletteCranberryForeground2": "#6e0811",
|
||||||
|
"colorPaletteDarkGreenBackground2": "#9ad29a",
|
||||||
|
"colorPaletteDarkGreenBorderActive": "#0b6a0b",
|
||||||
|
"colorPaletteDarkGreenForeground2": "#063b06",
|
||||||
|
"colorPaletteDarkOrangeBackground1": "#fdf6f3",
|
||||||
|
"colorPaletteDarkOrangeBackground2": "#f4bfab",
|
||||||
|
"colorPaletteDarkOrangeBackground3": "#da3b01",
|
||||||
|
"colorPaletteDarkOrangeBorder1": "#f4bfab",
|
||||||
|
"colorPaletteDarkOrangeBorder2": "#da3b01",
|
||||||
|
"colorPaletteDarkOrangeBorderActive": "#da3b01",
|
||||||
|
"colorPaletteDarkOrangeForeground1": "#c43501",
|
||||||
|
"colorPaletteDarkOrangeForeground2": "#7a2101",
|
||||||
|
"colorPaletteDarkOrangeForeground3": "#da3b01",
|
||||||
|
"colorPaletteDarkRedBackground2": "#d69ca5",
|
||||||
|
"colorPaletteDarkRedBorderActive": "#750b1c",
|
||||||
|
"colorPaletteDarkRedForeground2": "#420610",
|
||||||
|
"colorPaletteForestBackground2": "#bdd99b",
|
||||||
|
"colorPaletteForestBorderActive": "#498205",
|
||||||
|
"colorPaletteForestForeground2": "#294903",
|
||||||
|
"colorPaletteGoldBackground2": "#ecdfa5",
|
||||||
|
"colorPaletteGoldBorderActive": "#c19c00",
|
||||||
|
"colorPaletteGoldForeground2": "#6c5700",
|
||||||
|
"colorPaletteGrapeBackground2": "#d9a7e0",
|
||||||
|
"colorPaletteGrapeBorderActive": "#881798",
|
||||||
|
"colorPaletteGrapeForeground2": "#4c0d55",
|
||||||
|
"colorPaletteGreenBackground1": "#f1faf1",
|
||||||
|
"colorPaletteGreenBackground2": "#9fd89f",
|
||||||
|
"colorPaletteGreenBackground3": "#107c10",
|
||||||
|
"colorPaletteGreenBorder1": "#9fd89f",
|
||||||
|
"colorPaletteGreenBorder2": "#107c10",
|
||||||
|
"colorPaletteGreenBorderActive": "#107c10",
|
||||||
|
"colorPaletteGreenForeground1": "#0e700e",
|
||||||
|
"colorPaletteGreenForeground2": "#094509",
|
||||||
|
"colorPaletteGreenForeground3": "#107c10",
|
||||||
|
"colorPaletteGreenForegroundInverted": "#359b35",
|
||||||
|
"colorPaletteLavenderBackground2": "#d2ccf8",
|
||||||
|
"colorPaletteLavenderBorderActive": "#7160e8",
|
||||||
|
"colorPaletteLavenderForeground2": "#3f3682",
|
||||||
|
"colorPaletteLightGreenBackground1": "#f2fbf2",
|
||||||
|
"colorPaletteLightGreenBackground2": "#a7e3a5",
|
||||||
|
"colorPaletteLightGreenBackground3": "#13a10e",
|
||||||
|
"colorPaletteLightGreenBorder1": "#a7e3a5",
|
||||||
|
"colorPaletteLightGreenBorder2": "#13a10e",
|
||||||
|
"colorPaletteLightGreenBorderActive": "#13a10e",
|
||||||
|
"colorPaletteLightGreenForeground1": "#11910d",
|
||||||
|
"colorPaletteLightGreenForeground2": "#0b5a08",
|
||||||
|
"colorPaletteLightGreenForeground3": "#13a10e",
|
||||||
|
"colorPaletteLightTealBackground2": "#a6e9ed",
|
||||||
|
"colorPaletteLightTealBorderActive": "#00b7c3",
|
||||||
|
"colorPaletteLightTealForeground2": "#00666d",
|
||||||
|
"colorPaletteLilacBackground2": "#e6bfed",
|
||||||
|
"colorPaletteLilacBorderActive": "#b146c2",
|
||||||
|
"colorPaletteLilacForeground2": "#63276d",
|
||||||
|
"colorPaletteMagentaBackground2": "#eca5d1",
|
||||||
|
"colorPaletteMagentaBorderActive": "#bf0077",
|
||||||
|
"colorPaletteMagentaForeground2": "#6b0043",
|
||||||
|
"colorPaletteMarigoldBackground1": "#fefbf4",
|
||||||
|
"colorPaletteMarigoldBackground2": "#f9e2ae",
|
||||||
|
"colorPaletteMarigoldBackground3": "#eaa300",
|
||||||
|
"colorPaletteMarigoldBorder1": "#f9e2ae",
|
||||||
|
"colorPaletteMarigoldBorder2": "#eaa300",
|
||||||
|
"colorPaletteMarigoldBorderActive": "#eaa300",
|
||||||
|
"colorPaletteMarigoldForeground1": "#d39300",
|
||||||
|
"colorPaletteMarigoldForeground2": "#835b00",
|
||||||
|
"colorPaletteMarigoldForeground3": "#eaa300",
|
||||||
|
"colorPaletteMinkBackground2": "#cecccb",
|
||||||
|
"colorPaletteMinkBorderActive": "#5d5a58",
|
||||||
|
"colorPaletteMinkForeground2": "#343231",
|
||||||
|
"colorPaletteNavyBackground2": "#a3b2e8",
|
||||||
|
"colorPaletteNavyBorderActive": "#0027b4",
|
||||||
|
"colorPaletteNavyForeground2": "#001665",
|
||||||
|
"colorPalettePeachBackground2": "#ffddb3",
|
||||||
|
"colorPalettePeachBorderActive": "#ff8c00",
|
||||||
|
"colorPalettePeachForeground2": "#8f4e00",
|
||||||
|
"colorPalettePinkBackground2": "#f7c0e3",
|
||||||
|
"colorPalettePinkBorderActive": "#e43ba6",
|
||||||
|
"colorPalettePinkForeground2": "#80215d",
|
||||||
|
"colorPalettePlatinumBackground2": "#cdd6d8",
|
||||||
|
"colorPalettePlatinumBorderActive": "#69797e",
|
||||||
|
"colorPalettePlatinumForeground2": "#3b4447",
|
||||||
|
"colorPalettePlumBackground2": "#d696c0",
|
||||||
|
"colorPalettePlumBorderActive": "#77004d",
|
||||||
|
"colorPalettePlumForeground2": "#43002b",
|
||||||
|
"colorPalettePumpkinBackground2": "#efc4ad",
|
||||||
|
"colorPalettePumpkinBorderActive": "#ca5010",
|
||||||
|
"colorPalettePumpkinForeground2": "#712d09",
|
||||||
|
"colorPalettePurpleBackground2": "#c6b1de",
|
||||||
|
"colorPalettePurpleBorderActive": "#5c2e91",
|
||||||
|
"colorPalettePurpleForeground2": "#341a51",
|
||||||
|
"colorPaletteRedBackground1": "#fdf6f6",
|
||||||
|
"colorPaletteRedBackground2": "#f1bbbc",
|
||||||
|
"colorPaletteRedBackground3": "#d13438",
|
||||||
|
"colorPaletteRedBorder1": "#f1bbbc",
|
||||||
|
"colorPaletteRedBorder2": "#d13438",
|
||||||
|
"colorPaletteRedBorderActive": "#d13438",
|
||||||
|
"colorPaletteRedForeground1": "#bc2f32",
|
||||||
|
"colorPaletteRedForeground2": "#751d1f",
|
||||||
|
"colorPaletteRedForeground3": "#d13438",
|
||||||
|
"colorPaletteRedForegroundInverted": "#dc5e62",
|
||||||
|
"colorPaletteRoyalBlueBackground2": "#9abfdc",
|
||||||
|
"colorPaletteRoyalBlueBorderActive": "#004e8c",
|
||||||
|
"colorPaletteRoyalBlueForeground2": "#002c4e",
|
||||||
|
"colorPaletteSeafoamBackground2": "#a8f0cd",
|
||||||
|
"colorPaletteSeafoamBorderActive": "#00cc6a",
|
||||||
|
"colorPaletteSeafoamForeground2": "#00723b",
|
||||||
|
"colorPaletteSteelBackground2": "#94c8d4",
|
||||||
|
"colorPaletteSteelBorderActive": "#005b70",
|
||||||
|
"colorPaletteSteelForeground2": "#00333f",
|
||||||
|
"colorPaletteTealBackground2": "#9bd9db",
|
||||||
|
"colorPaletteTealBorderActive": "#038387",
|
||||||
|
"colorPaletteTealForeground2": "#02494c",
|
||||||
|
"colorPaletteYellowBackground1": "#fffef5",
|
||||||
|
"colorPaletteYellowBackground2": "#fef7b2",
|
||||||
|
"colorPaletteYellowBackground3": "#fde300",
|
||||||
|
"colorPaletteYellowBorder1": "#fef7b2",
|
||||||
|
"colorPaletteYellowBorder2": "#fde300",
|
||||||
|
"colorPaletteYellowBorderActive": "#fde300",
|
||||||
|
"colorPaletteYellowForeground1": "#817400",
|
||||||
|
"colorPaletteYellowForeground2": "#817400",
|
||||||
|
"colorPaletteYellowForeground3": "#fde300",
|
||||||
|
"colorPaletteYellowForegroundInverted": "#fef7b2",
|
||||||
|
"colorScrollbarOverlay": "rgba(0, 0, 0, 0.5)",
|
||||||
|
"colorStatusDangerBackground1": "#fdf3f4",
|
||||||
|
"colorStatusDangerBackground2": "#eeacb2",
|
||||||
|
"colorStatusDangerBackground3": "#c50f1f",
|
||||||
|
"colorStatusDangerBorder1": "#eeacb2",
|
||||||
|
"colorStatusDangerBorder2": "#c50f1f",
|
||||||
|
"colorStatusDangerBorderActive": "#c50f1f",
|
||||||
|
"colorStatusDangerForeground1": "#b10e1c",
|
||||||
|
"colorStatusDangerForeground2": "#6e0811",
|
||||||
|
"colorStatusDangerForeground3": "#c50f1f",
|
||||||
|
"colorStatusDangerForegroundInverted": "#dc626d",
|
||||||
|
"colorStatusSuccessBackground1": "#f1faf1",
|
||||||
|
"colorStatusSuccessBackground2": "#9fd89f",
|
||||||
|
"colorStatusSuccessBackground3": "#107c10",
|
||||||
|
"colorStatusSuccessBorder1": "#9fd89f",
|
||||||
|
"colorStatusSuccessBorder2": "#107c10",
|
||||||
|
"colorStatusSuccessBorderActive": "#107c10",
|
||||||
|
"colorStatusSuccessForeground1": "#0e700e",
|
||||||
|
"colorStatusSuccessForeground2": "#094509",
|
||||||
|
"colorStatusSuccessForeground3": "#107c10",
|
||||||
|
"colorStatusSuccessForegroundInverted": "#54b054",
|
||||||
|
"colorStatusWarningBackground1": "#fff9f5",
|
||||||
|
"colorStatusWarningBackground2": "#fdcfb4",
|
||||||
|
"colorStatusWarningBackground3": "#f7630c",
|
||||||
|
"colorStatusWarningBorder1": "#fdcfb4",
|
||||||
|
"colorStatusWarningBorder2": "#bc4b09",
|
||||||
|
"colorStatusWarningBorderActive": "#f7630c",
|
||||||
|
"colorStatusWarningForeground1": "#bc4b09",
|
||||||
|
"colorStatusWarningForeground2": "#8a3707",
|
||||||
|
"colorStatusWarningForeground3": "#bc4b09",
|
||||||
|
"colorStatusWarningForegroundInverted": "#faa06b",
|
||||||
|
"colorStrokeFocus1": "#ffffff",
|
||||||
|
"colorStrokeFocus2": "#000000",
|
||||||
|
"colorSubtleBackground": "transparent",
|
||||||
|
"colorSubtleBackgroundHover": "#f5f5f5",
|
||||||
|
"colorSubtleBackgroundInverted": "transparent",
|
||||||
|
"colorSubtleBackgroundInvertedHover": "rgba(0, 0, 0, 0.1)",
|
||||||
|
"colorSubtleBackgroundInvertedPressed": "rgba(0, 0, 0, 0.3)",
|
||||||
|
"colorSubtleBackgroundInvertedSelected": "rgba(0, 0, 0, 0.2)",
|
||||||
|
"colorSubtleBackgroundLightAlphaHover": "rgba(255, 255, 255, 0.7)",
|
||||||
|
"colorSubtleBackgroundLightAlphaPressed": "rgba(255, 255, 255, 0.5)",
|
||||||
|
"colorSubtleBackgroundLightAlphaSelected": "transparent",
|
||||||
|
"colorSubtleBackgroundPressed": "#e0e0e0",
|
||||||
|
"colorSubtleBackgroundSelected": "#ebebeb",
|
||||||
|
"colorTransparentBackground": "transparent",
|
||||||
|
"colorTransparentBackgroundHover": "transparent",
|
||||||
|
"colorTransparentBackgroundPressed": "transparent",
|
||||||
|
"colorTransparentBackgroundSelected": "transparent",
|
||||||
|
"colorTransparentStroke": "transparent",
|
||||||
|
"colorTransparentStrokeDisabled": "transparent",
|
||||||
|
"colorTransparentStrokeInteractive": "transparent",
|
||||||
|
"curveAccelerateMax": "cubic-bezier(0.9,0.1,1,0.2)",
|
||||||
|
"curveAccelerateMid": "cubic-bezier(1,0,1,1)",
|
||||||
|
"curveAccelerateMin": "cubic-bezier(0.8,0,0.78,1)",
|
||||||
|
"curveDecelerateMax": "cubic-bezier(0.1,0.9,0.2,1)",
|
||||||
|
"curveDecelerateMid": "cubic-bezier(0,0,0,1)",
|
||||||
|
"curveDecelerateMin": "cubic-bezier(0.33,0,0.1,1)",
|
||||||
|
"curveEasyEase": "cubic-bezier(0.33,0,0.67,1)",
|
||||||
|
"curveEasyEaseMax": "cubic-bezier(0.8,0,0.2,1)",
|
||||||
|
"curveLinear": "cubic-bezier(0,0,1,1)",
|
||||||
|
"durationFast": "150ms",
|
||||||
|
"durationFaster": "100ms",
|
||||||
|
"durationGentle": "250ms",
|
||||||
|
"durationNormal": "200ms",
|
||||||
|
"durationSlow": "300ms",
|
||||||
|
"durationSlower": "400ms",
|
||||||
|
"durationUltraFast": "50ms",
|
||||||
|
"durationUltraSlow": "500ms",
|
||||||
|
"fontFamilyBase": "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif",
|
||||||
|
"fontFamilyMonospace": "Consolas, 'Courier New', Courier, monospace",
|
||||||
|
"fontFamilyNumeric": "Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif",
|
||||||
|
"fontSizeBase100": "10px",
|
||||||
|
"fontSizeBase200": "12px",
|
||||||
|
"fontSizeBase300": "14px",
|
||||||
|
"fontSizeBase400": "16px",
|
||||||
|
"fontSizeBase500": "20px",
|
||||||
|
"fontSizeBase600": "24px",
|
||||||
|
"fontSizeHero1000": "68px",
|
||||||
|
"fontSizeHero700": "28px",
|
||||||
|
"fontSizeHero800": "32px",
|
||||||
|
"fontSizeHero900": "40px",
|
||||||
|
"fontWeightBold": 700,
|
||||||
|
"fontWeightMedium": 500,
|
||||||
|
"fontWeightRegular": 400,
|
||||||
|
"fontWeightSemibold": 600,
|
||||||
|
"lineHeightBase100": "14px",
|
||||||
|
"lineHeightBase200": "16px",
|
||||||
|
"lineHeightBase300": "20px",
|
||||||
|
"lineHeightBase400": "22px",
|
||||||
|
"lineHeightBase500": "28px",
|
||||||
|
"lineHeightBase600": "32px",
|
||||||
|
"lineHeightHero1000": "92px",
|
||||||
|
"lineHeightHero700": "36px",
|
||||||
|
"lineHeightHero800": "40px",
|
||||||
|
"lineHeightHero900": "52px",
|
||||||
|
"shadow16": "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)",
|
||||||
|
"shadow16Brand": "0 0 2px rgba(0,0,0,0.30), 0 8px 16px rgba(0,0,0,0.25)",
|
||||||
|
"shadow2": "0 0 2px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.14)",
|
||||||
|
"shadow28": "0 0 8px rgba(0,0,0,0.12), 0 14px 28px rgba(0,0,0,0.14)",
|
||||||
|
"shadow28Brand": "0 0 8px rgba(0,0,0,0.30), 0 14px 28px rgba(0,0,0,0.25)",
|
||||||
|
"shadow2Brand": "0 0 2px rgba(0,0,0,0.30), 0 1px 2px rgba(0,0,0,0.25)",
|
||||||
|
"shadow4": "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)",
|
||||||
|
"shadow4Brand": "0 0 2px rgba(0,0,0,0.30), 0 2px 4px rgba(0,0,0,0.25)",
|
||||||
|
"shadow64": "0 0 8px rgba(0,0,0,0.12), 0 32px 64px rgba(0,0,0,0.14)",
|
||||||
|
"shadow64Brand": "0 0 8px rgba(0,0,0,0.30), 0 32px 64px rgba(0,0,0,0.25)",
|
||||||
|
"shadow8": "0 0 2px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.14)",
|
||||||
|
"shadow8Brand": "0 0 2px rgba(0,0,0,0.30), 0 4px 8px rgba(0,0,0,0.25)",
|
||||||
|
"spacingHorizontalL": "16px",
|
||||||
|
"spacingHorizontalM": "12px",
|
||||||
|
"spacingHorizontalMNudge": "10px",
|
||||||
|
"spacingHorizontalNone": "0",
|
||||||
|
"spacingHorizontalS": "8px",
|
||||||
|
"spacingHorizontalSNudge": "6px",
|
||||||
|
"spacingHorizontalXL": "20px",
|
||||||
|
"spacingHorizontalXS": "4px",
|
||||||
|
"spacingHorizontalXXL": "24px",
|
||||||
|
"spacingHorizontalXXS": "2px",
|
||||||
|
"spacingHorizontalXXXL": "32px",
|
||||||
|
"spacingVerticalL": "16px",
|
||||||
|
"spacingVerticalM": "12px",
|
||||||
|
"spacingVerticalMNudge": "10px",
|
||||||
|
"spacingVerticalNone": "0",
|
||||||
|
"spacingVerticalS": "8px",
|
||||||
|
"spacingVerticalSNudge": "6px",
|
||||||
|
"spacingVerticalXL": "20px",
|
||||||
|
"spacingVerticalXS": "4px",
|
||||||
|
"spacingVerticalXXL": "24px",
|
||||||
|
"spacingVerticalXXS": "2px",
|
||||||
|
"spacingVerticalXXXL": "32px",
|
||||||
|
"strokeWidthThick": "2px",
|
||||||
|
"strokeWidthThicker": "3px",
|
||||||
|
"strokeWidthThickest": "4px",
|
||||||
|
"strokeWidthThin": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="tab-pane active documentsTab"
|
||||||
|
role="tabpanel"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "flex",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="filterdivs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="filterDocCollapsed"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="selectQuery"
|
||||||
|
>
|
||||||
|
SELECT * FROM c
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="appliedQuery"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "100%",
|
||||||
|
"overflow": "hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Split
|
||||||
|
mode="horizontal"
|
||||||
|
prefixCls="w-split"
|
||||||
|
visiable={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 120,
|
||||||
|
"overflow": "hidden",
|
||||||
|
"position": "relative",
|
||||||
|
"width": "35%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
appearance="transparent"
|
||||||
|
aria-label="Refresh"
|
||||||
|
icon={<ArrowClockwise16Filled />}
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
size="small"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "white",
|
||||||
|
"color": undefined,
|
||||||
|
"float": "right",
|
||||||
|
"position": "absolute",
|
||||||
|
"right": 0,
|
||||||
|
"top": 6,
|
||||||
|
"zIndex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "100%",
|
||||||
|
"width": "calc(100% - 50px)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DocumentsTableComponent
|
||||||
|
columnHeaders={
|
||||||
|
Object {
|
||||||
|
"idHeader": "id",
|
||||||
|
"partitionKeyHeaders": Array [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items={Array []}
|
||||||
|
onItemClicked={[Function]}
|
||||||
|
onSelectedRowsChange={[Function]}
|
||||||
|
selectedRows={
|
||||||
|
Set {
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": "20%",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Split>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FluentProvider>
|
||||||
|
`;
|
File diff suppressed because it is too large
Load Diff
|
@ -1,320 +0,0 @@
|
||||||
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
|
||||||
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import Q from "q";
|
|
||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as Logger from "../../Common/Logger";
|
|
||||||
import {
|
|
||||||
createDocument,
|
|
||||||
deleteDocument,
|
|
||||||
queryDocuments,
|
|
||||||
readDocument,
|
|
||||||
updateDocument,
|
|
||||||
} from "../../Common/MongoProxyClient";
|
|
||||||
import MongoUtility from "../../Common/MongoUtility";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { useDialog } from "../Controls/Dialog";
|
|
||||||
import DocumentId from "../Tree/DocumentId";
|
|
||||||
import ObjectId from "../Tree/ObjectId";
|
|
||||||
import DocumentsTab from "./DocumentsTab";
|
|
||||||
|
|
||||||
export default class MongoDocumentsTab extends DocumentsTab {
|
|
||||||
public collection: ViewModels.Collection;
|
|
||||||
private continuationToken: string;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
|
||||||
super(options);
|
|
||||||
this.lastFilterContents = ko.observableArray<string>(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]);
|
|
||||||
|
|
||||||
this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => {
|
|
||||||
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
|
||||||
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) {
|
|
||||||
// From $v.shard.$v.key.$v > shard.key
|
|
||||||
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
|
||||||
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return partitionKeyProperty;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isFilterExpanded = ko.observable<boolean>(true);
|
|
||||||
super.buildCommandBarOptions.bind(this);
|
|
||||||
super.buildCommandBarOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
|
||||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
|
||||||
this.displayedError("");
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const partitionKeyProperty = this.partitionKeyProperties?.[0];
|
|
||||||
if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) {
|
|
||||||
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
|
|
||||||
this.displayedError(message);
|
|
||||||
let that = this;
|
|
||||||
setTimeout(() => {
|
|
||||||
that.displayedError("");
|
|
||||||
}, Constants.ClientDefaults.errorNotificationTimeoutMs);
|
|
||||||
this.isExecutionError(true);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.CreateDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: message,
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
|
|
||||||
throw new Error("Document without shard key");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isExecutionError(false);
|
|
||||||
this.isExecuting(true);
|
|
||||||
return createDocument(
|
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection,
|
|
||||||
this.partitionKeyProperties?.[0],
|
|
||||||
documentContent,
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
(savedDocument: any) => {
|
|
||||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
savedDocument,
|
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
|
|
||||||
let id = new ObjectId(this, savedDocument, partitionKeyArray);
|
|
||||||
let ids = this.documentIds();
|
|
||||||
ids.push(id);
|
|
||||||
delete savedDocument._self;
|
|
||||||
|
|
||||||
let value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
|
||||||
this.selectedDocumentContent.setBaseline(value);
|
|
||||||
|
|
||||||
this.selectedDocumentId(id);
|
|
||||||
this.documentIds(ids);
|
|
||||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.CreateDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
this.isExecutionError(true);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.CreateDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.finally(() => this.isExecuting(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
public onSaveExistingDocumentClick = (): Promise<any> => {
|
|
||||||
const selectedDocumentId = this.selectedDocumentId();
|
|
||||||
const documentContent = this.selectedDocumentContent();
|
|
||||||
this.isExecutionError(false);
|
|
||||||
this.isExecuting(true);
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
|
|
||||||
.then(
|
|
||||||
(updatedDocument: any) => {
|
|
||||||
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
|
|
||||||
this.selectedDocumentContent.setBaseline(value);
|
|
||||||
|
|
||||||
this.documentIds().forEach((documentId: DocumentId) => {
|
|
||||||
if (documentId.rid === updatedDocument._rid) {
|
|
||||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
updatedDocument,
|
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
|
|
||||||
const id = new ObjectId(this, updatedDocument, partitionKeyArray);
|
|
||||||
documentId.id(id.id());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.UpdateDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
this.isExecutionError(true);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.UpdateDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.finally(() => this.isExecuting(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
public buildQuery(filter: string): string {
|
|
||||||
return filter || "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
|
||||||
this.selectedDocumentId(documentId);
|
|
||||||
const content = await readDocument(this.collection.databaseId, this.collection, documentId);
|
|
||||||
this.initDocumentEditor(documentId, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public loadNextPage(): Q.Promise<any> {
|
|
||||||
this.isExecuting(true);
|
|
||||||
this.isExecutionError(false);
|
|
||||||
const filter: string = this.filterContent().trim();
|
|
||||||
const query: string = this.buildQuery(filter);
|
|
||||||
|
|
||||||
return Q(queryDocuments(this.collection.databaseId, this.collection, true, query, this.continuationToken))
|
|
||||||
.then(
|
|
||||||
({ continuationToken, documents }) => {
|
|
||||||
this.continuationToken = continuationToken;
|
|
||||||
let currentDocuments = this.documentIds();
|
|
||||||
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
|
|
||||||
const nextDocumentIds = documents
|
|
||||||
.filter((d: any) => {
|
|
||||||
return currentDocumentsRids.indexOf(d._rid) < 0;
|
|
||||||
})
|
|
||||||
.map((rawDocument: any) => {
|
|
||||||
const partitionKeyValue = rawDocument._partitionKeyValue;
|
|
||||||
return new DocumentId(this, rawDocument, [partitionKeyValue]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const merged = currentDocuments.concat(nextDocumentIds);
|
|
||||||
|
|
||||||
this.documentIds(merged);
|
|
||||||
currentDocuments = this.documentIds();
|
|
||||||
if (this.filterContent().length > 0 && currentDocuments.length > 0) {
|
|
||||||
currentDocuments[0].click();
|
|
||||||
} else {
|
|
||||||
this.selectedDocumentContent("");
|
|
||||||
this.selectedDocumentId(null);
|
|
||||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
|
||||||
}
|
|
||||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
},
|
|
||||||
this.onLoadStartKey,
|
|
||||||
);
|
|
||||||
this.onLoadStartKey = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
this.onLoadStartKey,
|
|
||||||
);
|
|
||||||
this.onLoadStartKey = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.finally(() => this.isExecuting(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _onEditorContentChange(newContent: string) {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
|
|
||||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid
|
|
||||||
) {
|
|
||||||
let parsed: any = JSON.parse(newContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit
|
|
||||||
this.onValidDocumentEdit();
|
|
||||||
} catch (e) {
|
|
||||||
this.onInvalidDocumentEdit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
|
||||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
|
||||||
return MongoUtility.tojson(value, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hasShardKeySpecified(document: any): boolean {
|
|
||||||
return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
|
||||||
let partitionKey: DataModels.PartitionKey = this.partitionKey;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.partitionKey &&
|
|
||||||
this.partitionKey.paths &&
|
|
||||||
this.partitionKey.paths.length &&
|
|
||||||
this.partitionKey.paths.length > 0 &&
|
|
||||||
this.partitionKey.paths[0].indexOf("$v") > -1
|
|
||||||
) {
|
|
||||||
// Convert BsonSchema2 to /path format
|
|
||||||
partitionKey = {
|
|
||||||
kind: partitionKey.kind,
|
|
||||||
paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")],
|
|
||||||
version: partitionKey.version,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return partitionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
|
||||||
return deleteDocument(this.collection.databaseId, this.collection, documentId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +1,17 @@
|
||||||
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { container } from "../Controls/Settings/TestUtils";
|
import { container } from "../Controls/Settings/TestUtils";
|
||||||
import DocumentId from "../Tree/DocumentId";
|
import DocumentId from "../Tree/DocumentId";
|
||||||
import DocumentsTab from "./DocumentsTab";
|
|
||||||
import { NewQueryTab } from "./QueryTab/QueryTab";
|
import { NewQueryTab } from "./QueryTab/QueryTab";
|
||||||
|
|
||||||
describe("useTabs tests", () => {
|
describe("useTabs tests", () => {
|
||||||
let database: ViewModels.Database;
|
let database: ViewModels.Database;
|
||||||
let collection: ViewModels.Collection;
|
let collection: ViewModels.Collection;
|
||||||
let queryTab: NewQueryTab;
|
let queryTab: NewQueryTab;
|
||||||
let documentsTab: DocumentsTab;
|
let documentsTab: DocumentsTabV2;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
@ -56,7 +56,7 @@ describe("useTabs tests", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTabV2({
|
||||||
partitionKey: undefined,
|
partitionKey: undefined,
|
||||||
documentIds: ko.observableArray<DocumentId>(),
|
documentIds: ko.observableArray<DocumentId>(),
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { BrandVariants, Theme, createLightTheme } from "@fluentui/react-components";
|
||||||
|
import { Platform } from "ConfigContext";
|
||||||
|
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
||||||
|
|
||||||
|
// These are the theme colors for Fluent UI 9 React components
|
||||||
|
const appThemePortalBrandRamp: BrandVariants = {
|
||||||
|
10: "#020305",
|
||||||
|
20: "#111723",
|
||||||
|
30: "#16263D",
|
||||||
|
40: "#193253",
|
||||||
|
50: "#1B3F6A",
|
||||||
|
60: "#1B4C82",
|
||||||
|
70: "#18599B",
|
||||||
|
80: "#1267B4",
|
||||||
|
90: "#3174C2",
|
||||||
|
100: "#4F82C8",
|
||||||
|
110: "#6790CF",
|
||||||
|
120: "#7D9ED5",
|
||||||
|
130: "#92ACDC",
|
||||||
|
140: "#A6BAE2",
|
||||||
|
150: "#BAC9E9",
|
||||||
|
160: "#CDD8EF",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPlatformTheme(platform: Platform): Theme {
|
||||||
|
if (platform === Platform.Fabric) {
|
||||||
|
return createLightTheme(appThemeFabricTealBrandRamp);
|
||||||
|
} else {
|
||||||
|
return createLightTheme(appThemePortalBrandRamp);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
@ -27,9 +28,7 @@ import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
|
||||||
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
|
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
|
||||||
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
|
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
|
||||||
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
||||||
|
@ -292,13 +291,13 @@ export default class Collection implements ViewModels.Collection {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentsTabs: DocumentsTab[] = useTabs
|
const documentsTabs: DocumentsTabV2[] = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
.getTabs(
|
.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(),
|
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(),
|
||||||
) as DocumentsTab[];
|
) as DocumentsTabV2[];
|
||||||
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
|
let documentsTab: DocumentsTabV2 = documentsTabs && documentsTabs[0];
|
||||||
|
|
||||||
if (documentsTab) {
|
if (documentsTab) {
|
||||||
useTabs.getState().activateTab(documentsTab);
|
useTabs.getState().activateTab(documentsTab);
|
||||||
|
@ -312,7 +311,7 @@ export default class Collection implements ViewModels.Collection {
|
||||||
});
|
});
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTabV2({
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
documentIds: ko.observableArray<DocumentId>([]),
|
documentIds: ko.observableArray<DocumentId>([]),
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
|
@ -494,13 +493,13 @@ export default class Collection implements ViewModels.Collection {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs
|
const mongoDocumentsTabs: DocumentsTabV2[] = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
.getTabs(
|
.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(),
|
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(),
|
||||||
) as MongoDocumentsTab[];
|
) as DocumentsTabV2[];
|
||||||
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
let mongoDocumentsTab: DocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
||||||
|
|
||||||
if (mongoDocumentsTab) {
|
if (mongoDocumentsTab) {
|
||||||
useTabs.getState().activateTab(mongoDocumentsTab);
|
useTabs.getState().activateTab(mongoDocumentsTab);
|
||||||
|
@ -514,7 +513,7 @@ export default class Collection implements ViewModels.Collection {
|
||||||
});
|
});
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
mongoDocumentsTab = new MongoDocumentsTab({
|
mongoDocumentsTab = new DocumentsTabV2({
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
documentIds: this.documentIds,
|
documentIds: this.documentIds,
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces DocumentsTab so we can plug any object
|
||||||
|
*/
|
||||||
|
export interface IDocumentIdContainer {
|
||||||
|
partitionKeyProperties?: string[];
|
||||||
|
partitionKey: DataModels.PartitionKey;
|
||||||
|
isEditorDirty: () => boolean;
|
||||||
|
selectDocument: (documentId: DocumentId) => Promise<void>;
|
||||||
|
}
|
||||||
export default class DocumentId {
|
export default class DocumentId {
|
||||||
public container: DocumentsTab;
|
public container: IDocumentIdContainer;
|
||||||
public rid: string;
|
public rid: string;
|
||||||
public self: string;
|
public self: string;
|
||||||
public ts: string;
|
public ts: string;
|
||||||
|
@ -15,7 +23,7 @@ export default class DocumentId {
|
||||||
public stringPartitionKeyValues: string[];
|
public stringPartitionKeyValues: string[];
|
||||||
public isDirty: ko.Observable<boolean>;
|
public isDirty: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(container: DocumentsTab, data: any, partitionKeyValue: any[]) {
|
constructor(container: IDocumentIdContainer, data: any, partitionKeyValue: any[]) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
this.rid = data._rid;
|
this.rid = data._rid;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import DocumentId from "./DocumentId";
|
import DocumentId, { IDocumentIdContainer } from "./DocumentId";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
|
||||||
|
|
||||||
export default class ObjectId extends DocumentId {
|
export default class ObjectId extends DocumentId {
|
||||||
constructor(container: DocumentsTab, data: any, partitionKeyValue: any) {
|
constructor(container: IDocumentIdContainer, data: any, partitionKeyValue: any) {
|
||||||
super(container, data, partitionKeyValue);
|
super(container, data, partitionKeyValue);
|
||||||
if (typeof data._id === "object") {
|
if (typeof data._id === "object") {
|
||||||
this.id = ko.observable(data._id[Object.keys(data._id)[0]]);
|
this.id = ko.observable(data._id[Object.keys(data._id)[0]]);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
@ -7,7 +8,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
|
||||||
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
|
@ -118,15 +118,15 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentsTabs: DocumentsTab[] = useTabs
|
const documentsTabs: DocumentsTabV2[] = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
.getTabs(
|
.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
(tab: TabsBase) =>
|
(tab: TabsBase) =>
|
||||||
tab.collection?.id() === this.id() &&
|
tab.collection?.id() === this.id() &&
|
||||||
(tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId,
|
(tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId,
|
||||||
) as DocumentsTab[];
|
) as DocumentsTabV2[];
|
||||||
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
|
let documentsTab: DocumentsTabV2 = documentsTabs && documentsTabs[0];
|
||||||
|
|
||||||
if (documentsTab) {
|
if (documentsTab) {
|
||||||
useTabs.getState().activateTab(documentsTab);
|
useTabs.getState().activateTab(documentsTab);
|
||||||
|
@ -139,7 +139,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||||
tabTitle: "Items",
|
tabTitle: "Items",
|
||||||
});
|
});
|
||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTabV2({
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
|
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
|
||||||
documentIds: ko.observableArray<DocumentId>([]),
|
documentIds: ko.observableArray<DocumentId>([]),
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import {
|
import {
|
||||||
BrandVariants,
|
|
||||||
FluentProvider,
|
FluentProvider,
|
||||||
Theme,
|
|
||||||
Tree,
|
Tree,
|
||||||
TreeItemValue,
|
TreeItemValue,
|
||||||
TreeOpenChangeData,
|
TreeOpenChangeData,
|
||||||
TreeOpenChangeEvent,
|
TreeOpenChangeEvent,
|
||||||
createLightTheme,
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
import { TreeNode2, TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
|
import { TreeNode2, TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
|
||||||
|
import { getPlatformTheme } from "Explorer/Theme/ThemeUtil";
|
||||||
import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes";
|
import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
|
@ -22,29 +21,6 @@ interface ResourceTreeProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cosmosdb: BrandVariants = {
|
|
||||||
10: "#020305",
|
|
||||||
20: "#111723",
|
|
||||||
30: "#16263D",
|
|
||||||
40: "#193253",
|
|
||||||
50: "#1B3F6A",
|
|
||||||
60: "#1B4C82",
|
|
||||||
70: "#18599B",
|
|
||||||
80: "#1267B4",
|
|
||||||
90: "#3174C2",
|
|
||||||
100: "#4F82C8",
|
|
||||||
110: "#6790CF",
|
|
||||||
120: "#7D9ED5",
|
|
||||||
130: "#92ACDC",
|
|
||||||
140: "#A6BAE2",
|
|
||||||
150: "#BAC9E9",
|
|
||||||
160: "#CDD8EF",
|
|
||||||
};
|
|
||||||
|
|
||||||
const lightTheme: Theme = {
|
|
||||||
...createLightTheme(cosmosdb),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DATA_TREE_LABEL = "DATA";
|
export const DATA_TREE_LABEL = "DATA";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,7 +89,7 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FluentProvider theme={lightTheme} style={{ overflow: "hidden" }}>
|
<FluentProvider theme={getPlatformTheme(configContext.platform)} style={{ overflow: "hidden" }}>
|
||||||
<Tree
|
<Tree
|
||||||
aria-label="CosmosDB resources"
|
aria-label="CosmosDB resources"
|
||||||
openItems={openItems}
|
openItems={openItems}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Theme, createTheme } from "@fluentui/react";
|
import { Theme, createTheme } from "@fluentui/react";
|
||||||
|
import { BrandVariants, createLightTheme } from "@fluentui/react-components";
|
||||||
|
|
||||||
export const appThemeFabric: Theme = createTheme({
|
export const appThemeFabric: Theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
|
@ -206,3 +207,24 @@ export const appThemeFabric: Theme = createTheme({
|
||||||
greenLight: "#13a10e",
|
greenLight: "#13a10e",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const appThemeFabricTealBrandRamp: BrandVariants = {
|
||||||
|
10: "#001919",
|
||||||
|
20: "#012826",
|
||||||
|
30: "#01322E",
|
||||||
|
40: "#033f38",
|
||||||
|
50: "#054d43",
|
||||||
|
60: "#0a5c50",
|
||||||
|
70: "#0c695a",
|
||||||
|
80: "#117865",
|
||||||
|
90: "#1f937e",
|
||||||
|
100: "#2aaC94",
|
||||||
|
110: "#3abb9f",
|
||||||
|
120: "#52c7aa",
|
||||||
|
130: "#78d3b9",
|
||||||
|
140: "#9ee0cb",
|
||||||
|
150: "#c0ecdd",
|
||||||
|
160: "#e3f7ef",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appThemeFabricV9 = createLightTheme(appThemeFabricTealBrandRamp);
|
||||||
|
|
|
@ -138,6 +138,7 @@ export enum Action {
|
||||||
QueryGenerationFromCopilotPrompt,
|
QueryGenerationFromCopilotPrompt,
|
||||||
QueryEdited,
|
QueryEdited,
|
||||||
ExecuteQueryGeneratedFromQueryCopilot,
|
ExecuteQueryGeneratedFromQueryCopilot,
|
||||||
|
DeleteDocuments,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|
|
@ -41,3 +41,9 @@ Object.defineProperty(window, "localStorage", {
|
||||||
require("jquery-ui-dist/jquery-ui");
|
require("jquery-ui-dist/jquery-ui");
|
||||||
(<any>global).TextEncoder = TextEncoder;
|
(<any>global).TextEncoder = TextEncoder;
|
||||||
(<any>global).TextDecoder = TextDecoder;
|
(<any>global).TextDecoder = TextDecoder;
|
||||||
|
|
||||||
|
(<any>global).ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
Loading…
Reference in New Issue