Compare commits

..

14 Commits

Author SHA1 Message Date
sunilyadav840
e230d286d9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react 2021-07-26 11:42:02 +05:30
sunilyadav840
4e1cf23ac8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react 2021-07-23 20:37:19 +05:30
sunilyadav840
52807a9a0e Fixed sqldocument delete and update issue 2021-07-23 20:27:47 +05:30
sunilyadav840
ea21ab2580 FIxed all PR change request 2021-07-19 18:11:54 +05:30
sunilyadav840
7d30268cd9 removed knockout DocumentTab file 2021-07-12 19:30:28 +05:30
sunilyadav840
42b7d8fe09 Merge branch 'master' 2021-07-12 01:26:43 +05:30
sunilyadav840
d9614cb751 refactor documentTab code 2021-07-12 01:03:03 +05:30
sunilyadav840
8f1f3c1c65 fixed mongo docuement create issue 2021-07-11 21:45:15 +05:30
sunilyadav840
bbae8ae301 Refactor code 2021-07-08 17:37:18 +05:30
sunilyadav840
f4a24610fe Clean knockout code 2021-07-06 19:28:57 +05:30
sunilyadav840
a456318099 Add update delete sqlDocumentTab 2021-07-05 19:22:03 +05:30
sunilyadav840
c01a382b4f complete functionality documentMongoTab migration 2021-06-28 18:38:57 +05:30
sunilyadav840
4559496640 add load, create, update and delete Document 2021-06-24 17:57:31 +05:30
sunilyadav840
b4443ac89f configire documenttab in react 2021-06-16 11:19:37 +05:30
102 changed files with 4770 additions and 5140 deletions

View File

@@ -21,8 +21,16 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Config.ts
src/Contracts/ActionContracts.ts
src/Contracts/DataModels.ts
src/Contracts/Diagnostics.ts
src/Contracts/ExplorerContracts.ts
src/Contracts/Versions.ts
src/Contracts/ViewModels.ts
src/Controls/Heatmap/Heatmap.test.ts src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts src/Controls/Heatmap/Heatmap.ts
src/Controls/Heatmap/HeatmapDatatypes.ts
src/Definitions/datatables.d.ts src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts src/Definitions/gif.d.ts
src/Definitions/globals.d.ts src/Definitions/globals.d.ts
@@ -36,10 +44,29 @@ src/Definitions/png.d.ts
src/Definitions/svg.d.ts src/Definitions/svg.d.ts
src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts src/Explorer/ComponentRegisterer.ts
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
src/Explorer/Controls/DynamicList/DynamicList.test.ts
src/Explorer/Controls/DynamicList/DynamicListComponent.ts
src/Explorer/Controls/Editor/EditorComponent.ts src/Explorer/Controls/Editor/EditorComponent.ts
src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts
src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts
src/Explorer/Controls/Toolbar/IToolbarDropDown.ts
src/Explorer/Controls/Toolbar/IToolbarItem.ts
src/Explorer/Controls/Toolbar/IToolbarSeperator.ts
src/Explorer/Controls/Toolbar/IToolbarToggle.ts
src/Explorer/Controls/Toolbar/KeyCodes.ts
src/Explorer/Controls/Toolbar/Toolbar.ts
src/Explorer/Controls/Toolbar/ToolbarAction.ts
src/Explorer/Controls/Toolbar/ToolbarDropDown.ts
src/Explorer/Controls/Toolbar/ToolbarToggle.ts
src/Explorer/Controls/Toolbar/Utilities.ts
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts src/Explorer/DataSamples/DataSamplesUtil.test.ts
@@ -137,10 +164,18 @@ src/Terminal/NotebookAppContracts.d.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts
src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx

View File

@@ -143,7 +143,7 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,7 +9,6 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@xSmallFontSize: 4px; @xSmallFontSize: 4px;
@smallFontSize: 8px; @smallFontSize: 8px;

View File

@@ -3068,6 +3068,78 @@ settings-pane {
display: none; display: none;
height: 0px; height: 0px;
} }
.react-editor {
height: 400px;
}
.documentTabSearchBar{
width: 80%;
margin: 15px;
}
.documentSqlTabSearchBar{
width: 68%;
margin: 15px;
}
.documentTabFiltetButton{
margin-top: 15px;
}
.filterSuggestions {
z-index: 1;
position: absolute;
top: 133px;
padding: 10px;
margin-left: 10px;
background: white;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
width: 20%;
height: auto;
}
.sqlFilterSuggestions {
margin-left: 10%;
}
.documentTabSuggestions {
padding: 5px;
cursor: pointer;
}
.documentTabNoFilterView {
margin: 15px;
}
.noFilterText {
margin-right: 10px;
}
.documentTabWatermark{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 10%;
margin-right: 12%;
}
.documentCreateText {
margin-top: 10px;
}
.documentLoadMore {
color: #0078D4;
font-size: 12px;
cursor: pointer;
margin-top: 10px;
}
.leftSplitter {
align-items: center;
text-align: center;
flex-direction: column;
}
.documentIdItem{
cursor: pointer;
}
.splitterWrapper .splitter-layout .layout-pane.layout-pane-primary{
max-height: 60%;
}
.queryText {
padding: 20px;
padding-left: 10px;
padding-right: 0px;
}
.spinner { .spinner {
width: 100%; width: 100%;
position: absolute; position: absolute;

View File

@@ -19,10 +19,6 @@
.notebookHeader { .notebookHeader {
font-size: 12px; font-size: 12px;
} }
.clickDisabled {
pointer-events: none;
}
} }

4621
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,9 +48,9 @@
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "3.0.0", "clean-webpack-plugin": "0.1.19",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "9.0.1", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "6.1.1",
@@ -81,10 +81,10 @@
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
"q": "1.5.1", "q": "1.5.1",
"react": "16.14.0", "react": "16.13.1",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
"react-dnd": "14.0.2", "react-dnd": "9.4.0",
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5", "react-i18next": "11.8.5",
@@ -98,7 +98,7 @@
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "5.1.4", "terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
@@ -132,7 +132,6 @@
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0", "@typescript-eslint/parser": "4.22.0",
"@webpack-cli/serve": "1.5.2",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
@@ -154,45 +153,44 @@
"html-inline-css-webpack-plugin": "1.11.0", "html-inline-css-webpack-plugin": "1.11.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "5.3.2", "html-webpack-plugin": "4.5.2",
"jest": "26.6.3", "jest": "25.5.4",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.1.0",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.13.0", "playwright": "1.13.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"process": "0.11.10",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "11.0.4",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.2.4", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typedoc": "0.20.36", "typedoc": "0.20.36",
"typescript": "4.3.4", "typescript": "4.3.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "5.47.0", "webpack": "4.46.0",
"webpack-bundle-analyzer": "4.4.2", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "4.7.2", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.2" "webpack-dev-server": "3.11.0"
}, },
"scripts": { "scripts": {
"start": "webpack serve --mode development", "start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build", "dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci", "build:dataExplorer:ci": "npm run build:ci",
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers", "build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast", "build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
"pack:prod": "webpack --mode production", "pack:prod": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production",
"pack:fast": "webpack --mode development --progress", "pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
"copyToConsumers": "node copyToConsumers", "copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest", "test": "rimraf coverage && jest",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles", "test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",

View File

@@ -95,8 +95,6 @@ export class Flights {
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -338,13 +336,6 @@ export enum ConflictOperationType {
Delete = "delete", Delete = "delete",
} }
export enum ConnectionStatusType {
Connecting = "Connecting",
Allocating = "Allocating",
Connected = "Connected",
Failed = "Connection Failed",
}
export const EmulatorMasterKey = export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
@@ -358,11 +349,6 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -54,7 +54,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
</div> </div>
{userContext.authType === AuthType.ResourceToken ? ( {userContext.authType === AuthType.ResourceToken ? (
<ResourceTokenTree /> <ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? ( ) : userContext.features.enableKOResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : ( ) : (
<ResourceTree container={container} /> <ResourceTree container={container} />

View File

@@ -1,9 +1,9 @@
import { CollectionBase } from "../../Contracts/ViewModels"; import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => { export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName(); const entityName: string = getEntityName();

View File

@@ -1,5 +1,3 @@
import { ConnectionStatusType } from "../Common/Constants";
export interface DatabaseAccount { export interface DatabaseAccount {
id: string; id: string;
name: string; name: string;
@@ -90,6 +88,9 @@ export interface SubscriptionPolicies {
} }
export interface Resource { export interface Resource {
_partitionKeyValue?: string;
_partitionKey?: string;
_attachments?: string;
_rid: string; _rid: string;
_self: string; _self: string;
_etag: string; _etag: string;
@@ -498,8 +499,3 @@ export interface MemoryUsageInfo {
freeKB: number; freeKB: number;
totalKB: number; totalKB: number;
} }
export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}

View File

@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
}; };
let heatmap: Heatmap; let heatmap: Heatmap;
const theme: PortalTheme = 1; let theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => { describe("drawHeatmap rendering", () => {
beforeEach(() => { beforeEach(() => {
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
}); });
it("should show a no data message with a dark theme", () => { it("should show a no data message with a dark theme", () => {
const data = { let data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement; document.body.innerHTML = divElement;
handleMessage(data as MessageEvent); handleMessage(data as MessageEvent);
@@ -120,7 +120,7 @@ describe("iframe rendering when there is no data", () => {
}); });
it("should show a no data message with a white theme", () => { it("should show a no data message with a white theme", () => {
const data = { let data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement; document.body.innerHTML = divElement;
handleMessage(data as MessageEvent); handleMessage(data as MessageEvent);

View File

@@ -39,7 +39,7 @@ export class Heatmap {
} }
} }
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings { private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
return { return {
family: StyleConstants.DataExplorerFont, family: StyleConstants.DataExplorerFont,
size, size,
@@ -78,9 +78,9 @@ export class Heatmap {
// go thru all rows and create 2d matrix for heatmap... // go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]); output.yAxisPoints.push(rows[i]);
const dataPoints: number[] = []; let dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) { for (let a = 0; a < output.xAxisPoints.length; a++) {
const row: PartitionTimeStampToData = data[rows[i]]; let row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]); dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
} }
output.dataPoints.push(dataPoints); output.dataPoints.push(dataPoints);
@@ -193,7 +193,7 @@ export class Heatmap {
this._getLayoutSettings(), this._getLayoutSettings(),
this._getChartDisplaySettings() this._getChartDisplaySettings()
); );
const plotDiv: any = document.getElementById(Heatmap.elementId); let plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => { plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x; let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T"); timeSelected = timeSelected.replace(" ", "T");
@@ -205,7 +205,7 @@ export class Heatmap {
break; break;
} }
} }
const output = []; let output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) { for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]); output.push(this._chartData.dataPoints[i][xAxisIndex]);
} }

View File

@@ -83,7 +83,6 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {

View File

@@ -23,75 +23,13 @@ export interface DialogState {
dialogProps?: DialogProps; dialogProps?: DialogProps;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
closeDialog: () => void; closeDialog: () => void;
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
) => void;
showOkModalDialog: (title: string, subText: string) => void;
} }
export const useDialog: UseStore<DialogState> = create((set, get) => ({ export const useDialog: UseStore<DialogState> = create((set) => ({
visible: false, visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () => closeDialog: () =>
set( set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true),
(state) => ({
visible: false,
openDialog: state.openDialog,
closeDialog: state.closeDialog,
showOkCancelModalDialog: state.showOkCancelModalDialog,
showOkModalDialog: state.showOkModalDialog,
}),
true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above
),
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
get().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
get().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
}),
showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();
},
onSecondaryButtonClick: undefined,
}),
})); }));
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
@@ -181,7 +119,8 @@ export const Dialog: FC = () => {
text: secondaryButtonText, text: secondaryButtonText,
onClick: onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: undefined; : {};
return visible ? ( return visible ? (
<FluentDialog {...dialogProps}> <FluentDialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}

View File

@@ -33,16 +33,20 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.createEditor(this.configureEditor.bind(this)); this.createEditor(this.configureEditor.bind(this));
} }
public componentDidUpdate(previous: EditorReactProps) { public shouldComponentUpdate(): boolean {
if (this.props.content !== previous.content) { return true;
this.editor.setValue(this.props.content);
}
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.selectionListener && this.selectionListener.dispose(); this.selectionListener && this.selectionListener.dispose();
} }
public componentDidUpdate(prevProps: EditorReactProps): void {
if (this.props.content !== prevProps.content) {
this.editor.setValue(this.props.content);
}
}
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<React.Fragment> <React.Fragment>

View File

@@ -5,9 +5,6 @@
display: inline-block; display: inline-block;
width: 100%; width: 100%;
.input-type-head-text-field {
width: 100%;
}
textarea { textarea {
width: 100%; width: 100%;
line-height: 1; line-height: 1;
@@ -24,11 +21,4 @@
} }
} }
} }
.input-typeahead-chocies-container {
border: 1px solid lightgrey;
padding: 5px 10px 5px 10px;
cursor: pointer;
.choice-caption{
font-size: 14px;
}
}

View File

