mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
90c1439d34
* Rev up prettier * Reformat * Remove deprecated tslint * Remove call to tslint and update package-lock.json
501 lines
16 KiB
TypeScript
501 lines
16 KiB
TypeScript
import {
|
|
CommandBar,
|
|
ICommandBarItemProps,
|
|
MessageBar,
|
|
MessageBarType,
|
|
Separator,
|
|
Spinner,
|
|
SpinnerSize,
|
|
Stack,
|
|
Text,
|
|
} from "@fluentui/react";
|
|
import { TFunction } from "i18next";
|
|
import promiseRetry, { AbortError } from "p-retry";
|
|
import React from "react";
|
|
import { WithTranslation } from "react-i18next";
|
|
import * as _ from "underscore";
|
|
import { sendMessage } from "../Common/MessageHandler";
|
|
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
|
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
|
import { trace } from "../Shared/Telemetry/TelemetryProcessor";
|
|
import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles";
|
|
import {
|
|
AnyDisplay,
|
|
BooleanInput,
|
|
ChoiceInput,
|
|
DescriptionDisplay,
|
|
InputType,
|
|
Node,
|
|
NumberInput,
|
|
RefreshResult,
|
|
SelfServeComponentTelemetryType,
|
|
SelfServeDescriptor,
|
|
SmartUiInput,
|
|
StringInput,
|
|
} from "./SelfServeTypes";
|
|
|
|
interface SelfServeNotification {
|
|
message: string;
|
|
type: MessageBarType;
|
|
isCancellable: boolean;
|
|
}
|
|
|
|
interface PortalNotificationContent {
|
|
retryIntervalInMs: number;
|
|
operationStatusUrl: string;
|
|
portalNotification?: {
|
|
initialize: {
|
|
title: string;
|
|
message: string;
|
|
};
|
|
success: {
|
|
title: string;
|
|
message: string;
|
|
};
|
|
failure: {
|
|
title: string;
|
|
message: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
export interface SelfServeComponentProps extends WithTranslation {
|
|
descriptor: SelfServeDescriptor;
|
|
}
|
|
|
|
export interface SelfServeComponentState {
|
|
root: SelfServeDescriptor;
|
|
currentValues: Map<string, SmartUiInput>;
|
|
baselineValues: Map<string, SmartUiInput>;
|
|
isInitializing: boolean;
|
|
isSaving: boolean;
|
|
hasErrors: boolean;
|
|
compileErrorMessage: string;
|
|
refreshResult: RefreshResult;
|
|
notification: SelfServeNotification;
|
|
}
|
|
|
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
|
private static readonly defaultRetryIntervalInMs = 30000;
|
|
private smartUiGeneratorClassName: string;
|
|
private retryIntervalInMs: number;
|
|
private retryOptions: promiseRetry.Options;
|
|
private translationFunction: TFunction;
|
|
|
|
componentDidMount(): void {
|
|
this.performRefresh().then(() => {
|
|
if (this.state.refreshResult?.isUpdateInProgress) {
|
|
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
|
}
|
|
});
|
|
this.initializeSmartUiComponent();
|
|
|
|
const telemetryData = {
|
|
selfServeClassName: this.props.descriptor.root.id,
|
|
eventType: SelfServeComponentTelemetryType.Load,
|
|
};
|
|
trace(Action.SelfServeComponent, ActionModifiers.Mark, telemetryData, SelfServeMessageTypes.TelemetryInfo);
|
|
}
|
|
|
|
constructor(props: SelfServeComponentProps) {
|
|
super(props);
|
|
this.state = {
|
|
root: this.props.descriptor,
|
|
currentValues: new Map(),
|
|
baselineValues: new Map(),
|
|
isInitializing: true,
|
|
isSaving: false,
|
|
hasErrors: false,
|
|
compileErrorMessage: undefined,
|
|
refreshResult: undefined,
|
|
notification: undefined,
|
|
};
|
|
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
|
this.retryIntervalInMs = this.props.descriptor.refreshParams?.retryIntervalInMs;
|
|
if (!this.retryIntervalInMs) {
|
|
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
|
|
}
|
|
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
|
|
|
|
// translation function passed to SelfServeComponent
|
|
this.translationFunction = this.props.t;
|
|
}
|
|
|
|
private onError = (hasErrors: boolean): void => {
|
|
this.setState({ hasErrors });
|
|
};
|
|
|
|
private initializeSmartUiComponent = async (): Promise<void> => {
|
|
this.setState({ isInitializing: true });
|
|
await this.setDefaults();
|
|
const { currentValues, baselineValues } = this.state;
|
|
await this.initializeSmartUiNode(this.props.descriptor.root, currentValues, baselineValues);
|
|
this.setState({ isInitializing: false, currentValues, baselineValues });
|
|
};
|
|
|
|
private setDefaults = async (): Promise<void> => {
|
|
let { currentValues, baselineValues } = this.state;
|
|
|
|
const initialValues = await this.props.descriptor.initialize();
|
|
this.props.descriptor.inputNames.map((inputName) => {
|
|
const initialValue = initialValues.get(inputName);
|
|
currentValues = currentValues.set(inputName, initialValue);
|
|
baselineValues = baselineValues.set(inputName, initialValue);
|
|
initialValues.delete(inputName);
|
|
});
|
|
|
|
if (initialValues.size > 0) {
|
|
const keys = [];
|
|
for (const key of initialValues.keys()) {
|
|
keys.push(key);
|
|
}
|
|
|
|
this.setState({
|
|
compileErrorMessage: `The following fields have default values set but are not input properties of this class: ${keys.join(
|
|
", ",
|
|
)}`,
|
|
});
|
|
}
|
|
this.setState({ currentValues, baselineValues });
|
|
};
|
|
|
|
public updateBaselineValues = (): void => {
|
|
const currentValues = this.state.currentValues;
|
|
let baselineValues = this.state.baselineValues;
|
|
for (const key of currentValues.keys()) {
|
|
const currentValue = currentValues.get(key);
|
|
baselineValues = baselineValues.set(key, { ...currentValue });
|
|
}
|
|
this.setState({ baselineValues });
|
|
};
|
|
|
|
public discard = (): void => {
|
|
let { currentValues } = this.state;
|
|
const { baselineValues } = this.state;
|
|
for (const key of currentValues.keys()) {
|
|
const baselineValue = baselineValues.get(key);
|
|
currentValues = currentValues.set(key, baselineValue ? { ...baselineValue } : baselineValue);
|
|
}
|
|
this.setState({ currentValues });
|
|
};
|
|
|
|
private initializeSmartUiNode = async (
|
|
currentNode: Node,
|
|
currentValues: Map<string, SmartUiInput>,
|
|
baselineValues: Map<string, SmartUiInput>,
|
|
): Promise<void> => {
|
|
currentNode.info = await this.getResolvedValue(currentNode.info);
|
|
|
|
if (currentNode.input) {
|
|
currentNode.input = await this.getResolvedInput(currentNode.input, currentValues, baselineValues);
|
|
}
|
|
|
|
const promises = currentNode.children?.map(
|
|
async (child: Node) => await this.initializeSmartUiNode(child, currentValues, baselineValues),
|
|
);
|
|
if (promises) {
|
|
await Promise.all(promises);
|
|
}
|
|
};
|
|
|
|
private getResolvedInput = async (
|
|
input: AnyDisplay,
|
|
currentValues: Map<string, SmartUiInput>,
|
|
baselineValues: Map<string, SmartUiInput>,
|
|
): Promise<AnyDisplay> => {
|
|
input.labelTKey = await this.getResolvedValue(input.labelTKey);
|
|
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
|
|
|
|
switch (input.type) {
|
|
case "string": {
|
|
if ("description" in input) {
|
|
const descriptionDisplay = input as DescriptionDisplay;
|
|
descriptionDisplay.description = await this.getResolvedValue(descriptionDisplay.description);
|
|
}
|
|
return input as StringInput;
|
|
}
|
|
case "number": {
|
|
const numberInput = input as NumberInput;
|
|
numberInput.min = await this.getResolvedValue(numberInput.min);
|
|
numberInput.max = await this.getResolvedValue(numberInput.max);
|
|
numberInput.step = await this.getResolvedValue(numberInput.step);
|
|
|
|
const dataFieldName = numberInput.dataFieldName;
|
|
const defaultValue = currentValues.get(dataFieldName)?.value;
|
|
|
|
if (!defaultValue) {
|
|
const newDefaultValue = { value: numberInput.min, hidden: currentValues.get(dataFieldName)?.hidden };
|
|
currentValues.set(dataFieldName, newDefaultValue);
|
|
baselineValues.set(dataFieldName, newDefaultValue);
|
|
}
|
|
|
|
return numberInput;
|
|
}
|
|
case "boolean": {
|
|
const booleanInput = input as BooleanInput;
|
|
booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey);
|
|
booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey);
|
|
return booleanInput;
|
|
}
|
|
default: {
|
|
const choiceInput = input as ChoiceInput;
|
|
choiceInput.choices = await this.getResolvedValue(choiceInput.choices);
|
|
return choiceInput;
|
|
}
|
|
}
|
|
};
|
|
|
|
public async getResolvedValue<T>(value: T | (() => Promise<T>)): Promise<T> {
|
|
if (value instanceof Function) {
|
|
return value();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
|
if (input.onChange) {
|
|
const newValues = input.onChange(
|
|
newValue,
|
|
this.state.currentValues,
|
|
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>,
|
|
);
|
|
this.setState({ currentValues: newValues });
|
|
} else {
|
|
const dataFieldName = input.dataFieldName;
|
|
const { currentValues } = this.state;
|
|
const currentInputValue = currentValues.get(dataFieldName);
|
|
currentValues.set(dataFieldName, { value: newValue, hidden: currentInputValue?.hidden });
|
|
this.setState({ currentValues });
|
|
}
|
|
};
|
|
|
|
public performSave = async (): Promise<void> => {
|
|
const telemetryData = {
|
|
selfServeClassName: this.props.descriptor.root.id,
|
|
eventType: SelfServeComponentTelemetryType.Save,
|
|
};
|
|
trace(Action.SelfServeComponent, ActionModifiers.Mark, telemetryData, SelfServeMessageTypes.TelemetryInfo);
|
|
|
|
this.setState({ isSaving: true, notification: undefined });
|
|
try {
|
|
const onSaveResult = await this.props.descriptor.onSave(
|
|
this.state.currentValues,
|
|
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>,
|
|
);
|
|
if (onSaveResult.portalNotification) {
|
|
const requestInitializedPortalNotification = onSaveResult.portalNotification.initialize;
|
|
const requestSucceededPortalNotification = onSaveResult.portalNotification.success;
|
|
const requestFailedPortalNotification = onSaveResult.portalNotification.failure;
|
|
|
|
this.sendNotificationMessage({
|
|
retryIntervalInMs: this.retryIntervalInMs,
|
|
operationStatusUrl: onSaveResult.operationStatusUrl,
|
|
portalNotification: {
|
|
initialize: {
|
|
title: this.getTranslation(requestInitializedPortalNotification.titleTKey),
|
|
message: this.getTranslation(requestInitializedPortalNotification.messageTKey),
|
|
},
|
|
success: {
|
|
title: this.getTranslation(requestSucceededPortalNotification.titleTKey),
|
|
message: this.getTranslation(requestSucceededPortalNotification.messageTKey),
|
|
},
|
|
failure: {
|
|
title: this.getTranslation(requestFailedPortalNotification.titleTKey),
|
|
message: this.getTranslation(requestFailedPortalNotification.messageTKey),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
|
} catch (error) {
|
|
this.setState({
|
|
notification: {
|
|
type: MessageBarType.error,
|
|
isCancellable: true,
|
|
message: this.getTranslation(error.message),
|
|
},
|
|
});
|
|
throw error;
|
|
} finally {
|
|
this.setState({ isSaving: false });
|
|
}
|
|
await this.onRefreshClicked();
|
|
this.updateBaselineValues();
|
|
};
|
|
|
|
public onSaveButtonClick = (): void => {
|
|
this.performSave();
|
|
};
|
|
|
|
public isInputModified = (): boolean => {
|
|
for (const key of this.state.currentValues.keys()) {
|
|
const currentValue = this.state.currentValues.get(key);
|
|
if (currentValue && currentValue.hidden === undefined) {
|
|
currentValue.hidden = false;
|
|
}
|
|
if (currentValue && currentValue.disabled === undefined) {
|
|
currentValue.disabled = false;
|
|
}
|
|
|
|
const baselineValue = this.state.baselineValues.get(key);
|
|
if (baselineValue && baselineValue.hidden === undefined) {
|
|
baselineValue.hidden = false;
|
|
}
|
|
if (baselineValue && baselineValue.disabled === undefined) {
|
|
baselineValue.disabled = false;
|
|
}
|
|
|
|
if (!_.isEqual(currentValue, baselineValue)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
public isRefreshing = (): boolean => {
|
|
return this.state.isSaving || this.state.isInitializing || this.state.refreshResult?.isUpdateInProgress;
|
|
};
|
|
|
|
public isDiscardButtonDisabled = (): boolean => {
|
|
return this.isRefreshing() || !this.isInputModified();
|
|
};
|
|
|
|
public isSaveButtonDisabled = (): boolean => {
|
|
return this.state.hasErrors || this.isRefreshing() || !this.isInputModified();
|
|
};
|
|
|
|
private performRefresh = async (): Promise<void> => {
|
|
const refreshResult = await this.props.descriptor.onRefresh();
|
|
let updateInProgressNotification: SelfServeNotification;
|
|
if (this.state.refreshResult?.isUpdateInProgress && !refreshResult.isUpdateInProgress) {
|
|
await this.initializeSmartUiComponent();
|
|
}
|
|
if (refreshResult.isUpdateInProgress) {
|
|
updateInProgressNotification = {
|
|
type: MessageBarType.info,
|
|
isCancellable: false,
|
|
message: this.getTranslation(refreshResult.updateInProgressMessageTKey),
|
|
};
|
|
}
|
|
this.setState({
|
|
refreshResult: { ...refreshResult },
|
|
notification: updateInProgressNotification,
|
|
});
|
|
};
|
|
|
|
public onRefreshClicked = async (): Promise<void> => {
|
|
this.setState({ isInitializing: true });
|
|
await this.performRefresh();
|
|
this.setState({ isInitializing: false });
|
|
};
|
|
|
|
public pollRefresh = async (): Promise<void> => {
|
|
try {
|
|
await this.performRefresh();
|
|
} catch (error) {
|
|
throw new AbortError(error);
|
|
}
|
|
const refreshResult = this.state.refreshResult;
|
|
if (refreshResult.isUpdateInProgress) {
|
|
throw new Error("update in progress. retrying ...");
|
|
}
|
|
};
|
|
|
|
public getCommonTranslation = (key: string): string => {
|
|
return this.getTranslation(key, "Common");
|
|
};
|
|
|
|
private getTranslation = (messageKey: string, namespace = `${this.smartUiGeneratorClassName}`): string => {
|
|
const translationKey = `${namespace}:${messageKey}`;
|
|
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
|
|
if (translation === translationKey) {
|
|
return messageKey;
|
|
}
|
|
return translation;
|
|
};
|
|
|
|
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
|
return [
|
|
{
|
|
key: "save",
|
|
text: this.getCommonTranslation("Save"),
|
|
iconProps: { iconName: "Save" },
|
|
disabled: this.isSaveButtonDisabled(),
|
|
onClick: () => this.onSaveButtonClick(),
|
|
},
|
|
{
|
|
key: "discard",
|
|
text: this.getCommonTranslation("Discard"),
|
|
iconProps: { iconName: "Undo" },
|
|
disabled: this.isDiscardButtonDisabled(),
|
|
onClick: () => {
|
|
this.discard();
|
|
},
|
|
buttonStyles: commandBarItemStyles,
|
|
},
|
|
{
|
|
key: "refresh",
|
|
text: this.getCommonTranslation("Refresh"),
|
|
disabled: this.state.isInitializing,
|
|
iconProps: { iconName: "Refresh" },
|
|
onClick: () => {
|
|
this.onRefreshClicked();
|
|
},
|
|
buttonStyles: commandBarItemStyles,
|
|
},
|
|
];
|
|
};
|
|
|
|
private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
|
|
sendMessage({
|
|
type: SelfServeMessageTypes.Notification,
|
|
data: { portalNotificationContent },
|
|
});
|
|
};
|
|
|
|
public render(): JSX.Element {
|
|
if (this.state.compileErrorMessage) {
|
|
return (
|
|
<MessageBar messageBarType={MessageBarType.error}>
|
|
<Text>{this.state.compileErrorMessage}</Text>
|
|
</MessageBar>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{ overflowX: "auto" }}>
|
|
<Stack tokens={containerStackTokens}>
|
|
<Stack.Item>
|
|
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />
|
|
<Separator styles={separatorStyles} />
|
|
</Stack.Item>
|
|
{this.state.isInitializing ? (
|
|
<Spinner size={SpinnerSize.large} />
|
|
) : (
|
|
<>
|
|
{this.state.notification && (
|
|
<MessageBar
|
|
messageBarType={this.state.notification.type}
|
|
onDismiss={
|
|
this.state.notification.isCancellable ? () => this.setState({ notification: undefined }) : undefined
|
|
}
|
|
>
|
|
<Text>{this.state.notification.message}</Text>
|
|
</MessageBar>
|
|
)}
|
|
<SmartUiComponent
|
|
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
|
|
descriptor={this.state.root as SmartUiDescriptor}
|
|
currentValues={this.state.currentValues}
|
|
onInputChange={this.onInputChange}
|
|
onError={this.onError}
|
|
getTranslation={this.getTranslation}
|
|
/>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
</div>
|
|
);
|
|
}
|
|
}
|