diff --git a/.gitignore b/.gitignore index 8bf65cd..51d829b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ cscope.* # Binary webconsole + +# design +design diff --git a/frontend/src/App.css b/frontend/src/App.css index 74b5e05..eb5d560 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,38 +1,4 @@ -.App { - text-align: center; -} - .App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + width: 120px; + height: auto; } diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx index 348a286..900fd40 100644 --- a/frontend/src/Dashboard.tsx +++ b/frontend/src/Dashboard.tsx @@ -1,6 +1,7 @@ import React, { useContext, useState, useEffect } from "react"; import { styled, createTheme, ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; +import useMediaQuery from "@mui/material/useMediaQuery"; import MuiDrawer from "@mui/material/Drawer"; import Box from "@mui/material/Box"; import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; @@ -14,6 +15,7 @@ import Grid from "@mui/material/Grid"; import Paper from "@mui/material/Paper"; import MenuIcon from "@mui/icons-material/Menu"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import EventAvailableIcon from "@mui/icons-material/EventAvailable"; import { MainListItems } from "./ListItems"; import { LoginContext } from "./LoginContext"; import SimpleListMenu from "./SimpleListMenu"; @@ -49,6 +51,8 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" position: "relative", whiteSpace: "nowrap", width: drawerWidth, + backgroundColor: "#FCFCFD", + borderRight: "1px solid #EEEEEE", transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, @@ -71,6 +75,128 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" const mdTheme = createTheme(); +const teslaTheme = createTheme({ + palette: { + primary: { + main: "#146ef5", + contrastText: "#FFFFFF", + }, + secondary: { + main: "#080808", + }, + background: { + default: "#FFFFFF", + paper: "#FFFFFF", + }, + text: { + primary: "#080808", + secondary: "#363636", + }, + divider: "#d8d8d8", + }, + shape: { + borderRadius: 6, + }, + typography: { + fontFamily: '"WF Visual Sans Variable", Arial, sans-serif', + h6: { + fontSize: "1rem", + fontWeight: 600, + letterSpacing: 0, + }, + button: { + fontSize: "1rem", + fontWeight: 500, + textTransform: "none", + letterSpacing: "-0.16px", + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + borderRadius: 4, + minHeight: 40, + transition: "transform 0.33s, border-color 0.33s, background-color 0.33s, color 0.33s, box-shadow 0.25s", + boxShadow: "none", + border: "1px solid transparent", + "&:hover": { + transform: "translateX(6px)", + }, + }, + containedPrimary: { + backgroundColor: "#146ef5", + color: "#FFFFFF", + "&:hover": { + backgroundColor: "#0055d4", + boxShadow: "none", + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + boxShadow: "0 6px 18px rgba(8, 8, 8, 0.07), 0 2px 6px rgba(8, 8, 8, 0.04)", + border: "1px solid #d8d8d8", + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + boxShadow: "0 5px 14px rgba(8, 8, 8, 0.06), 0 1px 4px rgba(8, 8, 8, 0.04)", + }, + }, + }, + MuiTableCell: { + styleOverrides: { + head: { + color: "#080808", + fontWeight: 600, + fontSize: "0.875rem", + letterSpacing: "0.8px", + textTransform: "uppercase", + }, + body: { + color: "#363636", + fontSize: "0.875rem", + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiInputBase-root": { + borderRadius: 4, + }, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + borderRadius: 4, + margin: "4px 10px", + minHeight: 40, + transition: "color 0.33s, background-color 0.33s, transform 0.33s", + "&.Mui-selected, &.Mui-selected:hover, &:hover": { + backgroundColor: "rgba(20, 110, 245, 0.1)", + transform: "translateX(6px)", + }, + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: "#d8d8d8", + }, + }, + }, + }, +}); + export interface DashboardProps { children: React.ReactNode; title: string; @@ -79,6 +205,14 @@ export interface DashboardProps { function Dashboard(props: DashboardProps) { const [open, setOpen] = React.useState(true); + const isMobile = useMediaQuery(mdTheme.breakpoints.down("md")); + + useEffect(() => { + if (isMobile) { + setOpen(false); + } + }, [isMobile]); + const toggleDrawer = () => { setOpen(!open); }; @@ -157,39 +291,48 @@ function Dashboard(props: DashboardProps) { } }; return ( - + - + - + {props.title} - - - - - - - - - - - {/* Moved to drop down menu */} - {/* */} - - + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + )} - theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900], + background: + "radial-gradient(circle at 10% -20%, rgba(122, 61, 255, 0.12) 0%, rgba(122, 61, 255, 0) 40%), radial-gradient(circle at 90% 10%, rgba(20, 110, 245, 0.12) 0%, rgba(20, 110, 245, 0) 45%), #ffffff", flexGrow: 1, height: "100vh", overflow: "auto", + pb: 10, }} > - + + + webconsole control plane + + + {props.title} + + + - + {props.children} + + + + + + Schedule a Drive Today + + + ); diff --git a/frontend/src/index.css b/frontend/src/index.css index 714ab0e..25cd17f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,11 +1,40 @@ +:root { + --wf-blue: #146ef5; + --wf-black: #080808; + --wf-border: #d8d8d8; + --wf-muted: #363636; +} + body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", - "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-family: "WF Visual Sans Variable", Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + color: var(--wf-black); + background-color: #ffffff; +} + +* { + box-sizing: border-box; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +textarea, +select { + font: inherit; +} + +::selection { + background: var(--wf-blue); + color: #ffffff; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c75d180..c96d520 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; +import "./App.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; diff --git a/frontend/src/pages/Component/ChargingCfg.tsx b/frontend/src/pages/Component/ChargingCfg.tsx index 397443a..3351ff3 100644 --- a/frontend/src/pages/Component/ChargingCfg.tsx +++ b/frontend/src/pages/Component/ChargingCfg.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Box, - Grid, + Card, Table, TableBody, TableCell, TableRow, + Typography, } from "@mui/material"; import { ChargingData } from "../../api/api"; @@ -17,34 +18,43 @@ const ChargingCfg = ({ const isOnlineCharging = chargingData.chargingMethod === "Online"; return ( - - - -

