mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-26 09:55:38 +00:00
fix: loading progress indicators, fixed login layout
This commit is contained in:
@@ -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