@@ -6,13 +6,14 @@
* typeaheadOverrideOptions: { dynamic:false } * typeaheadOverrideOptions: { dynamic:false }
* *
*/ */
import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react"; import "jquery-typeahead";
import * as React from "react"; import * as React from "react";
import { KeyCodes } from "../../../Common/Constants";
import "./InputTypeahead.less"; import "./InputTypeahead.less";
export interface Item { export interface Item {
caption: string; caption: string;
value: string; value: any;
} }
/** /**
@@ -50,7 +51,7 @@ export interface InputTypeaheadComponentProps {
* Override default jquery-typeahead options * Override default jquery-typeahead options
* WARNING: do not override input, source or callback to avoid breaking the components behavior. * WARNING: do not override input, source or callback to avoid breaking the components behavior.
*/ */
typeaheadOverrideOptions?: { dynamic: boolean }; typeaheadOverrideOptions?: any;
/** /**
* This function gets called when pressing ENTER on the input box * This function gets called when pressing ENTER on the input box
@@ -74,126 +75,170 @@ export interface InputTypeaheadComponentProps {
useTextarea?: boolean; useTextarea?: boolean;
} }
interface InputTypeaheadComponentState { interface OnClickItem {
isSuggestionVisible: boolean; matchedKey: string;
selectedChoice: Item; value: any;
filteredChoices: Item[]; caption: string;
group: string;
} }
interface Cache {
inputValue: string;
selection: Item;
}
interface InputTypeaheadComponentState {}
export class InputTypeaheadComponent extends React.Component< export class InputTypeaheadComponent extends React.Component<
InputTypeaheadComponentProps, InputTypeaheadComponentProps,
InputTypeaheadComponentState InputTypeaheadComponentState
> { > {
constructor(props: InputTypeaheadComponentProps) { private inputElt: HTMLElement;
private containerElt: HTMLElement;
private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(props: InputTypeaheadComponentProps) {
super(props); super(props);
this.state = { this.cache = {
isSuggestionVisible: false, inputValue: null,
filteredChoices: [], selection: null,
selectedChoice: {
caption: "",
value: "",
},
}; };
} }
private onRenderCell = (item: Item): JSX.Element => { /**
* Props have changed
* @param prevProps
* @param prevState
* @param snapshot
*/
public componentDidUpdate(
prevProps: InputTypeaheadComponentProps,
prevState: InputTypeaheadComponentState,
snapshot: any
): void {
if (prevProps.defaultValue !== this.props.defaultValue) {
$(this.inputElt).val(this.props.defaultValue);
this.initializeTypeahead();
}
}
/**
* Executed once react is done building the DOM for this component
*/
public componentDidMount(): void {
this.initializeTypeahead();
}
public render(): JSX.Element {
return ( return (
<div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}> <span className="input-typeahead-container">
<p className="choice-caption">{item.caption}</p> <div
<span>{item.value}</span> className="input-typehead"
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
>
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
<div className="typeahead__field">
<span className="typeahead__query">
{this.props.useTextarea ? (
<textarea
rows={1}
name="q"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
) : (
<input
name="q"
type="search"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
)}
</span>
{this.props.showSearchButton && (
<span className="typeahead__button">
<button type="submit">
<span className="typeahead__search-icon" />
</button>
</span>
)}
</div> </div>
</div>
</div>
</span>
); );
};
private onChoiceClick = (item: Item): void => {
this.props.onNewValue(item.caption);
this.setState({ isSuggestionVisible: false, selectedChoice: item });
};
private handleChange = (value: string): void => {
if (!value) {
this.setState({ isSuggestionVisible: true });
} }
this.props.onNewValue(value);
const filteredChoices = this.filterChoiceByValue(this.props.choices, value);
this.setState({ filteredChoices });
};
private onSubmit = (event: React.KeyboardEvent<HTMLElement>): void => { private onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
if (event.key === "Enter") { if (event.keyCode === KeyCodes.Enter) {
if (this.props.submitFct) { if (this.props.submitFct) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.submitFct(this.props.defaultValue, this.state.selectedChoice); this.props.submitFct(this.cache.inputValue, this.cache.selection);
this.setState({ isSuggestionVisible: false }); $(this.containerElt).children(".typeahead__result").hide();
}
} }
} }
};
private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => { /**
return choices.filter((choice) => * Must execute once ko is rendered, so that it can find the input element by id
// eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore private initializeTypeahead(): void {
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase())) const props = this.props;
); let cache = this.cache;
}; let options: any = {
input: this.inputElt,
public render(): JSX.Element { order: "asc",
const { defaultValue, useTextarea, placeholder, onNewValue } = this.props; minLength: 0,
const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state; searchOnFocus: true,
const theme = getTheme(); source: {
display: "caption",
const iconButtonStyles = { data: () => {
root: { return props.choices;
color: theme.palette.neutralPrimary,
marginLeft: "10px !important",
marginTop: "0px",
marginRight: "2px",
width: "42px",
}, },
rootHovered: {
color: theme.palette.neutralDark,
}, },
}; callback: {
const cancelIcon: IIconProps = { iconName: "cancel" }; onClick: (node: any, a: any, item: OnClickItem, event: any) => {
const searchIcon: IIconProps = { iconName: "Search" }; cache.selection = item;
return ( if (props.onSelected) {
<div className="input-typeahead-container"> props.onSelected(item);
<Stack horizontal> }
<TextField },
multiline={useTextarea} onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
rows={1} cache.inputValue = query;
defaultValue={defaultValue} if (props.onNewValue) {
ariaLabel="Input query" props.onNewValue(query);
placeholder={placeholder} }
className="input-type-head-text-field" },
value={defaultValue} },
onKeyDown={this.onSubmit} template: (query: string, item: any) => {
onFocus={() => this.setState({ isSuggestionVisible: true })} // Don't display id if caption *IS* the id
onChange={(_event, newValue?: string) => this.handleChange(newValue)} return item.caption === item.value
/> ? "<span>{{caption}}</span>"
{this.props.showCancelButton && ( : "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
<IconButton },
styles={iconButtonStyles} dynamic: true,
iconProps={cancelIcon} };
ariaLabel="cancel Button"
onClick={() => onNewValue("")} // Override options
/> if (props.typeaheadOverrideOptions) {
)} for (const p in props.typeaheadOverrideOptions) {
{this.props.showSearchButton && ( options[p] = props.typeaheadOverrideOptions[p];
<IconButton }
styles={iconButtonStyles} }
iconProps={searchIcon}
ariaLabel="Search Button" if (props.hasOwnProperty("showCancelButton")) {
onClick={() => this.props.submitFct(defaultValue, selectedChoice)} options.cancelButton = props.showCancelButton;
/> }
)}
</Stack> $(this.inputElt).typeahead(options);
{filteredChoices.length && isSuggestionVisible ? (
<List items={filteredChoices} onRenderCell={this.onRenderCell} />
) : undefined}
</div>
);
} }
} }

View File

@@ -1,43 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inputTypeahead renders <input /> 1`] = ` exports[`inputTypeahead renders <input /> 1`] = `
<div <span
className="input-typeahead-container" className="input-typeahead-container"
> >
<Stack <div
horizontal={true} className="input-typehead"
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={false}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
placeholder="placeholder" >
rows={1} <div
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<input
aria-label="Input query"
autoComplete="off"
name="q"
type="search"
/> />
</Stack> </span>
</div> </div>
</div>
</div>
</span>
`; `;
exports[`inputTypeahead renders <textarea /> 1`] = ` exports[`inputTypeahead renders <textarea /> 1`] = `
<div <span
className="input-typeahead-container" className="input-typeahead-container"
> >
<Stack <div
horizontal={true} className="input-typehead"
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={true}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
placeholder="placeholder" >
<div
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<textarea
aria-label="Input query"
autoComplete="off"
name="q"
rows={1} rows={1}
/> />
</Stack> </span>
</div> </div>
</div>
</div>
</span>
`; `;

View File

@@ -264,6 +264,6 @@ export class NotebookViewerComponent
}; };
private reportAbuse = (): void => { private reportAbuse = (): void => {
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => ({})); GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
}; };
} }

View File

@@ -29,7 +29,6 @@ import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Dialog";
const title = "Open Saved Queries"; const title = "Open Saved Queries";
@@ -223,11 +222,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
key: "Delete", key: "Delete",
text: "Delete query", text: "Delete query",
onClick: async () => { onClick: async () => {
useDialog.getState().showOkCancelModalDialog( if (window.confirm("Are you sure you want to delete this query?")) {
"Confirm delete",
"Are you sure you want to delete this query?",
"Delete",
async () => {
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title, paneTitle: title,
@@ -255,10 +250,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
); );
} }
await this.fetchSavedQueries(); // get latest state await this.fetchSavedQueries(); // get latest state
}, }
"Cancel",
undefined
);
}, },
}, },
], ],

View File

@@ -34,7 +34,6 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -102,7 +101,6 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -17,6 +17,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,7 +1,6 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
@@ -21,7 +20,7 @@ export class DataSamplesUtil {
const containerName = generator.getCollectionId(); const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); logConsoleError(msg);
return; return;
} }
@@ -30,7 +29,7 @@ export class DataSamplesUtil {
.createSampleContainerAsync() .createSampleContainerAsync()
.catch((error) => logConsoleError(`Error creating sample container: ${error}`)); .catch((error) => logConsoleError(`Error creating sample container: ${error}`));
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg); logConsoleInfo(msg);
} }

View File

@@ -1,10 +1,10 @@
import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { ConnectionStatusType } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
@@ -17,7 +17,6 @@ import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -36,7 +35,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
@@ -89,13 +88,12 @@ export default class Explorer {
}; };
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
private phoenixClient: PhoenixClient;
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient();
useNotebook.subscribe( useNotebook.subscribe(
() => this.refreshCommandBarButtons(), () => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount (state) => state.isNotebooksEnabledForAccount
@@ -346,30 +344,7 @@ export default class Explorer {
return; return;
} }
this._isInitializingNotebooks = true; this._isInitializingNotebooks = true;
if (userContext.features.phoenix) {
const connectionStatus: DataModels.ContainerConnectionInfo = {
status: ConnectionStatusType.Allocating,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const provisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
resourceId: userContext.databaseAccount.id,
dbAccountName: userContext.databaseAccount.name,
aadToken: userContext.authorizationToken,
resourceGroup: userContext.resourceGroup,
subscriptionId: userContext.subscriptionId,
};
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
}
} else {
await this.ensureNotebookWorkspaceRunning(); await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo( const connectionInfo = await listConnectionInfo(
userContext.subscriptionId, userContext.subscriptionId,
@@ -382,7 +357,6 @@ export default class Explorer {
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken, authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
}); });
}
useNotebook.getState().initializeNotebooksTree(this.notebookManager); useNotebook.getState().initializeNotebooksTree(this.notebookManager);
@@ -391,7 +365,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public resetNotebookWorkspace(): void { public resetNotebookWorkspace() {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError( handleError(
"Attempt to reset notebook workspace, but notebook is not enabled", "Attempt to reset notebook workspace, but notebook is not enabled",
@@ -416,6 +390,7 @@ export default class Explorer {
if (!databaseAccount) { if (!databaseAccount) {
return false; return false;
} }
try { try {
const { value: workspaces } = await listByDatabaseAccount( const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId, userContext.subscriptionId,
@@ -563,22 +538,17 @@ export default class Explorer {
} }
} }
public uploadFile( public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile"); handleError(error, "Explorer/uploadFile");
throw new Error(error); throw new Error(error);
} }
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); .catch((reason) => this.showOkModalDialog("Unable to upload file", reason));
return promise; return promise;
} }
@@ -644,6 +614,51 @@ export default class Explorer {
this.notebookManager?.openCopyNotebookPane(name, content); this.notebookManager?.openCopyNotebookPane(name, content);
} }
public showOkModalDialog(title: string, msg: string): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
},
onSecondaryButtonClick: undefined,
});
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
useDialog.getState().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled,
});
}
/** /**
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
@@ -703,7 +718,7 @@ export default class Explorer {
return true; return true;
} }
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { public renameNotebook(notebookFile: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled"; const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook"); handleError(error, "Explorer/renameNotebook");
@@ -717,9 +732,7 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
useDialog this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
.getState()
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
useSidePanel.getState().openSidePanel( useSidePanel.getState().openSidePanel(
"Rename Notebook", "Rename Notebook",
@@ -736,7 +749,7 @@ export default class Explorer {
paneTitle="Rename Notebook" paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
} }
notebookFile={notebookFile} notebookFile={notebookFile}
/> />
@@ -744,7 +757,7 @@ export default class Explorer {
} }
} }
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { public onCreateDirectory(parent: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled"; const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory"); handleError(error, "Explorer/onCreateDirectory");
@@ -766,7 +779,7 @@ export default class Explorer {
submitButtonLabel="Create" submitButtonLabel="Create"
defaultInput="" defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
} }
notebookFile={parent} notebookFile={parent}
/> />
@@ -835,7 +848,7 @@ export default class Explorer {
} }
}; };
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> { public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled"; const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile"); handleError(error, "Explorer/deleteNotebookFile");
@@ -849,9 +862,7 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
useDialog this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject(); return Promise.reject();
} }
@@ -868,7 +879,7 @@ export default class Explorer {
return Promise.reject(); return Promise.reject();
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`), () => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
); );
@@ -877,7 +888,7 @@ export default class Explorer {
/** /**
* This creates a new notebook file, then opens the notebook * This creates a new notebook file, then opens the notebook
*/ */
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { public onNewNotebookClicked(parent?: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled"; const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
@@ -892,7 +903,7 @@ export default class Explorer {
}); });
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree) .createNewNotebookFile(parent)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`); logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -932,7 +943,7 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind): void { public openNotebookTerminal(kind: ViewModels.TerminalKind) {
let title: string; let title: string;
switch (kind) { switch (kind) {
@@ -1052,10 +1063,7 @@ export default class Explorer {
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if ( if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1101,13 +1109,10 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(); : this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
let isNotebookEnabled = true; const isNotebookEnabled: boolean =
if (!userContext.features.phoenix) {
isNotebookEnabled =
userContext.authType !== AuthType.ResourceToken && userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks); userContext.features.enableNotebooks);
}
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
@@ -1116,7 +1121,6 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (!userContext.features.notebooksTemporarilyDown) {
if (isNotebookEnabled) { if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) { } else if (this.notebookToImport) {
@@ -1125,4 +1129,3 @@ export default class Explorer {
} }
} }
} }
}

View File

@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const partitionKeyProperty = this.props.collectionPartitionKeyProperty; const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
// aggregate all the properties, remove dropped ones // aggregate all the properties, remove dropped ones
const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties); let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
// Compose the query // Compose the query
const pkId = editedProperties.pkId; let pkId = editedProperties.pkId;
let updateQueryFragment = ""; let updateQueryFragment = "";
finalProperties.forEach((p) => { finalProperties.forEach((p) => {
@@ -473,7 +473,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false; return false;
} }
const pairs: any[] = data; let pairs: any[] = data;
for (let i = 0; i < pairs.length; i++) { for (let i = 0; i < pairs.length; i++) {
const item = pairs[i]; const item = pairs[i];
if ( if (
@@ -772,8 +772,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
const edge = edges[0]; let edge = edges[0];
const graphData = this.originalGraphData; let graphData = this.originalGraphData;
graphData.addEdge(edge); graphData.addEdge(edge);
// Allow loadNeighbors to load list new edge // Allow loadNeighbors to load list new edge
@@ -803,7 +803,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public removeEdge(edgeId: string): Q.Promise<any> { public removeEdge(edgeId: string): Q.Promise<any> {
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then( return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
() => { () => {
const graphData = this.originalGraphData; let graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false); graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
@@ -826,9 +826,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false; return false;
} }
const vertices: any[] = data; let vertices: any[] = data;
if (vertices.length > 0) { if (vertices.length > 0) {
const v0 = vertices[0]; let v0 = vertices[0];
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") { if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
return false; return false;
} }
@@ -933,7 +933,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
throw { title: err }; throw { title: err };
} }
const vertex = vertices[0]; let vertex = vertices[0];
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addVertex(vertex); graphData.addVertex(vertex);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
@@ -1082,7 +1082,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr = ""; let errorDataStr: string = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + JSON.stringify(errorData);
@@ -1224,7 +1224,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return $.map( return $.map(
this.state.rootMap, this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => { (value: any, index: number): LeftPane.CaptionId => {
const result = GraphData.GraphData.getNodePropValue(value, key); let result = GraphData.GraphData.getNodePropValue(value, key);
return { return {
caption: result !== undefined ? result : value.id, caption: result !== undefined ? result : value.id,
id: value.id, id: value.id,
@@ -1388,7 +1388,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return id * @return id
*/ */
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string { public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
const { id } = d; let { id } = d;
if (typeof id !== "string") { if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`; const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
logConsoleError(error); logConsoleError(error);
@@ -1425,7 +1425,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then( return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
const possibleVertices = [] as PossibleVertex[]; let possibleVertices = [] as PossibleVertex[];
$.each(documents, (index: number, item: any) => { $.each(documents, (index: number, item: any) => {
if (highlightedNodeId && item.id === highlightedNodeId) { if (highlightedNodeId && item.id === highlightedNodeId) {
// Exclude highlighed node in the list // Exclude highlighed node in the list
@@ -1463,16 +1463,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return promise when done * @return promise when done
*/ */
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> { private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
const promises = []; let promises = [];
// Drop edges // Drop edges
for (let i = 0; i < editedEdges.droppedIds.length; i++) { for (let i = 0; i < editedEdges.droppedIds.length; i++) {
const id = editedEdges.droppedIds[i]; let id = editedEdges.droppedIds[i];
promises.push(this.removeEdge(id)); promises.push(this.removeEdge(id));
} }
// Add edges // Add edges
for (let i = 0; i < editedEdges.addedEdges.length; i++) { for (let i = 0; i < editedEdges.addedEdges.length; i++) {
const e = editedEdges.addedEdges[i]; let e = editedEdges.addedEdges[i];
promises.push( promises.push(
this.createNewEdge(e).then(() => { this.createNewEdge(e).then(() => {
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph // Reload neighbors in case we linked to a vertex that isn't loaded in the graph
@@ -1533,7 +1533,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) { private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
const props = {} as any; // Hashset const props = {} as any; // Hashset
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => { $.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
for (const p in item) { for (var p in item) {
// DocDB: Exclude type because it's always 'vertex' // DocDB: Exclude type because it's always 'vertex'
if (p !== "type" && typeof (item as any)[p] === "string") { if (p !== "type" && typeof (item as any)[p] === "string") {
props[p] = true; props[p] = true;
@@ -1543,7 +1543,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (item.hasOwnProperty("properties")) { if (item.hasOwnProperty("properties")) {
// TODO This is DocDB-graph specific // TODO This is DocDB-graph specific
// Assume each property value is [{value:... }] // Assume each property value is [{value:... }]
for (const f in item.properties) { for (var f in item.properties) {
props[f] = true; props[f] = true;
} }
} }
@@ -1570,21 +1570,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
const data = this.originalGraphData.getVertexById(id); let data = this.originalGraphData.getVertexById(id);
// A bit of translation to make it easier to display // A bit of translation to make it easier to display
const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {}; let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
for (const p in data.properties) { for (let p in data.properties) {
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value); props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
} }
// update neighbors // update neighbors
const sources: NeighborVertexBasicInfo[] = []; let sources: NeighborVertexBasicInfo[] = [];
const targets: NeighborVertexBasicInfo[] = []; let targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues(); this.props.onResetDefaultGraphConfigValues();
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice; let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets); this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
const sData: GraphHighlightedNodeData = { let sData: GraphHighlightedNodeData = {
id: data.id, id: data.id,
label: data.label, label: data.label,
properties: props, properties: props,
@@ -1611,16 +1611,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
targets: NeighborVertexBasicInfo[] targets: NeighborVertexBasicInfo[]
): void { ): void {
// update neighbors // update neighbors
const gd = this.originalGraphData; let gd = this.originalGraphData;
const v = gd.getVertexById(id); let v = gd.getVertexById(id);
// Clear the array while keeping the references // Clear the array while keeping the references
sources.length = 0; sources.length = 0;
targets.length = 0; targets.length = 0;
const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
for (const p in v.inE) { for (let p in v.inE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.inE[p]; const edges = v.inE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
@@ -1629,7 +1629,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({ sources.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1639,7 +1639,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
} }
for (const p in v.outE) { for (let p in v.outE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.outE[p]; const edges = v.outE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
@@ -1648,7 +1648,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({ targets.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1681,20 +1681,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
const updatedVertex = vertices[0]; let updatedVertex = vertices[0];
if (this.originalGraphData.hasVertexId(updatedVertex.id)) { if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id); let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
// Copy updated properties // Copy updated properties
if (currentVertex.hasOwnProperty("properties")) { if (currentVertex.hasOwnProperty("properties")) {
delete currentVertex["properties"]; delete currentVertex["properties"];
} }
for (const p in updatedVertex) { for (var p in updatedVertex) {
(currentVertex as any)[p] = updatedVertex[p]; (currentVertex as any)[p] = updatedVertex[p];
} }
} }
// TODO This kind of assumes saveVertexProperty is done from property panes. // TODO This kind of assumes saveVertexProperty is done from property panes.
const hn = this.state.highlightedNode; let hn = this.state.highlightedNode;
if (hn && hn.id === updatedVertex.id) { if (hn && hn.id === updatedVertex.id) {
this.updatePropertiesPane(hn.id); this.updatePropertiesPane(hn.id);
} }
@@ -1708,7 +1708,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
igraphConfig?: IGraphConfig igraphConfig?: IGraphConfig
) { ) {
this.originalGraphData = graphData; this.originalGraphData = graphData;
const gd = JSON.parse(JSON.stringify(this.originalGraphData)); let gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return; return;

View File

@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
className={className} className={className}
as="tr" as="tr"
aria-label={node.caption} aria-label={node.caption}
onActivated={() => this.props.onRootNodeSelected(node.id)} onActivated={(e) => this.props.onRootNodeSelected(node.id)}
key={node.id} key={node.id}
> >
<td className="resultItem"> <td className="resultItem">

View File

@@ -1,8 +1,8 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import * as Q from "q"; import * as Q from "q";
import React from "react"; import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent";
import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer"; import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
describe("Property pane", () => { describe("Property pane", () => {
const title = "My Title"; const title = "My Title";
@@ -37,25 +37,17 @@ describe("Property pane", () => {
return { return {
expandedTitle: title, expandedTitle: title,
isCollapsed: false, isCollapsed: false,
onCollapsedChanged: (): void => { onCollapsedChanged: (newValue: boolean): void => {},
("");
},
node: highlightedNode, node: highlightedNode,
getPkIdFromNodeData: (): string => undefined, getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null,
collectionPartitionKeyProperty: undefined, collectionPartitionKeyProperty: null,
updateVertexProperties: (): Q.Promise<void> => Q.resolve(), updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(),
selectNode: (): void => { selectNode: (id: string): void => {},
(""); updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null),
}, possibleEdgeLabels: null,
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined), editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(),
possibleEdgeLabels: undefined, deleteHighlightedNode: (): void => {},
editGraphEdges: (): Q.Promise<unknown> => Q.resolve(), onModeChanged: (newMode: Mode): void => {},
deleteHighlightedNode: (): void => {
("");
},
onModeChanged: (): void => {
("");
},
viewMode: Mode.READONLY_PROP, viewMode: Mode.READONLY_PROP,
}; };
}; };

View File

@@ -6,9 +6,9 @@
import * as React from "react"; import * as React from "react";
import CancelIcon from "../../../../images/cancel.svg"; import CancelIcon from "../../../../images/cancel.svg";
import CheckIcon from "../../../../images/check-1.svg"; import CheckIcon from "../../../../images/check.svg";
import DeleteIcon from "../../../../images/delete.svg"; import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit-1.svg"; import EditIcon from "../../../../images/edit.svg";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel"; import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
@@ -45,7 +45,7 @@ export interface NodePropertiesComponentProps {
selectNode: (id: string) => void; selectNode: (id: string) => void;
updatePossibleVertices: () => Q.Promise<PossibleVertex[]>; updatePossibleVertices: () => Q.Promise<PossibleVertex[]>;
possibleEdgeLabels: Item[]; possibleEdgeLabels: Item[];
editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<unknown>; editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<any>;
deleteHighlightedNode: () => void; deleteHighlightedNode: () => void;
onModeChanged: (newMode: Mode) => void; onModeChanged: (newMode: Mode) => void;
viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it
@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
super(props); super(props);
this.state = { this.state = {
editedProperties: { editedProperties: {
pkId: undefined, pkId: null,
readOnlyProperties: [], readOnlyProperties: [],
existingProperties: [], existingProperties: [],
addedProperties: [], addedProperties: [],
@@ -98,12 +98,15 @@ export class NodePropertiesComponent extends React.Component<
}; };
} }
public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> { public static getDerivedStateFromProps(
props: NodePropertiesComponentProps,
state: NodePropertiesComponentState
): Partial<NodePropertiesComponentState> {
if (props.viewMode !== Mode.READONLY_PROP) { if (props.viewMode !== Mode.READONLY_PROP) {
return { isDeleteConfirm: false }; return { isDeleteConfirm: false };
} }
return undefined; return null;
} }
public render(): JSX.Element { public render(): JSX.Element {
@@ -134,14 +137,11 @@ export class NodePropertiesComponent extends React.Component<
* Get type option. Limit to string, number or boolean * Get type option. Limit to string, number or boolean
* @param value * @param value
*/ */
private static getTypeOption( private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
value: null | string | number | undefined | boolean if (value == null) {
): ViewModels.InputPropertyValueTypeString {
// eslint-disable-next-line no-null/no-null
if (value === null) {
return "null"; return "null";
} }
const type = typeof value; let type = typeof value;
switch (type) { switch (type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -173,10 +173,9 @@ export class NodePropertiesComponent extends React.Component<
const existingProps: ViewModels.InputProperty[] = []; const existingProps: ViewModels.InputProperty[] = [];
// eslint-disable-next-line no-prototype-builtins
if (this.props.node.hasOwnProperty("properties")) { if (this.props.node.hasOwnProperty("properties")) {
const hProps = this.props.node["properties"]; const hProps = this.props.node["properties"];
for (const p in hProps) { for (let p in hProps) {
const propValues = hProps[p]; const propValues = hProps[p];
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({ (p === partitionKeyProperty ? readOnlyProps : existingProps).push({
key: p, key: p,
@@ -438,7 +437,7 @@ export class NodePropertiesComponent extends React.Component<
</div> </div>
); );
} else { } else {
return undefined; return null;
} }
} }

View File

@@ -54,8 +54,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }

View File

@@ -103,25 +103,19 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
//TODO: modify once notebooks are available expect(enableNotebookBtn.disabled).toBe(false);
expect(enableNotebookBtn).toBeUndefined(); expect(enableNotebookBtn.tooltipText).toBe("");
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
}); });
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
//TODO: modify once notebooks are available expect(enableNotebookBtn.disabled).toBe(true);
expect(enableNotebookBtn).toBeUndefined(); expect(enableNotebookBtn.tooltipText).toBe(
//expect(enableNotebookBtn).toBeDefined(); "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//expect(enableNotebookBtn.disabled).toBe(true); );
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
}); });
}); });
@@ -198,11 +192,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -212,11 +203,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
@@ -302,13 +290,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -318,11 +302,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
}); });

View File

@@ -67,51 +67,35 @@ export function createStaticCommandBarButtons(
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
} }
if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider()); buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton); buttons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container)); buttons.push(createManageGitHubAccountButton(container));
} }
notebookButtons.push(createOpenTerminalButton(container)); buttons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
notebookButtons.push(createNotebookWorkspaceResetButton(container)); buttons.push(createNotebookWorkspaceResetButton(container));
}
if ( if (
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" userContext.apiType === "Cassandra"
) { ) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container));
} else {
notebookButtons.push(createOpenMongoTerminalButton(container));
}
}
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
}
buttons.push(btn);
});
} else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
buttons.push(createDivider()); buttons.push(createDivider());
if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container));
} else {
buttons.push(createOpenMongoTerminalButton(container));
}
}
} else {
if (!isRunningOnNationalCloud()) {
buttons.push(createEnableNotebooksButton(container)); buttons.push(createEnableNotebooksButton(container));
} }
} }
@@ -168,9 +152,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
@@ -178,13 +160,7 @@ export function createContextCommandBarButtons(
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
tooltipText: disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
}; };
buttons.push(newMongoShellBtn); buttons.push(newMongoShellBtn);
} }
@@ -412,13 +388,6 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons; return buttons;
} }
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps { function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {

View File

@@ -13,7 +13,6 @@ import { StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
/** /**
@@ -23,13 +22,6 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => { export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight; const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
};
return btns return btns
.filter((btn) => btn) .filter((btn) => btn)
.map( .map(
@@ -45,7 +37,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
style: { style: {
width: StyleConstants.CommandBarIconWidth, // 16 width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined, alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
}, },
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName, iconName: btn.iconName,
@@ -132,12 +123,8 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
width: 12, width: 12,
paddingLeft: 1, paddingLeft: 1,
paddingTop: 6, paddingTop: 6,
filter: getFilter(btn.disabled),
},
imageProps: {
src: ChevronDownIcon,
alt: btn.iconAlt,
}, },
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
}; };
} }
@@ -202,10 +189,3 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
onRender: () => <MemoryTracker />, onRender: () => <MemoryTracker />,
}; };
}; };
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus />,
};
};

View File

@@ -1,79 +0,0 @@
@import "../../../../less/Common/Constants";
.connectionStatusContainer {
cursor: default;
align-items: center;
margin: 0 9px;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 13px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
}
.connectionStatusFailed{
color: #bd1919;
}
.ring-container {
position: relative;
}
.ringringGreen {
border: 3px solid green;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringYellow{
border: 3px solid #ffbf00;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringRed{
border: 3px solid #bd1919;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
@keyframes pulsate {
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
15% {opacity: 0.8;}
25% {opacity: 0.6;}
45% {opacity: 0.4;}
70% {opacity: 0.3;}
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
}
.locationGreenDot{
font-size: 20px;
margin-right: 0.07em;
color: green;
}
.locationYellowDot{
font-size: 20px;
margin-right: 0.07em;
color: #ffbf00;
}
.locationRedDot{
font-size: 20px;
margin-right: 0.07em;
color: #bd1919;
}

View File

@@ -1,77 +0,0 @@
import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react";
import { ConnectionStatusType } from "../../../Common/Constants";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
export const ConnectionStatus: React.FC = (): JSX.Element => {
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
const toolTipContent = "Hosted runtime status.";
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isActive) {
intervalId = setInterval(() => {
const secondCounter = counter % 60;
const minuteCounter = Math.floor(counter / 60);
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
setSecond(computedSecond);
setMinute(computedMinute);
setCounter((counter) => counter + 1);
}, 1000);
}
return () => clearInterval(intervalId);
}, [isActive, counter]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
setSecond("00");
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
if (!connectionInfo) {
return (
<Stack className="connectionStatusContainer" horizontal>
<span>Connecting</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) {
setIsActive(true);
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("locationGreenDot");
setStatusColorAnimation("ringringGreen");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("locationRedDot");
setStatusColorAnimation("ringringRed");
}
return (
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<div className="ring-container">
<div className={statusColorAnimation}></div>
<Icon iconName="LocationDot" className={statusColor} />
</div>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Allocating && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
</Stack>
</TooltipHost>
);
};

View File

@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear-1.svg"; import ClearIcon from "../../../../images/Clear.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";

View File

@@ -277,10 +277,6 @@ export class NotebookComponentBootstrapper {
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>); return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
} }
public isNotebookUntrusted(): boolean {
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
}
/** /**
* For display purposes, only return non-killed kernels * For display purposes, only return non-killed kernels
*/ */

View File

@@ -1,14 +1,12 @@
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;
kernelStatus: string; kernelStatus: string;
currentCellType: string; currentCellType: string;
isNotebookUntrusted: boolean;
onRender: () => void; onRender: () => void;
} }
@@ -22,8 +20,7 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
return ( return (
this.props.kernelStatus !== nextProps.kernelStatus || this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName || this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType || this.props.currentCellType !== nextProps.currentCellType
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
); );
} }
@@ -53,7 +50,6 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps; } as VirtualCommandBarComponentProps;
} }
@@ -73,7 +69,6 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender, onRender: initialProps.onRender,
}; };
}; };

