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 commitId 1f4d0f2

* 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 commit 1b8138ade0.

* Attempt 2 at troubleshooting failing test

* Revert "Attempt 2 at troubleshooting failing test"

This reverts commit 3e51a593bf.

* 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:
Laurent Nguyen 2024-05-29 09:09:13 +02:00 committed by GitHub
parent 19d1e0d1df
commit 36736882ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 5143 additions and 2114 deletions

View File

@ -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

View File

@ -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: {

View File

@ -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;

653
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;
}

View File

@ -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";
}; };

View File

@ -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(

View File

@ -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();
}
};

View File

@ -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({

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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();
});
});
});

View File

@ -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();
});
});

View File

@ -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>
);
};

View File

@ -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>
`;

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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]]);

View File

@ -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>([]),

View File

@ -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}

View File

@ -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);

View File

@ -138,6 +138,7 @@ export enum Action {
QueryGenerationFromCopilotPrompt, QueryGenerationFromCopilotPrompt,
QueryEdited, QueryEdited,
ExecuteQueryGeneratedFromQueryCopilot, ExecuteQueryGeneratedFromQueryCopilot,
DeleteDocuments,
} }
export const ActionModifiers = { export const ActionModifiers = {

View File

@ -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(),
}));