Added LDAP config and auth
This commit is contained in:
+123
-80
@@ -1,91 +1,134 @@
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
Stack,
|
||||
useRecipe,
|
||||
Center,
|
||||
Text,
|
||||
Flex,
|
||||
Heading,
|
||||
Highlight,
|
||||
GridItem,
|
||||
Grid
|
||||
} from "@chakra-ui/react"
|
||||
import { PasswordInput } from "@/components/ui/password-input.jsx";
|
||||
Box, Heading, Stack, Input, Button, Text, Separator, Field, Link, Flex
|
||||
} from '@chakra-ui/react';
|
||||
import useAuthStore from '../../store/authStore';
|
||||
import { toaster } from "../../components/ui/toaster.jsx"; // Ensure this path matches your project
|
||||
|
||||
export default function Login( signedIn, setSignedIn) {
|
||||
const recipe = useRecipe({ key: "dashboard" }); // styling recipes option
|
||||
const styles = recipe(); // use style
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitSuccessful },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const { login, isAuthenticated, isLoading, error } = useAuthStore();
|
||||
|
||||
// --- VDAP COLOR STYLES ---
|
||||
const fieldStyles = {
|
||||
borderRadius: "3xl",
|
||||
bg: "gray.100",
|
||||
borderColor: "gray.100",
|
||||
px: 4,
|
||||
_focus: {
|
||||
bg: "white",
|
||||
borderColor: "blue.400",
|
||||
boxShadow: "0 0 0 1px #4299E1",
|
||||
outline: "none"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log(data);
|
||||
// setUser(data.username);
|
||||
const buttonStyles = {
|
||||
borderRadius: "3xl",
|
||||
bg: "blue.600",
|
||||
color: "white",
|
||||
_hover: { bg: 'blue.700', transform: 'translateY(-1px)' },
|
||||
_active: { bg: 'blue.800', transform: 'translateY(0)' },
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
// The Redirect Watcher
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/home', { replace: true });
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
// The Error Watcher
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toaster.create({
|
||||
title: "Login Failed",
|
||||
description: error,
|
||||
type: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (username && password) {
|
||||
await login(username, password);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex css={styles} width="100%" height="100%">
|
||||
<Grid
|
||||
templateRows="1fr 11fr"
|
||||
height="100vh"
|
||||
width="100vw"
|
||||
>
|
||||
<GridItem rowSpan={1} >
|
||||
<Flex width="100%" height="100%" align="center" >
|
||||
<Heading size="4xl" fontWeight="bold" pl="1rem" cursor="default" >
|
||||
<Highlight query="A" styles={{ color: 'red.600'}}>
|
||||
VDAP
|
||||
</Highlight>
|
||||
</Heading>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
<GridItem rowSpan={11} >
|
||||
<Flex width="100%" height="100%" justify="center" align="center" direction="column" >
|
||||
<Center background="white" width="40%" height="50%" borderRadius="1rem">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="4" align="center" maxW="sm">
|
||||
<Text textStyle="2xl" fontWeight="bold" cursor="default">Sign In</Text>
|
||||
<Field.Root invalid={!!errors.username}>
|
||||
<Input
|
||||
placeholder="username"
|
||||
{...register("username", {required: "Required"})}
|
||||
/>
|
||||
<Field.ErrorText>{errors.username?.message}</Field.ErrorText>
|
||||
</Field.Root>
|
||||
<Flex p={8} bg="gray.50" _dark={{ bg: "gray.900" }} flex="1" minH="100vh" align="center" justify="center">
|
||||
<Box w="full" maxW="450px" bg="white" _dark={{ bg: "gray.800", borderColor: "gray.700" }} p={8} borderRadius="2xl" boxShadow="sm" borderWidth="1px" borderColor="gray.200">
|
||||
<Stack gap={6} align="stretch" as="form" onSubmit={handleSubmit}>
|
||||
|
||||
<Field.Root invalid={!!errors.password}>
|
||||
<PasswordInput
|
||||
placeholder="password"
|
||||
{...register("password", {required: "Required"})}
|
||||
/>
|
||||
<Field.ErrorText>{errors.password?.message}</Field.ErrorText>
|
||||
</Field.Root>
|
||||
<Stack gap={2} textAlign="center">
|
||||
<Heading as="h1" size="xl" color="gray.800" _dark={{ color: "white" }}>
|
||||
VDAP System Login
|
||||
</Heading>
|
||||
<Text color="gray.500">Sign in with your LDAP credentials.</Text>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitSuccessful}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Center>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Flex>
|
||||
<Separator />
|
||||
|
||||
<Field.Root required>
|
||||
<Field.Label ml={1} fontSize="sm" fontWeight="medium" color="gray.700" _dark={{ color: "gray.300" }}>
|
||||
Username
|
||||
</Field.Label>
|
||||
<Input
|
||||
name="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="jdoe"
|
||||
{...fieldStyles}
|
||||
/>
|
||||
</Field.Root>
|
||||
|
||||
<Field.Root required>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" w="full" mb={1} ml={1}>
|
||||
<Field.Label mb={0} fontSize="sm" fontWeight="medium" color="gray.700" _dark={{ color: "gray.300" }}>
|
||||
Password
|
||||
</Field.Label>
|
||||
</Box>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
{...fieldStyles}
|
||||
/>
|
||||
</Field.Root>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
loadingText="Authenticating..."
|
||||
size="lg"
|
||||
mt={2}
|
||||
w="full"
|
||||
{...buttonStyles}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<Text textAlign="center" fontSize="sm" color="gray.500">
|
||||
Need access?{' '}
|
||||
<Link href="mailto:it-chight@usgs.gov" color="blue.600" _dark={{ color: "blue.400" }} fontWeight="medium">
|
||||
Contact IT Administration
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,94 +1,146 @@
|
||||
import { Box, Text, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { Box, Text, Flex, IconButton, Spinner } from "@chakra-ui/react";
|
||||
import { X } from "lucide-react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
|
||||
import { Suspense } from 'react';
|
||||
|
||||
// 2. Import registry!
|
||||
import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
|
||||
|
||||
// React-Grid-Layout requires this wrapper to automatically calculate screen width
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
export default function MyDashboard() {
|
||||
// 1. ☁️ Connect to the Cloud (Zustand)
|
||||
const widgets = useDashboardStore(state => state.widgets);
|
||||
const isEditing = useDashboardStore(state => state.isEditing);
|
||||
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||
const removeWidget = useDashboardStore(state => state.removeWidget);
|
||||
// 1. ☁️ Connect to the Cloud (Zustand)
|
||||
const widgets = useDashboardStore(state => state.widgets);
|
||||
const isEditing = useDashboardStore(state => state.isEditing);
|
||||
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||
const removeWidget = useDashboardStore(state => state.removeWidget);
|
||||
|
||||
// 2. 🛠️ Format the data for RGL
|
||||
// RGL expects a very specific array format for its layout prop
|
||||
const rglLayout = widgets.map(w => ({
|
||||
i: w.id,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
||||
}));
|
||||
// 2. 🛠️ Format the data for RGL
|
||||
// RGL expects a very specific array format for its layout prop
|
||||
const rglLayout = widgets.map(w => ({
|
||||
i: w.id,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
||||
}));
|
||||
|
||||
// 3. THE COMPONENT MAPPER
|
||||
const renderWidget = (widget) => {
|
||||
const registryEntry = WIDGET_REGISTRY[widget.type];
|
||||
|
||||
if (registryEntry && registryEntry.component) {
|
||||
const Component = registryEntry.component;
|
||||
|
||||
return (
|
||||
// The "Frame" that holds both the Title and the Chart
|
||||
<Flex direction="column" height="100%" width="100%" bg="gray.800">
|
||||
|
||||
{/* 1. THE TITLE BAR (Dynamically pulled from registry!) */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
px={3}
|
||||
py={2}
|
||||
// borderBottom="1px solid"
|
||||
// borderColor="gray.700"
|
||||
>
|
||||
<Text color="gray.100" fontWeight="semibold" fontSize="md" isTruncated>
|
||||
{registryEntry.name}
|
||||
</Text>
|
||||
|
||||
{/* (Optional) This is exactly where the ellipsis/settings icon will go later! */}
|
||||
<Box w="20px" h="20px" />
|
||||
</Flex>
|
||||
|
||||
{/* 2. THE CHART CANVAS */}
|
||||
<Box flex="1" overflow="hidden" position="relative" p={1}>
|
||||
<Component />
|
||||
</Box>
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%">
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={{ lg: rglLayout }} // We feed it the formatted layout map
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||
rowHeight={30}
|
||||
isDraggable={isEditing} // Only allows dragging when your Settings tools toggle this
|
||||
isResizable={false} // Double lock-down
|
||||
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||
compactType="vertical" // Auto-packs widgets to the top
|
||||
>
|
||||
{/* 3. 🎨 Paint the Widgets */}
|
||||
{widgets.map(widget => (
|
||||
<Box
|
||||
key={widget.id}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
boxShadow={isEditing ? "outline" : "sm"} // Visual cue when editing
|
||||
border={isEditing ? "2px dashed gray" : "1px solid"}
|
||||
borderColor={isEditing ? "gray.400" : "gray.200"}
|
||||
overflow="hidden"
|
||||
cursor={isEditing ? "grab" : "default"}
|
||||
position="relative" // 3. REQUIRED so the absolute button stays inside this box
|
||||
>
|
||||
{/* 4. THE DELETE BUTTON */}
|
||||
{isEditing && (
|
||||
<IconButton
|
||||
aria-label="Remove widget"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
size="xs"
|
||||
bg="white"
|
||||
color="gray.400"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }}
|
||||
zIndex={2}
|
||||
onClick={() => removeWidget(widget.id)}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
|
||||
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
{/* 🚧 Temporary Placeholder!
|
||||
Eventually, we will swap this block out for a dynamic component loader
|
||||
that looks at widget.type and loads the correct graph or table.
|
||||
*/}
|
||||
<Flex width="100%" height="100%" align="center" justify="center" direction="column">
|
||||
<Text fontWeight="bold" color="gray.600">{widget.type}</Text>
|
||||
<Text fontSize="sm" color="gray.400">Size: {widget.size}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Empty State Helper */}
|
||||
{widgets.length === 0 && (
|
||||
<Flex width="100%" height="200px" align="center" justify="center">
|
||||
<Text color="gray.400">Your dashboard is empty. Click Settings to add widgets.</Text>
|
||||
// Fallback if someone asks for a widget that doesn't exist
|
||||
return (
|
||||
<Flex width="100%" height="100%" align="center" justify="center" direction="column" bg="gray.800">
|
||||
<Text fontWeight="bold" color="red.400">Error</Text>
|
||||
<Text fontSize="sm" color="gray.400">Widget '{widget.type}' not found.</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%">
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={{ lg: rglLayout }} // We feed it the formatted layout map
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||
rowHeight={30}
|
||||
isDraggable={isEditing} // Only allows dragging when your Settings tools toggle this
|
||||
isResizable={false} // Double lock-down
|
||||
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||
compactType="vertical" // Auto-packs widgets to the top
|
||||
>
|
||||
{/* 3. 🎨 Paint the Widgets */}
|
||||
{widgets.map(widget => (
|
||||
<Box
|
||||
key={widget.id}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
boxShadow={isEditing ? "outline" : "sm"} // Visual cue when editing
|
||||
border={isEditing ? "2px dashed gray" : "1px solid"}
|
||||
borderColor={isEditing ? "gray.400" : "gray.200"}
|
||||
overflow="hidden"
|
||||
cursor={isEditing ? "grab" : "default"}
|
||||
position="relative" // 3. REQUIRED so the absolute button stays inside this box
|
||||
>
|
||||
{/* 4. THE DELETE BUTTON */}
|
||||
{isEditing && (
|
||||
<IconButton
|
||||
aria-label="Remove widget"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
size="xs"
|
||||
bg="white"
|
||||
color="gray.400"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }}
|
||||
zIndex={2}
|
||||
onClick={() => removeWidget(widget.id)}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
|
||||
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* 4. SUSPENSE BOUNDARY wrapping the render function */}
|
||||
<Suspense fallback={
|
||||
<Flex w="100%" h="100%" align="center" justify="center" bg="gray.800">
|
||||
<Spinner size="md" color="blue.400" thickness="3px" />
|
||||
</Flex>
|
||||
}>
|
||||
{renderWidget(widget)}
|
||||
</Suspense>
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
|
||||
{/* Empty State Helper */}
|
||||
{widgets.length === 0 && (
|
||||
<Flex width="100%" height="200px" align="center" justify="center">
|
||||
<Text color="gray.400">Your dashboard is empty. Click Settings to add widgets.</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user