View File

@@ -38,7 +38,6 @@ import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
@@ -109,7 +108,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
const q = params.toString(); const q = params.toString();
const suffix = q !== "" ? `?${q}` : ""; const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`; const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1"); return url.replace(/^http(s)?/, "ws$1");
}; };
@@ -687,8 +686,10 @@ const handleKernelConnectionLostEpic = (
logConsoleError(msg); logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
useDialog.getState().showOkModalDialog("kernel restarts", msg); const explorer = window.dataExplorer;
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(EMPTY); return of(EMPTY);
} }
@@ -772,7 +773,8 @@ const closeUnsupportedMimetypesEpic = (
ofType(actions.FETCH_CONTENT_FULFILLED), ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action) => { mergeMap((action) => {
const mimetype = action.payload.model.mimetype; const mimetype = action.payload.model.mimetype;
if (!TextFile.handles(mimetype)) { const explorer = window.dataExplorer;
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
useTabs useTabs
@@ -781,7 +783,7 @@ const closeUnsupportedMimetypesEpic = (
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
useDialog.getState().showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
@@ -801,6 +803,8 @@ const closeContentFailedToFetchEpic = (
return action$.pipe( return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED), ofType(actions.FETCH_CONTENT_FAILED),
mergeMap((action) => { mergeMap((action) => {
const explorer = window.dataExplorer;
if (explorer) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
useTabs useTabs
@@ -809,8 +813,9 @@ const closeContentFailedToFetchEpic = (
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
useDialog.getState().showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg); logConsoleError(msg);
}
return EMPTY; return EMPTY;
}) })
); );

View File

@@ -56,7 +56,7 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try { try {
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, { const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: authToken, Authorization: authToken,

View File

@@ -36,10 +36,7 @@ export class NotebookContentClient {
* *
* @param parent parent folder * @param parent parent folder
*/ */
public async createNewNotebookFile( public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> {
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
@@ -60,8 +57,6 @@ export class NotebookContentClient {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -71,9 +66,9 @@ export class NotebookContentClient {
}); });
} }
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> { public async deleteContentItem(item: NotebookContentItem): Promise<void> {
const path = await this.deleteNotebookFile(item.path); const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item, isGithubTree); useNotebook.getState().deleteNotebookItem(item);
// TODO: Delete once old resource tree is removed // TODO: Delete once old resource tree is removed
if (!path || path !== item.path) { if (!path || path !== item.path) {
@@ -96,12 +91,12 @@ export class NotebookContentClient {
public async uploadFileAsync( public async uploadFileAsync(
name: string, name: string,
content: string, content: string,
parent: NotebookContentItem, parent: NotebookContentItem
isGithubTree?: boolean
): Promise<NotebookContentItem> { ): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
const filepath = NotebookUtil.getFilePath(parent.path, name); const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) { if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`); throw new Error(`File already exists: ${filepath}`);
@@ -120,8 +115,6 @@ export class NotebookContentClient {
.then((xhr: AjaxResponse) => { .then((xhr: AjaxResponse) => {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -144,11 +137,7 @@ export class NotebookContentClient {
* @param sourcePath * @param sourcePath
* @param targetName is not prefixed with path * @param targetName is not prefixed with path
*/ */
public renameNotebook( public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> {
item: NotebookContentItem,
targetName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
const sourcePath = item.path; const sourcePath = item.path;
// Match extension // Match extension
if (sourcePath.indexOf(".") !== -1) { if (sourcePath.indexOf(".") !== -1) {
@@ -174,9 +163,6 @@ export class NotebookContentClient {
item.name = notebookFile.name; item.name = notebookFile.name;
item.path = notebookFile.path; item.path = notebookFile.path;
item.timestamp = NotebookUtil.getCurrentTimestamp(); item.timestamp = NotebookUtil.getCurrentTimestamp();
useNotebook.getState().updateNotebookItem(item, isGithubTree);
return item; return item;
}); });
} }
@@ -186,11 +172,7 @@ export class NotebookContentClient {
* @param parent * @param parent
* @param newDirectoryName basename of the new directory * @param newDirectoryName basename of the new directory
*/ */
public async createDirectory( public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> {
parent: NotebookContentItem,
newDirectoryName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (parent.type !== NotebookContentItemType.Directory) { if (parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent is not a directory: ${parent.path}`); throw new Error(`Parent is not a directory: ${parent.path}`);
} }
@@ -217,11 +199,8 @@ export class NotebookContentClient {
const dir = xhr.response; const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
item.parent = parent; item.parent = parent;
parent.children?.push(item); parent.children?.push(item);
return item; return item;
}); });
} }

View File

@@ -18,7 +18,6 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
@@ -173,9 +172,7 @@ export default class NotebookManager {
if (error.status === HttpStatusCodes.Unauthorized) { if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken(); this.gitHubOAuthService.resetToken();
useDialog this.params.container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
undefined, undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub", "Connect to GitHub",
@@ -199,7 +196,7 @@ export default class NotebookManager {
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks"; let commitMsg = "Committed from Azure Cosmos DB Notebooks";
useDialog.getState().showOkCancelModalDialog( this.params.container.showOkCancelModalDialog(
title || "Commit", title || "Commit",
undefined, undefined,
primaryButtonLabel || "Commit", primaryButtonLabel || "Commit",

View File

@@ -6,7 +6,7 @@ import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react"; import * as React from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@@ -14,7 +14,6 @@ import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform"; import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types"; import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
import { AzureTheme } from "./AzureTheme"; import { AzureTheme } from "./AzureTheme";
import "./base.css"; import "./base.css";
import CellCreator from "./decorators/CellCreator"; import CellCreator from "./decorators/CellCreator";
@@ -108,7 +107,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
return ( return (
<> <>
<div className="NotebookRendererContainer"> <div className="NotebookRendererContainer">
<SecurityWarningBar contentRef={this.props.contentRef} />
<div className="NotebookRenderer" ref={this.notebookRendererRef}> <div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}> <KeyboardShortcuts contentRef={this.props.contentRef}>

View File

@@ -19,12 +19,6 @@
} }
} }
.disabledRunCellButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh;
}
}
.greyStopButton { .greyStopButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon { .runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh; color: @BaseMediumHigh;

View File

@@ -5,7 +5,6 @@ import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { CdbAppState } from "../NotebookComponent/types"; import { CdbAppState } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
export interface PassedPromptProps { export interface PassedPromptProps {
id: string; id: string;
@@ -13,7 +12,6 @@ export interface PassedPromptProps {
status?: string; status?: string;
executionCount?: number; executionCount?: number;
isHovered?: boolean; isHovered?: boolean;
isRunDisabled?: boolean;
runCell?: () => void; runCell?: () => void;
stopCell?: () => void; stopCell?: () => void;
} }
@@ -22,7 +20,6 @@ interface ComponentProps {
id: string; id: string;
contentRef: ContentRef; contentRef: ContentRef;
isHovered?: boolean; isHovered?: boolean;
isNotebookUntrusted?: boolean;
children: (props: PassedPromptProps) => React.ReactNode; children: (props: PassedPromptProps) => React.ReactNode;
} }
@@ -50,7 +47,6 @@ export class PromptPure extends React.Component<Props> {
runCell: this.props.executeCell, runCell: this.props.executeCell,
stopCell: this.props.stopExecution, stopCell: this.props.stopExecution,
isHovered: this.props.isHovered, isHovered: this.props.isHovered,
isRunDisabled: this.props.isNotebookUntrusted,
})} })}
</div> </div>
); );
@@ -79,7 +75,6 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
status, status,
executionCount, executionCount,
isHovered, isHovered,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,27 +0,0 @@
import { shallow } from "enzyme";
import { PassedPromptProps } from "./Prompt";
import { promptContent } from "./PromptContent";
describe("PromptContent", () => {
it("renders for busy status", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
status: "busy",
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
it("renders when hovered", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
isHovered: true,
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,6 +1,5 @@
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react"; import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NotebookUtil } from "../NotebookUtil";
import { PassedPromptProps } from "./Prompt"; import { PassedPromptProps } from "./Prompt";
import "./Prompt.less"; import "./Prompt.less";
@@ -24,18 +23,15 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div> </div>
); );
} else if (props.isHovered) { } else if (props.isHovered) {
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell"; const playButtonText = "Run cell";
return ( return (
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
<IconButton <IconButton
className="runCellButton" className="runCellButton"
iconProps={{ iconName: "MSNVideosSolid" }} iconProps={{ iconName: "MSNVideosSolid" }}
title={playButtonText} title={playButtonText}
ariaLabel={playButtonText} ariaLabel={playButtonText}
disabled={props.isRunDisabled}
onClick={props.runCell} onClick={props.runCell}
/> />
</div>
); );
} else { } else {
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>; return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;

View File

@@ -36,7 +36,6 @@ interface StateProps {
cellIdAbove: CellId; cellIdAbove: CellId;
cellIdBelow: CellId; cellIdBelow: CellId;
hasCodeOutput: boolean; hasCodeOutput: boolean;
isNotebookUntrusted: boolean;
} }
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> { class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
@@ -44,16 +43,12 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
render(): JSX.Element { render(): JSX.Element {
let items: IContextualMenuItem[] = []; let items: IContextualMenuItem[] = [];
const isNotebookUntrusted = this.props.isNotebookUntrusted;
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
if (this.props.cellType === "code") { if (this.props.cellType === "code") {
items = items.concat([ items = items.concat([
{ {
key: "Run", key: "Run",
text: "Run", text: "Run",
title: runTooltip,
disabled: isNotebookUntrusted,
onClick: () => { onClick: () => {
this.props.executeCell(); this.props.executeCell();
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark); this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
@@ -228,7 +223,6 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
cellIdAbove, cellIdAbove,
cellIdBelow, cellIdBelow,
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell), hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PromptContent renders for busy status 1`] = `
<div
className="greyStopButton"
style={
Object {
"left": 0,
"maxHeight": "100%",
"position": "sticky",
"top": 0,
"width": "100%",
"zIndex": 300,
}
}
>
<CustomizedIconButton
ariaLabel="Stop cell execution"
className="runCellButton"
iconProps={
Object {
"iconName": "CircleStopSolid",
}
}
style={
Object {
"position": "absolute",
}
}
title="Stop cell execution"
/>
<StyledSpinnerBase
size={3}
style={
Object {
"paddingTop": 5,
"position": "absolute",
"width": "100%",
}
}
/>
</div>
`;
exports[`PromptContent renders when hovered 1`] = `
<div
className=""
>
<CustomizedIconButton
ariaLabel="Run cell"
className="runCellButton"
iconProps={
Object {
"iconName": "MSNVideosSolid",
}
}
title="Run cell"
/>
</div>
`;

View File

@@ -11,6 +11,7 @@ import {
DropTargetConnector, DropTargetConnector,
DropTargetMonitor, DropTargetMonitor,
} from "react-dnd"; } from "react-dnd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import styled, { StyledComponent } from "styled-components"; import styled, { StyledComponent } from "styled-components";
@@ -122,10 +123,9 @@ export const cellTarget = {
drop(props: Props, monitor: DropTargetMonitor, component: any): void { drop(props: Props, monitor: DropTargetMonitor, component: any): void {
if (monitor) { if (monitor) {
const hoverUpperHalf = isDragUpper(props, monitor, component.el); const hoverUpperHalf = isDragUpper(props, monitor, component.el);
const item: Props = monitor.getItem();
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation. // DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
props.moveCell({ props.moveCell({
id: item.id, id: monitor.getItem().id,
destinationId: props.id, destinationId: props.id,
above: hoverUpperHalf, above: hoverUpperHalf,
contentRef: props.contentRef, contentRef: props.contentRef,

View File

@@ -4,7 +4,6 @@ import Immutable from "immutable";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { NotebookUtil } from "../../../NotebookUtil";
interface ComponentProps { interface ComponentProps {
contentRef: ContentRef; contentRef: ContentRef;
@@ -15,7 +14,6 @@ interface StateProps {
cellMap: Immutable.Map<string, any>; cellMap: Immutable.Map<string, any>;
cellOrder: Immutable.List<string>; cellOrder: Immutable.List<string>;
focusedCell?: string | null; focusedCell?: string | null;
isNotebookUntrusted: boolean;
} }
interface DispatchProps { interface DispatchProps {
@@ -61,13 +59,8 @@ export class KeyboardShortcuts extends React.Component<Props> {
cellOrder, cellOrder,
focusedCell, focusedCell,
cellMap, cellMap,
isNotebookUntrusted,
} = this.props; } = this.props;
if (isNotebookUntrusted) {
return;
}
let ctrlKeyPressed = e.ctrlKey; let ctrlKeyPressed = e.ctrlKey;
// Allow cmd + enter (macOS) to operate like ctrl + enter // Allow cmd + enter (macOS) to operate like ctrl + enter
if (process.platform === "darwin") { if (process.platform === "darwin") {
@@ -132,7 +125,6 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
cellOrder, cellOrder,
cellMap, cellMap,
focusedCell, focusedCell,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,5 +1,4 @@
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable"; import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image"; import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import path from "path"; import path from "path";
@@ -12,8 +11,6 @@ import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentI
export type FileType = "directory" | "file" | "notebook"; export type FileType = "directory" | "file" | "notebook";
// Utilities for notebooks // Utilities for notebooks
export class NotebookUtil { export class NotebookUtil {
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
/** /**
* It's a notebook file if the filename ends with .ipynb. * It's a notebook file if the filename ends with .ipynb.
*/ */
@@ -156,16 +153,6 @@ export class NotebookUtil {
); );
} }
public static isNotebookUntrusted(state: AppState, contentRef: string): boolean {
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const metadata = selectors.notebook.metadata(content.model);
return metadata.getIn(["untrusted"]) as boolean;
}
return false;
}
/** /**
* Find code cells with display * Find code cells with display
* @param notebookObject * @param notebookObject

View File

@@ -1,31 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { SecurityWarningBar } from "./SecurityWarningBar";
describe("SecurityWarningBar", () => {
it("renders if notebook is untrusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={true}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
it("renders if notebook is trusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={false}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,93 +0,0 @@
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { actions, AppState } from "@nteract/core";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { NotebookUtil } from "../NotebookUtil";
export interface SecurityWarningBarPureProps {
contentRef: string;
}
interface SecurityWarningBarDispatchProps {
markNotebookAsTrusted: (contentRef: string) => void;
saveNotebook: (contentRef: string) => void;
}
type SecurityWarningBarProps = SecurityWarningBarPureProps & StateProps & SecurityWarningBarDispatchProps;
interface SecurityWarningBarState {
isBarDismissed: boolean;
}
export class SecurityWarningBar extends React.Component<SecurityWarningBarProps, SecurityWarningBarState> {
constructor(props: SecurityWarningBarProps) {
super(props);
this.state = {
isBarDismissed: false,
};
}
render(): JSX.Element {
return this.props.isNotebookUntrusted && !this.state.isBarDismissed ? (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={false}
onDismiss={() => this.setState({ isBarDismissed: true })}
dismissButtonAriaLabel="Close"
actions={
<MessageBarButton
onClick={() => {
this.props.markNotebookAsTrusted(this.props.contentRef);
this.props.saveNotebook(this.props.contentRef);
}}
>
Trust Notebook
</MessageBarButton>
}
>
{" "}
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone
else may involve security risks.
</MessageBar>
) : (
<></>
);
}
}
interface StateProps {
isNotebookUntrusted: boolean;
}
interface InitialProps {
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const mapStateToProps = (state: AppState): StateProps => ({
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
});
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch): SecurityWarningBarDispatchProps => {
return {
markNotebookAsTrusted: (contentRef: string) => {
return dispatch(
actions.deleteMetadataField({
contentRef,
field: "untrusted",
})
);
},
saveNotebook: (contentRef: string) => dispatch(actions.save({ contentRef })),
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SecurityWarningBar);

View File

@@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SecurityWarningBar renders if notebook is trusted 1`] = `<Fragment />`;
exports[`SecurityWarningBar renders if notebook is untrusted 1`] = `
<StyledMessageBar
actions={
<CustomizedMessageBarButton
onClick={[Function]}
>
Trust Notebook
</CustomizedMessageBarButton>
}
dismissButtonAriaLabel="Close"
isMultiline={false}
messageBarType={5}
onDismiss={[Function]}
>
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone else may involve security risks.
</StyledMessageBar>
`;

View File

@@ -28,8 +28,6 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem;
connectionInfo: DataModels.ContainerConnectionInfo;
NotebookFolderName: string;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -40,12 +38,10 @@ interface NotebookState {
setNotebookBasePath: (notebookBasePath: string) => void; setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>; refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; updateNotebookItem: (item: NotebookContentItem) => void;
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; deleteNotebookItem: (item: NotebookContentItem) => void;
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -68,8 +64,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined, galleryContentRoot: undefined,
connectionInfo: undefined,
NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks",
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -147,34 +141,23 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
return undefined; return undefined;
}, },
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { updateNotebookItem: (item: NotebookContentItem): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const root = cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, parent);
item.parent = parentItem;
if (parentItem.children) {
parentItem.children.push(item);
} else {
parentItem.children = [item];
}
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent); const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item); parentItem.children.push(item);
item.parent = parentItem; item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); set({ myNotebooksContentRoot: root });
}, },
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { deleteNotebookItem: (item: NotebookContentItem): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const root = cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent); const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); set({ myNotebooksContentRoot: root });
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: get().NotebookFolderName, name: "My Notebooks",
path: get().notebookBasePath, path: get().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -183,11 +166,13 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
path: "Gallery", path: "Gallery",
type: NotebookContentItemType.File, type: NotebookContentItemType.File,
}; };
const gitHubNotebooksContentRoot = { const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos", name: "GitHub repos",
path: "PsuedoDir", path: "PsuedoDir",
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; }
: undefined;
set({ set({
myNotebooksContentRoot, myNotebooksContentRoot,
galleryContentRoot, galleryContentRoot,
@@ -231,7 +216,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
path: "PsuedoDir", path: "PsuedoDir",
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
children: [], children: [],
parent: gitHubNotebooksContentRoot,
}; };
pinnedRepo.branches.forEach((branch) => { pinnedRepo.branches.forEach((branch) => {
@@ -239,7 +223,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
name: branch.name, name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
parent: repoTreeItem,
}); });
}); });
@@ -249,5 +232,4 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot }); set({ gitHubNotebooksContentRoot });
} }
}, },
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
})); }));

View File

@@ -113,7 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: "", collectionId: "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(), partitionKey:
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
? "/id"
: "",
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"), createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false, useHashV2: false,
@@ -550,6 +554,61 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToAdvancedSection();
}}
>
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Checkbox
label="Create a Wildcard Index on all fields"
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ createMongoWildCardIndex: isChecked })
}
/>
</Stack>
)}
{userContext.apiType === "SQL" && (
<Checkbox
label="My partition key is larger than 100 bytes"
checked={this.state.useHashV2}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ useHashV2: isChecked })
}
/>
)}
{this.shouldShowAnalyticalStoreOptions() && ( {this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
@@ -615,61 +674,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
</Stack> </Stack>
)} )}
{userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToAdvancedSection();
}}
>
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Checkbox
label="Create a Wildcard Index on all fields"
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ createMongoWildCardIndex: isChecked })
}
/>
</Stack>
)}
{userContext.apiType === "SQL" && (
<Checkbox
label="My partition key is larger than 100 bytes"
checked={this.state.useHashV2}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ useHashV2: isChecked })
}
/>
)}
</Stack> </Stack>
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
)} )}
@@ -811,19 +815,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return tooltipText; return tooltipText;
} }
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
return "";
}
private getPartitionKeySubtext(): string { private getPartitionKeySubtext(): string {
if ( if (
userContext.features.partitionKeyDefault && userContext.features.partitionKeyDefault &&

View File

@@ -5,7 +5,6 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -76,8 +75,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner, selectedLocation.owner,
selectedLocation.repo selectedLocation.repo
)} - ${selectedLocation.branch}`; )} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = "My Notebooks Scratch";
} }
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
@@ -101,7 +98,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => { const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem; let parent: NotebookContentItem;
let isGithubTree: boolean;
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
@@ -109,23 +105,21 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = false;
break; break;
case "GitHub": case "GitHub":
parent = { parent = {
name: selectedLocation.branch, name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = true;
break; break;
default: default:
throw new Error(`Unsupported location type ${location.type}`); throw new Error(`Unsupported location type ${location.type}`);
} }
return container.uploadFile(name, content, parent, isGithubTree); return container.uploadFile(name, content, parent);
}; };
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => { const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {

View File

@@ -12,7 +12,6 @@ import {
import React, { FormEvent, FunctionComponent } from "react"; import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient"; import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location { interface Location {
@@ -47,10 +46,11 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
const getDropDownOptions = (): IDropdownOption[] => { const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = []; const options: IDropdownOption[] = [];
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: useNotebook.getState().NotebookFolderName, text: ResourceTreeAdapter.MyNotebooksTitle,
title: useNotebook.getState().NotebookFolderName, title: ResourceTreeAdapter.MyNotebooksTitle,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,

View File

@@ -23,7 +23,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -105,6 +105,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
notebookName, notebookName,
notebookDescription, notebookDescription,
notebookTags?.split(","), notebookTags?.split(","),
author,
imageSrc, imageSrc,
content content
); );

View File

@@ -13,7 +13,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -91,7 +91,9 @@ export const UploadItemsPane: FunctionComponent = () => {
accept="application/json" accept="application/json"
multiple multiple
tabIndex={0} tabIndex={0}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets." tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
can perform multiple upload operations for larger data sets."
/> />
{uploadFileData?.length > 0 && ( {uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer"> <div className="fileUploadSummaryContainer">

View File

@@ -83,11 +83,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public render(): JSX.Element { public render(): JSX.Element {
const mainItems = this.createMainItems(); const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems(); const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems(); const recentItems = this.createRecentItems();
if (userContext.features.notebooksTemporarilyDown) {
recentItems = recentItems.filter((item) => item.description !== "Notebook");
}
const tipsItems = this.createTipsItems(); const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent; const onClearRecent = this.clearMostRecent;
@@ -223,7 +219,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) { if (useNotebook.getState().isNotebookEnabled) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",

View File

@@ -19,7 +19,7 @@ export function createDataTable($dataTableElem: JQuery, settings: any): DataTabl
* @return The given settings with all columns having a rendering function * @return The given settings with all columns having a rendering function
*/ */
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy { function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
let tableColumns: DataTables.ColumnLegacy[] = null; var tableColumns: DataTables.ColumnLegacy[] = null;
if (settings.aoColumns) { if (settings.aoColumns) {
tableColumns = settings.aoColumns; tableColumns = settings.aoColumns;
@@ -34,7 +34,7 @@ function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
return settings; return settings;
} }
for (let i = 0; i < tableColumns.length; i++) { for (var i = 0; i < tableColumns.length; i++) {
// the column does not have a render function // the column does not have a render function
if (!tableColumns[i].mRender) { if (!tableColumns[i].mRender) {
tableColumns[i].mRender = defaultDataRender; tableColumns[i].mRender = defaultDataRender;

View File

@@ -1,6 +1,5 @@
import Q from "q"; import Q from "q";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities"; import * as DataTableUtilities from "./DataTableUtilities";
@@ -70,16 +69,11 @@ export default class TableCommands {
return null; // Error return null; // Error
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
const deleteMessage: string = let deleteMessage: string = "Are you sure you want to delete the selected entities?";
userContext.apiType === "Cassandra" if (userContext.apiType === "Cassandra") {
? "Are you sure you want to delete the selected rows?" deleteMessage = "Are you sure you want to delete the selected rows?";
: "Are you sure you want to delete the selected entities?"; }
if (window.confirm(deleteMessage)) {
useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
deleteMessage,
"Delete",
() => {
viewModel.queryTablesTab.container.tableDataClient viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => { .then((results: any) => {
@@ -87,11 +81,7 @@ export default class TableCommands {
viewModel.redrawTableThrottled(); viewModel.redrawTableThrottled();
}); });
}); });
}, }
"Cancel",
undefined
);
return null; return null;
} }

View File

@@ -21,7 +21,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import ConflictId from "../Tree/ConflictId"; import ConflictId from "../Tree/ConflictId";
@@ -229,7 +228,7 @@ export default class ConflictsTab extends TabsBase {
this._documentsIterator = this.createIterator(); this._documentsIterator = this.createIterator();
await this.loadNextPage(); await this.loadNextPage();
} catch (error) { } catch (error) {
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); window.alert(getErrorMessage(error));
} }
} }
@@ -253,23 +252,10 @@ export default class ConflictsTab extends TabsBase {
} }
public onAcceptChangesClick = async (): Promise<void> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
useDialog return;
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
async () => await this.resolveConflict(),
"Cancel",
undefined
);
} else {
await this.resolveConflict();
} }
};
private resolveConflict = async (): Promise<void> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -332,7 +318,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.ResolveConflict, Action.ResolveConflict,
{ {
@@ -386,7 +372,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteConflict, Action.DeleteConflict,
{ {
@@ -676,6 +662,11 @@ export default class ConflictsTab extends TabsBase {
return jsonObject; return jsonObject;
} }
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
private _getPartitionKeyPropertyHeader(): string { private _getPartitionKeyPropertyHeader(): string {
return ( return (
(this.partitionKey && (this.partitionKey &&

View File

@@ -0,0 +1,145 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import { IColumn, IImageProps, ImageFit } from "@fluentui/react";
import { Resource } from "../../../src/Contracts/DataModels";
import * as DataModels from "../../Contracts/DataModels";
import DocumentId from "../Tree/DocumentId";
export interface IDocumentsTabContentState {
columns: IColumn[];
isModalSelection: boolean;
isCompactMode: boolean;
announcedMessage?: string;
isSuggestionVisible: boolean;
filter: string;
isFilterOptionVisible: boolean;
isEditorVisible: boolean;
documentContent: string;
selectedSqlDocumentContent: string;
documentIds: Array<DocumentId>;
documentSqlIds: Array<Resource>;
selectedDocumentId?: DocumentId;
selectedSqlDocumentId?: DocumentId;
isEditorContentEdited: boolean;
isAllDocumentsVisible: boolean;
}
export interface IDocument {
value: string;
id: string;
}
export interface IButton {
visible: boolean;
enabled: boolean;
isSelected?: boolean;
}
export const imageProps: Partial<IImageProps> = {
imageFit: ImageFit.centerContain,
width: 40,
height: 40,
style: { marginTop: "15px" },
};
export function hasShardKeySpecified(
document: string,
partitionKey: DataModels.PartitionKey,
partitionKeyProperty: string
): boolean {
return Boolean(
extractPartitionKey(
document,
getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition
)
);
}
export function getPartitionKeyDefinition(
partitionKey: DataModels.PartitionKey,
partitionKeyProperty: string
): DataModels.PartitionKey {
if (partitionKey?.paths?.[0]?.indexOf("$v") > -1) {
// Convert BsonSchema2 to /path format
partitionKey = {
kind: partitionKey.kind,
paths: ["/" + partitionKeyProperty.replace(/\./g, "/")],
version: partitionKey.version,
};
}
return partitionKey;
}
export function formatDocumentContent(row: DocumentId): string {
const { partitionKeyProperty, partitionKeyValue, id } = row;
const documentContent = JSON.stringify({
_id: id(),
[partitionKeyProperty]: partitionKeyValue || "",
});
const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}");
return formattedDocumentContent;
}
export function formatSqlDocumentContent(row: Resource): string {
const { id, _rid, _self, _ts, _etag, _partitionKey, _attachments } = row;
const documentContent = JSON.stringify({
id: id || "",
_rid: _rid || "",
_self: _self || "",
_ts: _ts || "",
_etag: _etag || "",
_attachments: _attachments || "",
_partitionKey: _partitionKey || "",
});
const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}");
return formattedDocumentContent;
}
export function getFilterPlaceholder(isPreferredApiMongoDB: boolean): string {
const filterPlaceholder = 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.";
return filterPlaceholder;
}
export function getFilterSuggestions(isPreferredApiMongoDB: boolean): { value: string }[] {
const filterSuggestions = isPreferredApiMongoDB
? [{ value: `{"_id": "foo"}` }, { value: "{ qty: { $gte: 20 } }" }]
: [
{ value: 'WHERE c.id = "foo"' },
{ value: "ORDER BY c._ts DESC" },
{ value: 'WHERE c.id = "foo" ORDER BY c._ts DESC' },
];
return filterSuggestions;
}
export function getDocumentItems(
isPreferredApiMongoDB: boolean,
documentIds: Array<DocumentId>,
documentSqlIds: Array<Resource>,
isAllDocumentsVisible: boolean
): Array<DocumentId> | Array<Resource> {
if (isPreferredApiMongoDB) {
return isAllDocumentsVisible ? documentIds : documentIds.slice(0, 5);
}
return isAllDocumentsVisible ? documentSqlIds : documentSqlIds.slice(0, 5);
}
export const assignTabButtonVisibility = (
visible: boolean,
enabled: boolean
): { visible: boolean; enabled: boolean } => {
return {
visible,
enabled,
};
};
export const getfilterText = (isPreferredApiMongoDB: boolean, filter: string): string => {
if (isPreferredApiMongoDB) {
if (filter) {
return `Filter : ${filter}`;
}
return "No filter applied";
}
return `Select * from C ${filter}`;
};

View File

@@ -1,221 +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"
/>
<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,
enable: applyFilterButton.enabled"
aria-label="Apply filter"
tabindex="0"
>
Apply Filter
</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 -->
<th
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
data-bind="
attr: {
title: partitionKeyPropertyHeader
},
text: partitionKeyPropertyHeader"
tabindex="0"
></th>
<!-- /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"
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.partitionKeyProperty -->
<td class="tabdocumentsGridElement" colspan="2">
<a
data-bind="text: $data.stringPartitionKeyValue, attr: { title: $data.stringPartitionKeyValue }"
></a>
</td>
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
<div class="loadMore">
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
>Load more</a
>
</div>
<!-- Document Ids - End -->
<!-- Splitter -->
</div>
<div class="splitter ui-resizable-handle ui-resizable-e colResizePointer" id="h_splitter2"></div>
</div>
<div class="documentWaterMark" data-bind="visible: !shouldShowEditor()">
<p><img src="/DocumentWaterMark.svg" alt="Document WaterMark" /></p>
<p class="documentWaterMarkText">Create new or work with existing document(s).</p>
</div>
<!-- Editor - Start -->
<json-editor
class="editorDivContent"
data-bind="visible: shouldShowEditor, css: { mongoDocumentEditor: isPreferredApiMongoDB }"
params="{content: initialDocumentContent, isReadOnly: false,lineNumbers: 'on',ariaLabel: 'Document editor',
updatedContent: selectedDocumentContent}"
></json-editor>
<!-- Editor - End -->
</div>
<!-- Ids and Editor - End -->
</div>

View File

@@ -1,152 +0,0 @@
import * as ko from "knockout";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
describe("Documents tab", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.buildQuery("")).toContain("select");
});
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
container: explorer,
});
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: explorer,
});
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: false,
},
container: explorer,
});
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: mongoExplorer,
});
it("should be false for null or undefined collection", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be false for null or undefined partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithoutPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-Mongo accounts with system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
it("should be false for Mongo accounts with system partitionKey", () => {
updateUserContext({
apiType: "Mongo",
});
const documentsTab = new DocumentsTab({
collection: mongoCollectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithNonSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
});
});

View File

@@ -1,938 +0,0 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
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 { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId";
import { useSelectedNode } from "../useSelectedNode";
import template from "./DocumentsTab.html";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public readonly html = template;
public selectedDocumentId: ko.Observable<DocumentId>;
public selectedDocumentContent: ViewModels.Editable<string>;
public initialDocumentContent: ko.Observable<string>;
public documentContentsGridId: string;
public documentContentsContainerId: string;
public filterContent: ko.Observable<string>;
public appliedFilter: ko.Observable<string>;
public lastFilterContents: ko.ObservableArray<string>;
public isFilterExpanded: ko.Observable<boolean>;
public isFilterCreated: ko.Observable<boolean>;
public applyFilterButton: ViewModels.Button;
public isEditorDirty: ko.Computed<boolean>;
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
public newDocumentButton: ViewModels.Button;
public saveNewDocumentButton: ViewModels.Button;
public saveExisitingDocumentButton: ViewModels.Button;
public discardNewDocumentChangesButton: ViewModels.Button;
public discardExisitingDocumentChangesButton: ViewModels.Button;
public deleteExisitingDocumentButton: ViewModels.Button;
public displayedError: ko.Observable<string>;
public accessibleDocumentList: AccessibleVerticalList;
public dataContentsGridScrollHeight: ko.Observable<string>;
public isPreferredApiMongoDB: boolean;
public shouldShowEditor: ko.Computed<boolean>;
public splitter: Splitter;
public showPartitionKey: boolean;
public idHeader: string;
// TODO need to refactor
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public documentIds: ko.ObservableArray<DocumentId>;
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
this.editorState = ko.observable<ViewModels.DocumentExplorerState>(
ViewModels.DocumentExplorerState.noDocumentSelected
);
this.selectedDocumentId = ko.observable<DocumentId>();
this.selectedDocumentContent = editable.observable<string>("");
this.initialDocumentContent = ko.observable<string>("");
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.documentIds = options.documentIds;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;
this.isFilterExpanded = ko.observable<boolean>(false);
this.isFilterCreated = ko.observable<boolean>(true);
this.filterContent = ko.observable<string>("");
this.appliedFilter = ko.observable<string>("");
this.displayedError = ko.observable<string>("");
this.lastFilterContents = ko.observableArray<string>([
'WHERE c.id = "foo"',
"ORDER BY c._ts DESC",
'WHERE c.id = "foo" ORDER BY c._ts DESC',
]);
this.dataContentsGridScrollHeight = ko.observable<string>(null);
// initialize splitter only after template has been loaded so dom elements are accessible
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const tabContainer: HTMLElement = document.getElementById("content");
const splitterBounds: SplitterBounds = {
min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth,
max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth,
};
this.splitter = new Splitter({
splitterId: "h_splitter2",
leftId: this.documentContentsContainerId,
bounds: splitterBounds,
direction: SplitterDirection.Vertical,
});
}
});
this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds());
this.accessibleDocumentList.setOnSelect(
(selectedDocument: DocumentId) => selectedDocument && selectedDocument.click()
);
this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) =>
this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId)
);
this.documentIds.subscribe((newDocuments: DocumentId[]) => {
this.accessibleDocumentList.updateItemList(newDocuments);
if (newDocuments.length > 0) {
this.dataContentsGridScrollHeight(
newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
} else {
this.dataContentsGridScrollHeight(
DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
}
});
this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return (
this.selectedDocumentContent.getEditableOriginalValue() !==
this.selectedDocumentContent.getEditableCurrentValue()
);
default:
return false;
}
});
this.newDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.saveNewDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.discardNewDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.saveExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.discardExisitingDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.deleteExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.applyFilterButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.buildCommandBarOptions();
this.shouldShowEditor = ko.computed<boolean>(() => {
const documentHasContent: boolean =
this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0;
const isNewDocument: boolean =
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid;
return documentHasContent || isNewDocument;
});
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey();
}
private _shouldShowPartitionKey(): boolean {
if (!this.collection) {
return false;
}
if (!this.collection.partitionKey) {
return false;
}
if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) {
return false;
}
return true;
}
public onShowFilterClick(): Q.Promise<any> {
this.isFilterCreated(true);
this.isFilterExpanded(true);
$(".filterDocExpanded").addClass("active");
$("#content").addClass("active");
$(".querydropdown").focus();
return Q();
}
public onHideFilterClick(): Q.Promise<any> {
this.isFilterExpanded(false);
$(".filterDocExpanded").removeClass("active");
$("#content").removeClass("active");
$(".queryButton").focus();
return Q();
}
public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.onHideFilterClick();
event.stopPropagation();
return false;
}
return true;
};
public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid
this.documentIds([]);
try {
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
}
}
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.refreshDocumentsGrid();
event.stopPropagation();
return false;
}
return true;
};
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
}
public onNewDocumentClick = (): void => {
if (this.isEditorDirty()) {
useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
() => this.initializeNewDocument(),
"Cancel",
undefined
);
} else {
this.initializeNewDocument();
}
};
private initializeNewDocument = (): void => {
this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
};
public onSaveNewDocumentClick = (): Promise<any> => {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
const document = JSON.parse(this.selectedDocumentContent());
this.isExecuting(true);
return createDocument(this.collection, document)
.then(
(savedDocument: any) => {
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
const partitionKeyValueArray = extractPartitionKey(
savedDocument,
this.partitionKey as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
let id = new DocumentId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
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 onRevertNewDocumentClick = (): Q.Promise<any> => {
this.initialDocumentContent("");
this.selectedDocumentContent("");
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
return Q();
};
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent());
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
selectedDocumentId.partitionKeyValue = partitionKeyValue;
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return updateDocument(this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
this.documentIds().forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
documentId.id(updatedDocument.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 onRevertExisitingDocumentClick = (): Q.Promise<any> => {
this.selectedDocumentContent.setBaseline(this.initialDocumentContent());
this.initialDocumentContent.valueHasMutated();
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
};
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?";
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
msg,
"Delete",
async () => await this._deleteDocument(selectedDocumentId),
"Cancel",
undefined
);
};
public onValidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
return Q();
}
public onInvalidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid);
return Q();
}
if (
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
) {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
return Q();
}
return Q();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public async onActivate(): Promise<void> {
super.onActivate();
if (!this._documentsIterator) {
try {
this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) {
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;
}
}
}
}
protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId);
}
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId)
.then(
() => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
TelemetryProcessor.traceSuccess(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
}
public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId);
const content = await readDocument(this.collection, documentId);
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
return this._loadNextPageInternal()
.then(
(documentsIdsResponse = []) => {
const currentDocuments = this.documentIds();
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
const nextDocumentIds = documentsIdsResponse
// filter documents already loaded in observable
.filter((d: any) => {
return currentDocumentsRids.indexOf(d._rid) < 0;
})
// map raw response to view model
.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this, rawDocument, partitionKeyValue);
});
const merged = currentDocuments.concat(nextDocumentIds);
this.documentIds(merged);
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) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
logConsoleError(errorMessage);
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: errorMessage,
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
)
.finally(() => this.isExecuting(false));
}
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
if (event.key === " " || event.key === "Enter") {
const focusElement = document.getElementById(this.documentContentsGridId);
this.loadNextPage();
focusElement && focusElement.focus();
event.stopPropagation();
event.preventDefault();
}
};
protected _loadNextPageInternal(): Q.Promise<DataModels.DocumentId[]> {
return Q(this._documentsIterator.fetchNext().then((response) => response.resources));
}
protected _onEditorContentChange(newContent: string) {
try {
let parsed: any = JSON.parse(newContent);
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();
}
}
public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise<any> {
if (documentId) {
const content: string = this.renderObjectForEditor(documentContent, null, 4);
this.selectedDocumentContent.setBaseline(content);
this.initialDocumentContent(content);
const newState = documentId
? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits
: ViewModels.DocumentExplorerState.newDocumentValid;
this.editorState(newState);
}
return Q();
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
if (this.newDocumentButton.visible()) {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.newDocumentButton.enabled(),
});
}
if (this.saveNewDocumentButton.visible()) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveNewDocumentButton.enabled(),
});
}
if (this.discardNewDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardNewDocumentChangesButton.enabled(),
});
}
if (this.saveExisitingDocumentButton.visible()) {
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveExisitingDocumentButton.enabled(),
});
}
if (this.discardExisitingDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
});
}
if (this.deleteExisitingDocumentButton.visible()) {
const label = "Delete";
buttons.push({
iconSrc: DeleteDocumentIcon,
iconAlt: label,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.deleteExisitingDocumentButton.enabled(),
});
}
if (!this.isPreferredApiMongoDB) {
buttons.push(DocumentsTab._createUploadButton(this.collection.container));
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.newDocumentButton.visible,
this.newDocumentButton.enabled,
this.saveNewDocumentButton.visible,
this.saveNewDocumentButton.enabled,
this.discardNewDocumentChangesButton.visible,
this.discardNewDocumentChangesButton.enabled,
this.saveExisitingDocumentButton.visible,
this.saveExisitingDocumentButton.enabled,
this.discardExisitingDocumentChangesButton.visible,
this.discardExisitingDocumentChangesButton.enabled,
this.deleteExisitingDocumentButton.visible,
this.deleteExisitingDocumentButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return (
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null
);
}
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload Item";
return {
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
};
}
}

