mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-22 10:21:37 +00:00
Added more selfserve changes (#443)
* exposed baselineValues * added getOnSaveNotification * disable UI when onSave is taking place * added optional polling * Added portal notifications * minor edits * added label for description * Added correlationids and polling of refresh * added label tooltip * removed ClassInfo decorator * Added dynamic decription * added info and warninf types for description * promise retry changes * compile errors fixed * merged sqlxEdits * undid sqlx changes * added completed notification * passed retryInterval in notif options * added polling on landing on the page * edits for error display * added link generation * addressed PR comments * modified test * fixed compilation error
This commit is contained in:
committed by
GitHub
parent
c1b74266eb
commit
ecdc41ada9
@@ -15,20 +15,45 @@ import {
|
||||
InputType,
|
||||
RefreshResult,
|
||||
SelfServeDescriptor,
|
||||
SelfServeNotification,
|
||||
SmartUiInput,
|
||||
DescriptionDisplay,
|
||||
StringInput,
|
||||
NumberInput,
|
||||
BooleanInput,
|
||||
ChoiceInput,
|
||||
SelfServeNotificationType,
|
||||
} from "./SelfServeTypes";
|
||||
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { getMessageBarType } from "./SelfServeUtils";
|
||||
import { Translation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
import "../i18n";
|
||||
import { sendMessage } from "../Common/MessageHandler";
|
||||
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
|
||||
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 {
|
||||
descriptor: SelfServeDescriptor;
|
||||
@@ -39,17 +64,26 @@ export interface SelfServeComponentState {
|
||||
currentValues: Map<string, SmartUiInput>;
|
||||
baselineValues: Map<string, SmartUiInput>;
|
||||
isInitializing: boolean;
|
||||
isSaving: boolean;
|
||||
hasErrors: boolean;
|
||||
compileErrorMessage: string;
|
||||
notification: SelfServeNotification;
|
||||
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();
|
||||
this.performRefresh().then(() => {
|
||||
if (this.state.refreshResult?.isUpdateInProgress) {
|
||||
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
||||
}
|
||||
});
|
||||
this.initializeSmartUiComponent();
|
||||
}
|
||||
|
||||
@@ -60,12 +94,18 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
currentValues: new Map(),
|
||||
baselineValues: new Map(),
|
||||
isInitializing: true,
|
||||
isSaving: false,
|
||||
hasErrors: false,
|
||||
compileErrorMessage: undefined,
|
||||
notification: 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 };
|
||||
}
|
||||
|
||||
private onError = (hasErrors: boolean): void => {
|
||||
@@ -109,7 +149,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
this.setState({ currentValues, baselineValues });
|
||||
};
|
||||
|
||||
public resetBaselineValues = (): void => {
|
||||
public updateBaselineValues = (): void => {
|
||||
const currentValues = this.state.currentValues;
|
||||
let baselineValues = this.state.baselineValues;
|
||||
for (const key of currentValues.keys()) {
|
||||
@@ -204,7 +244,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
|
||||
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
||||
if (input.onChange) {
|
||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
||||
const newValues = input.onChange(
|
||||
newValue,
|
||||
this.state.currentValues,
|
||||
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
|
||||
);
|
||||
this.setState({ currentValues: newValues });
|
||||
} else {
|
||||
const dataFieldName = input.dataFieldName;
|
||||
@@ -215,42 +259,60 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
}
|
||||
};
|
||||
|
||||
public performSave = async (): Promise<void> => {
|
||||
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 => {
|
||||
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
||||
onSavePromise.catch((error) => {
|
||||
this.setState({
|
||||
notification: {
|
||||
message: `${error.message}`,
|
||||
type: SelfServeNotificationType.error,
|
||||
},
|
||||
});
|
||||
});
|
||||
onSavePromise.then((notification: SelfServeNotification) => {
|
||||
this.setState({
|
||||
notification: {
|
||||
message: notification.message,
|
||||
type: notification.type,
|
||||
},
|
||||
});
|
||||
this.resetBaselineValues();
|
||||
this.onRefreshClicked();
|
||||
});
|
||||
this.performSave();
|
||||
};
|
||||
|
||||
public isDiscardButtonDisabled = (): boolean => {
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||
|
||||
if (currentValue !== baselineValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public isSaveButtonDisabled = (): boolean => {
|
||||
if (this.state.hasErrors) {
|
||||
if (this.state.isSaving) {
|
||||
return true;
|
||||
}
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
@@ -264,38 +326,84 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
return true;
|
||||
};
|
||||
|
||||
private performRefresh = async (): Promise<RefreshResult> => {
|
||||
public isSaveButtonDisabled = (): boolean => {
|
||||
if (this.state.hasErrors || this.state.isSaving) {
|
||||
return true;
|
||||
}
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||
|
||||
if (currentValue !== baselineValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private performRefresh = async (): Promise<void> => {
|
||||
const refreshResult = await this.props.descriptor.onRefresh();
|
||||
this.setState({ refreshResult: { ...refreshResult } });
|
||||
return refreshResult;
|
||||
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 });
|
||||
const refreshResult = await this.performRefresh();
|
||||
if (!refreshResult.isUpdateInProgress) {
|
||||
this.initializeSmartUiComponent();
|
||||
}
|
||||
await this.performRefresh();
|
||||
this.setState({ isInitializing: false });
|
||||
};
|
||||
|
||||
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
||||
return translationFunction(`Common.${key}`);
|
||||
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 ...");
|
||||
}
|
||||
};
|
||||
|
||||
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
||||
public getCommonTranslation = (key: string): string => {
|
||||
return this.getTranslation(key, "Common");
|
||||
};
|
||||
|
||||
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
|
||||
const translationKey = `${prefix}.${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(translate, "Save"),
|
||||
text: this.getCommonTranslation("Save"),
|
||||
iconProps: { iconName: "Save" },
|
||||
split: true,
|
||||
disabled: this.isSaveButtonDisabled(),
|
||||
onClick: this.onSaveButtonClick,
|
||||
onClick: () => this.onSaveButtonClick(),
|
||||
},
|
||||
{
|
||||
key: "discard",
|
||||
text: this.getCommonTranslation(translate, "Discard"),
|
||||
text: this.getCommonTranslation("Discard"),
|
||||
iconProps: { iconName: "Undo" },
|
||||
split: true,
|
||||
disabled: this.isDiscardButtonDisabled(),
|
||||
@@ -305,7 +413,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
text: this.getCommonTranslation(translate, "Refresh"),
|
||||
text: this.getCommonTranslation("Refresh"),
|
||||
disabled: this.state.isInitializing,
|
||||
iconProps: { iconName: "Refresh" },
|
||||
split: true,
|
||||
@@ -316,12 +424,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
];
|
||||
};
|
||||
|
||||
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => {
|
||||
const translation = translationFunction(messageKey);
|
||||
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) {
|
||||
return messageKey;
|
||||
}
|
||||
return translation;
|
||||
private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
|
||||
sendMessage({
|
||||
type: SelfServeMessageTypes.Notification,
|
||||
data: { portalNotificationContent },
|
||||
});
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -332,14 +439,14 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
return (
|
||||
<Translation>
|
||||
{(translate) => {
|
||||
const getTranslation = (key: string): string => {
|
||||
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
||||
};
|
||||
if (!this.translationFunction) {
|
||||
this.translationFunction = translate;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
|
||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
||||
{this.state.isInitializing ? (
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
@@ -347,27 +454,25 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{this.state.refreshResult?.isUpdateInProgress && (
|
||||
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||
{getTranslation(this.state.refreshResult.notificationMessage)}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.state.notification && (
|
||||
<MessageBar
|
||||
messageBarType={getMessageBarType(this.state.notification.type)}
|
||||
styles={{ root: { width: 400 } }}
|
||||
onDismiss={() => this.setState({ notification: undefined })}
|
||||
messageBarType={this.state.notification.type}
|
||||
onDismiss={
|
||||
this.state.notification.isCancellable
|
||||
? () => this.setState({ notification: undefined })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
||||
{this.state.notification.message}
|
||||
</MessageBar>
|
||||
)}
|
||||
<SmartUiComponent
|
||||
disabled={this.state.refreshResult?.isUpdateInProgress}
|
||||
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={getTranslation}
|
||||
getTranslation={this.getTranslation}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user