Compare commits

..
67 changed files with 605 additions and 397 deletions
+30 -16
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
@@ -28,6 +27,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/go-archive"
)
@@ -325,18 +325,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
}
go func() {
client, err := docker.NewDockerClient()
if err != nil {
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()
for _, itemPath := range req.Paths {
currentPath := itemPath
itemName := path.Base(currentPath)
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
file, err := os.Open(currentPath)
if err != nil {
return err
@@ -347,14 +345,7 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return err
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
return consumeImageLoadResponse(res.Body, taskItem.Log)
}, nil)
}
_ = taskItem.Execute()
@@ -362,6 +353,29 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return nil
}
// Docker may report load failures in a successful HTTP response's JSON stream.
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
decoder := json.NewDecoder(reader)
for {
var message jsonmessage.JSONMessage
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != nil && message.Error.Message != "" {
return message.Error
}
if message.ErrorMessage != "" {
return errors.New(message.ErrorMessage)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}
func (u *ImageService) ImageSave(req dto.ImageSave) error {
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
+7 -2
View File
@@ -144,8 +144,13 @@ export const imagePull = (params: Container.ImagePull) => {
export const imagePush = (params: Container.ImagePush) => {
return http.post<string>(`/containers/image/push`, params);
};
export const imageLoad = (params: Container.ImageLoad) => {
return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M);
export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => {
return http.post(
`/containers/image/load`,
params,
TimeoutEnum.T_10M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const imageSave = (params: Container.ImageSave) => {
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
@@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onActivated, ref, watch } from 'vue';
defineOptions({ name: 'TableSearch' });
const emit = defineEmits(['search', 'update:searchName']);
@@ -42,6 +42,10 @@ watch(
{ immediate: true },
);
onActivated(() => {
searchInfo.value = props.searchName;
});
const search = () => {
emit('update:searchName', searchInfo.value);
emit('search');
@@ -167,7 +167,7 @@
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, useAttrs } from 'vue';
import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
import { useCardColumns } from './useCardColumns';
@@ -459,6 +459,8 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
flush: 'post',
});
onDeactivated(clearSelects);
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
flush: 'sync',
});
+2 -7
View File
@@ -1,11 +1,6 @@
import { reactive, type UnwrapNestedRefs } from 'vue';
import { useRoute } from 'vue-router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { getPageState } from '@/utils/page-state-cache';
/** A new page visit starts from its defaults instead of restoring the previous page number. */
export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => {
const route = useRoute();
const { currentNode } = useGlobalStore();
const key = `${currentNode.value}:${String(route.name || route.path)}`;
return reactive(getPageState(key, factory));
return reactive(factory());
};
@@ -1,63 +0,0 @@
import { storeToRefs } from 'pinia';
import { watch, type Ref } from 'vue';
import GlobalStore from '@/store/modules/global';
type SearchValue = string | number | undefined;
type SearchFields = Record<string, Ref<SearchValue>>;
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
const { currentNode } = storeToRefs(GlobalStore());
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
let initialized = false;
let restoring = false;
let activeKey: string | undefined;
watch(
() => {
const context = scope?.();
if (scope && context === undefined) return undefined;
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
},
(key) => {
activeKey = key;
let saved: Record<string, unknown> = {};
try {
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
} catch {}
restoring = true;
for (const [name, field] of Object.entries(fields)) {
const value = saved[name];
const initial = defaults[name];
let fallback = initial;
if (initialized) {
fallback = typeof initial === 'string' ? '' : undefined;
}
const valid =
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
(fallback === undefined || typeof value === typeof fallback);
if (value === null) {
field.value = typeof initial === 'string' ? '' : undefined;
} else if (valid) {
field.value = value as SearchValue;
} else {
field.value = fallback;
}
}
restoring = false;
initialized = true;
},
{ immediate: true, flush: 'sync' },
);
watch(
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
(value) => {
if (restoring || !activeKey) return;
try {
localStorage.setItem(activeKey, JSON.stringify(value));
} catch {}
},
{ flush: 'sync' },
);
};
@@ -0,0 +1,24 @@
import { storeToRefs } from 'pinia';
import { onActivated, onDeactivated } from 'vue';
import GlobalStore from '@/store/modules/global';
import { bindSearchReset, type SearchFields } from '@/utils/search-reset';
interface SearchResetOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
export const useSearchReset = (fields: SearchFields, options: SearchResetOptions) => {
const { currentNode } = storeToRefs(GlobalStore());
const reset = bindSearchReset(options.scope ? [currentNode, options.scope] : [currentNode], fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
});
onActivated(() => {
if (!deactivated) return;
deactivated = false;
reset();
return options.onRevisit();
});
};
+5 -3
View File
@@ -1878,6 +1878,11 @@ const message = {
imageName: 'Image name',
pull: 'Pull',
path: 'Path',
imageUploadDrop: 'Drop an image file here, or click to upload',
imageUploadHelper:
'Any file format is allowed. Docker validates the contents. Uploaded files are retained for retrying imports or manual deletion.',
imageUploadEmpty: 'The file is empty. Select a valid image archive.',
imageImportSubmitted: 'Image import task submitted. Check the task log for the result.',
importImage: 'Import',
buildArgs: 'Build Arguments',
imageBuild: 'Build',
@@ -4175,9 +4180,6 @@ const message = {
'Enabling this can avoid certificate leakage, setting a default website will invalidate this setting',
},
ssl: {
deleteConfirm:
'The following certificates will be deleted. ACME certificates will be automatically revoked. Continue?',
deleteAndRevoke: 'Delete and revoke',
create: 'Request',
provider: 'Type',
manualCreate: 'Created manually',
+6 -3
View File
@@ -1902,6 +1902,12 @@ const message = {
imageName: 'Nombre de la imagen',
pull: 'Descargar',
path: 'Ruta',
imageUploadDrop: 'Arrastre un archivo de imagen aquí o haga clic para subirlo',
imageUploadHelper:
'Se permite cualquier formato de archivo. Docker valida el contenido. Los archivos subidos se conservan para reintentar la importación o eliminarlos manualmente.',
imageUploadEmpty: 'El archivo está vacío. Seleccione un archivo de imagen válido.',
imageImportSubmitted:
'Tarea de importación de imagen enviada. Consulte el registro de la tarea para ver el resultado.',
importImage: 'Importar',
buildArgs: 'Argumentos de Construcción',
imageBuild: 'Construir',
@@ -4225,9 +4231,6 @@ const message = {
'Habilitar esto puede evitar la fuga de certificados, establecer un sitio web predeterminado invalidará esta configuración',
},
ssl: {
deleteConfirm:
'Se eliminarán los siguientes certificados. Los certificados ACME se revocarán automáticamente. ¿Desea continuar?',
deleteAndRevoke: 'Eliminar y revocar',
create: 'Solicitar',
provider: 'Tipo',
manualCreate: 'Creado manualmente',
+5 -2
View File
@@ -1862,6 +1862,11 @@ const message = {
imageName: 'نام تصویر',
pull: 'دریافت',
path: 'مسیر',
imageUploadDrop: 'فایل ایمیج را اینجا بکشید یا برای بارگذاری کلیک کنید',
imageUploadHelper:
'همه قالب‌های فایل مجاز هستند. Docker محتوای فایل را اعتبارسنجی می‌کند. فایل‌های بارگذاری‌شده برای تلاش مجدد جهت وارد کردن یا حذف دستی نگه داشته می‌شوند.',
imageUploadEmpty: 'فایل خالی است. یک فایل ایمیج معتبر انتخاب کنید.',
imageImportSubmitted: 'وظیفه وارد کردن ایمیج ثبت شد. برای مشاهده نتیجه، گزارش وظیفه را بررسی کنید.',
importImage: 'واردات',
buildArgs: 'آرگومان‌های ساخت',
imageBuild: 'ساخت',
@@ -4141,8 +4146,6 @@ const message = {
'فعال‌سازی این گزینه می‌تواند از نشت گواهی جلوگیری کند، تنظیم یک وب‌سایت پیش‌فرض این تنظیم را بی‌اثر می‌کند',
},
ssl: {
deleteConfirm: 'گواهی‌های زیر حذف خواهند شد. گواهی‌های ACME به‌طور خودکار باطل خواهند شد. آیا ادامه می‌دهید؟',
deleteAndRevoke: 'حذف و ابطال',
create: 'درخواست',
provider: 'نوع',
manualCreate: 'ایجاد دستی',
+5 -2
View File
@@ -1859,6 +1859,11 @@ const message = {
imageName: '画像名',
pull: '引く',
path: 'パス',
imageUploadDrop: 'イメージファイルをここにドラッグするか、クリックしてアップロードしてください',
imageUploadHelper:
'ファイル形式に制限はありません。Docker が内容を検証します。アップロードしたファイルは保持されるため、インポートを再試行したり、手動で削除したりできます。',
imageUploadEmpty: 'ファイルが空です。有効なイメージファイルを選択してください。',
imageImportSubmitted: 'イメージのインポートタスクを送信しました。結果はタスクログで確認してください。',
importImage: '輸入',
buildArgs: 'ビルド引数',
imageBuild: '建てる',
@@ -4163,8 +4168,6 @@ const message = {
'有効にすると証明書の漏洩を防げますが、デフォルト Web サイトを設定するとこの設定は無効になります',
},
ssl: {
deleteConfirm: '以下の証明書を削除します。ACME 証明書は自動的に失効されます。続行しますか?',
deleteAndRevoke: '削除して失効',
create: 'リクエスト',
provider: 'タイプ',
manualCreate: '手動で作成されます',
+5 -2
View File
@@ -1841,6 +1841,11 @@ const message = {
imageName: '이미지 이름',
pull: '풀',
path: '경로',
imageUploadDrop: '이미지 파일을 여기로 끌어 놓거나 클릭하여 업로드하세요',
imageUploadHelper:
'파일 형식에는 제한이 없습니다. Docker가 내용을 검증합니다. 업로드한 파일은 보관되므로 가져오기를 다시 시도하거나 수동으로 삭제할 수 있습니다.',
imageUploadEmpty: '빈 파일입니다. 유효한 이미지 파일을 선택하세요.',
imageImportSubmitted: '이미지 가져오기 작업이 제출되었습니다. 작업 로그에서 결과를 확인하세요.',
importImage: '가져오기',
buildArgs: '빌드 인수',
imageBuild: '이미지 빌드',
@@ -4086,8 +4091,6 @@ const message = {
'활성화하면 인증서 누출을 방지할 수 있지만, 기본 웹사이트를 설정하면 이 설정이 무효화됩니다',
},
ssl: {
deleteConfirm: '다음 인증서를 삭제합니다. ACME 인증서는 자동으로 폐기됩니다. 계속하시겠습니까?',
deleteAndRevoke: '삭제 및 폐기',
create: '요청',
provider: '유형',
manualCreate: '수동 생성됨',
+5 -2
View File
@@ -1831,6 +1831,11 @@ const message = {
imageName: 'ຊື່ Image',
pull: 'ດຶງຂໍ້ມູນ (Pull)',
path: 'ເສັ້ນທາງ',
imageUploadDrop: 'ລາກໄຟລ໌ອິມເມຈມາວາງບ່ອນນີ້ ຫຼື ຄລິກເພື່ອອັບໂຫຼດ',
imageUploadHelper:
'ບໍ່ຈຳກັດຮູບແບບໄຟລ໌. Docker ຈະກວດສອບເນື້ອຫາ. ໄຟລ໌ທີ່ອັບໂຫຼດຈະຖືກເກັບໄວ້ ເພື່ອໃຫ້ສາມາດລອງນຳເຂົ້າອີກຄັ້ງ ຫຼື ລຶບດ້ວຍຕົນເອງ.',
imageUploadEmpty: 'ໄຟລ໌ຫວ່າງເປົ່າ. ກະລຸນາເລືອກໄຟລ໌ອິມເມຈທີ່ຖືກຕ້ອງ.',
imageImportSubmitted: 'ສົ່ງວຽກນຳເຂົ້າອິມເມຈແລ້ວ. ກະລຸນາເບິ່ງບັນທຶກວຽກເພື່ອກວດສອບຜົນ.',
importImage: 'ນຳເຂົ້າ',
buildArgs: 'Build Arguments',
imageBuild: 'ສ້າງ (Build)',
@@ -4058,8 +4063,6 @@ const message = {
'ການເປີດໃຊ້ສິ່ງນີ້ສາມາດຫຼີກລ່ຽງການຮົ່ວໄຫຼຂອງໃບຢັ້ງຢືນ, ແຕ່ການຕັ້ງເວັບໄຊເລີ່ມຕົ້ນຈະເຮັດໃຫ້ການຕັ້ງຄ່ານີ້ບໍ່ມີຜົນ',
},
ssl: {
deleteConfirm: 'ຈະລຶບໃບຢັ້ງຢືນຕໍ່ໄປນີ້. ໃບຢັ້ງຢືນ ACME ຈະຖືກຖອນໂດຍອັດຕະໂນມັດ. ຕ້ອງການສືບຕໍ່ບໍ?',
deleteAndRevoke: 'ລຶບ ແລະ ຖອນ',
create: 'ຮ້ອງຂໍ',
provider: 'ປະເພດ',
manualCreate: 'ສ້າງດ້ວຍຕົນເອງ',
+5 -2
View File
@@ -1891,6 +1891,11 @@ const message = {
imageName: 'Nama imej',
pull: 'Tarik',
path: 'Laluan',
imageUploadDrop: 'Seret fail imej ke sini atau klik untuk memuat naik',
imageUploadHelper:
'Semua format fail dibenarkan. Docker mengesahkan kandungannya. Fail yang dimuat naik disimpan untuk mencuba semula import atau dipadam secara manual.',
imageUploadEmpty: 'Fail kosong. Pilih arkib imej yang sah.',
imageImportSubmitted: 'Tugas import imej telah dihantar. Semak log tugas untuk melihat hasilnya.',
importImage: 'Import',
buildArgs: 'Argumen Binaan',
imageBuild: 'Bina',
@@ -4240,8 +4245,6 @@ const message = {
'Mengaktifkan ini boleh mengelakkan kebocoran sijil, menetapkan laman web lalai akan membatalkan tetapan ini',
},
ssl: {
deleteConfirm: 'Sijil berikut akan dipadamkan. Sijil ACME akan dibatalkan secara automatik. Teruskan?',
deleteAndRevoke: 'Padam dan batalkan',
create: 'Permintaan',
provider: 'Jenis',
manualCreate: 'Dicipta secara manual',
+5 -3
View File
@@ -1893,6 +1893,11 @@ const message = {
imageName: 'Nome da imagem',
pull: 'Puxar',
path: 'Caminho',
imageUploadDrop: 'Arraste um arquivo de imagem para cá ou clique para enviar',
imageUploadHelper:
'Qualquer formato de arquivo é permitido. O Docker valida o conteúdo. Os arquivos enviados são mantidos para novas tentativas de importação ou exclusão manual.',
imageUploadEmpty: 'O arquivo está vazio. Selecione um arquivo de imagem válido.',
imageImportSubmitted: 'Tarefa de importação de imagem enviada. Consulte o log da tarefa para ver o resultado.',
importImage: 'Importar',
buildArgs: 'Argumentos de Build',
imageBuild: 'Construção de imagem',
@@ -4259,9 +4264,6 @@ const message = {
'Ativar isso pode evitar vazamento de certificados, definir um site padrão invalidará esta configuração',
},
ssl: {
deleteConfirm:
'Os certificados a seguir serão excluídos. Os certificados ACME serão revogados automaticamente. Continuar?',
deleteAndRevoke: 'Excluir e revogar',
create: 'Solicitar',
provider: 'Tipo',
manualCreate: 'Criado manualmente',
+5 -3
View File
@@ -1883,6 +1883,11 @@ const message = {
imageName: 'Имя образа',
pull: 'Загрузить',
path: 'Путь',
imageUploadDrop: 'Перетащите файл образа сюда или нажмите для загрузки',
imageUploadHelper:
'Допускаются файлы любого формата. Docker проверяет содержимое. Загруженные файлы сохраняются для повторного импорта или удаления вручную.',
imageUploadEmpty: 'Файл пуст. Выберите корректный архив образа.',
imageImportSubmitted: 'Задача импорта образа отправлена. Результат можно посмотреть в журнале задачи.',
importImage: 'Импорт',
buildArgs: 'Аргументы Сборки',
imageBuild: 'Сборка',
@@ -4226,9 +4231,6 @@ const message = {
'Включение этого может предотвратить утечку сертификатов, установка веб-сайта по умолчанию сделает эту настройку недействительной',
},
ssl: {
deleteConfirm:
'Следующие сертификаты будут удалены. Сертификаты ACME будут автоматически отозваны. Продолжить?',
deleteAndRevoke: 'Удалить и отозвать',
create: 'Запросить',
provider: 'Тип',
manualCreate: 'Создан вручную',
+5 -3
View File
@@ -1892,6 +1892,11 @@ const message = {
imageName: 'İmaj adı',
pull: 'Çek',
path: 'Yol',
imageUploadDrop: 'Bir imaj dosyasını buraya sürükleyin veya yüklemek için tıklayın',
imageUploadHelper:
'Tüm dosya biçimlerine izin verilir. Docker içeriği doğrular. Yüklenen dosyalar, içe aktarmayı yeniden denemek veya elle silmek için saklanır.',
imageUploadEmpty: 'Dosya boş. Geçerli bir imaj arşivi seçin.',
imageImportSubmitted: 'İmajı içe aktarma görevi gönderildi. Sonuç için görev günlüğünü kontrol edin.',
importImage: 'İçe aktar',
buildArgs: 'Derleme Argümanları',
imageBuild: 'Oluştur',
@@ -4244,9 +4249,6 @@ const message = {
'Etkinleştirilmesi sertifika sızıntısını önleyebilir, varsayılan bir web sitesi ayarlamak bu ayarı geçersiz kılar',
},
ssl: {
deleteConfirm:
'Aşağıdaki sertifikalar silinecek. ACME sertifikaları otomatik olarak iptal edilecek. Devam edilsin mi?',
deleteAndRevoke: 'Sil ve iptal et',
create: 'İstek',
provider: 'Tür',
manualCreate: 'Manuel olarak oluşturuldu',
+4 -2
View File
@@ -1781,6 +1781,10 @@ const message = {
delInsecureHelper: '刪除授信需要重新啟動 docker 服務,是否刪除?',
pull: '拉取',
path: '路徑',
imageUploadDrop: '拖曳映像檔案至此處,或點擊上傳',
imageUploadHelper: '不限制檔案格式,由 Docker 驗證內容。上傳的檔案會保留,可重試匯入或手動刪除。',
imageUploadEmpty: '不能上傳空檔案,請選擇有效的映像檔案。',
imageImportSubmitted: '映像匯入任務已提交,請查看任務日誌',
importImage: '匯入映像',
imageBuild: '構建映像',
buildArgs: '構建參數',
@@ -3898,8 +3902,6 @@ const message = {
sslRejectHandshakeHelper: '開啟之後可以避免憑證洩露,設置預設網站會讓此設置失效',
},
ssl: {
deleteConfirm: '將對以下憑證進行 刪除 操作,ACME 類型憑證會被自動撤銷,是否繼續?',
deleteAndRevoke: '刪除並撤銷',
create: '申請憑證',
provider: '類型',
manualCreate: '手動建立',
+4 -2
View File
@@ -1801,6 +1801,10 @@ const message = {
delInsecureHelper: '删除授信需要重启 docker 服务,是否删除?',
pull: '拉取',
path: '路径',
imageUploadDrop: '拖拽镜像文件到此处,或点击上传',
imageUploadHelper: '不限制文件格式,由 Docker 校验内容。上传的文件会保留,可重试导入或手动删除。',
imageUploadEmpty: '不能上传空文件,请选择有效的镜像文件。',
imageImportSubmitted: '镜像导入任务已提交,请查看任务日志',
importImage: '导入镜像',
imageBuild: '构建镜像',
buildArgs: '构建参数',
@@ -3943,8 +3947,6 @@ const message = {
sslRejectHandshakeHelper: '开启之后可以避免证书泄露,设置默认网站会让此设置失效',
},
ssl: {
deleteConfirm: '将对以下证书进行 删除 操作,ACME类型证书会被自动吊销,是否继续?',
deleteAndRevoke: '删除并吊销',
create: '申请证书',
provider: '类型',
manualCreate: '手动创建',
+6 -4
View File
@@ -1,4 +1,6 @@
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { bindSearchReset } from '@/utils/search-reset';
import router from '@/routers/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { defineStore } from 'pinia';
import { ref, reactive, toRef } from 'vue';
import { MsgError } from '@/utils/message';
@@ -47,12 +49,12 @@ export const ProcessStore = defineStore('ProcessStore', () => {
port: undefined,
});
useSearchPersistence('host/process/process', {
const { currentNode } = useGlobalStore();
bindSearchReset([() => router.currentRoute.value.path, currentNode], {
pid: toRef(psSearch, 'pid'),
name: toRef(psSearch, 'name'),
username: toRef(psSearch, 'username'),
});
useSearchPersistence('host/process/network', {
processID: toRef(netSearch, 'processID'),
processName: toRef(netSearch, 'processName'),
port: toRef(netSearch, 'port'),
+57
View File
@@ -0,0 +1,57 @@
import { chunkUploadFileData, type FileUploadRequestConfig } from '@/api/modules/files';
export const CHUNK_SIZE = 5 * 1024 * 1024;
const MAX_CHUNK_RETRIES = 3;
const shouldRetryChunkUpload = (error: unknown) => {
const item = error as {
code?: string | number;
data?: { retryable?: boolean };
response?: { status?: number };
};
if (item?.code === 'ERR_CANCELED') {
return false;
}
if (typeof item?.data?.retryable === 'boolean') {
return item.data.retryable;
}
const status = item?.response?.status ?? (typeof item?.code === 'number' ? item.code : undefined);
return !status || status === 408 || status === 429 || status >= 500;
};
const waitForChunkRetry = (attempt: number, signal: AbortSignal) => {
return new Promise<void>((resolve) => {
if (signal.aborted) {
resolve();
return;
}
const timer = window.setTimeout(done, 500 * 2 ** attempt);
function done() {
window.clearTimeout(timer);
signal.removeEventListener('abort', done);
resolve();
}
signal.addEventListener('abort', done, { once: true });
});
};
export const uploadChunkWithRetry = async (
formData: FormData,
config: FileUploadRequestConfig,
signal: AbortSignal,
) => {
let retryCount = 0;
while (true) {
signal.throwIfAborted();
try {
await chunkUploadFileData(formData, config);
return;
} catch (error) {
if (signal.aborted || retryCount >= MAX_CHUNK_RETRIES || !shouldRetryChunkUpload(error)) {
throw error;
}
await waitForChunkRetry(retryCount, signal);
retryCount++;
}
}
};
+12
View File
@@ -0,0 +1,12 @@
import { watch, type Ref, type WatchSource } from 'vue';
export type SearchFields = Record<string, Ref<string | number | undefined>>;
export const bindSearchReset = (sources: WatchSource[], fields: SearchFields) => {
const defaults = Object.values(fields).map((field) => ({ field, value: field.value }));
const reset = () => {
for (const { field, value } of defaults) field.value = value;
};
watch(sources, reset, { flush: 'sync' });
return reset;
};
@@ -172,7 +172,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
@@ -193,10 +193,16 @@ const searching = ref(false);
const operating = ref('');
const keyword = ref('');
const marketKeyword = ref('');
useSearchPersistence(
'ai/agents/agent/config/tabs/plugins',
useSearchReset(
{ keyword, marketKeyword },
() => agentId.value || undefined,
{
scope: () => agentId.value || undefined,
onRevisit: () => {
page.value = 1;
marketResults.value = [];
marketSearched.value = false;
},
},
);
const origin = ref('');
const status = ref('');
@@ -138,7 +138,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -169,10 +169,15 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchPersistence(
'ai/agents/agent/config/tabs/skills/hermes',
useSearchReset(
{ installedKeyword, marketKeyword },
() => agentId.value || undefined,
{
scope: () => agentId.value || undefined,
onRevisit: () => {
marketResults.value = [];
marketSearched.value = false;
},
},
);
const installedSkills = ref<AI.AgentSkillItem[]>([]);
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
@@ -142,7 +142,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -181,10 +181,15 @@ const getDefaultMarketSource = (): SkillMarketSource => {
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchPersistence(
'ai/agents/agent/config/tabs/skills/openclaw',
useSearchReset(
{ installedKeyword, marketKeyword },
() => agentId.value || undefined,
{
scope: () => agentId.value || undefined,
onRevisit: () => {
marketResults.value = [];
marketSearched.value = false;
},
},
);
const skills = ref<AI.AgentSkillItem[]>([]);
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
+3 -3
View File
@@ -272,8 +272,8 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai';
import { checkAppInstalled, installedOp, searchApp, searchAppInstalled } from '@/api/modules/app';
@@ -341,7 +341,6 @@ const isActive = ref(false);
const isExist = ref(false);
const noApp = ref(false);
const searchName = ref('');
useSearchPersistence('ai/agents/agent/index', { search: searchName });
const defaultHttpsPort = ref(443);
const openrestyPortLoaded = ref(false);
const websiteDomainsMap = ref<Record<number, Website.Domain[]>>({});
@@ -401,6 +400,7 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const checkNoApp = async () => {
try {
+3 -3
View File
@@ -69,8 +69,8 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { onMounted, reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import AddDialog from '@/views/ai/agents/model/add/index.vue';
@@ -86,7 +86,6 @@ const items = ref<AI.AgentAccountItem[]>([]);
const addRef = ref();
const modelPoolRef = ref();
const searchName = ref('');
useSearchPersistence('ai/agents/model/index', { search: searchName });
const apiType = ref('');
const apiTypeOptions = ref<string[]>([]);
@@ -112,6 +111,7 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
const req: AI.AgentAccountSearch = {
+3 -3
View File
@@ -186,7 +186,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import AppStatus from '@/components/app-status/index.vue';
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
import Conn from '@/views/ai/model/ollama/conn/index.vue';
@@ -195,7 +195,7 @@ import Terminal from '@/views/ai/model/ollama/terminal/index.vue';
import Del from '@/views/ai/model/ollama/del/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { App } from '@/api/interface/app';
import {
@@ -237,7 +237,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
useSearchPersistence('ai/model/ollama/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const appInstallID = ref(0);
const opRef = ref();
+5 -2
View File
@@ -70,7 +70,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { App } from '@/api/interface/app';
import { toRef, onMounted, reactive, ref } from 'vue';
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
@@ -105,7 +105,10 @@ const req = reactive({
resource: 'all',
showCurrentArch: false,
});
useSearchPersistence('app-store/apps/index', { search: toRef(req, 'name') });
useSearchReset(
{ search: toRef(req, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search(req) },
);
const apps = ref<App.AppItem[]>([]);
const loading = ref(false);
@@ -139,7 +139,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import AppCard from '@/views/app-store/installed/app/card.vue';
import Backups from '@/components/backup/index.vue';
import Uploads from '@/components/upload/index.vue';
@@ -211,7 +211,10 @@ const searchReq = reactive({
update: false,
sync: false,
});
useSearchPersistence('app-store/installed/index', { search: toRef(searchReq, 'name') });
useSearchReset(
{ search: toRef(searchReq, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const router = useRouter();
const activeName = ref(i18n.global.t('app.installed'));
const mode = ref('installed');
@@ -521,7 +521,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, h, ref } from 'vue';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import ContainerLog from '@/components/log/container/index.vue';
@@ -576,7 +576,7 @@ const dialogBackupRef = ref();
const uploadRef = ref();
const searchName = ref('');
useSearchPersistence('container/compose/index', { search: searchName });
useSearchReset({ search: searchName }, { onRevisit: () => search() });
const includeAppStore = ref(localStorage.getItem('includeAppStore') !== 'false');
const showType = ref('compose');
const containerStats = ref<any[]>([]);
@@ -474,7 +474,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import PruneDialog from '@/views/container/container/prune/index.vue';
import RenameDialog from '@/views/container/container/rename/index.vue';
import UpgradeDialog from '@/views/container/container/upgrade/index.vue';
@@ -490,7 +490,7 @@ import Uploads from '@/components/upload/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import Status from '@/components/status/index.vue';
import { computed, reactive, ref } from 'vue';
import { toRef, computed, reactive, ref } from 'vue';
import {
containerItemStats,
containerListStats,
@@ -536,7 +536,7 @@ const paginationConfig = reactive({
order: 'ascending',
});
const searchName = ref();
useSearchPersistence('container/container/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const dialogUpgradeRef = ref();
const dialogCommitRef = ref();
const dialogPortJumpRef = ref();
+5 -2
View File
@@ -157,7 +157,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, reactive, ref, computed } from 'vue';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
@@ -200,7 +200,10 @@ const paginationConfig = reactive({
orderBy: 'createdAt',
order: 'null',
});
useSearchPersistence('container/image/index', { search: toRef(paginationConfig, 'name') });
useSearchReset(
{ search: toRef(paginationConfig, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const columns = ref([]);
const isActive = ref(false);
+224 -75
View File
@@ -1,105 +1,254 @@
<template>
<DrawerPro v-model="loadVisible" :header="$t('container.importImage')" @close="handleClose" size="small">
<el-form @submit.prevent v-loading="loading" ref="formRef" :model="form" label-position="top">
<el-form-item :label="$t('container.path')" :rules="Rules.requiredInput" prop="path">
<el-input v-model="form.path" @input="handlePathChange">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: false, multiple: true })" />
</template>
</el-input>
<DrawerPro
v-model="loadVisible"
:header="$t('container.importImage')"
:auto-close="!busy"
:confirm-before-close="true"
@before-close="beforeClose"
size="small"
>
<el-form @submit.prevent label-position="top">
<div class="flex items-center gap-3 mb-5">
<el-button :disabled="busy" @click="source = 'local'">
{{ $t('database.localUpload') }}
</el-button>
<el-button :disabled="busy" @click="onServerSelect">
{{ $t('database.hostSelect') }}
</el-button>
</div>
<el-form-item v-if="source === 'server'" :label="$t('container.path')">
<el-input v-model="form.path" :disabled="busy" @input="handlePathChange" />
</el-form-item>
<template v-else>
<el-upload
ref="uploadRef"
drag
:limit="1"
:auto-upload="false"
:show-file-list="false"
:disabled="busy"
:on-change="onFileChange"
:on-exceed="onFileExceed"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">{{ $t('container.imageUploadDrop') }}</div>
</el-upload>
<p class="input-help">{{ $t('container.imageUploadHelper') }}</p>
<template v-if="localFile">
<p class="break-all">{{ localFile.name }} ({{ computeSize(localFile.size) }})</p>
<el-progress
:percentage="uploadPercent"
:status="uploadFailed ? 'exception' : uploadedPath ? 'success' : undefined"
/>
<p v-if="uploadedPath" class="input-help break-all">{{ uploadedPath }}</p>
<el-button v-if="uploadFailed" :disabled="busy" @click="uploadLocalFile">
{{ $t('commons.button.retry') }}
</el-button>
</template>
</template>
<el-button v-if="submittedTaskID" link type="primary" @click="openTaskLog">
{{ $t('commons.button.log') }}
</el-button>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="loadVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
:disabled="loading || !form.path.trim() || form.paths.length === 0"
type="primary"
@click="onSubmit(formRef)"
>
{{ $t('commons.button.import') }}
</el-button>
</span>
<el-button :disabled="busy" @click="loadVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
v-if="source === 'server'"
:disabled="busy || !!submittedTaskID || !form.paths.length"
:loading="loading"
type="primary"
@click="onSubmit"
>
{{ $t('commons.button.import') }}
</el-button>
</template>
</DrawerPro>
<FileList ref="fileRef" @choose="loadLoadDir" />
<TaskLog ref="taskLogRef" width="70%" />
<FileList v-if="loadVisible" ref="fileRef" @choose="loadLoadDir" />
<TaskLog ref="taskLogRef" width="70%" @close="emit('search')" />
</template>
<script lang="ts" setup>
import FileList from '@/components/file-list/index.vue';
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
import { genFileId, type UploadFile, type UploadInstance, type UploadProps, type UploadRawFile } from 'element-plus';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { imageLoad } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
import { stopChunkUpload } from '@/api/modules/files';
import { loadBaseDir } from '@/api/modules/setting';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { MsgError, MsgSuccess } from '@/utils/message';
import { computeSize } from '@/utils/size';
import { newUUID } from '@/utils/id';
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
const { currentNode } = useGlobalStore();
const loading = ref(false);
const uploading = ref(false);
const busy = computed(() => loading.value || uploading.value);
const fileRef = ref();
const taskLogRef = ref();
const uploadRef = ref<UploadInstance>();
const loadVisible = ref(false);
const form = reactive({
path: '',
paths: [] as string[],
taskID: '',
});
const acceptParams = () => {
loadVisible.value = true;
form.path = '';
form.paths = [];
};
const handleClose = () => {
loadVisible.value = false;
};
const source = ref<'server' | 'local'>('local');
const operateNode = ref('');
const form = reactive({ path: '', paths: [] as string[] });
const localFile = ref<UploadRawFile>();
const uploadPercent = ref(0);
const uploadFailed = ref(false);
const uploadedPath = ref('');
const submittedTaskID = ref('');
let uploadID = '';
let uploadController: AbortController | undefined;
let disposed = false;
const emit = defineEmits<{ (e: 'search'): void }>();
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl || !form.path.trim() || form.paths.length === 0) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
form.taskID = newUUID();
await imageLoad({ paths: form.paths, taskID: form.taskID })
.then(() => {
loading.value = false;
loadVisible.value = false;
openTaskLog(form.taskID);
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
const acceptParams = () => {
if (busy.value) return;
operateNode.value = currentNode.value;
source.value = 'local';
form.path = '';
form.paths = [];
localFile.value = undefined;
uploadedPath.value = '';
uploadPercent.value = 0;
uploadFailed.value = false;
submittedTaskID.value = '';
uploadRef.value?.clearFiles();
loadVisible.value = true;
};
const beforeClose = (done: () => void) => {
if (!busy.value) done();
};
const onServerSelect = () => {
if (busy.value) return;
source.value = 'server';
fileRef.value.acceptParams({ dir: false, multiple: true });
};
const onFileChange = (file: UploadFile) => {
if (!file.raw || busy.value) return;
if (file.raw.size === 0) {
MsgError(i18n.global.t('container.imageUploadEmpty'));
uploadRef.value?.clearFiles();
return;
}
localFile.value = file.raw;
uploadedPath.value = '';
submittedTaskID.value = '';
void uploadLocalFile();
};
const onFileExceed: UploadProps['onExceed'] = (files) => {
if (busy.value || !files.length) return;
uploadRef.value?.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadRef.value?.handleStart(file);
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
const uploadLocalFile = async () => {
const file = localFile.value;
if (!file || busy.value) return;
uploading.value = true;
uploadFailed.value = false;
uploadPercent.value = 0;
const node = operateNode.value;
const id = newUUID();
uploadID = id;
const controller = new AbortController();
uploadController = controller;
try {
const { data: baseDir } = await loadBaseDir(node);
controller.signal.throwIfAborted();
const directory = `${baseDir}/uploads/image/${id}`;
const count = Math.ceil(file.size / CHUNK_SIZE);
for (let index = 0; index < count; index++) {
const data = new FormData();
data.append('filename', file.name);
data.append('path', directory);
data.append('uploadID', id);
data.append('chunk', file.slice(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE));
data.append('chunkIndex', String(index));
data.append('chunkCount', String(count));
data.append('offset', String(index * CHUNK_SIZE));
data.append('fileSize', String(file.size));
await uploadChunkWithRetry(
data,
{
headers: { CurrentNode: node },
signal: controller.signal,
skipErrorMessage: true,
timeout: 0,
onUploadProgress: (event: { total?: number; loaded: number }) => {
const fraction = event.total ? Math.min(1, event.loaded / event.total) : 0;
uploadPercent.value = Math.min(99, Math.floor(((index + fraction) * 100) / count));
},
},
controller.signal,
);
}
controller.signal.throwIfAborted();
uploadedPath.value = `${directory}/${file.name}`;
uploadPercent.value = 100;
} catch {
if (!controller.signal.aborted) uploadFailed.value = true;
await stopChunkUpload(id, node).catch(() => {});
return;
} finally {
uploading.value = false;
uploadController = undefined;
uploadID = '';
}
if (!disposed && currentNode.value === node) await onSubmit();
};
const handlePathChange = () => {
if (!form.path.trim()) {
form.paths = [];
const openTaskLog = () => {
taskLogRef.value.openWithTaskID(submittedTaskID.value, true, operateNode.value);
};
const onSubmit = async () => {
const paths = source.value === 'local' ? [uploadedPath.value].filter(Boolean) : [...form.paths];
if (busy.value || submittedTaskID.value || !paths.length) return;
loading.value = true;
const taskID = newUUID();
const node = operateNode.value;
submittedTaskID.value = taskID;
try {
await imageLoad({ paths, taskID }, node);
if (disposed || currentNode.value !== node) return;
loadVisible.value = false;
openTaskLog();
MsgSuccess(i18n.global.t('container.imageImportSubmitted'));
} catch {
return;
} finally {
loading.value = false;
}
};
const loadLoadDir = async (paths: string | string[]) => {
const newPaths = Array.isArray(paths) ? paths : [paths];
form.paths = [...new Set(newPaths)];
const handlePathChange = () => {
submittedTaskID.value = '';
form.paths = [
...new Set(
form.path
.split(';')
.map((path) => path.trim())
.filter(Boolean),
),
];
};
const loadLoadDir = (paths: string | string[]) => {
if (currentNode.value !== operateNode.value) return;
source.value = 'server';
submittedTaskID.value = '';
form.paths = [...new Set(Array.isArray(paths) ? paths : [paths])];
form.path = form.paths.join('; ');
};
defineExpose({
acceptParams,
watch(currentNode, () => {
loadVisible.value = false;
uploadController?.abort();
});
onBeforeUnmount(() => {
disposed = true;
uploadController?.abort();
if (uploadID) void stopChunkUpload(uploadID, operateNode.value).catch(() => {});
});
defineExpose({ acceptParams });
</script>
@@ -95,10 +95,10 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import CreateDialog from '@/views/container/network/create/index.vue';
import DetailDrawer from '@/views/container/network/detail/index.vue';
import { reactive, ref } from 'vue';
import { toRef, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
import { deleteNetwork, searchNetwork, inspect, containerPrune } from '@/api/modules/container';
@@ -121,7 +121,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
useSearchPersistence('container/network/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
const isActive = ref(false);
+3 -3
View File
@@ -59,9 +59,9 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import OperatorDialog from '@/views/container/repo/operator/index.vue';
import { reactive, ref } from 'vue';
import { toRef, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
import { Container } from '@/api/interface/container';
import { checkRepoStatus, deleteImageRepo, searchImageRepo } from '@/api/modules/container';
@@ -78,7 +78,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
useSearchPersistence('container/repo/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
const confirmDialog = ref();
@@ -81,8 +81,8 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, reactive, ref } from 'vue';
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
import { downloadWithContent } from '@/utils/file';
import { Container } from '@/api/interface/container';
@@ -111,7 +111,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
useSearchPersistence('container/template/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
if (!isActive.value || !isExist.value) {
@@ -124,11 +124,11 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import CreateDialog from '@/views/container/volume/create/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import { reactive, ref } from 'vue';
import { toRef, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
import { deleteVolume, searchVolume, inspect, containerPrune } from '@/api/modules/container';
@@ -158,7 +158,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
useSearchPersistence('container/volume/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const isActive = ref(false);
const isExist = ref(false);
+3 -3
View File
@@ -272,11 +272,11 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import Records from '@/views/cronjob/cronjob/record/index.vue';
import Backups from '@/views/cronjob/cronjob/backup/index.vue';
import Import from '@/views/cronjob/cronjob/import/index.vue';
import { onMounted, ref, toRefs } from 'vue';
import { toRef, onMounted, ref, toRefs } from 'vue';
import {
deleteCronjob,
editCronjobGroup,
@@ -332,7 +332,7 @@ const pageState = usePageState(() => ({
}));
const paginationConfig = pageState.paginationConfig;
const { defaultGroupID, searchName, searchGroupID } = toRefs(pageState);
useSearchPersistence('cronjob/cronjob/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
if (column) {
+3 -3
View File
@@ -132,7 +132,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
import { deepCopy } from '@/utils/misc';
import { newUUID } from '@/utils/id';
@@ -141,7 +141,7 @@ import TaskLog from '@/components/log/task/index.vue';
import OperateDialog from '@/views/cronjob/library/operate/index.vue';
import TerminalDialog from '@/views/cronjob/library/run/index.vue';
import { deleteScript, searchScript, syncScript } from '@/api/modules/cronjob';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { Cronjob } from '@/api/interface/cronjob';
import i18n from '@/lang';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -170,7 +170,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchInfo = ref<string>('');
useSearchPersistence('cronjob/library/index', { search: searchInfo });
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const group = ref<string>('');
const groupOptions = ref();
@@ -323,8 +323,8 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { nextTick, onMounted, reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, nextTick, onMounted, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
import { getRandomStr } from '@/utils/id';
import { Position } from '@element-plus/icons-vue';
@@ -366,7 +366,6 @@ const loading = ref(false);
const maskShow = ref(true);
const submitLoading = ref(false);
const searchName = ref('');
useSearchPersistence('database/mongodb/index', { search: searchName });
const createVisible = ref(false);
const appStatusRef = ref();
const bindRef = ref();
@@ -400,6 +399,7 @@ const paginationConfig = reactive({
orderBy: 'createdAt',
order: 'null',
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@@ -75,9 +75,9 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/postgresql/check/index.vue';
import OperateDialog from '@/views/database/mongodb/remote/operate/index.vue';
@@ -101,7 +101,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/mongodb/remote/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
+3 -3
View File
@@ -226,7 +226,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import BindDialog from '@/views/database/mysql/bind/index.vue';
import UserDialog from '@/views/database/mysql/user/index.vue';
import OperateDialog from '@/views/database/mysql/create/index.vue';
@@ -241,7 +241,7 @@ import PortJumpDialog from '@/components/port-jump/index.vue';
import Tooltip from '@/components/tooltip/index.vue';
import { dateFormat } from '@/utils/date';
import { ElMessageBox } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import {
deleteCheckMysqlDB,
listDatabases,
@@ -301,7 +301,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/mysql/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const mysqlContainer = ref();
const mysqlStatus = ref();
@@ -74,9 +74,9 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/mysql/check/index.vue';
import OperateDialog from '@/views/database/mysql/remote/operate/index.vue';
@@ -100,7 +100,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/mysql/remote/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -241,7 +241,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import BindDialog from '@/views/database/postgresql/bind/index.vue';
import OperateDialog from '@/views/database/postgresql/create/index.vue';
import DeleteDialog from '@/views/database/postgresql/delete/index.vue';
@@ -256,7 +256,7 @@ import UploadDialog from '@/components/upload/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import Tooltip from '@/components/tooltip/index.vue';
import { dateFormat } from '@/utils/date';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import {
deleteCheckPostgresqlDB,
listDatabases,
@@ -313,7 +313,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/postgresql/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const postgresqlContainer = ref();
const postgresqlStatus = ref();
@@ -74,9 +74,9 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/postgresql/check/index.vue';
import OperateDialog from '@/views/database/postgresql/remote/operate/index.vue';
@@ -100,7 +100,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/postgresql/remote/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -73,9 +73,9 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/redis/check/index.vue';
import OperateDialog from '@/views/database/redis/remote/operate/index.vue';
@@ -99,7 +99,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('database/redis/remote/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -739,7 +739,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import {
addFavorite,
@@ -853,7 +853,6 @@ const initData = () => ({
sortOrder: 'ascending',
});
let req = reactive(initData());
useSearchPersistence('host/file-management/index', { search: toRef(req, 'search') });
let loading = ref(false);
const paths = ref<FilePaths[]>([]);
const breadcrumbVisiblePaths = ref<FilePaths[]>([]);
@@ -986,6 +985,10 @@ const paginationConfig = reactive({
pageSize: Number(localStorage.getItem('file-page-size')) || 100,
total: 0,
});
useSearchReset(
{ search: toRef(req, 'search'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const btnWrapperRefs = ref<Record<string, any>>({});
@@ -121,13 +121,8 @@
<script setup lang="ts">
import { computed, nextTick, reactive, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import {
batchCheckFiles,
chunkUploadFileData,
stopChunkUpload,
uploadFileData,
type FileUploadRequestConfig,
} from '@/api/modules/files';
import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files';
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
import i18n from '@/lang';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import { Close, Document, UploadFilled } from '@element-plus/icons-vue';
@@ -225,8 +220,6 @@ const uploaderFiles = ref<UploadFiles>([]);
const hoverIndex = ref<number | null>(null);
const tmpFiles = ref<UploadFiles>([]);
const breakFlag = ref(false);
const CHUNK_SIZE = 1024 * 1024 * 5;
const MAX_CHUNK_RETRIES = 3;
const upload = (command: string) => {
if (uploadLocked.value) {
@@ -504,54 +497,6 @@ const cleanupActiveChunkUpload = async () => {
}
};
const shouldRetryChunkUpload = (error: unknown) => {
const item = error as {
code?: string | number;
data?: { retryable?: boolean };
response?: { status?: number };
};
if (item?.code === 'ERR_CANCELED') {
return false;
}
if (typeof item?.data?.retryable === 'boolean') {
return item.data.retryable;
}
const status = item?.response?.status ?? (typeof item?.code === 'number' ? item.code : undefined);
return !status || status === 408 || status === 429 || status >= 500;
};
const waitForChunkRetry = (attempt: number, signal: AbortSignal) => {
return new Promise<void>((resolve) => {
if (signal.aborted) {
resolve();
return;
}
const timer = window.setTimeout(done, 500 * 2 ** attempt);
function done() {
window.clearTimeout(timer);
signal.removeEventListener('abort', done);
resolve();
}
signal.addEventListener('abort', done, { once: true });
});
};
const uploadChunkWithRetry = async (formData: FormData, config: FileUploadRequestConfig, signal: AbortSignal) => {
let retryCount = 0;
while (true) {
try {
await chunkUploadFileData(formData, config);
return;
} catch (error) {
if (signal.aborted || retryCount >= MAX_CHUNK_RETRIES || !shouldRetryChunkUpload(error)) {
throw error;
}
await waitForChunkRetry(retryCount, signal);
retryCount++;
}
}
};
const uploadChunkedFile = async (
file: { size: number; raw: Blob; name: string },
controller: AbortController,
@@ -219,7 +219,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, reactive, ref, watch } from 'vue';
import FireRouter from '@/views/host/firewall/index.vue';
import DockerGuardStatus from '@/views/host/firewall/docker/status/index.vue';
@@ -263,7 +263,14 @@ const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID
const orphanDrawerVisible = ref(false);
const orphanSearchName = ref('');
const searchName = ref('');
useSearchPersistence('host/firewall/docker/index', { search: searchName });
useSearchReset(
{ search: searchName },
{
onRevisit: () => {
paginationConfig.currentPage = 1;
},
},
);
const selects = ref<Firewall.DockerGuardContainer[]>([]);
const orphanSelects = ref<Firewall.DockerGuardEndpoint[]>([]);
const data = reactive<Firewall.DockerGuardList>({
@@ -117,14 +117,14 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import OperateDialog from './operate/index.vue';
import ImportDialog from './import/index.vue';
import FireRouter from '@/views/host/firewall/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import RuleReset from '@/views/host/firewall/components/rule-reset.vue';
import TaskLog from '@/components/log/task/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { resetFirewallRules, operateForwardRule, searchForwardRule } from '@/api/modules/firewall';
import { Firewall } from '@/api/interface/firewall';
import { buildForwardRuleExport } from './transfer';
@@ -138,7 +138,6 @@ import { ElMessageBox } from 'element-plus';
const loading = ref();
const selects = ref<any>([]);
const searchName = ref();
useSearchPersistence('host/firewall/forward/index', { search: searchName });
const isInit = ref(false);
const isBind = ref(false);
@@ -189,6 +188,7 @@ const paginationConfig = reactive({
pageSize: Number(localStorage.getItem('firewall-forward-page-size')) || 20,
total: 0,
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
if (!isInit.value || !isBind.value || fireName.value === '-') {
@@ -302,7 +302,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { Firewall } from '@/api/interface/firewall';
import type { FuTableOperationButton } from '@/components/table/shared';
import { buildHostRuleExport } from './transfer';
@@ -330,7 +330,7 @@ import RuleReset from '@/views/host/firewall/components/rule-reset.vue';
import TaskLog from '@/components/log/task/index.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
import { loadDockerStatus } from '@/api/modules/container';
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { toRef, computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useElementSize } from '@vueuse/core';
import { ElMessageBox } from 'element-plus';
import { Expand, Loading, Lock, Refresh } from '@element-plus/icons-vue';
@@ -405,7 +405,6 @@ const showFirewallUnavailablePrompt = computed(
const firewallVersion = ref('');
const selectedRuleFilters = ref<RuleFilter[]>(loadCachedFilterValues(ruleFilterStorageKey, ruleFilterOptions, []));
const searchName = ref('');
useSearchPersistence('host/firewall/rule/index', { search: searchName });
const inventoryItems = ref<Firewall.InventoryItem[]>([]);
const positionRanges = ref<Partial<Record<Firewall.Family, Firewall.PositionRange>>>({});
const inventoryTotal = ref(0);
@@ -433,6 +432,7 @@ const paginationConfig = reactive({
pageSize: Number(localStorage.getItem('firewall-rule-page-size')) || 20,
total: 0,
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const providerScopes = (): Firewall.Scope[] => {
if (provider.value === 'iptables' || provider.value === 'nftables') {
+3 -3
View File
@@ -93,11 +93,11 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { dateFormatServerTimezone } from '@/utils/date';
import { downloadFile } from '@/utils/file';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { cleanSSHLogs, exportSSHLogs, loadSSHLogs } from '@/api/modules/host';
import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
@@ -120,7 +120,7 @@ const exportConfig = reactive({
status: 'All',
});
const searchInfo = ref();
useSearchPersistence('host/ssh/log/log', { search: searchInfo });
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchStatus = ref('All');
const search = async () => {
@@ -24,7 +24,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import FireRouter from '@/views/host/ssh/index.vue';
import { toRef, ref, onMounted, onUnmounted, reactive } from 'vue';
import i18n from '@/lang';
@@ -38,7 +38,7 @@ const sshSearch = reactive({
type: 'ssh',
loginUser: '',
});
useSearchPersistence('host/ssh/session/index', { search: toRef(sshSearch, 'loginUser') });
useSearchReset({ search: toRef(sshSearch, 'loginUser') }, { onRevisit: () => search() });
const buttons = [
{
+6 -4
View File
@@ -101,7 +101,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { Log } from '@/api/interface/log';
import { getSystemLogStatus, listRunningServices, readSystemLogs } from '@/api/modules/log';
@@ -114,7 +114,7 @@ const logs = ref<Log.SystemLogItem[]>([]);
const loading = ref(false);
const watching = ref(false);
const keyword = ref('');
useSearchPersistence('log/host-system/index', { keyword });
useSearchReset({ keyword }, { onRevisit: () => scheduleFilterSearch() });
const priority = ref('');
const service = ref('');
const services = ref<string[]>([]);
@@ -274,11 +274,13 @@ onUnmounted(() => {
if (filterTimer) clearTimeout(filterTimer);
});
watch([keyword, priority, service], () => {
const scheduleFilterSearch = () => {
requestID++;
if (filterTimer) clearTimeout(filterTimer);
filterTimer = setTimeout(resetAndLoadLogs, 300);
});
};
watch([keyword, priority, service], scheduleFilterSearch);
</script>
<style scoped lang="scss">
+3 -3
View File
@@ -45,12 +45,12 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import LogRouter from '@/views/log/router/index.vue';
import { dateFormat } from '@/utils/date';
import { cleanLogs, getLoginLogs } from '@/api/modules/log';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -67,7 +67,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchInfo = ref<string>('');
useSearchPersistence('log/login/index', { search: searchInfo });
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchStatus = ref<string>('');
const search = async () => {
+3 -3
View File
@@ -125,12 +125,12 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import LogRouter from '@/views/log/router/index.vue';
import { dateFormat } from '@/utils/date';
import { cleanLogs, getOperationLogs } from '@/api/modules/log';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -146,7 +146,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref<string>('');
useSearchPersistence('log/operation/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchGroup = ref<string>('');
const searchStatus = ref<string>('');
const searchNode = ref<string>('');
@@ -120,7 +120,7 @@
</div>
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { toRef, onMounted, ref } from 'vue';
import { searchBackup, deleteBackup, refreshToken } from '@/api/modules/backup';
@@ -143,7 +143,10 @@ const paginationConfig = reactive({
type: '',
name: '',
});
useSearchPersistence('setting/backup-account/index', { search: toRef(paginationConfig, 'name') });
useSearchReset(
{ search: toRef(paginationConfig, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const opRef = ref();
const dialogRef = ref();
const detailRef = ref();
@@ -225,7 +225,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import {
searchSnapshotPage,
snapshotDelete,
@@ -233,7 +233,7 @@ import {
snapshotRollback,
updateSnapshotDescription,
} from '@/api/modules/setting';
import { onMounted, reactive, ref } from 'vue';
import { toRef, onMounted, reactive, ref } from 'vue';
import { computeSize } from '@/utils/size';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
@@ -261,7 +261,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('setting/snapshot/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
@@ -100,13 +100,13 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { Command } from '@/api/interface/command';
import GroupDialog from '@/components/group/index.vue';
import OperateDialog from '@/views/terminal/command/operate/index.vue';
import ImportDialog from '@/views/terminal/command/import/index.vue';
import { editCommand, deleteCommand, getCommandPage, exportCommands } from '@/api/modules/command';
import { reactive, ref } from 'vue';
import { toRef, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { getGroupList } from '@/api/modules/group';
@@ -124,7 +124,7 @@ const paginationConfig = reactive({
order: 'ascending',
});
const info = ref();
useSearchPersistence('terminal/command/index', { search: info });
useSearchReset({ search: info, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const group = ref<string>('');
const dialogRef = ref();
const opRef = ref();
+3 -3
View File
@@ -79,12 +79,12 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import GroupDialog from '@/components/agent-group/index.vue';
import OperateDialog from '@/views/terminal/host/operate/index.vue';
import { deleteHost, editHostGroup, searchHosts } from '@/api/modules/terminal';
import { getAgentGroupList } from '@/api/modules/group';
import { reactive, ref } from 'vue';
import { toRef, reactive, ref } from 'vue';
import i18n from '@/lang';
import { Host } from '@/api/interface/host';
import { MsgSuccess } from '@/utils/message';
@@ -100,7 +100,7 @@ const paginationConfig = reactive({
total: 0,
});
const info = ref();
useSearchPersistence('terminal/host/index', { search: info });
useSearchReset({ search: info, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const group = ref<string>('');
const opRef = ref();
+3 -3
View File
@@ -190,8 +190,8 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { onMounted, reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { deleteClam, handleClamScan, searchClam, updateClam, updateClamStatus } from '@/api/modules/toolbox';
@@ -218,7 +218,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('toolbox/clam/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
const dialogRef = ref();
+3 -3
View File
@@ -174,8 +174,8 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { onMounted, reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { deleteFtp, searchFtp, updateFtp, syncFtp, operateFtp, getFtpBase } from '@/api/modules/toolbox';
@@ -201,7 +201,7 @@ const paginationConfig = reactive({
order: 'null',
});
const searchName = ref();
useSearchPersistence('toolbox/ftp/index', { search: searchName });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const form = reactive({
isActive: true,
@@ -33,7 +33,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { Runtime } from '@/api/interface/runtime';
import { GetPHPExtensions, InstallPHPExtension, UnInstallPHPExtension } from '@/api/modules/runtime';
import i18n from '@/lang';
@@ -49,7 +49,7 @@ const supportExtensions = ref([]);
const loading = ref(false);
const taskLogRef = ref();
const searchName = ref('');
useSearchPersistence('website/runtime/php/extension-management/index', { search: searchName });
useSearchReset({ search: searchName }, { onRevisit: () => searchByName() });
const data = ref([]);
const handleClose = () => {
+11 -22
View File
@@ -221,7 +221,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { deleteSSL, downloadFile, pushSSLToNode, searchSSL, updateSSL } from '@/api/modules/website';
import DnsAccount from './dns-account/index.vue';
@@ -287,7 +287,10 @@ const req = reactive({
orderBy: 'updated_at',
order: 'descending',
});
useSearchPersistence('website/ssl/index', { search: toRef(req, 'domain') });
useSearchReset(
{ search: toRef(req, 'domain'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const pushForm = ref({
id: 0,
pushNode: true,
@@ -304,8 +307,6 @@ const routerButton = [
},
];
const shouldRevokeSSL = (row: Website.SSLDTO) => ['dnsAccount', 'dnsManual', 'http'].includes(row.provider);
const buttons = [
{
label: i18n.global.t('ssl.detail'),
@@ -381,15 +382,6 @@ const buttons = [
click: function (row: Website.SSLDTO) {
deletessl(row);
},
show: (row: Website.SSLDTO) => !shouldRevokeSSL(row),
},
{
label: i18n.global.t('ssl.deleteAndRevoke'),
permission: true,
click: function (row: Website.SSLDTO) {
deletessl(row);
},
show: shouldRevokeSSL,
},
];
@@ -543,13 +535,9 @@ const applySSL = (row: Website.SSLDTO) => {
};
const deletessl = async (row: any) => {
const isBatch = row == null;
const deleteLabel = i18n.global.t(
!isBatch && shouldRevokeSSL(row) ? 'ssl.deleteAndRevoke' : 'commons.button.delete',
);
let names = [];
let params = {};
if (isBatch) {
if (row == null) {
names = selects.value.map((item: Website.SSLDTO) => item.primaryDomain);
params = { ids: selects.value.map((item: Website.SSLDTO) => item.id) };
} else {
@@ -558,11 +546,12 @@ const deletessl = async (row: any) => {
}
opRef.value.acceptParams({
title: deleteLabel,
title: i18n.global.t('commons.button.delete'),
names: names,
msg: isBatch
? i18n.global.t('ssl.deleteConfirm')
: i18n.global.t('commons.msg.operatorHelper', [i18n.global.t('website.ssl'), deleteLabel]),
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('website.ssl'),
i18n.global.t('commons.button.delete'),
]),
api: deleteSSL,
params: params,
});
@@ -59,7 +59,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import { Website } from '@/api/interface/website';
import { deleteTemplate, searchTemplates } from '@/api/modules/website';
import TemplateOperate from '@/views/website/template/operate/index.vue';
@@ -67,12 +67,11 @@ import OutputCreate from '@/views/website/template/output/create.vue';
import OutputList from '@/views/website/template/output/index.vue';
import { dateFormat } from '@/utils/date';
import i18n from '@/lang';
import { reactive, ref, onMounted } from 'vue';
import { toRef, reactive, ref, onMounted } from 'vue';
const loading = ref(false);
const data = ref<Website.Template[]>([]);
const searchName = ref('');
useSearchPersistence('website/template/index', { search: searchName });
const opRef = ref();
const operateRef = ref();
const outputCreateRef = ref();
@@ -91,6 +90,7 @@ const paginationConfig = reactive({
pageSize: 20,
total: 0,
});
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const buttons = [
{
+5 -2
View File
@@ -346,7 +346,7 @@
</template>
<script lang="ts" setup>
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { useSearchReset } from '@/composables/useSearchReset';
import Backups from '@/components/backup/index.vue';
import UploadDialog from '@/components/upload/index.vue';
import DefaultServer from '@/views/website/website/default/index.vue';
@@ -451,7 +451,10 @@ const pageState = usePageState(() => ({
}));
const paginationConfig = pageState.paginationConfig;
const req = pageState.req;
useSearchPersistence('website/website/index', { search: toRef(req, 'name') });
useSearchReset(
{ search: toRef(req, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const tableSort = pageState.tableSort;
const goRouter = async (key: string) => {