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 @@ -
- -
-
User Defined Function Id
- - - -
User Defined Function Body
-
-
- -
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 ( +
+ + + +
+ ); + } +}