View File

@@ -0,0 +1,50 @@
import * as ko from "knockout";
import React from "react";
import "react-splitter-layout/lib/index.css";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentsTabContent from "./DocumentsTabContent";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public documentContentsGridId: string;
public documentContentsContainerId: string;
public displayedError: ko.Observable<string>;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public _resourceTokenPartitionKey: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: undefined;
this.displayedError = ko.observable<string>("");
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return this.partitionKey?.paths?.[0];
}
render(): JSX.Element {
return <DocumentsTabContent {...this} />;
}
}

View File

@@ -0,0 +1,793 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import {
DetailsList,
DetailsListLayoutMode,
IColumn,
IIconProps,
Image,
List,
PrimaryButton,
SelectionMode,
Stack,
Text,
TextField,
} from "@fluentui/react";
import * as React from "react";
import SplitterLayout from "react-splitter-layout";
import CloseIcon from "../../../images/close-black.svg";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import DocumentWaterMark from "../../../images/DocumentWaterMark.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import { Resource } from "../../../src/Contracts/DataModels";
import * as NotificationConsoleUtils from "../../../src/Utils/NotificationConsoleUtils";
import { Areas } from "../../Common/Constants";
import { createDocument as createSqlDocuments } from "../../Common/dataAccess/createDocument";
import { deleteDocument as deleteSqlDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments as querySqlDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument as updateSqlDocuments } from "../../Common/dataAccess/updateDocument";
import { getEntityName } from "../../Common/DocumentUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { logError } from "../../Common/Logger";
import { createDocument, deleteDocument, queryDocuments, updateDocument } from "../../Common/MongoProxyClient";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import { useSelectedNode } from "../useSelectedNode";
import DocumentsTab from "./DocumentsTab";
import {
assignTabButtonVisibility,
formatDocumentContent,
formatSqlDocumentContent,
getDocumentItems,
getFilterPlaceholder,
getFilterSuggestions,
getfilterText,
getPartitionKeyDefinition,
hasShardKeySpecified,
IDocumentsTabContentState,
imageProps,
} from "./DocumentTabUtils";
const filterIcon: IIconProps = { iconName: "Filter" };
let newDocumentButton = assignTabButtonVisibility(true, true);
let saveNewDocumentButton = assignTabButtonVisibility(false, true);
let discardNewDocumentChangesButton = assignTabButtonVisibility(false, false);
let saveExisitingDocumentButton = assignTabButtonVisibility(false, false);
let discardExisitingDocumentChangesButton = assignTabButtonVisibility(false, false);
let deleteExisitingDocumentButton = assignTabButtonVisibility(false, false);
export default class DocumentsTabContent extends React.Component<DocumentsTab, IDocumentsTabContentState> {
public initialDocumentContent: string;
constructor(props: DocumentsTab) {
super(props);
const isPreferredApiMongoDB = userContext.apiType === "Mongo";
const columns: IColumn[] = [
{
key: "_id",
name: isPreferredApiMongoDB ? "_id" : "id",
minWidth: 50,
maxWidth: 100,
isResizable: true,
isCollapsible: true,
data: "string",
onRender: (item: DocumentId) => {
return (
<div onClick={() => this.handleRow(item)} className="documentIdItem">
{isPreferredApiMongoDB ? item.id() : item.id}
</div>
);
},
isPadded: true,
},
{
key: "column2",
name: props.partitionKeyPropertyHeader,
minWidth: 50,
maxWidth: 100,
isResizable: true,
isCollapsible: true,
data: "number",
onRender: (item: DocumentId) => {
return (
<div onClick={() => this.handleRow(item)} className="documentIdItem">
{isPreferredApiMongoDB ? item.partitionKeyValue : item._partitionKeyValue}
</div>
);
},
},
];
this.initialDocumentContent = `{ \n ${
isPreferredApiMongoDB ? '"_id"' : '"id"'
}: "replace_with_new_document_id" \n }`;
this.state = {
columns: columns,
isModalSelection: false,
isCompactMode: false,
announcedMessage: undefined,
isSuggestionVisible: false,
filter: "",
isFilterOptionVisible: true,
isEditorVisible: false,
documentContent: this.initialDocumentContent,
documentIds: [],
documentSqlIds: [],
isEditorContentEdited: false,
isAllDocumentsVisible: false,
selectedSqlDocumentContent: this.initialDocumentContent,
};
}
componentDidMount(): void {
this.props.isExecuting(true);
this.updateTabButton();
if (userContext.apiType === "Mongo") {
this.queryDocumentsData();
} else {
this.querySqlDocumentsData();
}
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.props.partitionKeyProperty, this.props.partitionKey);
}
private querySqlDocumentsData = async (): Promise<void> => {
this.props.isExecuting(true);
this.props.isExecutionError(false);
const { filter } = this.state;
const query: string = this.buildQuery(filter);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this.props._resourceTokenPartitionKey) {
options.partitionKey = this.props._resourceTokenPartitionKey;
}
try {
const sqlQuery = querySqlDocuments(this.props.collection.databaseId, this.props.collection.id(), query, options);
const querySqlDocumentsData = await sqlQuery.fetchNext();
this.setState({ documentSqlIds: querySqlDocumentsData.resources?.length ? querySqlDocumentsData.resources : [] });
} catch (error) {
this.props.isExecutionError(true);
const errorMessage = getErrorMessage(error);
NotificationConsoleUtils.logConsoleError(errorMessage);
} finally {
this.props.isExecuting(false);
}
};
private queryDocumentsData = async (): Promise<void> => {
this.props.isExecuting(true);
this.props.isExecutionError(false);
try {
const { filter } = this.state;
const query: string = filter || "{}";
const queryDocumentsData = await queryDocuments(
this.props.collection.databaseId,
this.props.collection as ViewModels.Collection,
true,
query,
undefined
);
if (queryDocumentsData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nextDocumentIds = queryDocumentsData.documents.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this.props as DocumentsTab, rawDocument, partitionKeyValue);
});
this.setState({ documentIds: nextDocumentIds });
}
if (this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: this.props.collection.databaseId,
collectionName: this.props.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
},
this.props.onLoadStartKey
);
}
} catch (error) {
if (this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.props.collection.databaseId,
collectionName: this.props.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
this.props.onLoadStartKey
);
}
} finally {
this.props.isExecuting(false);
}
};
private handleRow = (row: DocumentId | Resource): void => {
if (this.state.isEditorContentEdited) {
this.props.collection.container.showOkCancelModalDialog(
"Are you sure you want to continue?",
"Your unsaved changes will be lost.",
"Okay",
() => {
this.handleRowContent(row);
this.setState({ isEditorContentEdited: false });
return;
},
"Cancel",
undefined
);
} else {
this.handleRowContent(row);
}
};
private handleRowContent = (row: DocumentId | Resource): void => {
userContext.apiType === "Mongo"
? this.updateContent(row as DocumentId, formatDocumentContent(row as DocumentId))
: this.updateSqlContent(row as Resource);
this.setDefaultUpdateTabButtonVisibility();
};
private updateContent = (row: DocumentId, formattedDocumentContent: string): void => {
this.setState(
{
documentContent: formattedDocumentContent,
isEditorVisible: true,
selectedDocumentId: row,
},
() => {
this.updateTabButton();
}
);
};
private updateSqlContent = async (row: Resource): Promise<void> => {
const selectedDocumentId: DocumentId = new DocumentId(this.props as DocumentsTab, row, row._partitionKeyValue);
const content = await readDocument(this.props.collection, selectedDocumentId);
const formattedDocumentContent = formatSqlDocumentContent((content as unknown) as Resource);
this.setState(
{
documentContent: formattedDocumentContent,
isEditorVisible: true,
selectedSqlDocumentContent: formattedDocumentContent,
selectedSqlDocumentId: selectedDocumentId,
},
() => {
this.updateTabButton();
}
);
};
private handleFilter = (): void => {
userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData();
this.setState({
isSuggestionVisible: false,
});
};
async updateSqlDocument(): Promise<void> {
const { isExecutionError, isExecuting, collection } = this.props;
const { documentContent, selectedSqlDocumentId } = this.state;
isExecutionError(false);
const startKey: number = this.getStartKey(Action.UpdateDocument);
try {
isExecuting(true);
const updateSqlDocumentRes = await updateSqlDocuments(
collection as ViewModels.Collection,
selectedSqlDocumentId,
JSON.parse(documentContent)
);
if (updateSqlDocumentRes) {
this.setTraceSuccess(Action.UpdateDocument, startKey);
this.querySqlDocumentsData();
}
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.UpdateDocument, startKey, error);
}
}
private async updateMongoDocument(): Promise<void> {
const { selectedDocumentId, documentContent, documentIds } = this.state;
const { isExecutionError, isExecuting, collection, partitionKey, partitionKeyProperty } = this.props;
isExecutionError(false);
isExecuting(true);
const startKey: number = this.getStartKey(Action.UpdateDocument);
try {
const updatedDocument = await updateDocument(
collection.databaseId,
collection as ViewModels.Collection,
selectedDocumentId,
documentContent
);
documentIds.forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
const partitionKeyArray = extractPartitionKey(
updatedDocument,
getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this.props as DocumentsTab, updatedDocument, partitionKeyValue);
documentId.id(id.id());
}
});
this.setTraceSuccess(Action.UpdateDocument, startKey);
this.setState({ isEditorContentEdited: false });
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.UpdateDocument, startKey, error);
}
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = `New ${getEntityName()}`;
if (newDocumentButton.visible) {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !newDocumentButton.enabled,
});
}
if (saveNewDocumentButton.visible) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !saveNewDocumentButton.enabled,
});
}
if (discardNewDocumentChangesButton.visible) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !discardNewDocumentChangesButton.enabled,
});
}
if (saveExisitingDocumentButton.visible) {
const label = "Update";
buttons.push({
...this,
updateMongoDocument: this.updateMongoDocument,
updateSqlDocument: this.updateSqlDocument,
setState: this.setState,
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !saveExisitingDocumentButton.enabled,
});
}
if (discardExisitingDocumentChangesButton.visible) {
const label = "Discard";
buttons.push({
...this,
setState: this.setState,
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !discardExisitingDocumentChangesButton.enabled,
});
}
if (deleteExisitingDocumentButton.visible) {
const label = "Delete";
buttons.push({
...this,
setState: this.setState,
iconSrc: DeleteDocumentIcon,
iconAlt: label,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !deleteExisitingDocumentButton.enabled,
});
}
if (userContext.apiType !== "Mongo") {
const { collection } = this.props;
const label = "Upload Item";
buttons.push({
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && collection.container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
});
}
return buttons;
}
private onSaveExisitingDocumentClick(): void {
userContext.apiType === "Mongo" ? this.updateMongoDocument() : this.updateSqlDocument();
}
private async onDeleteExisitingDocumentClick(): Promise<void> {
const confirmationMessage = `Are you sure you want to delete the selected ${getEntityName()} ?`;
const { isExecuting, collection } = this.props;
const startKey: number = this.getStartKey(Action.DeleteDocument);
this.props.collection.container.showOkCancelModalDialog(
confirmationMessage,
`This ${getEntityName()} will be deleted immediately. You can't undo this action`,
"Delete",
async () => {
try {
isExecuting(true);
if (userContext.apiType === "Mongo") {
await deleteDocument(
collection.databaseId,
collection as ViewModels.Collection,
this.state.selectedDocumentId
);
this.queryDocumentsData();
} else {
const { selectedSqlDocumentId } = this.state;
await deleteSqlDocument(collection as ViewModels.Collection, selectedSqlDocumentId);
this.querySqlDocumentsData();
}
this.setTraceSuccess(Action.DeleteDocument, startKey);
this.setState({ isEditorVisible: false });
} catch (error) {
this.setTraceFail(Action.DeleteDocument, startKey, error);
}
},
"Cancel",
undefined
);
}
private onRevertExisitingDocumentClick(): void {
const { selectedDocumentId, selectedSqlDocumentContent } = this.state;
const documentContent =
userContext.apiType === "Mongo" ? formatDocumentContent(selectedDocumentId) : selectedSqlDocumentContent;
this.setState({
documentContent: documentContent,
});
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, false);
saveExisitingDocumentButton = assignTabButtonVisibility(true, false);
this.updateTabButton();
}
private onNewDocumentClick = () => {
newDocumentButton = assignTabButtonVisibility(true, false);
saveNewDocumentButton = assignTabButtonVisibility(true, true);
discardNewDocumentChangesButton = assignTabButtonVisibility(true, true);
saveExisitingDocumentButton = assignTabButtonVisibility(false, false);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(false, false);
deleteExisitingDocumentButton = assignTabButtonVisibility(false, false);
this.updateTabButton();
this.setState({
documentContent: this.initialDocumentContent,
isEditorVisible: true,
});
};
private onSaveNewDocumentClick = () => {
if (userContext.apiType === "Mongo") {
this.onSaveNewMongoDocumentClick();
} else {
this.onSaveSqlNewMongoDocumentClick();
}
};
public onSaveSqlNewMongoDocumentClick = async (): Promise<void> => {
const { isExecutionError, isExecuting, collection } = this.props;
isExecutionError(false);
const startKey: number = this.getStartKey(Action.CreateDocument);
const document = JSON.parse(this.state.documentContent);
isExecuting(true);
try {
const savedDocument = await createSqlDocuments(collection, document);
if (savedDocument) {
const formattedDocumentContent = formatSqlDocumentContent((savedDocument as unknown) as Resource);
this.setState({ documentContent: formattedDocumentContent });
this.setDefaultUpdateTabButtonVisibility();
this.setTraceSuccess(Action.CreateDocument, startKey);
}
this.querySqlDocumentsData();
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.CreateDocument, startKey, error);
}
};
private setDefaultUpdateTabButtonVisibility = (): void => {
newDocumentButton = assignTabButtonVisibility(true, true);
saveNewDocumentButton = assignTabButtonVisibility(false, false);
discardNewDocumentChangesButton = assignTabButtonVisibility(false, false);
saveExisitingDocumentButton = assignTabButtonVisibility(true, false);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, false);
deleteExisitingDocumentButton = assignTabButtonVisibility(true, true);
this.updateTabButton();
};
public onSaveNewMongoDocumentClick = async (): Promise<void> => {
const parsedDocumentContent = JSON.parse(this.state.documentContent);
const {
partitionKey,
partitionKeyProperty,
displayedError,
isExecutionError,
isExecuting,
collection,
} = this.props;
displayedError("");
const startKey: number = this.getStartKey(Action.CreateDocument);
if (
partitionKeyProperty &&
partitionKeyProperty !== "_id" &&
!hasShardKeySpecified(parsedDocumentContent, partitionKey, partitionKeyProperty)
) {
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
displayedError(message);
this.setTraceFail(Action.CreateDocument, startKey, message);
logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
throw new Error("Document without shard key");
}
isExecutionError(false);
isExecuting(true);
try {
const savedDocument = await createDocument(
collection.databaseId,
collection as ViewModels.Collection,
partitionKeyProperty,
parsedDocumentContent
);
if (savedDocument) {
this.handleLoadMoreDocument();
this.setDefaultUpdateTabButtonVisibility();
this.setTraceSuccess(Action.CreateDocument, startKey);
}
this.setState({ isEditorContentEdited: false });
this.queryDocumentsData();
} catch (error) {
window.alert(getErrorMessage(error));
this.setTraceFail(Action.CreateDocument, startKey, error);
}
};
private getStartKey = (action: number): number => {
const startKey: number = TelemetryProcessor.traceStart(action, {
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
});
return startKey;
};
private setTraceSuccess = (action: number, startKey: number): void => {
const { isExecuting, tabTitle } = this.props;
TelemetryProcessor.traceSuccess(
action,
{
dataExplorerArea: Areas.Tab,
tabTitle: tabTitle(),
},
startKey
);
isExecuting(false);
};
private setTraceFail = (action: number, startKey: number, error: Error | string): void => {
const { isExecuting, tabTitle, isExecutionError } = this.props;
TelemetryProcessor.traceFailure(
action,
{
dataExplorerArea: Areas.Tab,
tabTitle: tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
isExecuting(false);
isExecutionError(true);
};
private onRevertNewDocumentClick = () => {
newDocumentButton = assignTabButtonVisibility(true, true);
saveNewDocumentButton = assignTabButtonVisibility(true, false);
discardNewDocumentChangesButton = assignTabButtonVisibility(true, false);
this.updateTabButton();
this.setState({
isEditorVisible: false,
isEditorContentEdited: false,
});
};
private onRenderCell = (item: { value: string }): JSX.Element => {
return (
<div
className="documentTabSuggestions"
onClick={() =>
this.setState({
filter: item.value,
isSuggestionVisible: false,
})
}
>
<Text>{item.value}</Text>
</div>
);
};
private handleLoadMoreDocument = (): void => {
userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData();
this.setState({
isSuggestionVisible: false,
isAllDocumentsVisible: true,
});
};
private updateTabButton = (): void => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
};
private getKey(item: DocumentId): string {
return item.rid;
}
private handleDocumentContentChange = (newContent: string): void => {
if (saveExisitingDocumentButton.visible && newContent !== this.state.documentContent) {
saveExisitingDocumentButton = assignTabButtonVisibility(true, true);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, true);
}
this.setState(
{
documentContent: newContent,
isEditorContentEdited: true,
},
() => {
this.updateTabButton();
}
);
};
public render(): JSX.Element {
const {
columns,
isCompactMode,
isSuggestionVisible,
filter,
isFilterOptionVisible,
isEditorVisible,
documentContent,
documentIds,
documentSqlIds,
isAllDocumentsVisible,
} = this.state;
const isPreferredApiMongoDB = userContext.apiType === "Mongo";
return (
<>
{isFilterOptionVisible && (
<>
<div>
<Stack horizontal verticalFill wrap>
{!isPreferredApiMongoDB && <Text className="queryText">SELECT * FROM c</Text>}
<TextField
iconProps={filterIcon}
placeholder={getFilterPlaceholder(isPreferredApiMongoDB)}
className={isPreferredApiMongoDB ? "documentTabSearchBar" : "documentSqlTabSearchBar"}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newInput?: string) => {
this.setState({ filter: newInput });
}}
value={filter}
/>
<PrimaryButton text="Apply Filter" onClick={this.handleFilter} className="documentTabFiltetButton" />
<Image
src={CloseIcon}
alt="Close icon"
{...imageProps}
onClick={() => this.setState({ isFilterOptionVisible: false })}
/>
</Stack>
</div>
{isSuggestionVisible && (
<div className={isPreferredApiMongoDB ? "filterSuggestions" : "filterSuggestions sqlFilterSuggestions"}>
<List items={getFilterSuggestions(isPreferredApiMongoDB)} onRenderCell={this.onRenderCell} />
</div>
)}
</>
)}
{!isFilterOptionVisible && (
<Stack horizontal verticalFill wrap className="documentTabNoFilterView">
<Text className="noFilterText">{getfilterText(isPreferredApiMongoDB, filter)}</Text>
<PrimaryButton text="Edit Filter" onClick={() => this.setState({ isFilterOptionVisible: true })} />
</Stack>
)}
<div className="splitterWrapper" onClick={() => this.setState({ isSuggestionVisible: false })}>
<SplitterLayout primaryIndex={0} secondaryInitialSize={1000}>
<div className="leftSplitter">
<DetailsList
items={getDocumentItems(isPreferredApiMongoDB, documentIds, documentSqlIds, isAllDocumentsVisible)}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this.getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
/>
<Text onClick={this.handleLoadMoreDocument} className="documentLoadMore" block={true}>
Load More
</Text>
</div>
{isEditorVisible ? (
<div className="react-editor">
<EditorReact
language={"json"}
content={documentContent}
isReadOnly={false}
ariaLabel={"Document json"}
onContentChanged={this.handleDocumentContentChange}
lineNumbers="on"
/>
</div>
) : (
<div className="documentTabWatermark">
<Image src={DocumentWaterMark} alt="Document watermark" />
<Text className="documentCreateText">Create new or work with existing document(s).</Text>
</div>
)}
</SplitterLayout>
</div>
</>
);
}
}

