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