mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-05-04 12:00:36 +00:00
fix: loading progress indicators, fixed login layout
This commit is contained in:
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "ستؤدي خاصية الفتح التلقائي إلى إزالة ملفات القفل عند بدء معظم العمليات. قد يكون هذا غير آمن إذا تمت مشاركة المستودع بين عدة أجهزة عميلة، وإلا يُنصح باستخدامه في إعدادات العميل الواحد.",
|
||||
"op_row_cancel_op": "إلغاء العملية",
|
||||
"op_row_confirm_cancel": "تأكيد إلغاء؟"
|
||||
"op_row_confirm_cancel": "تأكيد إلغاء؟",
|
||||
"app_menu_dashboard": "لوحة التحكم"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "অ্যাপ_মেনু_সম্পাদনা_পরিকল্পনা",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "বেশিরভাগ অপারেশনের শুরুতেই অটো-আনলক লকফাইলগুলি সরিয়ে ফেলবে। একাধিক ক্লায়েন্ট ডিভাইস দ্বারা রেপো শেয়ার করা হলে এটি সম্ভাব্যভাবে অনিরাপদ, অন্যথায় একক-ক্লায়েন্ট সেটআপের জন্য সুপারিশ করা হয়।",
|
||||
"op_row_cancel_op": "অপারেশন বাতিল করুন",
|
||||
"op_row_confirm_cancel": "বাতিল নিশ্চিত করবেন?"
|
||||
"op_row_confirm_cancel": "বাতিল নিশ্চিত করবেন?",
|
||||
"app_menu_dashboard": "ড্যাশবোর্ড"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Die automatische Entsperrung entfernt Sperrdateien zu Beginn der meisten Operationen. Dies kann unsicher sein, wenn das Repository von mehreren Clientgeräten gemeinsam genutzt wird; ansonsten wird diese Option für Einzelclient-Setups empfohlen.",
|
||||
"op_row_cancel_op": "Vorgang abbrechen",
|
||||
"op_row_confirm_cancel": "Abbrechen bestätigen?"
|
||||
"op_row_confirm_cancel": "Abbrechen bestätigen?",
|
||||
"app_menu_dashboard": "Armaturenbrett"
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"app_menu_add_repo": "Add Repo",
|
||||
"app_menu_remote_instances": "Remote Instances",
|
||||
"app_menu_settings": "Settings",
|
||||
"app_menu_dashboard": "Dashboard",
|
||||
"app_error_initial_config": "Failed to fetch initial config, typically this means the UI could not connect to the backend",
|
||||
"login_success": "Logged in",
|
||||
"login_error": "Login failed: ",
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "menú_aplicación_editar_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "El desbloqueo automático eliminará los archivos de bloqueo al inicio de la mayoría de las operaciones. Esto puede ser peligroso si el repositorio se comparte entre varios dispositivos cliente; de lo contrario, se recomienda para configuraciones de un solo cliente.",
|
||||
"op_row_cancel_op": "Cancelar operación",
|
||||
"op_row_confirm_cancel": "¿Confirmar Cancelar?"
|
||||
"op_row_confirm_cancel": "¿Confirmar Cancelar?",
|
||||
"app_menu_dashboard": "Panel"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Le déverrouillage automatique supprime les fichiers de verrouillage au début de la plupart des opérations. Cette opération peut s'avérer dangereuse si le dépôt est partagé par plusieurs appareils clients ; elle est par ailleurs recommandée pour les configurations mono-client.",
|
||||
"op_row_cancel_op": "Opération annulée",
|
||||
"op_row_confirm_cancel": "Confirmer Annuler ?"
|
||||
"op_row_confirm_cancel": "Confirmer Annuler ?",
|
||||
"app_menu_dashboard": "Tableau de bord"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "ऐप_मेनू_एडिट_प्लान",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "अधिकांश कार्यों की शुरुआत में ऑटो-अनलॉक लॉकफाइलों को हटा देगा। यदि रिपॉजिटरी को कई क्लाइंट डिवाइसों द्वारा साझा किया जाता है तो यह संभावित रूप से असुरक्षित हो सकता है, अन्यथा एकल-क्लाइंट सेटअप के लिए इसकी अनुशंसा की जाती है।",
|
||||
"op_row_cancel_op": "ऑपरेशन रद्द करें",
|
||||
"op_row_confirm_cancel": "पुष्टि करें/रद्द करें?"
|
||||
"op_row_confirm_cancel": "पुष्टि करें/रद्द करें?",
|
||||
"app_menu_dashboard": "डैशबोर्ड"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Fitur buka kunci otomatis akan menghapus file kunci di awal sebagian besar operasi. Ini berpotensi tidak aman jika repositori digunakan bersama oleh beberapa perangkat klien, jika tidak, fitur ini direkomendasikan untuk pengaturan klien tunggal.",
|
||||
"op_row_cancel_op": "Batalkan Operasi",
|
||||
"op_row_confirm_cancel": "Konfirmasi Batal?"
|
||||
"op_row_confirm_cancel": "Konfirmasi Batal?",
|
||||
"app_menu_dashboard": "Dasbor"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Lo sblocco automatico rimuoverà i file di blocco all'inizio della maggior parte delle operazioni. Questa opzione è potenzialmente pericolosa se il repository è condiviso da più dispositivi client, altrimenti è consigliata per configurazioni con un solo client.",
|
||||
"op_row_cancel_op": "Annulla operazione",
|
||||
"op_row_confirm_cancel": "Confermare l'annullamento?"
|
||||
"op_row_confirm_cancel": "Confermare l'annullamento?",
|
||||
"app_menu_dashboard": "Pannello di controllo"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_editar_plano",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "O desbloqueio automático removerá os arquivos de bloqueio no início da maioria das operações. Isso pode ser inseguro se o repositório for compartilhado por vários dispositivos cliente; caso contrário, é recomendado para configurações com um único cliente.",
|
||||
"op_row_cancel_op": "Cancelar operação",
|
||||
"op_row_confirm_cancel": "Confirmar Cancelar?"
|
||||
"op_row_confirm_cancel": "Confirmar Cancelar?",
|
||||
"app_menu_dashboard": "Painel"
|
||||
}
|
||||
|
||||
@@ -363,5 +363,6 @@
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Автоматическая разблокировка удаляет файлы блокировки в начале большинства операций. Это потенциально небезопасно, если репозиторий используется несколькими клиентскими устройствами, в противном случае рекомендуется для конфигураций с одним клиентом.",
|
||||
"op_row_cancel_op": "Отменить операцию",
|
||||
"op_row_confirm_cancel": "Подтвердить отмену?"
|
||||
"op_row_confirm_cancel": "Подтвердить отмену?",
|
||||
"app_menu_dashboard": "Панель управления"
|
||||
}
|
||||
|
||||
@@ -365,5 +365,6 @@
|
||||
"op_row_cancel_op": "取消操作",
|
||||
"op_row_confirm_cancel": "确认取消?",
|
||||
"op_row_delete": "删除操作",
|
||||
"op_row_confirm_delete": "确认删除?"
|
||||
"op_row_confirm_delete": "确认删除?",
|
||||
"app_menu_dashboard": "仪表板"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FiServer,
|
||||
FiEdit2,
|
||||
FiMenu,
|
||||
FiHome,
|
||||
} from "react-icons/fi";
|
||||
|
||||
import {
|
||||
@@ -237,6 +238,21 @@ const SidebarContent = ({ onClose }: { onClose?: () => void }) => {
|
||||
defaultValue={["plans", "repos", "authorized-clients"]}
|
||||
variant="plain"
|
||||
>
|
||||
{/* DASHBOARD */}
|
||||
<Box
|
||||
cursor="pointer"
|
||||
onClick={() => handleNav("/")}
|
||||
px={4}
|
||||
py={2}
|
||||
_hover={{ bg: "bg.muted" }}
|
||||
userSelect="none"
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiHome />
|
||||
<Text fontWeight="medium">{m.app_menu_dashboard()}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* PLANS SECTION */}
|
||||
<AccordionItem value="plans">
|
||||
<AccordionItemTrigger px={4} py={2} _hover={{ bg: "bg.muted" }}>
|
||||
|
||||
@@ -283,9 +283,9 @@ export const DynamicList = ({
|
||||
</Stack>
|
||||
</DndContext>
|
||||
{tooltip && (
|
||||
<CText fontSize="xs" color="fg.muted">
|
||||
<Box fontSize="xs" color="fg.muted">
|
||||
{tooltip}
|
||||
</CText>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,20 @@ interface FormModalProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
size?: "default" | "large";
|
||||
size?:
|
||||
| "xs"
|
||||
| "sm"
|
||||
| "md"
|
||||
| "lg"
|
||||
| "xl"
|
||||
| "2xl"
|
||||
| "3xl"
|
||||
| "4xl"
|
||||
| "5xl"
|
||||
| "6xl"
|
||||
| "full"
|
||||
| "default"
|
||||
| "large";
|
||||
}
|
||||
|
||||
export const FormModal: React.FC<FormModalProps> = ({
|
||||
@@ -33,19 +46,28 @@ export const FormModal: React.FC<FormModalProps> = ({
|
||||
}) => {
|
||||
// Map size "default" to "md" and "large" to "xl" or "2xl"
|
||||
// Chakra default sizes are xs, sm, md, lg, xl, 2xl, etc.
|
||||
const chakraSize = size === "large" ? "xl" : "md";
|
||||
let chakraSize = size;
|
||||
if (size === "default") chakraSize = "md";
|
||||
if (size === "large") chakraSize = "xl";
|
||||
|
||||
// Identify if the requested size is supported by the DialogRoot component directly
|
||||
// definition: "xs" | "sm" | "md" | "lg" | "xl" | "full" | "cover" | undefined
|
||||
const validRootSizes = ["xs", "sm", "md", "lg", "xl", "full", "cover"];
|
||||
const isRootSize = validRootSizes.includes(chakraSize);
|
||||
const rootSize = isRootSize ? (chakraSize as any) : undefined;
|
||||
const contentMaxW = !isRootSize ? chakraSize : undefined;
|
||||
|
||||
return (
|
||||
<DialogRoot
|
||||
open={isOpen}
|
||||
onOpenChange={(e: { open: boolean }) => !e.open && onClose()}
|
||||
size={chakraSize}
|
||||
size={rootSize}
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<Portal>
|
||||
<DialogBackdrop />
|
||||
<DialogPositioner>
|
||||
<DialogContent>
|
||||
<DialogContent maxW={contentMaxW}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -98,8 +98,8 @@ export const LogView = ({ logref }: { logref: string }) => {
|
||||
))}
|
||||
|
||||
{loading && lines.length === 0 && (
|
||||
<Center py={4}>
|
||||
<Spinner size="sm" />
|
||||
<Center h="100%" minH="100px">
|
||||
<Spinner color="fg.muted" />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export const LoginModal = () => {
|
||||
isOpen={true}
|
||||
onClose={() => {}} // Non-closable
|
||||
title={m.login_title()}
|
||||
size="2xl"
|
||||
footer={
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -63,13 +64,14 @@ export const LoginModal = () => {
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" gap={4}>
|
||||
<Field
|
||||
flex={1}
|
||||
label="Username"
|
||||
required
|
||||
errorText={!username ? m.login_username_required() : undefined}
|
||||
>
|
||||
<InputGroup startElement={<LuUser />}>
|
||||
<InputGroup width="100%" startElement={<LuUser />}>
|
||||
<Input
|
||||
placeholder={m.login_username_placeholder()}
|
||||
value={username}
|
||||
@@ -79,11 +81,12 @@ export const LoginModal = () => {
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
flex={1}
|
||||
label="Password"
|
||||
required
|
||||
errorText={!password ? m.login_password_required() : undefined}
|
||||
>
|
||||
<InputGroup startElement={<LuLock />}>
|
||||
<InputGroup width="100%" startElement={<LuLock />}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={m.login_password_placeholder()}
|
||||
@@ -93,6 +96,8 @@ export const LoginModal = () => {
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</Stack>
|
||||
{/* Allows submitting the form by pressing enter in one of the fields */}
|
||||
<button type="submit" style={{ display: "none" }} />
|
||||
</form>
|
||||
</FormModal>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { OperationRow } from "./OperationRow";
|
||||
import { OplogState, syncStateFromRequest } from "../../api/logState";
|
||||
import { shouldHideStatus } from "../../api/oplog";
|
||||
import { toJsonString } from "@bufbuild/protobuf";
|
||||
import { Stack, Box, Flex } from "@chakra-ui/react";
|
||||
import { Stack, Box, Flex, Center, Spinner } from "@chakra-ui/react";
|
||||
import { EmptyState } from "../../components/ui/empty-state";
|
||||
import { FiList } from "react-icons/fi";
|
||||
import {
|
||||
@@ -37,25 +37,28 @@ export const OperationListView = ({
|
||||
showDelete?: boolean; // allows deleting individual operation rows, useful for the list view in the plan / repo panels.
|
||||
}>) => {
|
||||
const [operations, setOperations] = useState<Operation[]>([]);
|
||||
const [loading, setLoading] = useState(!!req);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 25;
|
||||
|
||||
if (req) {
|
||||
useEffect(() => {
|
||||
const logState = new OplogState(
|
||||
(op) => !shouldHideStatus(op.status) && (!filter || filter(op)),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!req) return;
|
||||
setLoading(true);
|
||||
const logState = new OplogState(
|
||||
(op) => !shouldHideStatus(op.status) && (!filter || filter(op)),
|
||||
);
|
||||
|
||||
logState.subscribe((ids, flowIDs, event) => {
|
||||
const ops = logState.getAll();
|
||||
setOperations(ops);
|
||||
});
|
||||
logState.subscribe((ids, flowIDs, event) => {
|
||||
const ops = logState.getAll();
|
||||
setOperations(ops);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return syncStateFromRequest(logState, req, (e) => {
|
||||
alerts.error("Failed to fetch operations: " + e.message);
|
||||
});
|
||||
}, [toJsonString(GetOperationsRequestSchema, req)]);
|
||||
}
|
||||
return syncStateFromRequest(logState, req, (e) => {
|
||||
alerts.error("Failed to fetch operations: " + e.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [req ? toJsonString(GetOperationsRequestSchema, req) : ""]);
|
||||
|
||||
const hookExecutionsForOperation: Map<bigint, Operation[]> = new Map();
|
||||
let operationsForDisplay: Operation[] = [];
|
||||
@@ -87,6 +90,13 @@ export const OperationListView = ({
|
||||
});
|
||||
|
||||
if (!operationsForDisplay || operationsForDisplay.length === 0) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Center py={8}>
|
||||
<Spinner />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EmptyState
|
||||
title="No operations yet"
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
EmptyState,
|
||||
VStack,
|
||||
TreeCollection,
|
||||
Center,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
TreeViewRoot,
|
||||
@@ -92,6 +94,7 @@ export const OperationTreeView = ({
|
||||
const setScreenWidth = useState(window.innerWidth)[1];
|
||||
const [backups, setBackups] = useState<FlowDisplayInfo[]>([]);
|
||||
const [selectedBackupId, setSelectedBackupId] = useState<bigint | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// track the screen width so we can switch between mobile and desktop layouts.
|
||||
useEffect(() => {
|
||||
@@ -137,13 +140,23 @@ export const OperationTreeView = ({
|
||||
}
|
||||
|
||||
setBackups([...backupInfoByFlowID.values()]);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return syncStateFromRequest(logState, req, (err) => {
|
||||
alerts.error("API error: " + err.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [toJsonString(GetOperationsRequestSchema, req)]);
|
||||
|
||||
if (loading && backups.length === 0) {
|
||||
return (
|
||||
<Center height="100%">
|
||||
<Spinner size="lg" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (backups.length === 0) {
|
||||
return (
|
||||
<EmptyState.Root>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const isMobile = () => {
|
||||
// check if window is narrow
|
||||
if (window.innerWidth <= 1024) {
|
||||
if (window.innerWidth <= 768) {
|
||||
return true;
|
||||
}
|
||||
// check if user agent is mobile
|
||||
|
||||
Reference in New Issue
Block a user