View File

@@ -0,0 +1,90 @@
import * as ko from "knockout";
import { PartitionKey, Resource } from "../../../src/Contracts/DataModels";
import DocumentId from "../Tree/DocumentId";
import * as DocumentTabUtils from "./DocumentTabUtils";
describe("DocumentTabUtils", () => {
describe("getfilterText()", () => {
it("Should return filter if isPreferredApiMongoDB is true and filter is applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(true, `{"_id": "foo"}`);
expect(filteredText).toBe(`Filter : {"_id": "foo"}`);
});
it("Should return `No filter applied` if isPreferredApiMongoDB is true and filter is not applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(true, "");
expect(filteredText).toBe("No filter applied");
});
it("Should return `Select * from C` with filter if isPreferredApiMongoDB is false and filter is applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(false, `WHERE c.id = "foo"`);
expect(filteredText).toBe(`Select * from C WHERE c.id = "foo"`);
});
});
describe("formatDocumentContent()", () => {
const fakeDocumentData = {} as DocumentId;
fakeDocumentData.partitionKeyProperty = "test";
fakeDocumentData.id = ko.observable("id");
it("should return formatted content with new line each property.", () => {
fakeDocumentData.partitionKeyValue = "partitionValue";
const formattedContent: string = DocumentTabUtils.formatDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(`{\n"_id":"id",\n"test":"partitionValue"\n}`);
});
it("should return formatted content with empty partitionKeyValue when partitionKeyValue is undefined.", () => {
fakeDocumentData.partitionKeyValue = undefined;
const formattedContent: string = DocumentTabUtils.formatDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(`{\n"_id":"id",\n"test":""\n}`);
});
});
describe("formatSqlDocumentContent()", () => {
const fakeDocumentData = {} as Resource;
it("should return formatted content with new line each property.", () => {
fakeDocumentData.id = "testId";
fakeDocumentData._rid = "testRid";
fakeDocumentData._self = "testSelf";
fakeDocumentData._ts = "testTs";
fakeDocumentData._etag = "testEtag";
fakeDocumentData._attachments = "testAttachments";
fakeDocumentData._partitionKey = "testPartitionKey";
const formattedContent: string = DocumentTabUtils.formatSqlDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(
`{\n"id":"testId",\n"_rid":"testRid",\n"_self":"testSelf",\n"_ts":"testTs",\n"_etag":"testEtag",\n"_attachments":"testAttachments",\n"_partitionKey":"testPartitionKey"\n}`
);
});
it("should return formatted content with empty value when key value is undefined.", () => {
fakeDocumentData.id = undefined;
fakeDocumentData._rid = undefined;
fakeDocumentData._self = undefined;
fakeDocumentData._ts = undefined;
fakeDocumentData._etag = undefined;
fakeDocumentData._attachments = undefined;
fakeDocumentData._partitionKey = undefined;
const formattedContent: string = DocumentTabUtils.formatSqlDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(
`{\n"id":"",\n"_rid":"",\n"_self":"",\n"_ts":"",\n"_etag":"",\n"_attachments":"",\n"_partitionKey":""\n}`
);
});
});
describe("getPartitionKeyDefinition()", () => {
const partitionKey = {} as PartitionKey;
partitionKey.kind = "Hash";
partitionKey.version = 1;
partitionKey.systemKey = true;
const partitionKeyProperty = "testPartitionKey";
it("should return formatted partitionKey with formatted path.", () => {
partitionKey.paths = ["test"];
const formattedPartitionKey = DocumentTabUtils.getPartitionKeyDefinition(partitionKey, partitionKeyProperty);
expect(formattedPartitionKey).toEqual({ kind: "Hash", version: 1, systemKey: true, paths: ["test"] });
});
it("should return partitionKey with undefined paths if paths is undefined.", () => {
partitionKey.paths = undefined;
const formattedPartitionKey = DocumentTabUtils.getPartitionKeyDefinition(partitionKey, partitionKeyProperty);
expect(formattedPartitionKey).toEqual({ kind: "Hash", version: 1, systemKey: true, paths: undefined });
});
});
});

