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
+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