diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.html b/src/Explorer/Tabs/UserDefinedFunctionTab.html
deleted file mode 100644
index 259604f29..000000000
--- a/src/Explorer/Tabs/UserDefinedFunctionTab.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.ts b/src/Explorer/Tabs/UserDefinedFunctionTab.ts
deleted file mode 100644
index ff5a7753d..000000000
--- a/src/Explorer/Tabs/UserDefinedFunctionTab.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
-import * as Constants from "../../Common/Constants";
-import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
-import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
-import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
-import * as ViewModels from "../../Contracts/ViewModels";
-import { Action } from "../../Shared/Telemetry/TelemetryConstants";
-import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
-import UserDefinedFunction from "../Tree/UserDefinedFunction";
-import ScriptTabBase from "./ScriptTabBase";
-import template from "./UserDefinedFunctionTab.html";
-
-export default class UserDefinedFunctionTab extends ScriptTabBase {
- public readonly html = template;
- public collection: ViewModels.Collection;
- public node: UserDefinedFunction;
- constructor(options: ViewModels.ScriptTabOption) {
- super(options);
- this.ariaLabel("User Defined Function Body");
- super.onActivate.bind(this);
- super.buildCommandBarOptions.bind(this);
- super.buildCommandBarOptions();
- }
-
- public onSaveClick = (): Promise => {
- const data = this._getResource();
- return this._createUserDefinedFunction(data);
- };
-
- public onUpdateClick = (): Promise => {
- const data = this._getResource();
- this.isExecutionError(false);
- this.isExecuting(true);
- const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, {
- dataExplorerArea: Constants.Areas.Tab,
- tabTitle: this.tabTitle(),
- });
-
- return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data)
- .then(
- (createdResource) => {
- this.resource(createdResource);
- this.tabTitle(createdResource.id);
-
- this.node.id(createdResource.id);
- this.node.body(createdResource.body as string);
- TelemetryProcessor.traceSuccess(
- Action.UpdateUDF,
- {
- dataExplorerArea: Constants.Areas.Tab,
- tabTitle: this.tabTitle(),
- },
- startKey
- );
-
- this.setBaselines();
-
- const editorModel = this.editor().getModel();
- editorModel.setValue(createdResource.body as string);
- this.editorContent.setBaseline(createdResource.body as string);
- },
- (createError: any) => {
- this.isExecutionError(true);
- TelemetryProcessor.traceFailure(
- Action.UpdateUDF,
- {
- dataExplorerArea: Constants.Areas.Tab,
- tabTitle: this.tabTitle(),
- error: getErrorMessage(createError),
- errorStack: getErrorStack(createError),
- },
- startKey
- );
- }
- )
- .finally(() => this.isExecuting(false));
- };
-
- protected updateSelectedNode(): void {
- if (this.collection == null) {
- return;
- }
-
- const database: ViewModels.Database = this.collection.getDatabase();
- if (!database.isDatabaseExpanded()) {
- this.collection.container.selectedNode(database);
- } else if (!this.collection.isCollectionExpanded() || !this.collection.isUserDefinedFunctionsExpanded()) {
- this.collection.container.selectedNode(this.collection);
- } else {
- this.collection.container.selectedNode(this.node);
- }
- }
-
- private _createUserDefinedFunction(
- resource: UserDefinedFunctionDefinition
- ): Promise {
- this.isExecutionError(false);
- this.isExecuting(true);
- const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, {
- dataExplorerArea: Constants.Areas.Tab,
- tabTitle: this.tabTitle(),
- });
-
- return createUserDefinedFunction(this.collection.databaseId, this.collection.id(), resource)
- .then(
- (createdResource) => {
- this.tabTitle(createdResource.id);
- this.isNew(false);
- this.resource(createdResource);
- this.hashLocation(
- `${Constants.HashRoutePrefixes.collectionsWithIds(this.collection.databaseId, this.collection.id())}/udfs/${
- createdResource.id
- }`
- );
- this.setBaselines();
-
- const editorModel = this.editor().getModel();
- editorModel.setValue(createdResource.body as string);
- this.editorContent.setBaseline(createdResource.body as string);
-
- this.node = this.collection.createUserDefinedFunctionNode(createdResource);
- TelemetryProcessor.traceSuccess(
- Action.CreateUDF,
- {
- dataExplorerArea: Constants.Areas.Tab,
-
- tabTitle: this.tabTitle(),
- },
- startKey
- );
- this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
- return createdResource;
- },
- (createError: any) => {
- this.isExecutionError(true);
- TelemetryProcessor.traceFailure(
- Action.CreateUDF,
- {
- dataExplorerArea: Constants.Areas.Tab,
- tabTitle: this.tabTitle(),
- error: getErrorMessage(createError),
- errorStack: getErrorStack(createError),
- },
- startKey
- );
- return Promise.reject(createError);
- }
- )
- .finally(() => this.isExecuting(false));
- }
-
- private _getResource() {
- const resource = {
- _rid: this.resource()._rid,
- _self: this.resource()._self,
- id: this.id(),
- body: this.editorContent(),
- };
-
- return resource;
- }
-}
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.tsx b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx
new file mode 100644
index 000000000..ba3744422
--- /dev/null
+++ b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx
@@ -0,0 +1,41 @@
+import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
+import React from "react";
+import * as ViewModels from "../../Contracts/ViewModels";
+import UserDefinedFunction from "../Tree/UserDefinedFunction";
+import ScriptTabBase from "./ScriptTabBase";
+import UserDefinedFunctionTabContent from "./UserDefinedFunctionTabContent";
+
+export default class UserDefinedFunctionTab extends ScriptTabBase {
+ public onSaveClick: () => Promise;
+ public onUpdateClick: () => Promise;
+ public collection: ViewModels.Collection;
+ public node: UserDefinedFunction;
+ constructor(options: ViewModels.ScriptTabOption) {
+ super(options);
+ this.ariaLabel("User Defined Function Body");
+ super.onActivate.bind(this);
+ super.buildCommandBarOptions.bind(this);
+ super.buildCommandBarOptions();
+ }
+
+ addNodeInCollection(createdResource: Resource & UserDefinedFunctionDefinition): void {
+ this.node = this.collection.createUserDefinedFunctionNode(createdResource);
+ }
+
+ updateNodeInCollection(updateResource: Resource & UserDefinedFunctionDefinition): void {
+ this.node.id(updateResource.id);
+ this.node.body(updateResource.body as string);
+ }
+
+ render(): JSX.Element {
+ return (
+ this.addNodeInCollection(createdResource)}
+ updateNodeInCollection={(updateResource: Resource & UserDefinedFunctionDefinition) =>
+ this.updateNodeInCollection(updateResource)
+ }
+ />
+ );
+ }
+}
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx
new file mode 100644
index 000000000..b927d55a0
--- /dev/null
+++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx
@@ -0,0 +1,306 @@
+import { UserDefinedFunctionDefinition } from "@azure/cosmos";
+import { Label, TextField } from "@fluentui/react";
+import React, { Component } from "react";
+import DiscardIcon from "../../../images/discard.svg";
+import SaveIcon from "../../../images/save-cosmos.svg";
+import * as Constants from "../../Common/Constants";
+import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
+import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
+import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
+import * as ViewModels from "../../Contracts/ViewModels";
+import { Action } from "../../Shared/Telemetry/TelemetryConstants";
+import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
+import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
+import { EditorReact } from "../Controls/Editor/EditorReact";
+import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
+import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
+
+interface IUserDefinedFunctionTabContentState {
+ udfId: string;
+ udfBody: string;
+ isUdfIdEditable: boolean;
+}
+
+interface Ibutton {
+ visible: boolean;
+ enabled: boolean;
+}
+
+export default class UserDefinedFunctionTabContent extends Component<
+ UserDefinedFunctionTab,
+ IUserDefinedFunctionTabContentState
+> {
+ public saveButton: Ibutton;
+ public updateButton: Ibutton;
+ public discardButton: Ibutton;
+
+ constructor(props: UserDefinedFunctionTab) {
+ super(props);
+
+ this.saveButton = {
+ visible: props.isNew(),
+ enabled: false,
+ };
+ this.updateButton = {
+ visible: !props.isNew(),
+ enabled: true,
+ };
+
+ this.discardButton = {
+ visible: true,
+ enabled: true,
+ };
+
+ const { id, body } = props.resource();
+ this.state = {
+ udfId: id,
+ udfBody: body,
+ isUdfIdEditable: props.isNew() ? true : false,
+ };
+ }
+
+ private handleUdfIdChange = (
+ _event: React.FormEvent,
+ newValue?: string
+ ): void => {
+ this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
+ this.setState({ udfId: newValue });
+ };
+
+ private handleUdfBodyChange = (newContent: string) => {
+ this.setState({ udfBody: newContent });
+ };
+
+ protected getTabsButtons(): CommandButtonComponentProps[] {
+ const buttons: CommandButtonComponentProps[] = [];
+ const label = "Save";
+ if (this.saveButton.visible) {
+ buttons.push({
+ ...this,
+ setState: this.setState,
+ iconSrc: SaveIcon,
+ iconAlt: label,
+ onCommandClick: this.onSaveClick,
+ commandButtonLabel: label,
+ ariaLabel: label,
+ hasPopup: false,
+ disabled: !this.saveButton.enabled,
+ });
+ }
+
+ if (this.updateButton.visible) {
+ const label = "Update";
+ buttons.push({
+ ...this,
+ iconSrc: SaveIcon,
+ iconAlt: label,
+ onCommandClick: this.onUpdateClick,
+ commandButtonLabel: label,
+ ariaLabel: label,
+ hasPopup: false,
+ disabled: !this.updateButton.enabled,
+ });
+ }
+
+ if (this.discardButton.visible) {
+ const label = "Discard";
+ buttons.push({
+ setState: this.setState,
+ ...this,
+ iconSrc: DiscardIcon,
+ iconAlt: label,
+ onCommandClick: this.onDiscard,
+ commandButtonLabel: label,
+ ariaLabel: label,
+ hasPopup: false,
+ disabled: !this.discardButton.enabled,
+ });
+ }
+ return buttons;
+ }
+
+ private async onSaveClick(): Promise {
+ const { udfId, udfBody } = this.state;
+ const resource: UserDefinedFunctionDefinition = {
+ id: udfId,
+ body: udfBody,
+ };
+
+ this.props.isExecutionError(false);
+ this.props.isExecuting(true);
+ const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, {
+ dataExplorerArea: Constants.Areas.Tab,
+ tabTitle: this.props.tabTitle(),
+ });
+
+ try {
+ const createdResource = await createUserDefinedFunction(
+ this.props.collection.databaseId,
+ this.props.collection.id(),
+ resource
+ );
+ if (createdResource) {
+ this.props.tabTitle(createdResource.id);
+ this.props.isNew(false);
+ this.updateButton.visible = true;
+ this.saveButton.visible = false;
+ this.props.resource(createdResource);
+ this.props.hashLocation(
+ `${Constants.HashRoutePrefixes.collectionsWithIds(
+ this.props.collection.databaseId,
+ this.props.collection.id()
+ )}/udfs/${createdResource.id}`
+ );
+ this.props.addNodeInCollection(createdResource);
+ this.setState({ isUdfIdEditable: false });
+ this.props.isExecuting(false);
+ TelemetryProcessor.traceSuccess(
+ Action.CreateUDF,
+ {
+ dataExplorerArea: Constants.Areas.Tab,
+
+ tabTitle: this.props.tabTitle(),
+ },
+ startKey
+ );
+ this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
+ }
+ } catch (createError) {
+ this.props.isExecutionError(true);
+ TelemetryProcessor.traceFailure(
+ Action.CreateUDF,
+ {
+ dataExplorerArea: Constants.Areas.Tab,
+ tabTitle: this.props.tabTitle(),
+ error: getErrorMessage(createError),
+ errorStack: getErrorStack(createError),
+ },
+ startKey
+ );
+ this.props.isExecuting(false);
+ return Promise.reject(createError);
+ }
+ }
+
+ private async onUpdateClick(): Promise {
+ const { udfId, udfBody } = this.state;
+ const resource: UserDefinedFunctionDefinition = {
+ id: udfId,
+ body: udfBody,
+ };
+ this.props.isExecutionError(false);
+ this.props.isExecuting(true);
+ const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, {
+ dataExplorerArea: Constants.Areas.Tab,
+ tabTitle: this.props.tabTitle(),
+ });
+
+ try {
+ const createdResource = await updateUserDefinedFunction(
+ this.props.collection.databaseId,
+ this.props.collection.id(),
+ resource
+ );
+
+ this.props.resource(createdResource);
+ this.props.tabTitle(createdResource.id);
+ this.props.updateNodeInCollection(createdResource);
+ this.props.isExecuting(false);
+ TelemetryProcessor.traceSuccess(
+ Action.UpdateUDF,
+ {
+ dataExplorerArea: Constants.Areas.Tab,
+ tabTitle: this.props.tabTitle(),
+ },
+ startKey
+ );
+
+ this.props.editorContent.setBaseline(createdResource.body as string);
+ } catch (createError) {
+ this.props.isExecutionError(true);
+ TelemetryProcessor.traceFailure(
+ Action.UpdateUDF,
+ {
+ dataExplorerArea: Constants.Areas.Tab,
+ tabTitle: this.props.tabTitle(),
+ error: getErrorMessage(createError),
+ errorStack: getErrorStack(createError),
+ },
+ startKey
+ );
+ this.props.isExecuting(false);
+ }
+ }
+
+ private onDiscard(): void {
+ const { id, body } = this.props.resource();
+ this.setState({
+ udfId: id,
+ udfBody: body,
+ });
+ }
+
+ private isValidId(id: string): boolean {
+ if (!id) {
+ return false;
+ }
+
+ const invalidStartCharacters = /^[/?#\\]/;
+ if (invalidStartCharacters.test(id)) {
+ return false;
+ }
+
+ const invalidMiddleCharacters = /^.+[/?#\\]/;
+ if (invalidMiddleCharacters.test(id)) {
+ return false;
+ }
+
+ const invalidEndCharacters = /.*[/?#\\ ]$/;
+ if (invalidEndCharacters.test(id)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private isNotEmpty(value: string): boolean {
+ return !!value;
+ }
+
+ componentDidUpdate(_prevProps: UserDefinedFunctionTab, prevState: IUserDefinedFunctionTabContentState): void {
+ const { udfBody, udfId } = this.state;
+ if (udfId !== prevState.udfId || udfBody !== prevState.udfBody) {
+ useCommandBar.getState().setContextButtons(this.getTabsButtons());
+ }
+ }
+
+ render(): JSX.Element {
+ const { udfId, udfBody, isUdfIdEditable } = this.state;
+ return (
+
+
+
+
+
+ );
+ }
+}