View File

@@ -1,34 +1,10 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
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 * 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"; import DocumentsTab from "./DocumentsTab";
export default class MongoDocumentsTab extends DocumentsTab { export default class MongoDocumentsTab extends DocumentsTab {
public collection: ViewModels.Collection; public collection: ViewModels.Collection;
private continuationToken: string;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
this.lastFilterContents = ko.observableArray<string>(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]);
if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
} }
@@ -39,279 +15,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
} }
this.isFilterExpanded = ko.observable<boolean>(true);
super.buildCommandBarOptions.bind(this); super.buildCommandBarOptions.bind(this);
super.buildCommandBarOptions(); 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(),
});
if (
this.partitionKeyProperty &&
this.partitionKeyProperty !== "_id" &&
!this._hasShardKeySpecified(documentContent)
) {
const message = `The document is lacking the shard property: ${this.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.partitionKeyProperty, documentContent)
.then(
(savedDocument: any) => {
let partitionKeyArray = extractPartitionKey(
savedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
let id = new ObjectId(this, savedDocument, partitionKeyValue);
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 onSaveExisitingDocumentClick = (): 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 = extractPartitionKey(
updatedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this, updatedDocument, partitionKeyValue);
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(extractPartitionKey(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.partitionKeyProperty.replace(/\./g, "/")],
version: partitionKey.version,
};
}
return partitionKey;
}
protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection.databaseId, this.collection, documentId);
}
} }

View File

@@ -17,14 +17,12 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import * as CdbActions from "../Notebook/NotebookComponent/actions"; import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
@@ -61,9 +59,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
}; };
if (this.notebookComponentAdapter.isContentDirty()) { if (this.notebookComponentAdapter.isContentDirty()) {
useDialog this.container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
"Close without saving?", "Close without saving?",
`File has unsaved changes, close without saving?`, `File has unsaved changes, close without saving?`,
"Close", "Close",
@@ -88,13 +84,11 @@ export default class NotebookTabV2 extends NotebookTabBase {
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted();
const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
const saveLabel = "Save"; const saveLabel = "Save";
const copyToLabel = "Copy to ..."; const copyToLabel = "Copy to ...";
const publishLabel = "Publish to gallery"; const publishLabel = "Publish to gallery";
const workspaceLabel = "No Workspace";
const kernelLabel = "No Kernel"; const kernelLabel = "No Kernel";
const runLabel = "Run"; const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell"; const runActiveCellLabel = "Run Active Cell";
@@ -111,6 +105,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
const copyLabel = "Copy"; const copyLabel = "Copy";
const cutLabel = "Cut"; const cutLabel = "Cut";
const pasteLabel = "Paste"; const pasteLabel = "Paste";
const undoLabel = "Undo";
const redoLabel = "Redo";
const cellCodeType = "code"; const cellCodeType = "code";
const cellMarkdownType = "markdown"; const cellMarkdownType = "markdown";
const cellRawType = "raw"; const cellRawType = "raw";
@@ -191,10 +187,9 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.traceTelemetry(Action.ExecuteCell); this.traceTelemetry(Action.ExecuteCell);
}, },
commandButtonLabel: runLabel, commandButtonLabel: runLabel,
tooltipText: runBtnTooltip,
ariaLabel: runLabel, ariaLabel: runLabel,
hasPopup: false, hasPopup: false,
disabled: isNotebookUntrusted, disabled: false,
children: [ children: [
{ {
iconSrc: RunIcon, iconSrc: RunIcon,

View File

@@ -1,11 +1,12 @@
import { extractPartitionKey } from "@azure/cosmos"; import Q from "q";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readDocument } from "../../Common/dataAccess/readDocument";
import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/dataAccess/readDocument";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -49,20 +50,13 @@ export default class ConflictId {
} }
public click() { public click() {
if (this.container.isEditorDirty()) { if (
useDialog !this.container.isEditorDirty() ||
.getState() window.confirm("Your unsaved changes will be lost. Do you want to continue?")
.showOkCancelModalDialog( ) {
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadConflict(),
"Cancel",
undefined
);
} else {
this.loadConflict(); this.loadConflict();
} }
return;
} }
public async loadConflict(): Promise<void> { public async loadConflict(): Promise<void> {

View File

@@ -1,6 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
export default class DocumentId { export default class DocumentId {
@@ -14,6 +13,7 @@ export default class DocumentId {
public partitionKeyValue: any; public partitionKeyValue: any;
public stringPartitionKeyValue: string; public stringPartitionKeyValue: string;
public isDirty: ko.Observable<boolean>; public isDirty: ko.Observable<boolean>;
public _partitionKeyValue: string;
constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { constructor(container: DocumentsTab, data: any, partitionKeyValue: any) {
this.container = container; this.container = container;
@@ -28,23 +28,6 @@ export default class DocumentId {
this.isDirty = ko.observable(false); this.isDirty = ko.observable(false);
} }
public click() {
if (this.container.isEditorDirty()) {
useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadDocument(),
"Cancel",
undefined
);
} else {
this.loadDocument();
}
}
public partitionKeyHeader(): Object { public partitionKeyHeader(): Object {
if (!this.partitionKeyProperty) { if (!this.partitionKeyProperty) {
return undefined; return undefined;
@@ -75,8 +58,4 @@ export default class DocumentId {
return JSON.stringify(partitionKeyValue); return JSON.stringify(partitionKeyValue);
} }
public async loadDocument(): Promise<void> {
await this.container.selectDocument(this);
}
} }

View File

@@ -1,6 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import DocumentId from "./DocumentId";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import DocumentId from "./DocumentId";
export default class ObjectId extends DocumentId { export default class ObjectId extends DocumentId {
constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { constructor(container: DocumentsTab, data: any, partitionKeyValue: any) {

View File

@@ -1,6 +1,5 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg";
@@ -11,7 +10,7 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, Notebook } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -25,7 +24,6 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
@@ -56,16 +54,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
galleryContentRoot, galleryContentRoot,
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
updateNotebookItem, updateNotebookItem,
} = useNotebook( } = useNotebook();
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
myNotebooksContentRoot: state.myNotebooksContentRoot,
galleryContentRoot: state.galleryContentRoot,
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
updateNotebookItem: state.updateNotebookItem,
}),
shallow
);
const { activeTab, refreshActiveTab } = useTabs(); const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const pseudoDirPath = "PsuedoDir"; const pseudoDirPath = "PsuedoDir";
@@ -121,9 +110,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [], children: [],
}; };
if (userContext.features.notebooksTemporarilyDown) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) { if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree()); notebooksTree.children.push(buildGalleryNotebooksTree());
} }
@@ -131,22 +117,14 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (myNotebooksContentRoot) { if (myNotebooksContentRoot) {
notebooksTree.children.push(buildMyNotebooksTree()); notebooksTree.children.push(buildMyNotebooksTree());
} }
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes // collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree(true)); notebooksTree.children.push(buildGitHubNotebooksTree());
} else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) {
notebooksTree.children.push(buildGitHubNotebooksTree(false));
} }
}
return notebooksTree;
};
const buildNotebooksTemporarilyDownTree = (): TreeNode => { return notebooksTree;
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
}; };
const buildGalleryNotebooksTree = (): TreeNode => { const buildGalleryNotebooksTree = (): TreeNode => {
@@ -178,7 +156,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return myNotebooksTree; return myNotebooksTree;
}; };
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { const buildGitHubNotebooksTree = (): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
@@ -187,10 +165,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, }
true
); );
const manageGitContextMenu: TreeNodeMenuItem[] = [
gitHubNotebooksTree.contextMenu = [
{ {
label: "Manage GitHub settings", label: "Manage GitHub settings",
onClick: () => onClick: () =>
@@ -215,23 +193,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
}, },
]; ];
const connectGitContextMenu: TreeNodeMenuItem[] = [
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true; gitHubNotebooksTree.isAlphaSorted = true;
@@ -239,9 +201,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}; };
const buildChildNodes = ( const buildChildNodes = (
container: Explorer,
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void
isGithubTree?: boolean
): TreeNode[] => { ): TreeNode[] => {
if (!item || !item.children) { if (!item || !item.children) {
return []; return [];
@@ -249,8 +211,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return item.children.map((item) => { return item.children.map((item) => {
const result = const result =
item.type === NotebookContentItemType.Directory item.type === NotebookContentItemType.Directory
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) ? buildNotebookDirectoryNode(item, onFileClick)
: buildNotebookFileNode(item, onFileClick, isGithubTree); : buildNotebookFileNode(item, onFileClick);
result.timestamp = item.timestamp; result.timestamp = item.timestamp;
return result; return result;
}); });
@@ -259,8 +221,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildNotebookFileNode = ( const buildNotebookFileNode = (
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void
isGithubTree?: boolean
): TreeNode => { ): TreeNode => {
return { return {
label: item.name, label: item.name,
@@ -277,33 +238,27 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: createFileContextMenu(container, item, isGithubTree), contextMenu: createFileContextMenu(container, item),
data: item, data: item,
}; };
}; };
const createFileContextMenu = ( const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => {
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [ let items: TreeNodeMenuItem[] = [
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree), onClick: () => container.renameNotebook(item),
}, },
{ {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}"`, `Are you sure you want to delete "${item.name}"`,
"Delete", "Delete",
() => container.deleteNotebookFile(item, isGithubTree), () => container.deleteNotebookFile(item),
"Cancel", "Cancel",
undefined undefined
); );
@@ -353,28 +308,22 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
} }
}; };
const createDirectoryContextMenu = ( const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => {
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [ let items: TreeNodeMenuItem[] = [
{ {
label: "Refresh", label: "Refresh",
iconSrc: RefreshIcon, iconSrc: RefreshIcon,
onClick: () => loadSubitems(item, isGithubTree), onClick: () => loadSubitems(item),
}, },
{ {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}?"`, `Are you sure you want to delete "${item.name}?"`,
"Delete", "Delete",
() => container.deleteNotebookFile(item, isGithubTree), () => container.deleteNotebookFile(item),
"Cancel", "Cancel",
undefined undefined
); );
@@ -383,17 +332,17 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree), onClick: () => container.renameNotebook(item),
}, },
{ {
label: "New Directory", label: "New Directory",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree), onClick: () => container.onCreateDirectory(item),
}, },
{ {
label: "New Notebook", label: "New Notebook",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onNewNotebookClicked(item, isGithubTree), onClick: () => container.onNewNotebookClicked(item),
}, },
{ {
label: "Upload File", label: "Upload File",
@@ -418,8 +367,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildNotebookDirectoryNode = ( const buildNotebookDirectoryNode = (
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void
isGithubTree?: boolean
): TreeNode => { ): TreeNode => {
return { return {
label: item.name, label: item.name,
@@ -429,7 +377,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isLeavesParentsSeparate: true, isLeavesParentsSeparate: true,
onClick: () => { onClick: () => {
if (!item.children) { if (!item.children) {
loadSubitems(item, isGithubTree); loadSubitems(item);
} }
}, },
isSelected: () => { isSelected: () => {
@@ -442,9 +390,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined,
data: item, data: item,
children: buildChildNodes(item, onFileClick, isGithubTree), children: buildChildNodes(container, item, onFileClick),
}; };
}; };
@@ -529,12 +477,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
}); });
if ( if (isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) {
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
) {
children.push({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection), onClick: collection.onSchemaAnalyzerClick.bind(collection),
@@ -751,9 +694,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return traverse(schema); return traverse(schema);
}; };
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => { const loadSubitems = async (item: NotebookContentItem): Promise<void> => {
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
updateNotebookItem(updatedItem, isGithubTree); updateNotebookItem(updatedItem);
}; };
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();

View File

@@ -27,7 +27,6 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
@@ -45,7 +44,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks"; public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos"; public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";
@@ -131,8 +129,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
path: "Gallery", path: "Gallery",
type: NotebookContentItemType.File, type: NotebookContentItemType.File,
}; };
this.myNotebooksContentRoot = { this.myNotebooksContentRoot = {
name: useNotebook.getState().NotebookFolderName, name: ResourceTreeAdapter.MyNotebooksTitle,
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -146,11 +145,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
}) })
); );
} }
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
this.gitHubNotebooksContentRoot = { this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle, name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath, path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks); return Promise.all(refreshTasks);
} }
@@ -708,9 +712,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog this.container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}"`, `Are you sure you want to delete "${item.name}"`,
"Delete", "Delete",
@@ -775,9 +777,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog this.container.showOkCancelModalDialog(
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}?"`, `Are you sure you want to delete "${item.name}?"`,
"Delete", "Delete",

