Support upload prediction results (#864)
* Add toggle for switching between file/service. * Implement prediction file picker (#861) * Handle load file flow. * Add validate logic. * handle the predict result file and draw the result * add condition to prevent nofile error (#862) * Fix error while fetching with file URL. * update prediction file reader and disable prediction selector while no file. Co-authored-by: Buddha Wang <shihw@microsoft.com>
This commit is contained in:
Родитель
b65cea1334
Коммит
0f6c260263
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
.title {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
|
@ -13,7 +13,6 @@
|
|||
}
|
||||
.sourceDropdown {
|
||||
width: 95px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.local-file {
|
||||
float: right;
|
||||
|
|
|
@ -9,9 +9,10 @@
|
|||
flex: 1;
|
||||
input[name="pagerange"] {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
}
|
||||
.page-range-alert {
|
||||
border: 1px solid red;
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
.ms-Checkbox {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
.prebuilt-setting {
|
||||
.apikeyContainer {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
.ms-TooltipHost,
|
||||
.apikey {
|
||||
width: 100%;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
.prediction-file-picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.title {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.container-space-between {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.source-dropdown {
|
||||
width: 95px;
|
||||
}
|
||||
|
||||
.local-file {
|
||||
float: right;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
import { Dropdown, IDropdownOption, PrimaryButton, TextField } from '@fluentui/react';
|
||||
import React from 'react';
|
||||
import HtmlFileReader from '../../../../common/htmlFileReader';
|
||||
import { strings } from '../../../../common/strings';
|
||||
import { getGreenWithWhiteBackgroundTheme, getPrimaryGreenTheme } from '../../../../common/themes';
|
||||
import "./predictionFilePicker.scss";
|
||||
|
||||
interface IPredictionFile {
|
||||
file: File;
|
||||
fileLabel: string;
|
||||
fetchedFileURL: string;
|
||||
}
|
||||
|
||||
interface IPredictionFilePickerProps {
|
||||
disabled: boolean;
|
||||
onFileChange?: (file: IPredictionFile) => void;
|
||||
onError?: (err: { alertTitle: string, alertMessage: string }) => void;
|
||||
onSelectSourceChange?: () => void;
|
||||
}
|
||||
|
||||
interface IPredictionFilePickerState {
|
||||
sourceOption: string;
|
||||
inputedLocalFileName: string;
|
||||
inputedFileURL: string;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export class PredictionFilePicker extends React.Component<IPredictionFilePickerProps, IPredictionFilePickerState>{
|
||||
state = {
|
||||
sourceOption: "localFile",
|
||||
inputedLocalFileName: strings.predict.defaultLocalFileInput,
|
||||
inputedFileURL: "",
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
private filePicker: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
render() {
|
||||
const sourceOptions: IDropdownOption[] = [
|
||||
{ key: "localFile", text: strings.documentFilePicker.localFile },
|
||||
{ key: "url", text: strings.documentFilePicker.url },
|
||||
];
|
||||
|
||||
let { disabled } = this.props;
|
||||
disabled = !!disabled;
|
||||
const urlInputDisabled = disabled || this.state.isFetching;
|
||||
const fetchDisabled: boolean =
|
||||
disabled ||
|
||||
this.state.isFetching ||
|
||||
this.state.inputedFileURL.length === 0 ||
|
||||
this.state.inputedFileURL === strings.prebuiltPredict.defaultURLInput;
|
||||
|
||||
return <>
|
||||
<div className="prediction-file-picker">
|
||||
<div className="title mr-2">Prediction:</div>
|
||||
<div className="container-space-between">
|
||||
<Dropdown
|
||||
className="source-dropdown"
|
||||
selectedKey={this.state.sourceOption}
|
||||
options={sourceOptions}
|
||||
disabled={disabled}
|
||||
onChange={this.onSelectSourceChange}
|
||||
/>
|
||||
{this.state.sourceOption === "localFile" &&
|
||||
<>
|
||||
<input ref={this.filePicker}
|
||||
aria-hidden="true"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
disabled={disabled}
|
||||
onChange={this.handleInputFileChange}
|
||||
/>
|
||||
<TextField
|
||||
className="ml-2 local-file"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
style={{ cursor: (disabled ? "default" : "pointer") }}
|
||||
onClick={this.handleInputFileClick}
|
||||
readOnly={true}
|
||||
aria-label={strings.prebuiltPredict.defaultLocalFileInput}
|
||||
value={this.state.inputedLocalFileName}
|
||||
placeholder={strings.predict.defaultLocalFileInput}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{this.state.sourceOption === "url" &&
|
||||
<>
|
||||
<TextField
|
||||
className="mr-2 ml-2"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
onFocus={this.removeDefaultInputedFileURL}
|
||||
onChange={this.setInputedFileURL}
|
||||
aria-label={strings.prebuiltPredict.defaultLocalFileInput}
|
||||
value={this.state.inputedFileURL}
|
||||
disabled={urlInputDisabled}
|
||||
placeholder={strings.predict.defaultURLInput}
|
||||
/>
|
||||
<PrimaryButton
|
||||
theme={getPrimaryGreenTheme()}
|
||||
className="keep-button-80px"
|
||||
text="Fetch"
|
||||
allowDisabledFocus
|
||||
disabled={fetchDisabled}
|
||||
autoFocus={true}
|
||||
onClick={this.getFileFromURL}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
private onSelectSourceChange = (event, option) => {
|
||||
if (option.key !== this.state.sourceOption) {
|
||||
this.setState({
|
||||
sourceOption: option.key,
|
||||
inputedFileURL: ""
|
||||
}, () => {
|
||||
if (this.props.onSelectSourceChange) {
|
||||
this.props.onSelectSourceChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputFileChange = async () => {
|
||||
const { current } = this.filePicker;
|
||||
if (!current.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = current.value.split("\\").pop();
|
||||
if (!fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileInfo = await HtmlFileReader.readAsText(current.files[0]);
|
||||
if (!this.isValidSchema(JSON.parse(fileInfo.content as string))) {
|
||||
// Throw error when invalid schema.
|
||||
throw new Error("The file is not a proper prediction result, please try other file.");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputedLocalFileName: fileName
|
||||
}, () => {
|
||||
if (this.props.onFileChange) {
|
||||
this.props.onFileChange({
|
||||
file: current.files[0],
|
||||
fileLabel: fileName,
|
||||
fetchedFileURL: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// Report error.
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Load prediction file error",
|
||||
alertMessage: err?.message ? err.message : err,
|
||||
});
|
||||
}
|
||||
|
||||
// Reset file input.
|
||||
this.setState({ inputedLocalFileName: "" });
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputFileClick = () => {
|
||||
this.filePicker.current?.click();
|
||||
}
|
||||
|
||||
private removeDefaultInputedFileURL = () => {
|
||||
if (this.state.inputedFileURL === strings.prebuiltPredict.defaultURLInput) {
|
||||
this.setState({ inputedFileURL: "" });
|
||||
}
|
||||
}
|
||||
|
||||
private setInputedFileURL = (event) => {
|
||||
this.setState({ inputedFileURL: event.target.value });
|
||||
}
|
||||
|
||||
private getFileFromURL = async () => {
|
||||
this.setState({ isFetching: true });
|
||||
|
||||
try {
|
||||
const response = await fetch(this.state.inputedFileURL, { headers: { Accept: "application/json" } });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.status.toString() + " " + response.statusText);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
if (!["application/json", "application/octet-stream"].includes(contentType)) {
|
||||
throw new Error("Content-Type " + contentType + " not supported.");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
if (!this.isValidSchema(JSON.parse(await blob.text()))) {
|
||||
throw new Error("The file is not a proper prediction result, please try other file.");
|
||||
}
|
||||
|
||||
const fileAsURL = new URL(this.state.inputedFileURL);
|
||||
const fileName = fileAsURL.pathname.split("/").pop();
|
||||
const file = new File([blob], fileName, { type: contentType });
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
}, () => {
|
||||
if (this.props.onFileChange) {
|
||||
this.props.onFileChange({
|
||||
file,
|
||||
fileLabel: fileName,
|
||||
fetchedFileURL: ""
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ isFetching: false });
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Fetch failed",
|
||||
alertMessage: err?.message ? err.message : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isValidSchema = (jsonData) => {
|
||||
if (jsonData && jsonData.analyzeResult) {
|
||||
// We should ensure version and documentResults exists.
|
||||
const { version, documentResults } = jsonData.analyzeResult;
|
||||
return !!version && !!documentResults;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -20,3 +20,9 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.predict-mode-toggle {
|
||||
label {
|
||||
color: rgb(250, 251, 251);
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ import ServiceHelper from "../../../../services/serviceHelper";
|
|||
import { getAppInsights } from "../../../../services/telemetryService";
|
||||
import Alert from "../../common/alert/alert";
|
||||
import { DocumentFilePicker } from "../../common/documentFilePicker/documentFilePicker";
|
||||
import { PredictionFilePicker } from "../../common/predictionFilePicker/predictionFilePicker";
|
||||
import { ImageMap } from "../../common/imageMap/imageMap";
|
||||
import { PageRange } from "../../common/pageRange/pageRange";
|
||||
import { PrebuiltSetting } from "../../common/prebuiltSetting/prebuiltSetting";
|
||||
|
@ -41,6 +42,7 @@ import PredictResult from "../predict/predictResult";
|
|||
import { ILoadFileHelper, ILoadFileResult, LoadFileHelper } from "./LoadFileHelper";
|
||||
import "./prebuiltPredictPage.scss";
|
||||
import { ITableHelper, ITableState, TableHelper } from "./tableHelper";
|
||||
import { Toggle } from "office-ui-fabric-react/lib/Toggle";
|
||||
import { ILayoutHelper, LayoutHelper } from "./layoutHelper";
|
||||
|
||||
interface IPrebuiltTypes {
|
||||
|
@ -77,6 +79,8 @@ export interface IPrebuiltPredictPageState extends ILoadFileResult, ITableState
|
|||
pageRange: string;
|
||||
pageRangeIsValid?: boolean;
|
||||
predictionEndpointUrl: string;
|
||||
|
||||
liveMode: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: IApplicationState) {
|
||||
|
@ -150,6 +154,8 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
withPageRange: false,
|
||||
pageRange: "",
|
||||
predictionEndpointUrl: "",
|
||||
|
||||
liveMode: true,
|
||||
};
|
||||
|
||||
private analyzeResults: any;
|
||||
|
@ -248,35 +254,9 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
<FontIcon className="mr-1" iconName="ContactCard" />
|
||||
<span>{interpolate(strings.prebuiltPredict.anlayWithPrebuiltModels, this.state.currentPrebuiltType)}</span>
|
||||
</h6>
|
||||
<div className="p-3 prebuilt-setting" style={{ marginTop: "8px" }}>
|
||||
<h5>{strings.prebuiltSetting.serviceConfigurationTitle}</h5>
|
||||
</div>
|
||||
<PrebuiltSetting prebuiltSettings={this.props.prebuiltSettings}
|
||||
disabled={this.state.isPredicting}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<div className="p-3" style={{ marginTop: "-3rem" }}>
|
||||
<div className="formtype-section">
|
||||
<div style={{ marginBottom: "3px" }}>{strings.prebuiltPredict.formTypeTitle}</div>
|
||||
<Dropdown
|
||||
disabled={this.state.isPredicting}
|
||||
className="prebuilt-type-dropdown"
|
||||
options={this.prebuiltTypes.map(type => ({ key: type.name, text: type.name }))}
|
||||
defaultSelectedKey={this.state.currentPrebuiltType.name}
|
||||
onChange={this.onPrebuiltTypeChange}></Dropdown>
|
||||
</div>
|
||||
<div className="locales-section" style={{ display: this.state.currentPrebuiltType.useLocale ? "block" : "none" }}>
|
||||
<div style={{ marginBottom: "3px" }}>{strings.prebuiltPredict.locale}</div>
|
||||
<Dropdown
|
||||
disabled={this.state.isPredicting}
|
||||
className="prebuilt-type-dropdown"
|
||||
options={this.locales.map(type => ({ key: type, text: type }))}
|
||||
defaultSelectedKey={this.state.currentLocale}
|
||||
onChange={this.onLocaleChange}></Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3" style={{ marginTop: "8px" }}>
|
||||
<h5>{strings.prebuiltPredict.selectFileAndRunAnalysis}</h5>
|
||||
<Separator className="separator-right-pane-main">1. Choose file for analysis.</Separator>
|
||||
<div className="p-3">
|
||||
{/* <h5>{strings.prebuiltPredict.selectFileAndRunAnalysis}</h5> */}
|
||||
<DocumentFilePicker
|
||||
disabled={this.state.isPredicting || this.state.isFetching}
|
||||
onFileChange={(data) => this.onFileChange(data)}
|
||||
|
@ -290,8 +270,49 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
onPageRangeChange={this.onPageRangeChange} />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="separator-right-pane-main">{strings.prebuiltPredict.analysis}</Separator>
|
||||
<Separator className="separator-right-pane-main">2. Get prediction.</Separator>
|
||||
<div className="p-3" style={{ marginTop: "8px" }}>
|
||||
<Toggle theme={getLightGreyTheme()}
|
||||
className="predict-mode-toggle"
|
||||
defaultChecked
|
||||
onText="Call live service"
|
||||
offText="Use predicted file"
|
||||
onChange={this.handleLiveModeToggleChange} />
|
||||
</div>
|
||||
{!this.state.liveMode &&
|
||||
<div className="p-3" style={{ marginTop: "-2rem" }}>
|
||||
<PredictionFilePicker
|
||||
disabled={this.state.isPredicting || this.state.isFetching || !this.state.file}
|
||||
onFileChange={this.onPredictionFileChange}
|
||||
onSelectSourceChange={this.onPredictionSelectSourceChange}
|
||||
onError={this.onFileLoadError} />
|
||||
</div>
|
||||
}
|
||||
{this.state.liveMode &&
|
||||
<>
|
||||
<PrebuiltSetting prebuiltSettings={this.props.prebuiltSettings}
|
||||
disabled={this.state.isPredicting}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<div className="p-3" style={{ marginTop: "-28px" }}>
|
||||
<div className="formtype-section">
|
||||
<div style={{ marginBottom: "3px" }}>{strings.prebuiltPredict.formTypeTitle}</div>
|
||||
<Dropdown
|
||||
disabled={this.state.isPredicting}
|
||||
className="prebuilt-type-dropdown"
|
||||
options={this.prebuiltTypes.map(type => ({ key: type.name, text: type.name }))}
|
||||
defaultSelectedKey={this.state.currentPrebuiltType.name}
|
||||
onChange={this.onPrebuiltTypeChange}></Dropdown>
|
||||
</div>
|
||||
<div className="locales-section" style={{ visibility: this.state.currentPrebuiltType.useLocale ? "visible" : "hidden" }}>
|
||||
<div style={{ marginBottom: "3px" }}>{strings.prebuiltPredict.locale}</div>
|
||||
<Dropdown
|
||||
disabled={this.state.isPredicting}
|
||||
className="prebuilt-type-dropdown"
|
||||
options={this.locales.map(type => ({ key: type, text: type }))}
|
||||
defaultSelectedKey={this.state.currentLocale}
|
||||
onChange={this.onLocaleChange}></Dropdown>
|
||||
</div>
|
||||
<div style={{ marginBottom: "3px" }}>{"The composed API request is"}</div>
|
||||
<TextField
|
||||
className="mb-1 request-uri-textfield"
|
||||
|
@ -314,6 +335,8 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</> }
|
||||
{this.state.isFetching &&
|
||||
<div className="loading-container">
|
||||
<Spinner
|
||||
|
@ -352,9 +375,6 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
<div>{strings.prebuiltPredict.noFieldCanBeExtracted}</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
show={this.state.shouldShowAlert}
|
||||
|
@ -381,7 +401,7 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
});
|
||||
}
|
||||
|
||||
onSelectSourceChange(): void {
|
||||
onSelectSourceChange = (): void => {
|
||||
this.setState({
|
||||
file: undefined,
|
||||
analyzeResult: {},
|
||||
|
@ -392,18 +412,18 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
}
|
||||
}
|
||||
|
||||
onFileLoadError(err: { alertTitle: string; alertMessage: string; }): void {
|
||||
onFileLoadError = (err: { alertTitle: string; alertMessage: string; }): void => {
|
||||
this.setState({
|
||||
...err,
|
||||
shouldShowAlert: true,
|
||||
isPredicting: false,
|
||||
});
|
||||
}
|
||||
onFileChange(data: {
|
||||
onFileChange = (data: {
|
||||
file: File,
|
||||
fileLabel: string,
|
||||
fetchedFileURL: string
|
||||
}): void {
|
||||
}): void => {
|
||||
this.setState({
|
||||
currentPage: 1,
|
||||
analyzeResult: null,
|
||||
|
@ -418,6 +438,57 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
});
|
||||
}
|
||||
|
||||
onPredictionSelectSourceChange = (): void => {
|
||||
this.setState({
|
||||
analyzeResult: {},
|
||||
predictionLoaded: false,
|
||||
});
|
||||
if (this.imageMap) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
}
|
||||
}
|
||||
|
||||
onPredictionFileChange = (data: {
|
||||
file: File,
|
||||
fileLabel: string,
|
||||
fetchedFileURL: string
|
||||
}): void => {
|
||||
if (this.imageMap) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
}
|
||||
if (data.file) {
|
||||
const makeHandleFile = () => {
|
||||
const handlePredictionResult = this.handlePredictionResult.bind(this);
|
||||
const setState = this.setState.bind(this);
|
||||
return () => {
|
||||
let { result } = reader;
|
||||
if (result instanceof ArrayBuffer) {
|
||||
const dataView = new DataView(result);
|
||||
const decoder = new TextDecoder();
|
||||
result = decoder.decode(dataView)
|
||||
}
|
||||
result = JSON.parse(result)
|
||||
setState({
|
||||
currentPage: 1,
|
||||
analyzeResult: null,
|
||||
predictionLoaded: false,
|
||||
fileLoaded: false,
|
||||
}, () => {
|
||||
handlePredictionResult(result);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = makeHandleFile();
|
||||
reader.readAsText(data.file);
|
||||
}
|
||||
}
|
||||
|
||||
handleLiveModeToggleChange = (event, checked: boolean) => {
|
||||
this.setState({ liveMode: checked });
|
||||
}
|
||||
|
||||
private onPrebuiltTypeChange = (e, option: IDropdownOption) => {
|
||||
const currentPrebuiltType = this.prebuiltTypes.find(type => type.name === option.key);
|
||||
if (currentPrebuiltType && this.state.currentPrebuiltType.name !== currentPrebuiltType.name) {
|
||||
|
@ -612,11 +683,7 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
this.setState({ imageAngle: this.state.imageAngle + degrees });
|
||||
}
|
||||
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({ predictionLoaded: false, isPredicting: true });
|
||||
this.getPrediction()
|
||||
.then((result) => {
|
||||
private handlePredictionResult = (result) => {
|
||||
this.analyzeResults = _.cloneDeep(result);
|
||||
this.tableHelper.setAnalyzeResult(result?.analyzeResult);
|
||||
const tags = this.getTagsForPredictResults(this.getPredictionsFromAnalyzeResult(result?.analyzeResult));
|
||||
|
@ -630,7 +697,13 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
|
|||
this.layoutHelper.drawLayout(this.state.currentPage);
|
||||
this.drawPredictionResult();
|
||||
});
|
||||
}).catch((error) => {
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({ predictionLoaded: false, isPredicting: true });
|
||||
this.getPrediction()
|
||||
.then(this.handlePredictionResult)
|
||||
.catch((error) => {
|
||||
let alertMessage = "";
|
||||
if (error.response) {
|
||||
alertMessage = error.response.data;
|
||||
|
|
Загрузка…
Ссылка в новой задаче