From 43c52eee1e2d987d5f0bda644c04289cfd7592ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E9=BE=99=E9=BE=99?= Date: Sun, 24 Dec 2023 18:43:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(portal-web):=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD=20(#1012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 优化文件编辑功能 ## 修改 monaco-editor 默认换行符 monaco 编辑器会自动判断文件的换行符类型。 1. 如果是新建的空文件,则会根据当前运行环境(例如 windows)来决定换行符。 2. 如果不是空文件会根据当前文件的换行符自动判断后续的换行符,如果是 \r\n 后续也是 \r\n 如果原本就是 \n 那后续也是 \n 提交文件运行的模式下,如果文件换行符为 \r\n 时无法提交成功。 用户在 scow 上编写的时 monaco-editor 可能处于 windows 环境,如果在编辑一个新文件就会导致换行符为 \r\n 所以直接将 monaco-editor 的默认换行符设置为 \n,来避免上述情况的发生。但这样对于原本就是 \r\n 的文件,后续编辑会存在两种换行符。 如果要完全避免文件提交运行报错这个问题,还是应该在提交该文件时将换行符进行切换,这种情况仍然会导致该问题: 1. 用户直接上传的文件的换行符为 \r\n 2. 原本的文件就是 \r\n ## 增加流式文件下载中断逻辑 如果流式下载文件未结束时直接关闭模态框打开其他文件模态框会有内容覆盖问题,所需要增加中断流式加载 ## 新增文件加载时禁止点击编辑按钮 --- .changeset/empty-dancers-film.md | 5 + apps/portal-web/src/i18n/en.ts | 1 + apps/portal-web/src/i18n/zh_cn.ts | 1 + .../filemanager/FileEditModal.tsx | 131 +++++++++++------- 4 files changed, 85 insertions(+), 53 deletions(-) create mode 100644 .changeset/empty-dancers-film.md diff --git a/.changeset/empty-dancers-film.md b/.changeset/empty-dancers-film.md new file mode 100644 index 0000000000..01e44eaed7 --- /dev/null +++ b/.changeset/empty-dancers-film.md @@ -0,0 +1,5 @@ +--- +"@scow/portal-web": patch +--- + +优化文件编辑功能 diff --git a/apps/portal-web/src/i18n/en.ts b/apps/portal-web/src/i18n/en.ts index 7c181567ad..ea908ad1c2 100644 --- a/apps/portal-web/src/i18n/en.ts +++ b/apps/portal-web/src/i18n/en.ts @@ -233,6 +233,7 @@ export default { saveFileFail: "File save failed: {}", saveFileSuccess: "File saved successfully", fileSizeExceeded: "File too large (maximum {}), please download and edit", + fileFetchAbortPrompt: "Fetch {} operation was aborted", }, createFileModal: { createErrorMessage: "File or directory with the same name already exists!", diff --git a/apps/portal-web/src/i18n/zh_cn.ts b/apps/portal-web/src/i18n/zh_cn.ts index 4ad2dc211b..c81b937196 100644 --- a/apps/portal-web/src/i18n/zh_cn.ts +++ b/apps/portal-web/src/i18n/zh_cn.ts @@ -233,6 +233,7 @@ export default { saveFileFail: "文件保存失败: {}", saveFileSuccess: "文件保存成功", fileSizeExceeded: "文件过大(最大{}),请下载后编辑", + fileFetchAbortPrompt: "获取文件 {} 操作被终止", }, createFileModal: { createErrorMessage: "同名文件或者目录已经存在!", diff --git a/apps/portal-web/src/pageComponents/filemanager/FileEditModal.tsx b/apps/portal-web/src/pageComponents/filemanager/FileEditModal.tsx index 812e5adbbe..506b9c9329 100644 --- a/apps/portal-web/src/pageComponents/filemanager/FileEditModal.tsx +++ b/apps/portal-web/src/pageComponents/filemanager/FileEditModal.tsx @@ -13,16 +13,16 @@ import { CloseOutlined, FullscreenExitOutlined, FullscreenOutlined } from "@ant-design/icons"; import Editor, { loader } from "@monaco-editor/react"; import { App, Badge, Button, Modal, Space, Spin, Tabs, Tooltip } from "antd"; +import { editor } from "monaco-editor"; import { join } from "path"; -import { Dispatch, SetStateAction, useEffect, useState } from "react"; -import { api } from "src/apis"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { prefix, useI18nTranslateToString } from "src/i18n"; import { publicConfig } from "src/utils/config"; import { convertToBytes } from "src/utils/format"; import { getLanguage } from "src/utils/staticFiles"; import { styled } from "styled-components"; -import { urlToUpload } from "./api"; +import { urlToDownload, urlToUpload } from "./api"; const StyledTabs = styled(Tabs)` .ant-tabs-nav { @@ -150,7 +150,7 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) const { open, filename, fileSize, filePath, clusterId } = previewFile; const [mode, setMode] = useState(Mode.PREVIEW); - const [fileContent, setFileContent] = useState(""); + const [fileContent, setFileContent] = useState(undefined); const [isEdit, setIsEdit] = useState(false); const [loading, setLoading] = useState(false); const [downloading, setDownloading] = useState(false); @@ -158,6 +158,8 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) const [confirm, setConfirm] = useState(false); const [exitType, setExitType] = useState(ExitType.CLOSE); const [isFullScreen, setIsFullScreen] = useState(false); + const editorRef = useRef(null); + const [abortController, setAbortController] = useState(new AbortController()); const [options, setOptions] = useState({ readOnly: true, @@ -170,20 +172,29 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) } }, [open]); + useEffect(() => { + if (editorRef.current) { + const editorInstance = editorRef.current.getModel(); + + // 设置换行符为 LF (\n) + editorInstance?.setEOL(0); // 0 代表 LF,1 代表 CRLF + } + }, [editorRef.current]); + const { message } = App.useApp(); const handleEdit = (content) => { - if (content && !downloading) { + if (mode === Mode.EDIT) { setIsEdit(true); - setFileContent(content); } + setFileContent(content); }; const closeProcess = () => { setConfirm(false); setIsEdit(false); setMode(Mode.PREVIEW); - setFileContent(""); + setFileContent(undefined); setOptions({ ...options, readOnly: true, @@ -196,6 +207,9 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) }; const handleClose = () => { + if (downloading) { + abortController.abort(); + } if (!isEdit) { closeProcess(); return; @@ -227,6 +241,9 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) }; const handleSave = async () => { + if (fileContent === undefined) { + return; + } setSaving(true); const blob = new Blob([fileContent], { type: "text/plain" }); @@ -258,53 +275,60 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) setLoading(true); setDownloading(true); - api.downloadFile({ - query: { download: false, path: filePath, cluster: clusterId }, - }).then((json) => { - setFileContent(JSON.stringify(json, null, 2)); - }).catch(async (res: Response) => { - - if (!res.ok) { - message.error(t(p("failedGetFile"), [filename])); - return; - } - const reader = res.body?.getReader(); - if (reader) { - let accumulatedChunks = ""; - let accumulatedSize = 0; - const CHUNK_SIZE = 3 * 1024 * 1024; - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - setFileContent(() => { - return accumulatedChunks; - }); - break; - } - const chunk = decoder.decode(value, { stream: true }); - accumulatedChunks += chunk; - accumulatedSize += chunk.length; - - if (accumulatedSize > CHUNK_SIZE) { - setFileContent(() => { - return accumulatedChunks; - }); - accumulatedSize = 0; - setLoading(false); + const newAbortController = new AbortController(); + setAbortController(newAbortController); + fetch(urlToDownload(clusterId, filePath, false), { signal: newAbortController.signal }) + .then((res) => { + if (!res.ok) { + message.error(t(p("failedGetFile"), [filename])); + return; + } + return res.body?.getReader(); + }) + .then(async (reader) => { + if (reader) { + let accumulatedChunks = ""; + let accumulatedSize = 0; + const CHUNK_SIZE = 3 * 1024 * 1024; + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + setFileContent(() => { + return accumulatedChunks; + }); + break; + } + + const chunk = decoder.decode(value, { stream: true }); + accumulatedChunks += chunk; + accumulatedSize += chunk.length; + + if (accumulatedSize > CHUNK_SIZE) { + setFileContent(() => { + return accumulatedChunks; + }); + accumulatedSize = 0; + setLoading(false); + } } + } else { + message.error(t(p("cantReadFile"), [filename])); } - } else { - message.error(t(p("cantReadFile"), [filename])); - } - - }).finally(() => { - setLoading(false); - setDownloading(false); - }); - + }) + .catch((error) => { + if (error.name === "AbortError") { + message.info(t(p("fileFetchAbortPrompt"), [filename])); + } else { + message.error(t(p("cantReadFile"), [filename])); + } + }) + .finally(() => { + setLoading(false); + setDownloading(false); + }); }; const modalTitle = ( @@ -332,7 +356,7 @@ export const FileEditModal: React.FC = ({ previewFile, setPreviewFile }) const fileEditLimitSize = publicConfig.FILE_EDIT_SIZE || DEFAULT_FILE_EDIT_LIMIT_SIZE; return ( mode === Mode.PREVIEW ? ( - fileSize <= convertToBytes(fileEditLimitSize) + fileSize <= convertToBytes(fileEditLimitSize) && !downloading ? (