View File

@@ -8,7 +8,6 @@ import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { getErrorMessage } from "../Tables/Utilities"; import { getErrorMessage } from "../Tables/Utilities";
import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab";
@@ -139,11 +138,10 @@ export default class StoredProcedure {
} }
}; };
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the stored procedure?")) {
"Confirm delete", return;
"Are you sure you want to delete the stored procedure?", }
"Delete",
() => {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
@@ -151,10 +149,6 @@ export default class StoredProcedure {
}, },
(reason) => {} (reason) => {}
); );
},
"Cancel",
undefined
);
} }
public execute(params: string[], partitionKeyValue?: string): void { public execute(params: string[], partitionKeyValue?: string): void {

View File

@@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import TriggerTab from "../Tabs/TriggerTab"; import TriggerTab from "../Tabs/TriggerTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -100,11 +99,10 @@ export default class Trigger {
}; };
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the trigger?")) {
"Confirm delete", return;
"Are you sure you want to delete the trigger?", }
"Delete",
() => {
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
@@ -112,9 +110,5 @@ export default class Trigger {
}, },
(reason) => {} (reason) => {}
); );
},
"Cancel",
undefined
);
} }
} }

View File

@@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -96,11 +95,10 @@ export default class UserDefinedFunction {
} }
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the user defined function?")) {
"Confirm delete", return;
"Are you sure you want to delete the user defined function?", }
"Delete",
() => {
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
@@ -110,9 +108,5 @@ export default class UserDefinedFunction {
/**/ /**/
} }
); );
},
"Cancel",
undefined
);
} }
} }