Charging Config

-
-
- - - - Charging Method - {chargingData.chargingMethod} - - - {isOnlineCharging && ( + + + + + Charging Config + + +
- Quota - {chargingData.quota} + Charging Method + {chargingData.chargingMethod} - )} - - - Unit Cost - {chargingData.unitCost} - - -
+ {isOnlineCharging && ( + + + Quota + {chargingData.quota} + + + )} + + + Unit Cost + {chargingData.unitCost} + + + +
); }; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index a2ed5df..178dcf5 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -7,11 +7,66 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Container from "@mui/material/Container"; import { createTheme, ThemeProvider } from "@mui/material/styles"; +import Paper from "@mui/material/Paper"; import axios from "../axios"; import { useNavigate } from "react-router-dom"; import { LoginContext } from "../LoginContext"; -const theme = createTheme(); +const theme = createTheme({ + palette: { + primary: { + main: "#146ef5", + contrastText: "#FFFFFF", + }, + background: { + default: "#FFFFFF", + paper: "#FFFFFF", + }, + text: { + primary: "#080808", + secondary: "#363636", + }, + }, + shape: { + borderRadius: 4, + }, + typography: { + fontFamily: '"WF Visual Sans Variable", Arial, sans-serif', + }, + components: { + MuiButton: { + styleOverrides: { + root: { + minHeight: 40, + borderRadius: 4, + textTransform: "none", + fontWeight: 500, + border: "1px solid transparent", + boxShadow: "none", + transition: "transform 0.33s, border-color 0.33s, background-color 0.33s, color 0.33s, box-shadow 0.25s", + "&:hover": { + transform: "translateX(6px)", + }, + }, + containedPrimary: { + "&:hover": { + backgroundColor: "#0055d4", + boxShadow: "none", + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiInputBase-root": { + borderRadius: 4, + }, + }, + }, + }, + }, +}); export default function SignIn() { const navigation = useNavigate(); @@ -42,47 +97,79 @@ export default function SignIn() { return ( - + - - logo -
- - {error} - - - - - + + logo + + Webconsole + + + {error} + + + + + + - +
); diff --git a/frontend/src/pages/SubscriberList.tsx b/frontend/src/pages/SubscriberList.tsx index 6867547..10e48c7 100644 --- a/frontend/src/pages/SubscriberList.tsx +++ b/frontend/src/pages/SubscriberList.tsx @@ -1,12 +1,5 @@ -import React, { - useState, - useEffect, - useMemo, - useCallback, - CSSProperties, -} from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { List } from "react-window"; import axios from "../axios"; import { Subscriber } from "../api/api"; @@ -21,6 +14,11 @@ import { LinearProgress, Snackbar, Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, TextField, Typography, Checkbox, @@ -31,124 +29,14 @@ import { formatMultipleDeleteSubscriberToJson, } from "../lib/jsonFormating"; -const ROW_HEIGHT = 52; -const MAX_LIST_HEIGHT = 530; const BULK_DELETE_WARN_THRESHOLD = 100; const BULK_DELETE_BATCH_SIZE = 500; -// Shared between header and virtual rows — both must use the same template. -const COL_CHECKBOX = "52px"; -const COL_PLMN = "18%"; -const COL_UEID = "1fr"; -const COL_DELETE = "110px"; -const COL_VIEW = "90px"; -const COL_EDIT = "90px"; -const GRID_COLS = `${COL_CHECKBOX} ${COL_PLMN} ${COL_UEID} ${COL_DELETE} ${COL_VIEW} ${COL_EDIT}`; - interface Props { refresh: boolean; setRefresh: (v: boolean) => void; } -type RowSharedProps = { - rows: Subscriber[]; - selected: MultipleDeleteSubscriberData[]; - onDelete: (id: string, plmn: string) => void; - onModify: (s: Subscriber) => void; - onEdit: (s: Subscriber) => void; - onRowClick: (item: MultipleDeleteSubscriberData) => void; -}; - -// Defined outside SubscriberList so its reference is stable across renders. -type VirtualRowProps = { - ariaAttributes: Record; - index: number; - style: CSSProperties; -} & RowSharedProps; - -function VirtualRow({ - index, - style, - rows, - selected, - onDelete, - onModify, - onEdit, - onRowClick, -}: VirtualRowProps) { - const row = rows[index]; - if (!row) return null; - - const item: MultipleDeleteSubscriberData = { - ueId: row.ueId!, - plmnID: row.plmnID!, - }; - - const isItemSelected = selected.some( - (s) => s.ueId === item.ueId && s.plmnID === item.plmnID - ); - - return ( - onRowClick(item)} - sx={{ - display: "grid", - gridTemplateColumns: GRID_COLS, - alignItems: "center", - borderBottom: "1px solid", - borderColor: "divider", - backgroundColor: isItemSelected ? "action.selected" : "transparent", - "&:hover": { - backgroundColor: isItemSelected ? "action.focus" : "action.hover", - }, - cursor: "pointer", - boxSizing: "border-box", - }} - > - - - - - {row.plmnID} - - - {row.ueId} - - - - - - - - - - - - ); -} - function SubscriberList(props: Props) { const navigation = useNavigate(); const [data, setData] = useState([]); @@ -207,6 +95,9 @@ function SubscriberList(props: Props) { setSearchTerm(e.target.value); }; + const isSelected = (item: MultipleDeleteSubscriberData) => + selected.some((s) => s.ueId === item.ueId && s.plmnID === item.plmnID); + const onDelete = useCallback( (id: string, plmn: string) => { if (!window.confirm("Delete subscriber?")) return; @@ -329,17 +220,6 @@ function SubscriberList(props: Props) { ); } - const listHeight = Math.min(filteredData.length * ROW_HEIGHT, MAX_LIST_HEIGHT); - - const rowProps: RowSharedProps = { - rows: filteredData, - selected, - onDelete, - onModify, - onEdit, - onRowClick: handleRowClick, - }; - return ( <>
@@ -379,47 +259,84 @@ function SubscriberList(props: Props) { )} - - - 0 && selected.length < filteredData.length} - checked={filteredData.length > 0 && selected.length === filteredData.length} - onChange={handleSelectAllClick} - size="small" - /> - - PLMN - UE ID - Delete - View - Edit - - - {/* react-window requires an explicit pixel height — percentage heights - resolve to 0 because ResizeObserver cannot measure unconstrained flex containers. */} - - - rowComponent={VirtualRow} - rowProps={rowProps} - rowCount={filteredData.length} - rowHeight={ROW_HEIGHT} - style={{ height: listHeight, width: "100%" }} - /> - + + + + + 0 && selected.length < filteredData.length} + checked={filteredData.length > 0 && selected.length === filteredData.length} + onChange={handleSelectAllClick} + /> + + PLMN + UE ID + Delete + View + Edit + + + + {filteredData.map((row, index) => { + const item = { ueId: row.ueId!, plmnID: row.plmnID! }; + const isItemSelected = isSelected(item); + + return ( + handleRowClick(item)} + role="checkbox" + aria-checked={isItemSelected} + selected={isItemSelected} + > + + + + {row.plmnID} + {row.ueId} + + + + + + + + + + + ); + })} + +