Migrate Trigger tab to React (#855)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
Sunil Kumar Yadav 2021-06-16 08:23:50 +05:30 committed by GitHub
parent af71a96d54
commit 914c372f5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 387 additions and 224 deletions

View File

@ -200,4 +200,12 @@
.migration:disabled {
background-color: #ccc;
}
.trigger-field {
width: 40%;
margin-top: 10px
}
.trigger-form {
padding: 10px 30px 10px 30px;
}

View File

@ -1,39 +0,0 @@
<div class="tab-pane flexContainer" data-bind="attr:{ id: tabId }" role="tabpanel">
<!-- Trigger Tab Form - Start -->
<div class="storedTabForm flexContainer">
<div class="formTitleFirst">Trigger Id</div>
<span class="formTitleTextbox">
<input
class="formTree"
type="text"
required
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter the new trigger id"
size="40"
data-bind="textInput: id"
aria-label="Trigger Id"
/>
</span>
<div class="formTitleFirst">Trigger Type</div>
<span class="formTitleTextbox">
<select class="formTree" required data-bind="value: triggerType" aria-label="Trigger Type">
<option>Pre</option>
<option>Post</option>
</select>
</span>
<div class="formTitleFirst">Trigger Operation</div>
<span class="formTitleTextbox">
<select class="formTree" required data-bind="value: triggerOperation" aria-label="Trigger Operation">
<option>All</option>
<option>Create</option>
<option>Delete</option>
<option>Replace</option>
</select>
</span>
<div class="spUdfTriggerHeader">Trigger Body</div>
<div class="storedUdfTriggerEditor" data-bind=" setTemplateReady: true, attr: { id: editorId } "></div>
</div>
<!-- Trigger Tab Form - End -->
</div>

View File

@ -1,185 +0,0 @@
import * as Constants from "../../Common/Constants";
import { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
import editable from "../../Common/EditableUtility";
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 { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import Trigger from "../Tree/Trigger";
import ScriptTabBase from "./ScriptTabBase";
import template from "./TriggerTab.html";
export default class TriggerTab extends ScriptTabBase {
public readonly html = template;
public collection: ViewModels.Collection;
public node: Trigger;
public triggerType: ViewModels.Editable<string>;
public triggerOperation: ViewModels.Editable<string>;
constructor(options: ViewModels.ScriptTabOption) {
super(options);
super.onActivate.bind(this);
this.ariaLabel("Trigger Body");
this.triggerType = editable.observable<string>(options.resource.triggerType);
this.triggerOperation = editable.observable<string>(options.resource.triggerOperation);
this.formFields([this.id, this.triggerType, this.triggerOperation, this.editorContent]);
super.buildCommandBarOptions.bind(this);
super.buildCommandBarOptions();
}
public onSaveClick = (): void => {
this._createTrigger({
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation() as SqlTriggerResource["triggerOperation"],
triggerType: this.triggerType() as SqlTriggerResource["triggerType"],
});
};
public onUpdateClick = (): Promise<any> => {
const data = this._getResource();
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, {
tabTitle: this.tabTitle(),
});
return updateTrigger(this.collection.databaseId, this.collection.id(), {
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation() as SqlTriggerResource["triggerOperation"],
triggerType: this.triggerType() as SqlTriggerResource["triggerType"],
})
.then(
(createdResource) => {
this.resource(createdResource);
this.tabTitle(createdResource.id);
this.node.id(createdResource.id);
this.node.body(createdResource.body as string);
this.node.triggerType(createdResource.triggerOperation);
this.node.triggerOperation(createdResource.triggerOperation);
TelemetryProcessor.traceSuccess(
Action.UpdateTrigger,
{
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.UpdateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(createError),
errorStack: getErrorStack(createError),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public setBaselines() {
super.setBaselines();
const resource = this.resource();
this.triggerOperation.setBaseline(resource.triggerOperation);
this.triggerType.setBaseline(resource.triggerType);
}
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.isTriggersExpanded()) {
this.collection.container.selectedNode(this.collection);
} else {
this.collection.container.selectedNode(this.node);
}
}
private _createTrigger(resource: SqlTriggerResource): void {
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
resource.body = String(resource.body); // Ensure trigger body is converted to string
createTrigger(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()
)}/triggers/${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.createTriggerNode(createdResource);
TelemetryProcessor.traceSuccess(
Action.CreateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
return createdResource;
},
(createError: any) => {
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.CreateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(createError),
errorStack: getErrorStack(createError),
},
startKey
);
return Promise.reject(createError);
}
)
.finally(() => this.isExecuting(false));
}
private _getResource() {
return {
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation(),
triggerType: this.triggerType(),
};
}
}

View File

@ -0,0 +1,36 @@
import { TriggerDefinition } from "@azure/cosmos";
import React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import Trigger from "../Tree/Trigger";
import ScriptTabBase from "./ScriptTabBase";
import { TriggerTabContent } from "./TriggerTabContent";
export default class TriggerTab extends ScriptTabBase {
public onSaveClick: () => void;
public onUpdateClick: () => Promise<void>;
public collection: ViewModels.Collection;
public node: Trigger;
public triggerType: ViewModels.Editable<string>;
public triggerOperation: ViewModels.Editable<string>;
public triggerOptions: ViewModels.ScriptTabOption;
constructor(options: ViewModels.ScriptTabOption) {
super(options);
super.onActivate.bind(this);
this.triggerOptions = options;
}
addNodeInCollection(createdResource: TriggerDefinition | SqlTriggerResource): void {
this.node = this.collection.createTriggerNode(createdResource);
}
public render(): JSX.Element {
return (
<TriggerTabContent
{...this}
addNodeInCollection={(createdResource) => this.addNodeInCollection(createdResource)}
/>
);
}
}

View File

@ -0,0 +1,343 @@
import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, 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 { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
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 { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import TriggerTab from "./TriggerTab";
const triggerTypeOptions: IDropdownOption[] = [
{ key: "Pre", text: "Pre" },
{ key: "Post", text: "Post" },
];
const triggerOperationOptions: IDropdownOption[] = [
{ key: "All", text: "All" },
{ key: "Create", text: "Create" },
{ key: "Delete", text: "Delete" },
{ key: "Replace", text: "Replace" },
];
interface Ibutton {
visible: boolean;
enabled: boolean;
}
interface ITriggerTabContentState {
[key: string]: string | boolean;
triggerId: string;
triggerBody: string;
triggerType?: "Pre" | "Post";
triggerOperation?: "All" | "Create" | "Update" | "Delete" | "Replace";
isIdEditable: boolean;
}
export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentState> {
public saveButton: Ibutton;
public updateButton: Ibutton;
public discardButton: Ibutton;
constructor(props: TriggerTab) {
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, triggerType, triggerOperation } = props.triggerOptions.resource;
this.state = {
triggerId: id,
triggerType: triggerType,
triggerOperation: triggerOperation,
triggerBody: body,
isIdEditable: props.isNew() ? true : false,
};
}
private async onSaveClick(): Promise<void> {
const { triggerId, triggerType, triggerBody, triggerOperation } = this.state;
const resource = {
id: triggerId,
body: triggerBody,
triggerOperation: triggerOperation,
triggerType: triggerType,
};
this.props.isExecutionError(false);
this.props.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.tabTitle(),
});
try {
resource.body = String(resource.body); // Ensure trigger body is converted to string
const createdResource: TriggerDefinition | SqlTriggerResource = await createTrigger(
this.props.collection.databaseId,
this.props.collection.id(),
resource
);
if (createdResource) {
this.props.tabTitle(createdResource.id);
this.props.isNew(false);
this.props.resource(createdResource);
this.props.hashLocation(
`${Constants.HashRoutePrefixes.collectionsWithIds(
this.props.collection.databaseId,
this.props.collection.id()
)}/triggers/${createdResource.id}`
);
this.props.editorContent.setBaseline(createdResource.body as string);
this.props.addNodeInCollection(createdResource);
this.saveButton.visible = false;
this.updateButton.visible = true;
this.setState({ isIdEditable: false });
TelemetryProcessor.traceSuccess(
Action.CreateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.tabTitle(),
},
startKey
);
this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
this.props.isExecuting(false);
}
} catch (createError) {
this.props.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.CreateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.tabTitle(),
error: getErrorMessage(createError),
errorStack: getErrorStack(createError),
},
startKey
);
this.props.isExecuting(false);
}
}
private async onUpdateClick(): Promise<void> {
this.props.isExecutionError(false);
this.props.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, {
tabTitle: this.props.tabTitle(),
});
try {
const { triggerId, triggerBody, triggerOperation, triggerType } = this.state;
const createdResource = await updateTrigger(this.props.collection.databaseId, this.props.collection.id(), {
id: triggerId,
body: triggerBody,
triggerOperation: triggerOperation as SqlTriggerResource["triggerOperation"],
triggerType: triggerType as SqlTriggerResource["triggerType"],
});
if (createdResource) {
this.props.resource(createdResource);
this.props.tabTitle(createdResource.id);
this.props.node.id(createdResource.id);
this.props.node.body(createdResource.body as string);
this.props.node.triggerType(createdResource.triggerType);
this.props.node.triggerOperation(createdResource.triggerOperation);
TelemetryProcessor.traceSuccess(
Action.UpdateTrigger,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.tabTitle(),
},
startKey
);
this.props.isExecuting(false);
}
} catch (createError) {
this.props.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.UpdateTrigger,
{
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, triggerType, triggerOperation } = this.props.triggerOptions.resource;
this.setState({
triggerId: id,
triggerType: triggerType,
triggerOperation: triggerOperation,
triggerBody: 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;
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = "Save";
if (this.saveButton.visible) {
buttons.push({
setState: this.setState,
...this,
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 handleTriggerIdChange = (
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
this.setState({ triggerId: newValue });
};
private handleTriggerTypeOprationChange = (
_event: React.FormEvent<HTMLElement>,
selectedParam: IDropdownOption,
key: string
): void => {
this.setState({ [key]: String(selectedParam.key) });
};
private handleTriggerBodyChange = (newContent: string) => {
this.setState({ triggerBody: newContent });
};
render(): JSX.Element {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state;
return (
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
<TextField
className="trigger-field"
label="Trigger Id"
id="entityTimeId"
autoFocus
required
type="text"
pattern="[^/?#\\]*[^/?# \\]"
placeholder="Enter the new trigger id"
size={40}
value={triggerId}
readOnly={!isIdEditable}
onChange={this.handleTriggerIdChange}
/>
<Dropdown
placeholder="Trigger Type"
label="Trigger Type"
options={triggerTypeOptions}
selectedKey={triggerType}
className="trigger-field"
onChange={(event, selectedKey) => this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")}
/>
<Dropdown
placeholder="Trigger Operation"
label="Trigger Operation"
selectedKey={triggerOperation}
options={triggerOperationOptions}
className="trigger-field"
onChange={(event, selectedKey) =>
this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation")
}
/>
<Label className="trigger-field">Trigger Body</Label>
<EditorReact
language={"json"}
content={triggerBody}
isReadOnly={false}
ariaLabel={"Graph JSON"}
onContentChanged={this.handleTriggerBodyChange}
/>
</div>
);
}
}