View File

@@ -364,6 +364,7 @@ describe("Gallery", () => {
const name = "name"; const name = "name";
const description = "description"; const description = "description";
const tags = ["tag"]; const tags = ["tag"];
const author = "author";
const thumbnailUrl = "thumbnailUrl"; const thumbnailUrl = "thumbnailUrl";
const content = `{ "key": "value" }`; const content = `{ "key": "value" }`;
const addLinkToNotebookViewer = true; const addLinkToNotebookViewer = true;
@@ -372,7 +373,7 @@ describe("Gallery", () => {
json: () => undefined as any, json: () => undefined as any,
}); });
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content); const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
@@ -390,6 +391,7 @@ describe("Gallery", () => {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer, addLinkToNotebookViewer,

View File

@@ -61,6 +61,7 @@ export interface IPublishNotebookRequest {
name: string; name: string;
description: string; description: string;
tags: string[]; tags: string[];
author: string;
thumbnailUrl: string; thumbnailUrl: string;
content: any; content: any;
addLinkToNotebookViewer: boolean; addLinkToNotebookViewer: boolean;
@@ -358,6 +359,7 @@ export class JunoClient {
name: string, name: string,
description: string, description: string,
tags: string[], tags: string[],
author: string,
thumbnailUrl: string, thumbnailUrl: string,
content: string content: string
): Promise<IJunoResponse<IGalleryItem>> { ): Promise<IJunoResponse<IGalleryItem>> {
@@ -368,6 +370,7 @@ export class JunoClient {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer: true, addLinkToNotebookViewer: true,

View File

@@ -37,7 +37,6 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";

View File

@@ -1,70 +0,0 @@
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export interface IPhoenixResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
}
export interface IProvosionData {
cosmosEndpoint: string;
resourceId: string;
dbAccountName: string;
aadToken: string;
resourceGroup: string;
subscriptionId: string;
}
export class PhoenixClient {
public async containerConnectionInfo(
provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
method: "POST",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
} else {
const connectionStatus: DataModels.ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
return {
status: response.status,
data,
};
}
public static getPhoenixEndpoint(): string {
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);
}
return phoenixEndpoint;
}
public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}

View File

@@ -10,14 +10,12 @@ export type Features = {
readonly enableSchema: boolean; readonly enableSchema: boolean;
autoscaleDefault: boolean; autoscaleDefault: boolean;
partitionKeyDefault: boolean; partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenix: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean; readonly enableAadDataPlane: boolean;
readonly enableKoResourceTree: boolean; readonly enableKOResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
@@ -29,7 +27,6 @@ export type Features = {
readonly pr?: string; readonly pr?: string;
readonly showMinRUSurvey: boolean; readonly showMinRUSurvey: boolean;
readonly ttl90Days: boolean; readonly ttl90Days: boolean;
readonly notebooksTemporarilyDown: boolean;
}; };
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
@@ -60,7 +57,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableSDKoperations: "true" === get("enablesdkoperations"), enableSDKoperations: "true" === get("enablesdkoperations"),
enableSpark: "true" === get("enablespark"), enableSpark: "true" === get("enablespark"),
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),
enableKoResourceTree: "true" === get("enablekoresourcetree"), enableKOResourceTree: "true" === get("enablekoresourcetree"),
executeSproc: "true" === get("dataexplorerexecutesproc"), executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
@@ -75,8 +72,5 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
ttl90Days: "true" === get("ttl90days"), ttl90Days: "true" === get("ttl90days"),
autoscaleDefault: "true" === get("autoscaledefault"), autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
phoenix: "true" === get("phoenix"),
}; };
} }

View File

@@ -1,9 +1,8 @@
import * as GalleryUtils from "./GalleryUtils";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { useDialog } from "../Explorer/Controls/Dialog";
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "./GalleryUtils";
const galleryItem: IGalleryItem = { const galleryItem: IGalleryItem = {
id: "id", id: "id",
@@ -30,11 +29,11 @@ describe("GalleryUtils", () => {
it("downloadItem shows dialog in data explorer", () => { it("downloadItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
expect(useDialog.getState().visible).toBe(true); expect(container.showOkCancelModalDialog).toBeCalled();
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
}); });
it("favoriteItem favorites item", async () => { it("favoriteItem favorites item", async () => {
@@ -67,11 +66,11 @@ describe("GalleryUtils", () => {
it("deleteItem shows dialog in data explorer", () => { it("deleteItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
expect(useDialog.getState().visible).toBe(true); expect(container.showOkCancelModalDialog).toBeCalled();
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook");
}); });
it("getGalleryViewerProps gets gallery viewer props correctly", () => { it("getGalleryViewerProps gets gallery viewer props correctly", () => {

View File

@@ -3,14 +3,13 @@ import { Notebook } from "@nteract/commutable";
import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog"; import { TextFieldProps } from "../Explorer/Controls/Dialog";
import { import {
GalleryTab, GalleryTab,
GalleryViewerComponent, GalleryViewerComponent,
SortBy, SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
@@ -223,14 +222,12 @@ export function downloadItem(
}); });
const name = data.name; const name = data.name;
useDialog.getState().showOkCancelModalDialog( container.showOkCancelModalDialog(
`Download to ${useNotebook.getState().NotebookFolderName}`, "Download to My Notebooks",
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
"Download", "Download",
async () => { async () => {
const clearInProgressMessage = logConsoleProgress( const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`);
`Downloading ${name} to ${useNotebook.getState().NotebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, { const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id, notebookId: data.id,
downloadCount: data.downloads, downloadCount: data.downloads,
@@ -246,11 +243,6 @@ export function downloadItem(
const notebook = JSON.parse(response.data) as Notebook; const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId); removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook)); await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`); logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
@@ -396,7 +388,7 @@ export function deleteItem(
if (container) { if (container) {
trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id });
useDialog.getState().showOkCancelModalDialog( container.showOkCancelModalDialog(
"Remove published notebook", "Remove published notebook",
`Would you like to remove ${data.name} from the gallery?`, `Would you like to remove ${data.name} from the gallery?`,
"Remove", "Remove",

View File

@@ -29,13 +29,7 @@ export async function update(
body: Types.DatabaseAccountUpdateParameters body: Types.DatabaseAccountUpdateParameters
): Promise<Types.DatabaseAccountGetResults> { ): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({ return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body });
host: configContext.ARM_ENDPOINT,
path,
method: "PATCH",
apiVersion,
body,
});
} }
/* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */ /* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */

View File

@@ -6,7 +6,6 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/ */
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { HttpHeaders } from "../../Common/Constants";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -46,7 +45,6 @@ interface Options {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
body?: unknown; body?: unknown;
queryParams?: ARMQueryParams; queryParams?: ARMQueryParams;
contentType?: string;
} }
export async function armRequestWithoutPolling<T>({ export async function armRequestWithoutPolling<T>({
@@ -56,7 +54,6 @@ export async function armRequestWithoutPolling<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}: Options): Promise<{ result: T; operationStatusUrl: string }> { }: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host); const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
@@ -73,7 +70,6 @@ export async function armRequestWithoutPolling<T>({
method, method,
headers: { headers: {
Authorization: userContext.authorizationToken, Authorization: userContext.authorizationToken,
[HttpHeaders.contentType]: contentType || "application/json",
}, },
body: requestBody ? JSON.stringify(requestBody) : undefined, body: requestBody ? JSON.stringify(requestBody) : undefined,
}); });
@@ -108,7 +104,6 @@ export async function armRequest<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}: Options): Promise<T> { }: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({ const armRequestResult = await armRequestWithoutPolling<T>({
host, host,
@@ -117,7 +112,6 @@ export async function armRequest<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}); });
const operationStatusUrl = armRequestResult.operationStatusUrl; const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {

View File

@@ -56,7 +56,6 @@ export function useAADAuth(): ReturnType {
}); });
setTenantId(response.tenantId); setTenantId(response.tenantId);
setAccount(response.account); setAccount(response.account);
localStorage.setItem("cachedTenantId", response.tenantId);
}, },
[account, tenantId] [account, tenantId]
); );

View File

@@ -98,11 +98,9 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
const msalInstance = getMsalInstance(); const msalInstance = getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount); msalInstance.setActiveAccount(cachedAccount);
const cachedTenantId = localStorage.getItem("cachedTenantId");
const aadTokenResponse = await msalInstance.acquireTokenSilent({ const aadTokenResponse = await msalInstance.acquireTokenSilent({
forceRefresh: true, forceRefresh: true,
scopes: [hrefEndpoint], scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
}); });
aadToken = aadTokenResponse.accessToken; aadToken = aadTokenResponse.accessToken;
} }
@@ -333,15 +331,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) { if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault = true; userContext.features.partitionKeyDefault = true;
} }
if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault = true;
}
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true;
}
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
userContext.features.phoenix = true;
}
} }
} }

View File

@@ -16,7 +16,7 @@ test("Mongo CRUD", async () => {
await explorer.click('[data-test="New Collection"]'); await explorer.click('[data-test="New Collection"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);
await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Collection id"]', containerId);
await explorer.fill('[aria-label="Shard key"]', "pk"); await explorer.fill('[aria-label="Shard key"]', "/pk");
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${databaseId}`);
await explorer.click(`.nodeItem >> text=${containerId}`); await explorer.click(`.nodeItem >> text=${containerId}`);

View File

@@ -37,6 +37,7 @@
"./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SelfServeContracts.ts",
"./src/Contracts/SubscriptionType.ts", "./src/Contracts/SubscriptionType.ts",
"./src/Contracts/Versions.ts", "./src/Contracts/Versions.ts",
"./src/Explorer/Controls/Dialog.tsx",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts",

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