fix: loading progress indicators, fixed login layout

This commit is contained in:
Gareth
2026-01-15 00:47:34 -08:00
parent 3850b8fed7
commit 90e1401cbd
20 changed files with 116 additions and 38 deletions
+2 -1
View File
@@ -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": "لوحة التحكم"
}
+2 -1
View File
@@ -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": "ড্যাশবোর্ড"
}
+2 -1
View File
@@ -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"
}
+1
View File
@@ -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: ",
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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": "डैशबोर्ड"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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": "Панель управления"
}
+2 -1
View File
@@ -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": "仪表板"
}
+16
View File
@@ -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" }}>
+2 -2
View File
@@ -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>
);
+26 -4
View File
@@ -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>
+2 -2
View File
@@ -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>
)}
+8 -3
View File
@@ -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 -1
View File
@@ -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