Refactor App and EmbeddedActiveEquipmentPage for theme support
- Renamed the main component from `App` to `ToirAdmin` for clarity. - Integrated theme support using `useEmbeddedParentTheme` to dynamically adjust the UI based on the parent theme. - Updated `EmbeddedActiveEquipmentPage` to utilize the new theme and improved styling for better user experience. - Added a new utility hook `useEmbeddedParentTheme` to manage theme changes via postMessage from parent origins. - Enhanced the `vite-env.d.ts` file to include environment variable definitions for parent origins.
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { Admin, Resource } from 'react-admin';
|
import { Admin, Resource } from 'react-admin';
|
||||||
import { authProvider } from './auth/authProvider';
|
import { authProvider } from './auth/authProvider';
|
||||||
import { dataProvider } from './dataProvider';
|
import { dataProvider } from './dataProvider';
|
||||||
|
import { useEmbeddedParentTheme } from './embed/useEmbeddedParentTheme';
|
||||||
import { EmbeddedActiveEquipmentPage } from './pages/EmbeddedActiveEquipmentPage';
|
import { EmbeddedActiveEquipmentPage } from './pages/EmbeddedActiveEquipmentPage';
|
||||||
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
|
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
|
||||||
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
|
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
|
||||||
@@ -11,13 +14,18 @@ import { EquipmentStatusChangeEdit } from './resources/equipment-status-change/E
|
|||||||
import { EquipmentStatusChangeList } from './resources/equipment-status-change/EquipmentStatusChangeList';
|
import { EquipmentStatusChangeList } from './resources/equipment-status-change/EquipmentStatusChangeList';
|
||||||
import { EquipmentStatusChangeShow } from './resources/equipment-status-change/EquipmentStatusChangeShow';
|
import { EquipmentStatusChangeShow } from './resources/equipment-status-change/EquipmentStatusChangeShow';
|
||||||
|
|
||||||
function App() {
|
function ToirAdmin() {
|
||||||
if (window.location.pathname === '/embedded/equipment-active') {
|
const paletteMode = useEmbeddedParentTheme();
|
||||||
return <EmbeddedActiveEquipmentPage />;
|
const theme = useMemo(
|
||||||
}
|
() =>
|
||||||
|
createTheme({
|
||||||
|
palette: { mode: paletteMode },
|
||||||
|
}),
|
||||||
|
[paletteMode],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Admin dataProvider={dataProvider} authProvider={authProvider}>
|
<Admin dataProvider={dataProvider} authProvider={authProvider} theme={theme}>
|
||||||
<Resource
|
<Resource
|
||||||
name="equipment"
|
name="equipment"
|
||||||
list={EquipmentList}
|
list={EquipmentList}
|
||||||
@@ -36,4 +44,12 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
if (window.location.pathname === '/embedded/equipment-active') {
|
||||||
|
return <EmbeddedActiveEquipmentPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ToirAdmin />;
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
48
client/src/embed/useEmbeddedParentTheme.ts
Normal file
48
client/src/embed/useEmbeddedParentTheme.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type EmbedPaletteMode = 'light' | 'dark';
|
||||||
|
|
||||||
|
const MESSAGE_TYPE = 'greact-theme' as const;
|
||||||
|
|
||||||
|
function parseThemeFromSearch(): EmbedPaletteMode {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
const t = new URLSearchParams(window.location.search).get('theme');
|
||||||
|
return t === 'dark' ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Comma-separated list of parent origins allowed to send theme via postMessage. If unset, all origins are accepted (theme-only payload). */
|
||||||
|
function getAllowedOrigins(): string[] {
|
||||||
|
const raw = import.meta.env.VITE_TOIR_EMBED_PARENT_ORIGINS;
|
||||||
|
if (typeof raw === 'string' && raw.trim()) {
|
||||||
|
return raw
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEmbeddedParentTheme(): EmbedPaletteMode {
|
||||||
|
const [mode, setMode] = useState<EmbedPaletteMode>(parseThemeFromSearch);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allowed = getAllowedOrigins();
|
||||||
|
const handler = (event: MessageEvent) => {
|
||||||
|
if (allowed.length > 0 && !allowed.includes(event.origin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = event.data as { type?: string; theme?: string } | null;
|
||||||
|
if (data && typeof data === 'object' && data.type === MESSAGE_TYPE) {
|
||||||
|
if (data.theme === 'dark' || data.theme === 'light') {
|
||||||
|
setMode(data.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handler);
|
||||||
|
return () => window.removeEventListener('message', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
@@ -9,9 +11,12 @@ import TableContainer from '@mui/material/TableContainer';
|
|||||||
import TableHead from '@mui/material/TableHead';
|
import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useEffect, useState } from 'react';
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useEmbeddedParentTheme } from '../embed/useEmbeddedParentTheme';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { ensureFreshToken, getAccessToken } from '../auth/keycloak';
|
import { ensureFreshToken, getAccessToken } from '../auth/keycloak';
|
||||||
|
import { downloadEquipmentAttachmentFile } from '../resources/equipment/attachmentDownload';
|
||||||
|
|
||||||
type EquipmentRecord = {
|
type EquipmentRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,11 +24,15 @@ type EquipmentRecord = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
dateOfInspection: string | null;
|
dateOfInspection: string | null;
|
||||||
commissionedAt: string | null;
|
commissionedAt: string | null;
|
||||||
|
attachment?: {
|
||||||
|
objectKey?: string;
|
||||||
|
originalFileName?: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDate(value: string | null) {
|
function formatDate(value: string | null) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '-';
|
return '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -35,6 +44,24 @@ function formatDate(value: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EmbeddedActiveEquipmentPage() {
|
export function EmbeddedActiveEquipmentPage() {
|
||||||
|
const paletteMode = useEmbeddedParentTheme();
|
||||||
|
const muiTheme = useMemo(
|
||||||
|
() =>
|
||||||
|
createTheme({
|
||||||
|
palette: { mode: paletteMode },
|
||||||
|
components: {
|
||||||
|
MuiPaper: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[paletteMode],
|
||||||
|
);
|
||||||
|
|
||||||
const [data, setData] = useState<EquipmentRecord[]>([]);
|
const [data, setData] = useState<EquipmentRecord[]>([]);
|
||||||
const [total, setTotal] = useState<number | null>(null);
|
const [total, setTotal] = useState<number | null>(null);
|
||||||
const [isPending, setIsPending] = useState(true);
|
const [isPending, setIsPending] = useState(true);
|
||||||
@@ -105,15 +132,39 @@ export function EmbeddedActiveEquipmentPage() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const pageBg =
|
||||||
|
paletteMode === 'dark' ? 'linear-gradient(180deg, #0d1117 0%, #0a0c10 100%)' : '#f3f6fa';
|
||||||
|
const headerBg = paletteMode === 'dark' ? '#161b22' : '#fff';
|
||||||
|
const headerBorder = paletteMode === 'dark' ? '1px solid #30363d' : '1px solid #d7e0ea';
|
||||||
|
const titleColor = paletteMode === 'dark' ? '#e6edf3' : '#10233a';
|
||||||
|
const subtitleColor = paletteMode === 'dark' ? '#8b949e' : '#5b7087';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ minHeight: '100vh', boxSizing: 'border-box', p: { xs: 2, md: 3 }, bgcolor: '#f3f6fa' }}>
|
<ThemeProvider theme={muiTheme}>
|
||||||
<Paper elevation={0} sx={{ overflow: 'hidden', borderRadius: 3, border: '1px solid #d7e0ea' }}>
|
<CssBaseline />
|
||||||
<Box sx={{ px: 3, py: 2, borderBottom: '1px solid #d7e0ea', bgcolor: '#fff' }}>
|
<Box
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, color: '#10233a' }}>
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
p: { xs: 2, md: 3 },
|
||||||
|
bgcolor: pageBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: headerBorder,
|
||||||
|
bgcolor: paletteMode === 'dark' ? '#161b22' : '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ px: 3, py: 2, borderBottom: headerBorder, bgcolor: headerBg }}>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, color: titleColor }}>
|
||||||
Оборудование в эксплуатации
|
Оборудование в эксплуатации
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mt: 0.5, color: '#5b7087' }}>
|
<Typography variant="body2" sx={{ mt: 0.5, color: subtitleColor }}>
|
||||||
Отображаются записи со статусом `Active`
|
Отображаются записи со статусом 'Active'
|
||||||
{typeof total === 'number' ? `: ${total}` : ''}
|
{typeof total === 'number' ? `: ${total}` : ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -135,6 +186,7 @@ export function EmbeddedActiveEquipmentPage() {
|
|||||||
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
|
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 700 }}>Дата изготовления</TableCell>
|
<TableCell sx={{ fontWeight: 700 }}>Дата изготовления</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
|
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 700 }}>Файл</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -142,8 +194,28 @@ export function EmbeddedActiveEquipmentPage() {
|
|||||||
<TableRow key={item.id} hover>
|
<TableRow key={item.id} hover>
|
||||||
<TableCell>{item.name}</TableCell>
|
<TableCell>{item.name}</TableCell>
|
||||||
<TableCell>{item.serialNumber}</TableCell>
|
<TableCell>{item.serialNumber}</TableCell>
|
||||||
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
|
|
||||||
<TableCell>{formatDate(item.commissionedAt)}</TableCell>
|
<TableCell>{formatDate(item.commissionedAt)}</TableCell>
|
||||||
|
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item?.attachment?.objectKey ? (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
underline="hover"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const label = item.attachment?.originalFileName?.trim() || 'файл';
|
||||||
|
void downloadEquipmentAttachmentFile(item.id, label).catch(() => {});
|
||||||
|
}}
|
||||||
|
sx={{ cursor: 'pointer', verticalAlign: 'inherit' }}
|
||||||
|
>
|
||||||
|
{item.attachment?.originalFileName?.trim() || 'Скачать'}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -152,5 +224,6 @@ export function EmbeddedActiveEquipmentPage() {
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
8
client/src/vite-env.d.ts
vendored
8
client/src/vite-env.d.ts
vendored
@@ -1 +1,9 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_TOIR_EMBED_PARENT_ORIGINS?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user