Migrate Delete Collection Panel to React (#628)
This commit is contained in:
parent
e8033f0bbc
commit
56f430ebd8
|
@ -121,8 +121,6 @@ src/Explorer/Panes/AddDatabasePane.ts
|
|||
src/Explorer/Panes/BrowseQueriesPane.ts
|
||||
src/Explorer/Panes/CassandraAddCollectionPane.ts
|
||||
src/Explorer/Panes/ContextualPaneBase.ts
|
||||
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
|
||||
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
|
||||
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
|
||||
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
|
||||
src/Explorer/Panes/GraphStylingPane.ts
|
||||
|
|
|
@ -73,10 +73,6 @@ describe("Component Registerer", () => {
|
|||
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register delete-collection-confirmation-pane component", () => {
|
||||
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register graph-new-vertex-pane component", () => {
|
||||
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
|
||||
});
|
||||
|
|
|
@ -58,10 +58,6 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
|
|||
// Panes
|
||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
||||
ko.components.register(
|
||||
"delete-collection-confirmation-pane",
|
||||
new PaneComponents.DeleteCollectionConfirmationPaneComponent()
|
||||
);
|
||||
|
||||
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
|
||||
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
|
||||
|
|
|
@ -157,21 +157,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -594,21 +579,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
@ -952,21 +922,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -1389,21 +1344,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
@ -1760,21 +1700,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -2197,21 +2122,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
@ -2555,21 +2465,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -2992,21 +2887,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
|
|
@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
|
|||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import * as PricingUtils from "../Utils/PricingUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
|
@ -52,7 +53,6 @@ import AddDatabasePane from "./Panes/AddDatabasePane";
|
|||
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
|
||||
|
@ -190,7 +190,6 @@ export default class Explorer {
|
|||
// Contextual panes
|
||||
public addDatabasePane: AddDatabasePane;
|
||||
public addCollectionPane: AddCollectionPane;
|
||||
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
|
||||
public graphStylingPane: GraphStylingPane;
|
||||
public addTableEntityPane: AddTableEntityPane;
|
||||
public editTableEntityPane: EditTableEntityPane;
|
||||
|
@ -536,13 +535,6 @@ export default class Explorer {
|
|||
container: this,
|
||||
});
|
||||
|
||||
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.graphStylingPane = new GraphStylingPane({
|
||||
id: "graphstylingpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
@ -597,7 +589,6 @@ export default class Explorer {
|
|||
this._panes = [
|
||||
this.addDatabasePane,
|
||||
this.addCollectionPane,
|
||||
this.deleteCollectionConfirmationPane,
|
||||
this.graphStylingPane,
|
||||
this.addTableEntityPane,
|
||||
this.editTableEntityPane,
|
||||
|
@ -634,8 +625,6 @@ export default class Explorer {
|
|||
this.addCollectionPane.collectionWithThroughputInSharedTitle(
|
||||
"Provision dedicated throughput for this container"
|
||||
);
|
||||
this.deleteCollectionConfirmationPane.title("Delete Container");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
|
||||
this.refreshTreeTitle("Refresh containers");
|
||||
break;
|
||||
case "Mongo":
|
||||
|
@ -662,8 +651,6 @@ export default class Explorer {
|
|||
this.addCollectionPane.title("Add Graph");
|
||||
this.addCollectionPane.collectionIdTitle("Graph id");
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
|
||||
this.deleteCollectionConfirmationPane.title("Delete Graph");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
|
||||
this.refreshTreeTitle("Refresh graphs");
|
||||
break;
|
||||
case "Tables":
|
||||
|
@ -679,8 +666,6 @@ export default class Explorer {
|
|||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add Table Entity");
|
||||
this.editTableEntityPane.title("Edit Table Entity");
|
||||
this.deleteCollectionConfirmationPane.title("Delete Table");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
|
||||
this.tableDataClient = new TablesAPIDataClient();
|
||||
break;
|
||||
case "Cassandra":
|
||||
|
@ -696,8 +681,6 @@ export default class Explorer {
|
|||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add Table Row");
|
||||
this.editTableEntityPane.title("Edit Table Row");
|
||||
this.deleteCollectionConfirmationPane.title("Delete Table");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
|
||||
this.tableDataClient = new CassandraAPIDataClient();
|
||||
break;
|
||||
}
|
||||
|
@ -2234,16 +2217,15 @@ export default class Explorer {
|
|||
}
|
||||
|
||||
public openDeleteCollectionConfirmationPane(): void {
|
||||
userContext.features.enableKOPanel
|
||||
? this.deleteCollectionConfirmationPane.open()
|
||||
: this.openSidePanel(
|
||||
"Delete Collection",
|
||||
<DeleteCollectionConfirmationPanel
|
||||
explorer={this}
|
||||
closePanel={() => this.closeSidePanel()}
|
||||
openNotificationConsole={() => this.expandConsole()}
|
||||
/>
|
||||
);
|
||||
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
|
||||
this.openSidePanel(
|
||||
"Delete " + collectionName,
|
||||
<DeleteCollectionConfirmationPanel
|
||||
explorer={this}
|
||||
collectionName={collectionName}
|
||||
closePanel={this.closeSidePanel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public openDeleteDatabaseConfirmationPane(): void {
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
|
||||
<div
|
||||
class="contextual-pane-out"
|
||||
data-bind="
|
||||
click: cancel,
|
||||
clickBubble: false"
|
||||
></div>
|
||||
<div class="contextual-pane" id="deletecollectionconfirmationpane">
|
||||
<!-- Delete Collection Confirmation form - Start -->
|
||||
<div class="contextual-pane-in">
|
||||
<form
|
||||
class="paneContentContainer"
|
||||
data-bind="
|
||||
submit: submit"
|
||||
>
|
||||
<!-- Delete Collection Confirmation header - Start -->
|
||||
<div class="firstdivbg headerline">
|
||||
<span role="heading" aria-level="2" data-bind="text: title"></span>
|
||||
<div
|
||||
class="closeImg"
|
||||
role="button"
|
||||
aria-label="Close pane"
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
click: cancel, event: { keypress: onCloseKeyPress }"
|
||||
>
|
||||
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Collection Confirmation header - End -->
|
||||
|
||||
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
|
||||
<div class="warningErrorContent">
|
||||
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
|
||||
<span class="warningErrorDetailsLinkContainer">
|
||||
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
|
||||
resource and all of its children resources.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Collection Confirmation errors - Start -->
|
||||
<div
|
||||
class="warningErrorContainer"
|
||||
aria-live="assertive"
|
||||
data-bind="
|
||||
visible: formErrors() && formErrors() !== ''"
|
||||
>
|
||||
<div class="warningErrorContent">
|
||||
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
|
||||
<span class="warningErrorDetailsLinkContainer">
|
||||
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
|
||||
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Collection Confirmation errors - End -->
|
||||
|
||||
<!-- Delete Collection Confirmation inputs - Start -->
|
||||
<div class="paneMainContent">
|
||||
<div>
|
||||
<span class="mandatoryStar">*</span> <span data-bind="text: collectionIdConfirmationText"></span>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
data-test="confirmCollectionId"
|
||||
name="collectionIdConfirmation"
|
||||
required
|
||||
class="collid"
|
||||
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div data-bind="visible: recordDeleteFeedback">
|
||||
<div>Help us improve Azure Cosmos DB!</div>
|
||||
<div>What is the reason why you are deleting this container?</div>
|
||||
<p>
|
||||
<textarea
|
||||
type="text"
|
||||
data-test="containerDeleteFeedback"
|
||||
name="containerDeleteFeedback"
|
||||
rows="3"
|
||||
cols="53"
|
||||
class="collid"
|
||||
maxlength="512"
|
||||
data-bind="value: containerDeleteFeedback"
|
||||
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this container?"
|
||||
>
|
||||
</textarea>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input type="submit" data-test="deleteCollection" value="OK" class="btncreatecoll1" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Collection Confirmation inputs - End -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- Delete Collection Confirmation form - End -->
|
||||
<!-- Loader - Start -->
|
||||
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
|
||||
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
|
||||
</div>
|
||||
<!-- Loader - End -->
|
||||
</div>
|
||||
</div>
|
|
@ -1,128 +0,0 @@
|
|||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {
|
||||
public collectionIdConfirmationText: ko.Observable<string>;
|
||||
public collectionIdConfirmation: ko.Observable<string>;
|
||||
public containerDeleteFeedback: ko.Observable<string>;
|
||||
public recordDeleteFeedback: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
this.collectionIdConfirmationText = ko.observable<string>("Confirm by typing the collection id");
|
||||
this.collectionIdConfirmation = ko.observable<string>();
|
||||
this.containerDeleteFeedback = ko.observable<string>();
|
||||
this.recordDeleteFeedback = ko.observable<boolean>(false);
|
||||
this.title("Delete Collection");
|
||||
this.resetData();
|
||||
}
|
||||
|
||||
public submit(): Promise<any> {
|
||||
if (!this._isValid()) {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
this.formErrors("Input collection name does not match the selected collection");
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}`
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.formErrors("");
|
||||
this.isExecuting(true);
|
||||
const selectedCollection = <ViewModels.Collection>this.container.findSelectedCollection();
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
|
||||
collectionId: selectedCollection.id(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: this.title(),
|
||||
});
|
||||
return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then(
|
||||
() => {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.selectedNode(selectedCollection.database);
|
||||
this.container.tabsManager?.closeTabsByComparator(
|
||||
(tab) =>
|
||||
tab.node?.id() === selectedCollection.id() &&
|
||||
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
|
||||
);
|
||||
this.container.refreshAllDatabases();
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
collectionId: selectedCollection.id(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: this.title(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
if (this.shouldRecordFeedback()) {
|
||||
let deleteFeedback = new DeleteFeedback(
|
||||
this.container.databaseAccount().id,
|
||||
this.container.databaseAccount().name,
|
||||
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
|
||||
this.containerDeleteFeedback()
|
||||
);
|
||||
|
||||
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
|
||||
this.containerDeleteFeedback("");
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
this.isExecuting(false);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.formErrors(errorMessage);
|
||||
this.formErrorsDetails(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
collectionId: selectedCollection.id(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: this.title(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public resetData() {
|
||||
this.collectionIdConfirmation("");
|
||||
super.resetData();
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.recordDeleteFeedback(this.shouldRecordFeedback());
|
||||
super.open();
|
||||
}
|
||||
|
||||
public shouldRecordFeedback(): boolean {
|
||||
return this.container.isLastCollection() && !this.container.isSelectedDatabaseShared();
|
||||
}
|
||||
|
||||
private _isValid(): boolean {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
|
||||
if (!selectedCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.collectionIdConfirmation() === selectedCollection.id();
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
import { Text, TextField } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { Collection } from "../../Contracts/ViewModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||
export interface DeleteCollectionConfirmationPanelProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
openNotificationConsole: () => void;
|
||||
}
|
||||
|
||||
export interface DeleteCollectionConfirmationPanelState {
|
||||
formError: string;
|
||||
isExecuting: boolean;
|
||||
}
|
||||
|
||||
export class DeleteCollectionConfirmationPanel extends React.Component<
|
||||
DeleteCollectionConfirmationPanelProps,
|
||||
DeleteCollectionConfirmationPanelState
|
||||
> {
|
||||
private inputCollectionName: string;
|
||||
private deleteCollectionFeedback: string;
|
||||
|
||||
constructor(props: DeleteCollectionConfirmationPanelProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
formError: "",
|
||||
isExecuting: false,
|
||||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
|
||||
<PanelInfoErrorComponent {...this.getPanelErrorProps()} />
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the collection id</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
this.inputCollectionName = newInput;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{this.shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this container?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
multiline
|
||||
rows={3}
|
||||
onChange={(event, newInput?: string) => {
|
||||
this.deleteCollectionFeedback = newInput;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" />
|
||||
{this.state.isExecuting && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private getPanelErrorProps(): PanelInfoErrorProps {
|
||||
if (this.state.formError) {
|
||||
return {
|
||||
messageType: "error",
|
||||
message: this.state.formError,
|
||||
showErrorDetails: true,
|
||||
openNotificationConsole: this.props.openNotificationConsole,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
messageType: "warning",
|
||||
showErrorDetails: false,
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
};
|
||||
}
|
||||
|
||||
private shouldRecordFeedback(): boolean {
|
||||
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
|
||||
}
|
||||
|
||||
public async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const collection = this.props.explorer.findSelectedCollection();
|
||||
if (!collection || this.inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input collection name does not match the selected collection";
|
||||
this.setState({ formError: errorMessage });
|
||||
NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ formError: "", isExecuting: true });
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
});
|
||||
|
||||
try {
|
||||
await deleteCollection(collection.databaseId, collection.id());
|
||||
|
||||
this.setState({ isExecuting: false });
|
||||
this.props.explorer.selectedNode(collection.database);
|
||||
this.props.explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
this.props.explorer.refreshAllDatabases();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
if (this.shouldRecordFeedback()) {
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
userContext.databaseAccount?.id,
|
||||
userContext.databaseAccount?.name,
|
||||
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
|
||||
this.deleteCollectionFeedback
|
||||
);
|
||||
|
||||
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
}
|
||||
|
||||
this.props.closePanel();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.setState({ formError: errorMessage, isExecuting: false });
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +1,18 @@
|
|||
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||
import * as ko from "knockout";
|
||||
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { Collection, Database } from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
jest.mock("../../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import Explorer from "../Explorer";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { DeleteCollectionConfirmationPanel } from ".";
|
||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
||||
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
|
||||
import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
|
@ -64,7 +63,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
@ -118,7 +117,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
});
|
||||
|
@ -132,8 +131,8 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedCollectionId } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
|
||||
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
wrapper.unmount();
|
||||
|
@ -153,8 +152,8 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
.hostNodes()
|
||||
.simulate("change", { target: { value: feedbackText } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
|
||||
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
const deleteFeedback = new DeleteFeedback(
|
|
@ -0,0 +1,152 @@
|
|||
import { Text, TextField } from "office-ui-fabric-react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
export interface DeleteCollectionConfirmationPanelProps {
|
||||
explorer: Explorer;
|
||||
collectionName: string;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollectionConfirmationPanelProps> = ({
|
||||
explorer,
|
||||
closePanel,
|
||||
collectionName,
|
||||
}: DeleteCollectionConfirmationPanelProps) => {
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
||||
};
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const submit = async (): Promise<void> => {
|
||||
const collection = explorer.findSelectedCollection();
|
||||
if (!collection || inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
|
||||
setFormError(errorMessage);
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const paneInfo = {
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle,
|
||||
};
|
||||
|
||||
setFormError("");
|
||||
setIsExecuting(true);
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, paneInfo);
|
||||
|
||||
try {
|
||||
await deleteCollection(collection.databaseId, collection.id());
|
||||
|
||||
setIsExecuting(false);
|
||||
explorer.selectedNode(collection.database);
|
||||
explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
explorer.refreshAllDatabases();
|
||||
|
||||
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
|
||||
|
||||
if (shouldRecordFeedback()) {
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
userContext.databaseAccount?.id,
|
||||
userContext.databaseAccount?.name,
|
||||
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
|
||||
deleteCollectionFeedback
|
||||
);
|
||||
|
||||
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
}
|
||||
|
||||
closePanel();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
setFormError(errorMessage);
|
||||
setIsExecuting(false);
|
||||
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
...paneInfo,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
};
|
||||
const genericPaneProps: GenericRightPaneProps = {
|
||||
container: explorer,
|
||||
formError: formError,
|
||||
formErrorDetail: formError,
|
||||
id: "deleteCollectionpane",
|
||||
isExecuting,
|
||||
title: paneTitle,
|
||||
submitButtonText: "OK",
|
||||
onClose: closePanel,
|
||||
onSubmit: submit,
|
||||
};
|
||||
return (
|
||||
<GenericRightPaneComponent {...genericPaneProps}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
value={inputCollectionName}
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this {collectionName}?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
multiline
|
||||
value={deleteCollectionFeedback}
|
||||
rows={3}
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDeleteCollectionFeedback(newInput);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</GenericRightPaneComponent>
|
||||
);
|
||||
};
|
|
@ -1,142 +0,0 @@
|
|||
import { Subscription } from "knockout";
|
||||
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
|
||||
import * as React from "react";
|
||||
import ErrorRedIcon from "../../../images/error_red.svg";
|
||||
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { KeyCodes } from "../../Common/Constants";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
export interface GenericRightPaneProps {
|
||||
container: Explorer;
|
||||
formError: string;
|
||||
formErrorDetail: string;
|
||||
id: string;
|
||||
isExecuting: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
submitButtonText: string;
|
||||
title: string;
|
||||
isSubmitButtonHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface GenericRightPaneState {
|
||||
panelHeight: number;
|
||||
}
|
||||
|
||||
export class GenericRightPaneComponent extends React.Component<GenericRightPaneProps, GenericRightPaneState> {
|
||||
private notificationConsoleSubscription: Subscription;
|
||||
|
||||
constructor(props: GenericRightPaneProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
panelHeight: this.getPanelHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div tabIndex={-1} onKeyDown={this.onKeyDown}>
|
||||
<div className="contextual-pane-out" onClick={this.props.onClose}></div>
|
||||
<div
|
||||
className="contextual-pane"
|
||||
id={this.props.id}
|
||||
style={{ height: this.state.panelHeight }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
<div className="panelContentWrapper">
|
||||
{this.renderPanelHeader()}
|
||||
{this.renderErrorSection()}
|
||||
{this.props.children}
|
||||
{this.renderPanelFooter()}
|
||||
</div>
|
||||
{this.renderLoadingScreen()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPanelHeader = (): JSX.Element => {
|
||||
return (
|
||||
<div className="firstdivbg headerline">
|
||||
<span id="databaseTitle" role="heading" aria-level={2}>
|
||||
{this.props.title}
|
||||
</span>
|
||||
<IconButton
|
||||
ariaLabel="Close pane"
|
||||
title="Close pane"
|
||||
onClick={this.props.onClose}
|
||||
tabIndex={0}
|
||||
className="closePaneBtn"
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private renderErrorSection = (): JSX.Element => {
|
||||
return (
|
||||
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
<span className="formErrors" title={this.props.formError}>
|
||||
{this.props.formError}
|
||||
</span>
|
||||
<a className="errorLink" role="link" hidden={!this.props.formErrorDetail} onClick={this.showErrorDetail}>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private renderPanelFooter = (): JSX.Element => {
|
||||
return (
|
||||
<div className="paneFooter">
|
||||
<div className="leftpanel-okbut">
|
||||
<PrimaryButton
|
||||
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
|
||||
ariaLabel="Submit"
|
||||
title="Submit"
|
||||
onClick={this.props.onSubmit}
|
||||
tabIndex={0}
|
||||
className="genericPaneSubmitBtn"
|
||||
text={this.props.submitButtonText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private renderLoadingScreen = (): JSX.Element => {
|
||||
return (
|
||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
||||
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.keyCode === KeyCodes.Escape) {
|
||||
this.props.onClose();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private showErrorDetail = (): void => {
|
||||
this.props.container.expandConsole();
|
||||
};
|
||||
|
||||
private getPanelHeight = (): number => {
|
||||
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
|
||||
return window.innerHeight - $(notificationConsoleElement).height();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
|
||||
import React, { FunctionComponent, ReactNode } from "react";
|
||||
import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface GenericRightPaneProps {
|
||||
container: Explorer;
|
||||
formError: string;
|
||||
formErrorDetail: string;
|
||||
id: string;
|
||||
isExecuting: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
submitButtonText: string;
|
||||
title: string;
|
||||
isSubmitButtonHidden?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface GenericRightPaneState {
|
||||
panelHeight: number;
|
||||
}
|
||||
|
||||
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
|
||||
container,
|
||||
formError,
|
||||
formErrorDetail,
|
||||
id,
|
||||
isExecuting,
|
||||
onClose,
|
||||
onSubmit,
|
||||
submitButtonText,
|
||||
title,
|
||||
isSubmitButtonHidden,
|
||||
children,
|
||||
}: GenericRightPaneProps) => {
|
||||
const getPanelHeight = (): number => {
|
||||
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
|
||||
return window.innerHeight - $(notificationConsoleElement).height();
|
||||
};
|
||||
|
||||
const panelHeight: number = getPanelHeight();
|
||||
|
||||
const renderPanelHeader = (): JSX.Element => {
|
||||
return (
|
||||
<div className="firstdivbg headerline">
|
||||
<span id="databaseTitle" role="heading" aria-level={2}>
|
||||
{title}
|
||||
</span>
|
||||
<IconButton
|
||||
ariaLabel="Close pane"
|
||||
title="Close pane"
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
className="closePaneBtn"
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderErrorSection = (): JSX.Element => {
|
||||
return (
|
||||
<div className="warningErrorContainer" aria-live="assertive" hidden={!formError}>
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
<span className="formErrors" title={formError}>
|
||||
{formError}
|
||||
</span>
|
||||
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={showErrorDetail}>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPanelFooter = (): JSX.Element => {
|
||||
return (
|
||||
<div className="paneFooter">
|
||||
<div className="leftpanel-okbut">
|
||||
<PrimaryButton
|
||||
style={{ visibility: isSubmitButtonHidden ? "hidden" : "visible" }}
|
||||
ariaLabel="Submit"
|
||||
title="Submit"
|
||||
onClick={onSubmit}
|
||||
tabIndex={0}
|
||||
className="genericPaneSubmitBtn"
|
||||
text={submitButtonText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLoadingScreen = (): JSX.Element => {
|
||||
return (
|
||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
|
||||
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.keyCode === KeyCodes.Escape) {
|
||||
onClose();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const showErrorDetail = (): void => {
|
||||
container.expandConsole();
|
||||
};
|
||||
|
||||
return (
|
||||
<div tabIndex={-1} onKeyDown={onKeyDown}>
|
||||
<div className="contextual-pane-out" onClick={onClose}></div>
|
||||
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
|
||||
<div className="panelContentWrapper">
|
||||
{renderPanelHeader()}
|
||||
{renderErrorSection()}
|
||||
{children}
|
||||
{renderPanelFooter()}
|
||||
</div>
|
||||
{renderLoadingScreen()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
|
||||
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
|
||||
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
|
||||
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
|
||||
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
|
||||
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
|
||||
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
|
||||
|
@ -34,15 +33,6 @@ export class AddCollectionPaneComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export class DeleteCollectionConfirmationPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: DeleteCollectionConfirmationPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GraphNewVertexPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
|
|
|
@ -133,21 +133,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -570,21 +555,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
@ -1016,21 +986,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -1453,21 +1408,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
|
|
@ -133,21 +133,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -570,21 +555,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -134,21 +134,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
|
@ -571,21 +556,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
|
|
|
@ -231,7 +231,6 @@ const App: React.FunctionComponent = () => {
|
|||
/>
|
||||
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
|
||||
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
|
||||
<div data-bind='component: { name: "delete-collection-confirmation-pane", params: { data: deleteCollectionConfirmationPane} }' />
|
||||
<div data-bind='component: { name: "graph-new-vertex-pane", params: { data: newVertexPane} }' />
|
||||
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
|
||||
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../Shared/Constants";
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
import * as Constants from "../Shared/Constants";
|
||||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||
|
||||
interface ComputeRUUsagePriceHourlyArgs {
|
||||
serverId: string;
|
||||
|
@ -282,7 +282,7 @@ export function getUpsellMessage(
|
|||
}
|
||||
}
|
||||
|
||||
function getCollectionName(defaultExperience: string): string {
|
||||
export function getCollectionName(defaultExperience: string): string {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
return "container";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "expect-puppeteer";
|
||||
import { Frame } from "puppeteer";
|
||||
import { generateDatabaseName, generateUniqueName, login } from "../utils/shared";
|
||||
import { generateDatabaseName, generateUniqueName } from "../utils/shared";
|
||||
|
||||
jest.setTimeout(300000);
|
||||
const LOADING_STATE_DELAY = 2500;
|
||||
|
@ -14,7 +14,9 @@ describe("Collection Add and Delete SQL spec", () => {
|
|||
const dbId = generateDatabaseName();
|
||||
const collectionId = generateUniqueName("col");
|
||||
const sharedKey = `/skey${generateUniqueName()}`;
|
||||
const frame = await login(process.env.PORTAL_RUNNER_CONNECTION_STRING);
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
const frame = await handle.contentFrame();
|
||||
|
||||
// create new collection
|
||||
await frame.waitFor('button[data-test="New Container"]', { visible: true });
|
||||
|
@ -99,8 +101,8 @@ describe("Collection Add and Delete SQL spec", () => {
|
|||
await frame.type('input[id="confirmCollectionId"]', textId);
|
||||
|
||||
// click delete
|
||||
await frame.waitFor('button[id="sidePanelOkButton"]', { visible: true });
|
||||
await frame.click('button[id="sidePanelOkButton"]');
|
||||
await frame.waitFor("button.genericPaneSubmitBtn", { visible: true });
|
||||
await frame.click("button.genericPaneSubmitBtn");
|
||||
await frame.waitFor(LOADING_STATE_DELAY);
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
|
||||
|
@ -109,25 +111,25 @@ describe("Collection Add and Delete SQL spec", () => {
|
|||
|
||||
// click context menu for database
|
||||
await frame.waitFor(`div[data-test="${selectedDbId}"] > div > button`);
|
||||
await frame.waitFor(RENDER_DELAY);
|
||||
await frame.waitFor(CREATE_DELAY);
|
||||
const button = await frame.$(`div[data-test="${selectedDbId}"] > div > button`);
|
||||
await button.focus();
|
||||
await button.asElement().click();
|
||||
|
||||
// click delete database
|
||||
await frame.waitFor(RENDER_DELAY);
|
||||
await frame.waitFor(CREATE_DELAY);
|
||||
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||
|
||||
// confirm delete database
|
||||
await frame.waitForSelector('input[id="confirmDatabaseId"]', { visible: true });
|
||||
await frame.waitFor(RENDER_DELAY);
|
||||
await frame.waitFor(CREATE_DELAY);
|
||||
await frame.type('input[id="confirmDatabaseId"]', selectedDbId);
|
||||
|
||||
// click delete
|
||||
await frame.click('button[id="sidePanelOkButton"]');
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
await frame.waitFor(LOADING_STATE_DELAY);
|
||||
await frame.waitFor(CREATE_DELAY);
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
await expect(page).not.toMatchElement(`div[data-test="${selectedDbId}"]`);
|
||||
} catch (error) {
|
||||
|
|
|
@ -72,8 +72,8 @@ describe("Collection Add and Delete Tables spec", () => {
|
|||
await frame.type('input[id="confirmCollectionId"]', textId);
|
||||
|
||||
// click delete
|
||||
await frame.waitFor('button[id="sidePanelOkButton"]', { visible: true });
|
||||
await frame.click('button[id="sidePanelOkButton"]');
|
||||
await frame.waitFor("button.genericPaneSubmitBtn", { visible: true });
|
||||
await frame.click("button.genericPaneSubmitBtn");
|
||||
await frame.waitFor(LOADING_STATE_DELAY);
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
await frame.waitFor(LOADING_STATE_DELAY);
|
||||
|
|
Loading…
Reference in New Issue