main
熊二 3 months ago
parent bf1d289e8e
commit 358b0b8c32
  1. 1
      apps/imsfe/package.json
  2. 31
      apps/imsfe/src/App.tsx
  3. 23
      apps/imsfe/src/config/i18nProvider.ts
  4. 7
      apps/imsfe/src/pages/CatchAll.tsx
  5. 18
      apps/imsfe/src/pages/PageError.tsx
  6. 10
      apps/imsfe/src/pages/SignIn.tsx
  7. 47
      apps/imsfe/src/views/SignInCard.tsx
  8. 77
      packages/joy-ui/src/auth/LoginForm.tsx
  9. 36
      packages/joy-ui/src/icons/WechatIcon.tsx
  10. 2
      packages/joy-ui/src/index.ts
  11. 39
      packages/joy-ui/src/language/en_US.ts
  12. 2
      packages/joy-ui/src/language/index.ts
  13. 1
      packages/joy-ui/src/language/zh_CN.ts
  14. 4
      packages/joy-ui/src/layout/ColorSchemeToggle.tsx
  15. 11
      packages/joy-ui/src/layout/Forbidden.tsx
  16. 18
      packages/joy-ui/src/layout/LoadingIndicator.tsx
  17. 11
      packages/joy-ui/src/layout/NotFound.tsx
  18. 138
      packages/joy-ui/src/layout/PageError.tsx
  19. 35
      packages/joy-ui/src/layout/RuntimeError.tsx
  20. 9
      packages/joy-ui/src/layout/index.ts
  21. 1
      packages/joy-ui/src/utils/index.ts
  22. 9
      packages/joy-ui/src/utils/useMediaQuery.ts
  23. 2
      packages/rakit/src/auth/useAuthState.ts
  24. 2
      packages/rakit/src/auth/useCheckAuth.ts
  25. 2
      packages/rakit/src/auth/useHandleAuthCallback.ts
  26. 2
      packages/rakit/src/auth/useLogin.ts
  27. 2
      packages/rakit/src/auth/useLogout.ts
  28. 71
      packages/rakit/src/core/CoreAdminRoutes.tsx
  29. 32
      packages/rakit/src/core/CoreAdminUI.tsx
  30. 111
      packages/rakit/src/core/DefaultReady.tsx
  31. 5
      packages/rakit/src/core/RoutesWithoutLayout.tsx
  32. 2
      packages/rakit/src/core/index.ts
  33. 97
      packages/rakit/src/core/useConfigureAdminRouterFromChildren.tsx

@ -12,6 +12,7 @@
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.7", "@mui/icons-material": "^5.16.7",
"@mui/joy": "^5.0.0-beta.48", "@mui/joy": "^5.0.0-beta.48",
"@mui/system": "^6.0.2",
"@mui/utils": "^5.16.6", "@mui/utils": "^5.16.6",
"@rakit/core": "workspace:*", "@rakit/core": "workspace:*",
"@rakit/fetch": "workspace:*", "@rakit/fetch": "workspace:*",

@ -1,6 +1,6 @@
import { CoreAdmin, CoreLayoutProps } from '@rakit/core'; import { CoreAdmin, CoreLayoutProps, RoutesWithoutLayout } from '@rakit/core';
import { FetchContextProvider, useFetch } from '@rakit/fetch'; import { FetchContextProvider, useFetch } from '@rakit/fetch';
import { Error, Loading, ThemeProvider } from '@rakit/joy-ui'; import { RuntimeError, Loading, ThemeProvider, NotFound } from '@rakit/joy-ui';
import { createAuthProvider } from './config/createAuthProvider'; import { createAuthProvider } from './config/createAuthProvider';
import { createDataProvider } from './config/createDataProvider'; import { createDataProvider } from './config/createDataProvider';
import { i18nProvider } from './config/i18nProvider'; import { i18nProvider } from './config/i18nProvider';
@ -17,7 +17,6 @@ const Layout = (props: CoreLayoutProps) => {
) )
} }
const CatchAll = lazy(() => import("./pages/CatchAll"));
const SignIn = lazy(() => import("./pages/SignIn")); const SignIn = lazy(() => import("./pages/SignIn"));
const PageError = lazy(() => import("./pages/PageError")); const PageError = lazy(() => import("./pages/PageError"));
@ -42,32 +41,24 @@ function AppContext() {
authProvider={authProvider} authProvider={authProvider}
// authCallbackPage={ } // authCallbackPage={ }
// basename={ } // basename={ }
catchAll={<CatchAll />} catchAll={NotFound}
dataProvider={dataProvider} dataProvider={dataProvider}
error={Error} error={RuntimeError}
dashboard={<CatchAll />} // dashboard={<CatchAll />}
initialLocation="/" // initialLocation="/"
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
loading={AppLoading} loading={AppLoading}
layout={Layout} layout={Layout}
loginPage={SignIn} loginPage={SignIn}
// ready={ } // ready={ }
requireAuth={false} requireAuth={true}
// store={ } // store={ }
title="进销存系统" title="进销存系统"
> >
<Route <Route path="/error/:status" element={<PageError />} />
path="/error/:status" <RoutesWithoutLayout>
element={<PageError />} <Route path="/" element={<div>sss</div>} />
/> </RoutesWithoutLayout>
<Route
path="/sasda"
element={<div></div>}
/>
<Route
path="/sasda2"
element={<div>222</div>}
/>
</CoreAdmin> </CoreAdmin>
) )
} }

@ -1,25 +1,26 @@
import Polyglot from 'node-polyglot'; import Polyglot from 'node-polyglot';
import { I18nProvider, Locale } from "@rakit/core"; import { I18nProvider, Locale } from "@rakit/core";
import merge from 'lodash/merge';
import { enUS, zhCN } from '@rakit/joy-ui';
const locale: Locale = { code: 'zh', name: "简体中文" }; const locale: Locale = { code: 'zh', name: "简体中文" };
const polyglot = new Polyglot({ const polyglot = new Polyglot({
locale: locale.code, locale: locale.code,
phrases: { phrases: merge({
'': '', '': '',
ra: { }, enUS, zhCN),
page: {
error: 'ra.page.error222',
},
message: {
error: "ra.message.error222",
}
},
},
}); });
function translate(key: string, options: any = {}) {
if (polyglot.has(key)) {
return polyglot.t(key, options);
}
return key;
}
export const i18nProvider: I18nProvider = { export const i18nProvider: I18nProvider = {
translate: (key: string, options: any = {}) => polyglot.t(key, options), translate,
changeLocale: () => Promise.resolve(), changeLocale: () => Promise.resolve(),
getLocale: () => "zh", getLocale: () => "zh",
getLocales: () => [locale], getLocales: () => [locale],

@ -1,7 +0,0 @@
import { Navigate } from "react-router-dom";
export default function CatchAll() {
return (
<Navigate to="/error/404" />
);
}

@ -1,10 +1,20 @@
import { StatusError } from "@rakit/joy-ui"; import {
Forbidden,
NotFound,
PageError as Error
} from "@rakit/joy-ui";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
export default function PageError() { export default function PageError() {
const { status } = useParams<'status'>(); const { status } = useParams<'status'>();
return ( switch (status) {
<StatusError status={status} /> case "403":
); return <Forbidden key="error403" />
case "404":
return <NotFound key="error404" />
case "500":
default:
return <Error key="error500" />
}
} }

@ -3,10 +3,13 @@ import Box from '@mui/joy/Box';
import IconButton from '@mui/joy/IconButton'; import IconButton from '@mui/joy/IconButton';
import Typography from '@mui/joy/Typography'; import Typography from '@mui/joy/Typography';
import BadgeRoundedIcon from '@mui/icons-material/BadgeRounded'; import BadgeRoundedIcon from '@mui/icons-material/BadgeRounded';
import { ColorSchemeToggle } from '@rakit/joy-ui'; import { ColorSchemeToggle, useMediaQuery } from '@rakit/joy-ui';
import { SignInCard } from 'src/views/SignInCard'; import { SignInCard } from 'src/views/SignInCard';
import { useDefaultTitle } from '@rakit/core';
export default function SignIn() { export default function SignIn() {
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('md'));
const title = useDefaultTitle();
return ( return (
<Fragment> <Fragment>
<Box <Box
@ -17,6 +20,7 @@ export default function SignIn() {
width: '100%', width: '100%',
px: 2, px: 2,
justifyContent: "space-between", justifyContent: "space-between",
background: isSmall ? "var(--palette-background-surface)" : undefined,
}} }}
> >
<Box <Box
@ -31,9 +35,9 @@ export default function SignIn() {
<IconButton variant="soft" color="primary" size="sm"> <IconButton variant="soft" color="primary" size="sm">
<BadgeRoundedIcon /> <BadgeRoundedIcon />
</IconButton> </IconButton>
<Typography level="title-lg">Company logo</Typography> <Typography level="title-lg">{title}</Typography>
</Box> </Box>
<ColorSchemeToggle /> <ColorSchemeToggle variant={isSmall ? 'outlined' : 'plain'} />
</Box> </Box>
<SignInCard /> <SignInCard />
<Box component="footer" sx={{ py: 3 }}> <Box component="footer" sx={{ py: 3 }}>

@ -9,9 +9,21 @@ import Box from '@mui/joy/Box';
import Divider from '@mui/joy/Divider'; import Divider from '@mui/joy/Divider';
import Stack from '@mui/joy/Stack'; import Stack from '@mui/joy/Stack';
import Checkbox from '@mui/joy/Checkbox'; import Checkbox from '@mui/joy/Checkbox';
import { GoogleIcon } from '@rakit/joy-ui'; import { useMediaQuery, WechatIcon } from '@rakit/joy-ui';
interface FormElements extends HTMLFormControlsCollection {
email: HTMLInputElement;
password: HTMLInputElement;
persistent: HTMLInputElement;
}
interface SignInFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
export function SignInCard() { export function SignInCard() {
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('md'));
return ( return (
<Sheet <Sheet
sx={{ sx={{
@ -24,9 +36,9 @@ export function SignInCard() {
flexDirection: 'column', flexDirection: 'column',
gap: 2, gap: 2,
borderRadius: 'sm', borderRadius: 'sm',
boxShadow: 'md', boxShadow: isSmall ? undefined : 'md',
}} }}
variant="outlined" variant={isSmall ? 'plain' : 'outlined'}
> >
<Box <Box
component="main" component="main"
@ -54,10 +66,10 @@ export function SignInCard() {
<Stack sx={{ gap: 4, mb: 2 }}> <Stack sx={{ gap: 4, mb: 2 }}>
<Stack sx={{ gap: 1 }}> <Stack sx={{ gap: 1 }}>
<Typography component="h1" level="h3"> <Typography component="h1" level="h3">
Sign in
</Typography> </Typography>
<Typography level="body-sm"> <Typography level="body-sm">
New to company?{' '} {' '}
<Link href="#replace-with-a-link" level="title-sm"> <Link href="#replace-with-a-link" level="title-sm">
Sign up! Sign up!
</Link> </Link>
@ -67,15 +79,16 @@ export function SignInCard() {
variant="soft" variant="soft"
color="neutral" color="neutral"
fullWidth fullWidth
startDecorator={<GoogleIcon />} startDecorator={<WechatIcon />}
size="lg"
> >
Continue with Google 使
</Button> </Button>
</Stack> </Stack>
<Divider <Divider
sx={(theme) => ({ sx={(theme) => ({
[theme.getColorSchemeSelector('light')]: { [theme.getColorSchemeSelector('light')]: {
color: { xs: '#FFF', md: 'text.tertiary' }, color: 'text.tertiary',
}, },
})} })}
> >
@ -96,11 +109,23 @@ export function SignInCard() {
> >
<FormControl required> <FormControl required>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<Input type="text" name="account" placeholder="用户名/手机/邮箱" /> <Input
type="text"
name="account"
placeholder="用户名/手机/邮箱"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl> </FormControl>
<FormControl required> <FormControl required>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<Input type="password" name="password" placeholder="至少6位" /> <Input
type="password"
name="password"
placeholder="至少6位"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl> </FormControl>
<Stack sx={{ gap: 4, mt: 2 }}> <Stack sx={{ gap: 4, mt: 2 }}>
<Box <Box
@ -115,7 +140,7 @@ export function SignInCard() {
</Link> </Link>
</Box> </Box>
<Button type="submit" fullWidth> <Button type="submit" fullWidth size="lg">
</Button> </Button>
</Stack> </Stack>

@ -0,0 +1,77 @@
import Sheet from '@mui/joy/Sheet';
import Typography from '@mui/joy/Typography';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import Input from '@mui/joy/Input';
import Button from '@mui/joy/Button';
import Link from '@mui/joy/Link';
import Box from '@mui/joy/Box';
import Divider from '@mui/joy/Divider';
import Stack from '@mui/joy/Stack';
import Checkbox from '@mui/joy/Checkbox';
import { FormEvent } from "react";
interface FormElements extends HTMLFormControlsCollection {
email: HTMLInputElement;
password: HTMLInputElement;
persistent: HTMLInputElement;
}
interface SignInFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
export function LoginForm() {
return (
<form
onSubmit={(event: FormEvent<SignInFormElement>) => {
event.preventDefault();
const formElements = event.currentTarget.elements;
const data = {
email: formElements.email.value,
password: formElements.password.value,
persistent: formElements.persistent.checked,
};
alert(JSON.stringify(data, null, 2));
}}
>
<FormControl required>
<FormLabel></FormLabel>
<Input
type="text"
name="account"
placeholder="用户名/手机/邮箱"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<FormControl required>
<FormLabel></FormLabel>
<Input
type="password"
name="password"
placeholder="至少6位"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<Stack sx={{ gap: 4, mt: 2 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Checkbox size="sm" label="记住我" name="persistent" />
<Link level="title-sm" href="#replace-with-a-link">
</Link>
</Box>
<Button type="submit" fullWidth size="lg">
</Button>
</Stack>
</form>
)
}

@ -2,25 +2,31 @@ import SvgIcon from '@mui/joy/SvgIcon';
export function WechatIcon() { export function WechatIcon() {
return ( return (
<SvgIcon fontSize="xl"> <SvgIcon fontSize="xl2">
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)"> <svg viewBox="0 0 512 512">
<path <path
fill="#4285F4" d="
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z" M342.248,169.517c8.712,0,17.221,0.724,25.616,1.759C352.687,104.625,282.682,54.21,198.503,54.21
c-95.28,0-172.502,64.541-172.502,144.134c0,45.889,25.819,86.602,65.866,112.945l-22.742,45.605l61.953-26.608
c13.285,4.731,27.09,8.627,41.835,10.44c-2.015-8.796-3.16-17.813-3.16-27.068C169.753,234.178,247.115,169.517,342.248,169.517z
M256.003,119.066c11.905,0,21.56,9.685,21.56,21.623c0,11.942-9.654,21.62-21.56,21.62c-11.912,0-21.563-9.678-21.563-21.62
C234.44,128.75,244.091,119.066,256.003,119.066z M141.001,162.309c-11.907,0-21.562-9.678-21.562-21.62
c0-11.938,9.656-21.623,21.562-21.623s21.563,9.685,21.563,21.623C162.563,152.631,152.906,162.309,141.001,162.309z
"
fill="#51C332"
/> />
<path <path
fill="#34A853" d="
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z" M485.999,313.656c0-63.684-64.376-115.312-143.751-115.312
c-79.378,0-143.745,51.628-143.745,115.312c0,63.679,64.367,115.308,143.745,115.308c13.054,0,25.471-1.845,37.519-4.465
l77.483,33.291l-26.798-53.701C464.035,382.983,485.999,350.527,485.999,313.656z M299.125,306.448
c-11.906,0-21.563-9.681-21.563-21.625c0-11.938,9.656-21.616,21.563-21.616c11.91,0,21.561,9.682,21.561,21.616
C320.686,296.768,311.033,306.448,299.125,306.448z M385.373,306.448c-11.912,0-21.561-9.681-21.561-21.625
c0-11.938,9.648-21.616,21.561-21.616c11.911,0,21.563,9.682,21.563,21.616C406.936,296.768,397.284,306.448,385.373,306.448z
"
fill="#51C332"
/> />
<path </svg>
fill="#FBBC05"
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
/>
<path
fill="#EA4335"
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
/>
</g>
</SvgIcon> </SvgIcon>
); );
} }

@ -1,3 +1,5 @@
export * from "./icons"; export * from "./icons";
export * from "./language";
export * from "./layout"; export * from "./layout";
export * from "./theme"; export * from "./theme";
export * from "./utils";

@ -0,0 +1,39 @@
export const enUS = {
ra: {
action: {
back: "back",
},
page: {
create: 'Create %{name}',
dashboard: 'Dashboard',
edit: '%{name} %{recordRepresentation}',
empty: 'No %{name} yet.',
error: 'Something went wrong',
forbidden: "No permission",
invite: 'Do you want to add one?',
list: '%{name}',
loading: 'Loading',
not_found: "Sorry, page not found!",
show: '%{name} %{recordRepresentation}',
},
message: {
about: 'About',
are_you_sure: 'Are you sure?',
auth_error: 'An error occurred while validating the authentication token.',
bulk_delete_content: 'Are you sure you want to delete this %{name}? |||| Are you sure you want to delete these %{smart_count} items?',
bulk_delete_title: 'Delete %{name} |||| Delete %{smart_count} %{name}',
clear_array_input: 'Are you sure you want to clear the whole list?',
delete_content: 'Are you sure you want to delete this item?',
delete_title: 'Delete %{name} #%{id}',
details: 'Details',
error: "A client error occurred and your request couldn't be completed, please try again later.",
forbidden: 'The page you’re trying to access has restricted access. Please refer to your system administrator.',
invalid_form: 'The form is not valid. Please check for errors',
loading: 'Please wait',
no: 'No',
not_found: 'Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.',
yes: 'Yes',
unsaved_changes: "Some of your changes weren't saved. Are you sure you want to ignore them?",
},
},
};

@ -0,0 +1,2 @@
export * from "./zh_CN";
export * from "./en_US";

@ -0,0 +1 @@
export const zhCN = {}

@ -38,10 +38,10 @@ export function ColorSchemeToggle(props: IconButtonProps) {
}} }}
sx={[ sx={[
{ {
"& > *:first-child": { "& > *:first-of-type": {
display: mode === "dark" ? "none" : "initial", display: mode === "dark" ? "none" : "initial",
}, },
"& > *:last-child": { "& > *:last-of-type": {
display: mode === "light" ? "none" : "initial", display: mode === "light" ? "none" : "initial",
}, },
}, },

@ -0,0 +1,11 @@
import { PageError } from "./PageError";
export function Forbidden() {
return (
<PageError
image="forbidden"
title="ra.page.forbidden"
message="ra.message.forbidden"
/>
);
}

@ -0,0 +1,18 @@
import IconButton, { IconButtonProps } from '@mui/joy/IconButton';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useLoading } from '@rakit/core';
type LoadingIndicatorProps = Omit<IconButtonProps, 'loading'>;
export function LoadingIndicator(props: LoadingIndicatorProps) {
const loading = useLoading();
return (
<IconButton
{...props}
loading={loading}
>
<RefreshIcon />
</IconButton>
);
}

@ -0,0 +1,11 @@
import { PageError } from "./PageError";
export function NotFound() {
return (
<PageError
image="not_found"
title="ra.page.not_found"
message="ra.message.not_found"
/>
);
}

@ -2,7 +2,8 @@ import AspectRatio from '@mui/joy/AspectRatio';
import Box from '@mui/joy/Box'; import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button'; import Button from '@mui/joy/Button';
import Typography from '@mui/joy/Typography'; import Typography from '@mui/joy/Typography';
import { useDefaultTitle } from '@rakit/core'; import { useDefaultTitle, useTranslate } from '@rakit/core';
import HistoryIcon from '@mui/icons-material/History';
import { ColorSchemeToggle } from './ColorSchemeToggle'; import { ColorSchemeToggle } from './ColorSchemeToggle';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
@ -10,22 +11,22 @@ const Icon403 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id=":r2:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%"> <linearGradient id=":r2:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" /> <stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" /> <stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient> </linearGradient>
</defs> </defs>
<path fill="url(#:r2:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" /> <path fill="url(#:r2:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-4.webp" height="300" x="220" y="30" /> <image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-4.webp" height="300" x="220" y="30" />
<path fill="var(--palette-primary-main)" d="M425.545 119.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zm-321.3 81.8c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" /><path fill="#FFAB00" d="M111.045 142.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" /> <path fill="var(--palette-primary-main)" d="M425.545 119.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zm-321.3 81.8c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" /><path fill="#FFAB00" d="M111.045 142.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" />
<path fill="#FFD666" d="M111.045 121c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" /> <path fill="#FFD666" d="M111.045 121c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" />
<path fill="#FBCDBE" d="M278.045 250.1c-4.6-6.5-14 5.1-18.1 7.2-.6-2.1 1.5-41.3-1.4-41.8-2.8-3-8.1-.7-8 3.3.2-4 .5-11.3-5.6-10.2-4.8.6-3.8 6.9-3.8 10.2.1-6.1-9.5-6.1-9.4 0v5.6c.2-4.2-5.7-6.4-8.3-3-2.6-.2-.4 41.8-1.1 43.3-.2 10 8.7 19 18.8 18.7 6.1.4 12.6-1.2 16.8-5.9l19.7-21c1.7-1.6 1.8-4.5.4-6.4z" /> <path fill="#FBCDBE" d="M278.045 250.1c-4.6-6.5-14 5.1-18.1 7.2-.6-2.1 1.5-41.3-1.4-41.8-2.8-3-8.1-.7-8 3.3.2-4 .5-11.3-5.6-10.2-4.8.6-3.8 6.9-3.8 10.2.1-6.1-9.5-6.1-9.4 0v5.6c.2-4.2-5.7-6.4-8.3-3-2.6-.2-.4 41.8-1.1 43.3-.2 10 8.7 19 18.8 18.7 6.1.4 12.6-1.2 16.8-5.9l19.7-21c1.7-1.6 1.8-4.5.4-6.4z" />
<path fill="#000" fill-opacity="0.24" fill-rule="evenodd" d="M248.745 212.3v32.8h1.9v-31.9c.1-2.9-2.8-5.2-5.6-4.6 2 0 3.7 1.7 3.7 3.7zm-9.4 5.6v27.2h1.9v-26.3c.1-2.8-2.8-5.2-5.5-4.6 1.9 0 3.6 1.8 3.6 3.7zm-9.4 27.2v-21.6c.1-2-1.7-3.7-3.7-3.8 2.8-.6 5.6 1.8 5.5 4.6V245h-1.8v.1z" clip-rule="evenodd" /> <path fill="#000" fillOpacity="0.24" fillRule="evenodd" d="M248.745 212.3v32.8h1.9v-31.9c.1-2.9-2.8-5.2-5.6-4.6 2 0 3.7 1.7 3.7 3.7zm-9.4 5.6v27.2h1.9v-26.3c.1-2.8-2.8-5.2-5.5-4.6 1.9 0 3.6 1.8 3.6 3.7zm-9.4 27.2v-21.6c.1-2-1.7-3.7-3.7-3.8 2.8-.6 5.6 1.8 5.5 4.6V245h-1.8v.1z" clipRule="evenodd" />
<path fill="var(--palette-primary-darker)" d="M244.945 189.8c-67.6 1.3-77 97-11 111.4 81 11.8 92.7-107.3 11-111.4zm-48.5 56.2c-1-40.4 49.8-63.8 79.9-36.9l-68.3 68.3c-7.5-8.7-11.6-19.9-11.6-31.4zm48.5 48.5c-11.5 0-22.7-4.1-31.4-11.6l68.3-68.3c27 30.1 3.5 80.9-36.9 79.9z" /> <path fill="var(--palette-primary-darker)" d="M244.945 189.8c-67.6 1.3-77 97-11 111.4 81 11.8 92.7-107.3 11-111.4zm-48.5 56.2c-1-40.4 49.8-63.8 79.9-36.9l-68.3 68.3c-7.5-8.7-11.6-19.9-11.6-31.4zm48.5 48.5c-11.5 0-22.7-4.1-31.4-11.6l68.3-68.3c27 30.1 3.5 80.9-36.9 79.9z" />
<path fill="url(#paint0_linear_1_129)" d="M169.245 261h-11.3v-66.6c0-4.5-1.5-5.6-5.6-5.6-5.3.3-13.8-1.4-17.1 4l-55 68.3c-2.7 3.3-1.8 8.8-2 12.8 0 4.1 1.5 5.6 5.6 5.6h54.7v21.7c-.9 7.9 9.1 5.2 13.7 5.6 4.1 0 5.6-1.5 5.6-5.6v-21.7h11.4c4.4 0 5.6-1.5 5.6-5.6-.3-4.8 2-13.8-5.6-12.9zm-30.8 0h-36l36-44.4V261zm263.9 12.1c1.9 44.8-78.7 46-78 1.2h19.3c-.8 15.3 18.3 21.4 30.1 15.5 12.7-6 12.3-29.1-1-34-5.6-2.8-16.6-2-23.1-2.1v-15.1c6.3-.2 17.6.9 22.7-2.3 11.6-5.5 11.9-25.4.9-31.4-10.8-5.9-29 .1-28.2 14.5h-19.4c-.5-28.1 35.4-38.5 57-28.2 23.4 9 24.1 45.5-.2 54.6 12.3 3.9 20.1 14.6 19.9 27.3z" /> <path fill="url(#paint0_linear_1_129)" d="M169.245 261h-11.3v-66.6c0-4.5-1.5-5.6-5.6-5.6-5.3.3-13.8-1.4-17.1 4l-55 68.3c-2.7 3.3-1.8 8.8-2 12.8 0 4.1 1.5 5.6 5.6 5.6h54.7v21.7c-.9 7.9 9.1 5.2 13.7 5.6 4.1 0 5.6-1.5 5.6-5.6v-21.7h11.4c4.4 0 5.6-1.5 5.6-5.6-.3-4.8 2-13.8-5.6-12.9zm-30.8 0h-36l36-44.4V261zm263.9 12.1c1.9 44.8-78.7 46-78 1.2h19.3c-.8 15.3 18.3 21.4 30.1 15.5 12.7-6 12.3-29.1-1-34-5.6-2.8-16.6-2-23.1-2.1v-15.1c6.3-.2 17.6.9 22.7-2.3 11.6-5.5 11.9-25.4.9-31.4-10.8-5.9-29 .1-28.2 14.5h-19.4c-.5-28.1 35.4-38.5 57-28.2 23.4 9 24.1 45.5-.2 54.6 12.3 3.9 20.1 14.6 19.9 27.3z" />
<defs> <defs>
<linearGradient id="paint0_linear_1_129" x1="78.245" x2="78.245" y1="187.309" y2="307.306" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1_129" x1="78.245" x2="78.245" y1="187.309" y2="307.306" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" /> <stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" /> <stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
@ -35,11 +36,11 @@ const Icon404 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id=":rm9:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%"> <linearGradient id=":rm9:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" /> <stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" /> <stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient> </linearGradient>
</defs> </defs>
<path fill="url(#:rm9:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" /> <path fill="url(#:rm9:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-6.webp" height="300" x="205" y="30" /> <image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-6.webp" height="300" x="205" y="30" />
<path fill="#FFAB00" d="M111.1 141.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" /> <path fill="#FFAB00" d="M111.1 141.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" />
<path fill="#FFD666" d="M111.1 120c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" /> <path fill="#FFD666" d="M111.1 120c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" />
@ -48,8 +49,8 @@ const Icon404 = (
<path fill="var(--palette-primary-main)" d="M425.6 118.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zM104.3 200c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" /> <path fill="var(--palette-primary-main)" d="M425.6 118.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zM104.3 200c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" />
<defs> <defs>
<linearGradient id="paint0_linear_1_119" x1="78.3" x2="78.3" y1="187.77" y2="305.935" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1_119" x1="78.3" x2="78.3" y1="187.77" y2="305.935" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" /> <stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" /> <stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
@ -59,11 +60,11 @@ const Icon500 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id=":r1:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%"> <linearGradient id=":r1:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" /> <stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" /> <stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient> </linearGradient>
</defs> </defs>
<path fill="url(#:r1:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" /> <path fill="url(#:r1:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-8.webp" height="300" x="340" y="30" /> <image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-8.webp" height="300" x="340" y="30" />
<path fill="var(--palette-primary-main)" d="M292.4 266.4h-7.3v-.6h6.7v-59.6h-25.7V118h-23.6v-.6h24.2v88.2h25.7v60.8zM146 164.5h-.6v-21.1h16.5v-19h.6v19.7H146v20.4z" /> <path fill="var(--palette-primary-main)" d="M292.4 266.4h-7.3v-.6h6.7v-59.6h-25.7V118h-23.6v-.6h24.2v88.2h25.7v60.8zM146 164.5h-.6v-21.1h16.5v-19h.6v19.7H146v20.4z" />
<path fill="var(--palette-primary-main)" d="M242.5 112.3c0 3.2-1.3 6.3-3.5 8.5-2.3 2.3-5.3 3.5-8.5 3.5h-82.9c-4.4.1-8.5-2.2-10.7-5.9-2.2-3.8-2.2-8.5 0-12.3 2.2-3.8 6.3-6.1 10.7-5.9h2.8c-2-7.2-.6-14.9 3.9-20.8s11.6-9.4 19-9.4h7c8.9 0 17 4.9 21.1 12.8 2-1 4.2-1.6 6.5-1.6h1.8c3.8 0 7.4 1.5 10.1 4.2 2.7 2.7 4.2 6.3 4.2 10.1v.7c0 1.3-.2 2.7-.6 3.9h6.9c6.8.2 12.2 5.6 12.2 12.2z" opacity="0.08" /> <path fill="var(--palette-primary-main)" d="M242.5 112.3c0 3.2-1.3 6.3-3.5 8.5-2.3 2.3-5.3 3.5-8.5 3.5h-82.9c-4.4.1-8.5-2.2-10.7-5.9-2.2-3.8-2.2-8.5 0-12.3 2.2-3.8 6.3-6.1 10.7-5.9h2.8c-2-7.2-.6-14.9 3.9-20.8s11.6-9.4 19-9.4h7c8.9 0 17 4.9 21.1 12.8 2-1 4.2-1.6 6.5-1.6h1.8c3.8 0 7.4 1.5 10.1 4.2 2.7 2.7 4.2 6.3 4.2 10.1v.7c0 1.3-.2 2.7-.6 3.9h6.9c6.8.2 12.2 5.6 12.2 12.2z" opacity="0.08" />
@ -83,52 +84,76 @@ const Icon500 = (
<path fill="var(--palette-primary-darker)" d="M264.4 267.7c.5-1.8-.8-3.7-1.2-5.5-.1-.3-.1-.7 0-1 .2-1.5 1.5-2.6 3-2.6s2.8 1.1 3 2.6c.1.3 0 .7 0 1-.3 1.8-1.6 3.8-1.1 5.6l.4 1.3c.5 1.5-.7 3.1-2.3 3.1-1.6 0-2.7-1.5-2.3-3l.5-1.5zM258 158.8l9.2-4.8 8.8 4.8s-1.6 11.8-8.6 15.2c0 0-8.6-3.3-9.4-15.2z" /> <path fill="var(--palette-primary-darker)" d="M264.4 267.7c.5-1.8-.8-3.7-1.2-5.5-.1-.3-.1-.7 0-1 .2-1.5 1.5-2.6 3-2.6s2.8 1.1 3 2.6c.1.3 0 .7 0 1-.3 1.8-1.6 3.8-1.1 5.6l.4 1.3c.5 1.5-.7 3.1-2.3 3.1-1.6 0-2.7-1.5-2.3-3l.5-1.5zM258 158.8l9.2-4.8 8.8 4.8s-1.6 11.8-8.6 15.2c0 0-8.6-3.3-9.4-15.2z" />
<defs> <defs>
<linearGradient id="paint0_linear_1_140" x1="277.574" x2="255.652" y1="143.24" y2="187.057" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1_140" x1="277.574" x2="255.652" y1="143.24" y2="187.057" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-main)" /> <stop stopColor="var(--palette-primary-main)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" /> <stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient> </linearGradient>
<linearGradient id="paint1_linear_1_140" x1="138" x2="138" y1="164" y2="287.9" gradientUnits="userSpaceOnUse"> <linearGradient id="paint1_linear_1_140" x1="138" x2="138" y1="164" y2="287.9" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" /> <stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" /> <stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
); );
const statusIcons = { function resolveMessageByTitlte(title: ReactNode): string | undefined {
"403": Icon403, switch (title) {
"404": Icon404, case "ra.page.error":
"500": Icon500, return "ra.message.error";
}; case "ra.page.forbidden":
return "ra.message.forbidden";
case "ra.page.not_found":
return "ra.message.not_found";
default:
return undefined;
}
}
const statusTitles = { function resolveImageByTitle(title: ReactNode): string | undefined {
"403": "No permission", switch (title) {
"404": "Sorry, page not found!", case "ra.page.error":
"500": "Internal server error", return "error";
}; case "forbidden":
return "ra.message.forbidden";
case "ra.page.not_found":
return "not_found";
default:
return undefined;
}
}
const statusDescriptions = { function goBack() {
"403": "The page you’re trying to access has restricted access. Please refer to your system administrator.", window.history.go(-1);
"404": "Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.", }
"500": "There was an error, please try again later.",
};
const isStatus = (status: string | undefined): status is keyof typeof statusIcons => { function resolveImage(image: string) {
return !!status && Object.hasOwn(statusIcons, status); switch (image) {
case "forbidden":
return Icon403;
case "not_found":
return Icon404;
case "error":
default: // TODO
return Icon500;
}
} }
export interface StatusErrorProps { export interface PageErrorProps {
status?: string; image?: ReactNode;
icon?: ReactNode;
title?: ReactNode; title?: ReactNode;
description?: ReactNode; message?: ReactNode;
button?: ReactNode;
} }
export function StatusError(props: StatusErrorProps) { export function PageError(props: PageErrorProps) {
const status = isStatus(props.status) ? props.status : "500"; const { title = "ra.page.error", button } = props;
const image = props.image || resolveImageByTitle(title);
const message = props.message || resolveMessageByTitlte(title);
const translate = useTranslate();
const siteTitle = useDefaultTitle(); const siteTitle = useDefaultTitle();
const icon = props.icon || statusIcons[status]; const imageNode = typeof image === 'string' ? resolveImage(image) : image;
const title = props.title || statusTitles[status]; const titleNode = typeof title === 'string' ? translate(title) : title;
const description = props.description ?? statusDescriptions[status]; const messageNode = typeof message === 'string' ? translate(message) : message;
return ( return (
<Box <Box
@ -157,12 +182,17 @@ export function StatusError(props: StatusErrorProps) {
<ColorSchemeToggle variant="plain" /> <ColorSchemeToggle variant="plain" />
</Box> </Box>
<Box sx={{ maxWidth: "448px", p: 2 }}> <Box sx={{ maxWidth: "448px", p: 2 }}>
{titleNode != null ? (
<Typography level="h2" textAlign="center"> <Typography level="h2" textAlign="center">
{title} {titleNode}
</Typography> </Typography>
) : null}
{messageNode != null ? (
<Typography color="neutral" pt={2} textAlign="center"> <Typography color="neutral" pt={2} textAlign="center">
{description} {messageNode}
</Typography> </Typography>
) : null}
{imageNode != null ? (
<AspectRatio <AspectRatio
ratio="1" ratio="1"
sx={(theme) => ({ sx={(theme) => ({
@ -176,10 +206,20 @@ export function StatusError(props: StatusErrorProps) {
})} })}
variant="plain" variant="plain"
> >
{icon} {imageNode}
</AspectRatio> </AspectRatio>
) : null}
<Box my={2} textAlign="center"> <Box my={2} textAlign="center">
<Button size="lg" color="neutral">Go to home</Button> {button != null ? button : (
<Button
size="lg"
color="neutral"
onClick={goBack}
startDecorator={<HistoryIcon />}
>
{translate('ra.action.back')}
</Button>
)}
</Box> </Box>
</Box> </Box>
</Box> </Box>

@ -3,19 +3,19 @@ import ErrorIcon from '@mui/icons-material/Report';
import Accordion from '@mui/joy/Accordion'; import Accordion from '@mui/joy/Accordion';
import AccordionDetails from '@mui/joy/AccordionDetails'; import AccordionDetails from '@mui/joy/AccordionDetails';
import AccordionSummary from '@mui/joy/AccordionSummary'; import AccordionSummary from '@mui/joy/AccordionSummary';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button'; import Button from '@mui/joy/Button';
import { styled } from '@mui/joy/styles'; import { styled } from '@mui/joy/styles';
import Typography from '@mui/joy/Typography'; import Typography from '@mui/joy/Typography';
import { import {
Title,
useDefaultTitle, useDefaultTitle,
useErrorContext, useErrorContext,
useResetErrorBoundaryOnLocationChange, useResetErrorBoundaryOnLocationChange,
useTranslate useTranslate
} from '@rakit/core'; } from '@rakit/core';
import { Fragment } from 'react'; import { ColorSchemeToggle } from './ColorSchemeToggle';
export function Error() { export function RuntimeError() {
const title = useDefaultTitle(); const title = useDefaultTitle();
const { error, errorInfo, resetErrorBoundary } = useErrorContext(); const { error, errorInfo, resetErrorBoundary } = useErrorContext();
const translate = useTranslate(); const translate = useTranslate();
@ -23,8 +23,31 @@ export function Error() {
useResetErrorBoundaryOnLocationChange(resetErrorBoundary); useResetErrorBoundaryOnLocationChange(resetErrorBoundary);
return ( return (
<Fragment> <Box
{title && <Title title={title} />} sx={{
display: "flex",
flexDirection: "column",
justifyContent: {
sm: "flex-start",
md: "center",
},
alignItems: "center",
pt: 12,
pb: 8,
minHeight: "100dvh",
}}
>
<Box sx={{
position: "fixed",
top: 0,
insetInline: 0,
display: "flex",
justifyContent: "space-between",
p: 4,
}}>
<div>{title}</div>
<ColorSchemeToggle variant="plain" />
</Box>
<Root> <Root>
<h1 className={ErrorClasses.title} role="alert"> <h1 className={ErrorClasses.title} role="alert">
<ErrorIcon className={ErrorClasses.icon} /> <ErrorIcon className={ErrorClasses.icon} />
@ -84,7 +107,7 @@ export function Error() {
</Button> </Button>
</div> </div>
</Root> </Root>
</Fragment> </Box>
); );
} }

@ -1,11 +1,16 @@
export * from "./AppBar"; export * from "./AppBar";
export * from "./ColorSchemeToggle"; export * from "./ColorSchemeToggle";
export * from "./Error"; export * from "./Forbidden";
export * from "./Layout"; export * from "./Layout";
export * from "./Loading"; export * from "./Loading";
export * from "./LoadingIndicator";
export * from "./NotFound";
export * from "./Notification"; export * from "./Notification";
export * from "./PageActions"; export * from "./PageActions";
export * from "./PageDock";
export * from "./PageError";
export * from "./PageRoot"; export * from "./PageRoot";
export * from "./PageTitle";
export * from "./Sidebar"; export * from "./Sidebar";
export * from "./StatusError"; export * from "./RuntimeError";
export * from "./utils"; export * from "./utils";

@ -0,0 +1 @@
export * from "./useMediaQuery"

@ -0,0 +1,9 @@
import { Theme } from "@mui/joy/styles";
import {
UseMediaQueryOptions,
useMediaQuery as useMediaQueryForMuiSystem
} from "@mui/system";
export function useMediaQuery(queryInput: string | ((theme: Theme) => string), options?: UseMediaQueryOptions): boolean {
return useMediaQueryForMuiSystem<Theme>(queryInput);
}

@ -7,7 +7,7 @@ import {
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useNotify } from "../notification"; import { useNotify } from "../notification";
import { useBasename } from "../routing"; import { useBasename } from "../core";
import { getErrorMessage, removeDoubleSlashes } from "../util"; import { getErrorMessage, removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider"; import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useLogout } from "./useLogout"; import { useLogout } from "./useLogout";

@ -1,7 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { To } from "react-router-dom"; import { To } from "react-router-dom";
import { useNotify } from "../notification"; import { useNotify } from "../notification";
import { useBasename } from "../routing"; import { useBasename } from "../core";
import { getErrorMessage, removeDoubleSlashes } from "../util"; import { getErrorMessage, removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider"; import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useLogout } from "./useLogout"; import { useLogout } from "./useLogout";

@ -3,7 +3,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useRedirect } from "../routing"; import { useRedirect } from "../core";
import { AuthRedirectResult } from "./types"; import { AuthRedirectResult } from "./types";
import { useAuthProvider } from "./useAuthProvider"; import { useAuthProvider } from "./useAuthProvider";

@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useNotificationContext } from '../notification'; import { useNotificationContext } from '../notification';
import { defaultAuthParams, useAuthProvider } from './useAuthProvider'; import { defaultAuthParams, useAuthProvider } from './useAuthProvider';
import { useBasename } from '../routing'; import { useBasename } from '../core';
import { removeDoubleSlashes } from '../util'; import { removeDoubleSlashes } from '../util';
/** /**

@ -1,10 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { Path, useLocation, useNavigate } from "react-router-dom"; import { Path, useLocation, useNavigate } from "react-router-dom";
import { useBasename } from "../routing";
import { useResetStore } from "../store"; import { useResetStore } from "../store";
import { removeDoubleSlashes } from "../util"; import { removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider"; import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useBasename } from '../core';
/** /**
* Log the current user out by calling the authProvider.logout() method, * Log the current user out by calling the authProvider.logout() method,

@ -1,11 +1,26 @@
import { ComponentType, ReactElement, useEffect, useState } from 'react'; import {
import { Navigate, Route, Routes, To } from 'react-router-dom'; ComponentType,
import { LogoutOnMount, useCheckAuth } from '../auth'; useEffect,
useState
} from 'react';
import {
Navigate,
Route,
Routes,
To
} from 'react-router-dom';
import {
AccessControlProvider,
LogoutOnMount,
useCheckAuth
} from '../auth';
import { useScrollToTop } from '../scrollPosition'; import { useScrollToTop } from '../scrollPosition';
import { getReactElement } from '../util';
import { DefaultLayout } from './DefaultLayout'; import { DefaultLayout } from './DefaultLayout';
import { HasDashboardContextProvider } from './HasDashboardContextProvider'; import { HasDashboardContextProvider } from './HasDashboardContextProvider';
import { AdminChildren, CoreLayoutProps } from './types'; import {
AdminChildren,
CoreLayoutProps
} from './types';
import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren'; import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren';
export interface CoreAdminRoutesProps { export interface CoreAdminRoutesProps {
@ -39,7 +54,7 @@ export interface CoreAdminRoutesProps {
* </Admin> * </Admin>
* ); * );
*/ */
catchAll?: ComponentType<any> | ReactElement | null; catchAll?: ComponentType<any> | null;
children?: AdminChildren; children?: AdminChildren;
@ -59,7 +74,7 @@ export interface CoreAdminRoutesProps {
* /> * />
* ) * )
*/ */
dashboard?: ComponentType | ReactElement | null; dashboard?: ComponentType<any> | null;
initialLocation?: To; initialLocation?: To;
@ -87,7 +102,7 @@ export interface CoreAdminRoutesProps {
/** /**
* The component displayed while fetching the auth provider if the admin child is an async function * The component displayed while fetching the auth provider if the admin child is an async function
*/ */
loading?: ComponentType<any> | ReactElement | null; loading?: ComponentType<any> | null;
/** /**
* The page to display when the admin has no Resource children * The page to display when the admin has no Resource children
@ -109,7 +124,7 @@ export interface CoreAdminRoutesProps {
* </Admin> * </Admin>
* ); * );
*/ */
ready?: ComponentType<any> | ReactElement | null; ready?: ComponentType<any> | null;
/** /**
* Flag to require authentication for all routes. Defaults to false. * Flag to require authentication for all routes. Defaults to false.
@ -136,16 +151,20 @@ export interface CoreAdminRoutesProps {
export function CoreAdminRoutes(props: CoreAdminRoutesProps) { export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
useScrollToTop(); useScrollToTop();
const [routes, status] = useConfigureAdminRouterFromChildren(props.children); const {
routesWithLayout,
routesWithoutLayout,
status,
} = useConfigureAdminRouterFromChildren(props.children);
const { const {
catchAll: catchAllElement, catchAll: CatchAll,
dashboard, dashboard: Dashboard,
initialLocation, initialLocation,
layout: Layout = DefaultLayout, layout: Layout = DefaultLayout,
loading: loadingElement, loading: Loading,
requireAuth, requireAuth,
ready: readyElement, ready: Ready,
} = props; } = props;
const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth); const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth);
@ -168,23 +187,25 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
}, [checkAuth, requireAuth]); }, [checkAuth, requireAuth]);
if (status === 'empty') { if (status === 'empty') {
if (!readyElement) { if (!Ready) {
throw new Error( throw new Error(
'The admin is empty. Please provide an empty component, ' + 'The admin is empty. Please provide an empty component, ' +
'or pass Route or CustomRoutes as children.' 'or pass Route or CustomRoutes as children.'
); );
} }
return getReactElement(readyElement); return <Ready />;
} }
if (status === 'loading' || checkAuthLoading) { if (status === 'loading' || checkAuthLoading) {
return ( return (
<Routes> <Routes>
{/* Render the routes that were outside the child function. */}
{routesWithoutLayout}
<Route <Route
path="*" path="*"
element={ element={
<div style={{ height: '100vh' }}> <div style={{ height: '100vh' }}>
{loadingElement ? getReactElement(loadingElement) : 'loading...'} {Loading ? <Loading /> : null}
</div> </div>
} }
/> />
@ -195,6 +216,7 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
if (onlyAnonymousRoutes) { if (onlyAnonymousRoutes) {
return ( return (
<Routes> <Routes>
{routesWithoutLayout}
<Route <Route
path="*" path="*"
element={<LogoutOnMount />} element={<LogoutOnMount />}
@ -205,28 +227,31 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
return ( return (
<Routes> <Routes>
{routesWithoutLayout}
<Route <Route
path="/*" path="/*"
element={ element={
<HasDashboardContextProvider value={!!dashboard}> <HasDashboardContextProvider value={!!Dashboard}>
<AccessControlProvider>
<Layout> <Layout>
<Routes> <Routes>
{routes} {routesWithLayout}
{dashboard {Dashboard
? (<Route path="/" element={getReactElement(dashboard)} />) ? (<Route path="/" element={<Dashboard />} />)
: (initialLocation && initialLocation !== "/") : (initialLocation && initialLocation !== "/")
? (<Route path="/" element={<Navigate to={initialLocation} />} />) ? (<Route path="/" element={<Navigate to={initialLocation} />} />)
: null} : null}
{catchAllElement ? ( {CatchAll ? (
<Route <Route
path="*" path="*"
element={getReactElement(catchAllElement)} element={<CatchAll />}
/> />
) : null} ) : null}
</Routes> </Routes>
</Layout> </Layout>
</AccessControlProvider>
</HasDashboardContextProvider> </HasDashboardContextProvider>
} }
/> />

@ -1,12 +1,12 @@
import { ComponentType, ReactElement } from "react"; import { ComponentType, ReactElement } from "react";
import { Route, Routes, To } from "react-router-dom"; import { Route, Routes, To } from "react-router-dom";
import { DefaultTitleContextProvider } from "../title";
import { getReactElement } from "../util";
import { CoreAdminRoutes } from "./CoreAdminRoutes"; import { CoreAdminRoutes } from "./CoreAdminRoutes";
import { DefaultError } from "./DefaultError"; import { DefaultError } from "./DefaultError";
import { DefaultLayout } from "./DefaultLayout"; import { DefaultLayout } from "./DefaultLayout";
import { DefaultTitleContextProvider } from "./DefaultTitleContextProvider";
import { ErrorBoundary } from "./ErrorBoundary"; import { ErrorBoundary } from "./ErrorBoundary";
import { AdminChildren, CoreLayoutProps } from "./types"; import { AdminChildren, CoreLayoutProps } from "./types";
import { DefaultReady } from "./DefaultReady";
export interface CoreAdminUIProps { export interface CoreAdminUIProps {
/** /**
@ -29,7 +29,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
authCallbackPage?: ComponentType<any> | ReactElement | null; authCallbackPage?: ComponentType<any> | null;
/** /**
* A catch-all react component to display when the URL does not match any * A catch-all react component to display when the URL does not match any
@ -61,7 +61,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
catchAll?: ComponentType<any> | ReactElement | null; catchAll?: ComponentType<any> | null;
children?: AdminChildren; children?: AdminChildren;
@ -80,7 +80,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
dashboard?: ComponentType | ReactElement | null; dashboard?: ComponentType | null;
/** /**
* Set to true to disable anonymous telemetry collection * Set to true to disable anonymous telemetry collection
@ -111,7 +111,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
error?: ComponentType<any> | ReactElement | null; error?: ComponentType<any> | null;
initialLocation?: To; initialLocation?: To;
@ -139,7 +139,7 @@ export interface CoreAdminUIProps {
/** /**
* The component displayed while fetching the auth provider if the admin child is an async function * The component displayed while fetching the auth provider if the admin child is an async function
*/ */
loading?: ComponentType<any> | ReactElement | null; loading?: ComponentType<any> | null;
/** /**
* The component displayed when the user visits the /login page * The component displayed when the user visits the /login page
@ -160,7 +160,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
loginPage?: ComponentType<any> | ReactElement | null; loginPage?: ComponentType<any> | null;
/** /**
* The page to display when the admin has no Resource children * The page to display when the admin has no Resource children
@ -182,7 +182,7 @@ export interface CoreAdminUIProps {
* </Admin> * </Admin>
* ); * );
*/ */
ready?: ComponentType<any> | ReactElement | null; ready?: ComponentType<any> | null;
/** /**
* Flag to require authentication for all routes. Defaults to false. * Flag to require authentication for all routes. Defaults to false.
@ -223,7 +223,7 @@ export interface CoreAdminUIProps {
export const CoreAdminUI = (props: CoreAdminUIProps) => { export const CoreAdminUI = (props: CoreAdminUIProps) => {
const { const {
authCallbackPage, authCallbackPage: AuthCallbackPage,
catchAll, catchAll,
children, children,
dashboard, dashboard,
@ -232,8 +232,8 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => {
initialLocation, initialLocation,
layout = DefaultLayout, layout = DefaultLayout,
loading, loading,
loginPage, loginPage: LoginPage,
ready = Ready, ready = DefaultReady,
requireAuth = false, requireAuth = false,
title = 'React Admin', title = 'React Admin',
} = props; } = props;
@ -256,17 +256,17 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => {
<DefaultTitleContextProvider value={title}> <DefaultTitleContextProvider value={title}>
<ErrorBoundary error={error}> <ErrorBoundary error={error}>
<Routes> <Routes>
{loginPage != null ? ( {LoginPage != null ? (
<Route <Route
path="/login" path="/login"
element={getReactElement(loginPage)} element={<LoginPage />}
/> />
) : null} ) : null}
{authCallbackPage != null ? ( {AuthCallbackPage != null ? (
<Route <Route
path="/auth-callback" path="/auth-callback"
element={getReactElement(authCallbackPage)} element={<AuthCallbackPage />}
/> />
) : null} ) : null}

@ -0,0 +1,111 @@
import { useState } from 'react';
const styles = {
root: {
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column' as 'column',
fontFamily: '"Roboto", sans-serif',
},
main: {
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center' as 'center',
flexDirection: 'column' as 'column',
background: 'linear-gradient(135deg, #00023b 0%, #00023b 50%, #313264 100%)',
color: 'white',
fontSize: '1.5em',
fontWeight: 'bold' as 'bold',
},
secondary: {
height: '20vh',
background: '#e8e8e8',
color: 'black',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-evenly',
},
link: {
textAlign: 'center' as 'center',
width: 150,
display: 'block',
textDecoration: 'none',
color: 'black',
opacity: 0.7,
},
linkHovered: {
opacity: 1,
},
image: {
width: 50,
},
logo: {
height: 100,
},
};
const Button = ({ img, label }: {
img: string;
label: string;
href: string
}) => {
const [hovered, setHovered] = useState(false);
return (
<div>
<a
href="#"
style={
hovered
? { ...styles.link, ...styles.linkHovered }
: styles.link
}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<img src={img} alt={label} style={styles.image} />
<br />
{label}
</a>
</div>
);
};
export function DefaultReady() {
if (process.env.NODE_ENV === 'production') {
return <span />
}
return (
<div style={styles.root}>
<div style={styles.main}>
<img
style={styles.logo}
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhbHF1ZV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTMxIDEzMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTMxIDEzMTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMyMjI0NTg7fQoJLnN0MXtmaWxsOiM1MTUzN0Q7fQoJLnN0MntmaWxsOiNBM0E0Qjk7fQoJLnN0M3tmaWxsOiMwMDAyM0I7fQoJLnN0NHtmaWxsOiNGRkZGRkY7fQoJLnN0NXtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO30KPC9zdHlsZT4KPHRpdGxlPkxvZ29fc29tYnJlX2FpPC90aXRsZT4KPGcgaWQ9IlJlY3RhbmdsZV81NiI+Cgk8Zz4KCQk8cmVjdCB4PSIxOS4xIiB5PSIxOSIgdHJhbnNmb3JtPSJtYXRyaXgoMC41IC0wLjg2NiAwLjg2NiAwLjUgLTIzLjkyMjYgODkuNTQ2KSIgY2xhc3M9InN0MCIgd2lkdGg9IjkyLjkiIGhlaWdodD0iOTIuOSIvPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik04MywxMzAuM0wwLjgsODIuOUw0OC4yLDAuN2w4Mi4yLDQ3LjVMODMsMTMwLjN6IE0zLjUsODIuMWw3OC43LDQ1LjVsNDUuNS03OC43TDQ5LDMuNEwzLjUsODIuMXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmVjdGFuZ2xlXzU2LTIiPgoJPGc+CgkJPHJlY3QgeD0iMTkiIHk9IjE5LjEiIHRyYW5zZm9ybT0ibWF0cml4KDAuODY2IC0wLjUgMC41IDAuODY2IC0yMy45Nzc3IDQxLjUyNykiIGNsYXNzPSJzdDAiIHdpZHRoPSI5Mi45IiBoZWlnaHQ9IjkyLjkiLz4KCQk8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNDcuOSwxMzFMMCw0OEw4My4xLDBsNDgsODMuMUw0Ny45LDEzMXogTTQuMSw0OS4xbDQ1LDc3LjlsNzcuOS00NUw4Miw0LjFMNC4xLDQ5LjF6Ii8+Cgk8L2c+CjwvZz4KPGcgaWQ9IlJlY3RhbmdsZV81Ni0zIj4KCTxnPgoJCTxyZWN0IHg9IjE5LjEiIHk9IjE5IiBjbGFzcz0ic3QzIiB3aWR0aD0iOTIuOSIgaGVpZ2h0PSI5Mi45Ii8+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTExNC41LDExNC41SDE2LjZWMTYuNWg5Ny45VjExNC41eiBNMjEuNiwxMDkuNWg4Ny45VjIxLjVIMjEuNlYxMDkuNXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmEiPgoJPGcgY2xhc3M9InN0NSI+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTU5LDg2LjdsLTYuNy0xOS4yaC0xLjJIMzguOXYxOS4yaC01LjZWMzguNWgxOC41YzMuNiwwLDYuMywwLjYsOC4xLDEuOGMxLjgsMS4yLDMsMi44LDMuNSw0LjgKCQkJYzAuNSwyLDAuOCw0LjYsMC44LDcuOGMwLDMuNS0wLjQsNi40LTEuMyw4LjdjLTAuOCwyLjMtMi42LDMuOS01LjMsNC44TDY1LDg2LjdINTl6IE01NS43LDYxLjZjMS4yLTAuNywyLTEuNywyLjQtMwoJCQljMC40LTEuMywwLjYtMy4yLDAuNi01LjZjMC0yLjUtMC4yLTQuMy0wLjUtNS42Yy0wLjMtMS4zLTEuMS0yLjItMi4zLTIuOWMtMS4yLTAuNy0zLTEtNS41LTFIMzguOXYxOS4xSDUwCgkJCUM1Mi41LDYyLjYsNTQuNCw2Mi4zLDU1LjcsNjEuNnoiLz4KCQk8cGF0aCBjbGFzcz0ic3Q0IiBkPSJNNzQuMyw4NWMtMS42LTEuNS0yLjUtNC4yLTIuNS04LjJjMC0yLjcsMC4zLTQuOCwwLjktNi4zYzAuNi0xLjUsMS42LTIuNiwzLTMuM2MxLjQtMC43LDMuNC0xLDYtMQoJCQljMS4zLDAsNS4xLDAuMSwxMS40LDAuM3YtMi40YzAtMi45LTAuMi01LTAuNy02LjJjLTAuNS0xLjItMS4zLTItMi42LTIuNGMtMS4yLTAuMy0zLjMtMC41LTYuMy0wLjVjLTEuMywwLTMsMC4xLTQuOSwwLjIKCQkJYy0yLDAuMS0zLjYsMC4zLTQuOCwwLjV2LTQuM2MzLjMtMC43LDcuMS0xLDExLjQtMWMzLjcsMCw2LjUsMC40LDguNCwxLjJjMS44LDAuOCwzLjEsMi4yLDMuOCw0LjFjMC43LDEuOSwxLDQuNywxLDguNHYyMi41aC00LjgKCQkJbC0wLjMtNWgtMC4zYy0wLjgsMi4yLTIuMiwzLjctNC4xLDQuNGMtMS45LDAuNy00LjEsMS4xLTYuNiwxLjFDNzguNiw4Ny4yLDc2LDg2LjUsNzQuMyw4NXogTTg5LjEsODJjMS4yLTAuNCwyLjItMS4yLDIuOC0yLjQKCQkJYzAuOS0xLjgsMS4zLTQuMywxLjMtNy4zdi0yaC0xMGMtMS43LDAtMywwLjItMy44LDAuNWMtMC44LDAuMy0xLjQsMC45LTEuNywxLjhjLTAuMywwLjktMC41LDIuMi0wLjUsNGMwLDEuOCwwLjIsMy4xLDAuNiwzLjkKCQkJYzAuNCwwLjgsMS4xLDEuNCwyLDEuOGMxLDAuMywyLjUsMC41LDQuNSwwLjVDODYuMiw4Mi42LDg3LjgsODIuNCw4OS4xLDgyeiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
alt="react-admin logo" />
<h1>Welcome to Rakit</h1>
<div>
Your application is properly configured.
<br />
Now you can add a &lt;Route&gt; as child of
&lt;Admin&gt;.
</div>
</div>
<div style={styles.secondary}>
<Button
href="https://marmelab.com/react-admin/documentation.html"
img="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IgoJIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJCb3VuZGluZ19Cb3giPgoJPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CjwvZz4KPGcgaWQ9IkZsYXQiPgoJPGcgaWQ9InVpX3g1Rl9zcGVjX3g1Rl9oZWFkZXJfY29weV8yIj4KCTwvZz4KCTxnPgoJCTxwYXRoIGQ9Ik0yMSw1Yy0xLjExLTAuMzUtMi4zMy0wLjUtMy41LTAuNWMtMS45NSwwLTQuMDUsMC40LTUuNSwxLjVjLTEuNDUtMS4xLTMuNTUtMS41LTUuNS0xLjVTMi40NSw0LjksMSw2djE0LjY1CgkJCWMwLDAuMjUsMC4yNSwwLjUsMC41LDAuNWMwLjEsMCwwLjE1LTAuMDUsMC4yNS0wLjA1QzMuMSwyMC40NSw1LjA1LDIwLDYuNSwyMGMxLjk1LDAsNC4wNSwwLjQsNS41LDEuNWMxLjM1LTAuODUsMy44LTEuNSw1LjUtMS41CgkJCWMxLjY1LDAsMy4zNSwwLjMsNC43NSwxLjA1YzAuMSwwLjA1LDAuMTUsMC4wNSwwLjI1LDAuMDVjMC4yNSwwLDAuNS0wLjI1LDAuNS0wLjVWNkMyMi40LDUuNTUsMjEuNzUsNS4yNSwyMSw1eiBNMywxOC41VjcKCQkJYzEuMS0wLjM1LDIuMy0wLjUsMy41LTAuNWMxLjM0LDAsMy4xMywwLjQxLDQuNSwwLjk5djExLjVDOS42MywxOC40MSw3Ljg0LDE4LDYuNSwxOEM1LjMsMTgsNC4xLDE4LjE1LDMsMTguNXogTTIxLDE4LjUKCQkJYy0xLjEtMC4zNS0yLjMtMC41LTMuNS0wLjVjLTEuMzQsMC0zLjEzLDAuNDEtNC41LDAuOTlWNy40OWMxLjM3LTAuNTksMy4xNi0wLjk5LDQuNS0wLjk5YzEuMiwwLDIuNCwwLjE1LDMuNSwwLjVWMTguNXoiLz4KCQk8cGF0aCBvcGFjaXR5PSIwLjMiIGQ9Ik0xMSw3LjQ5QzkuNjMsNi45MSw3Ljg0LDYuNSw2LjUsNi41QzUuMyw2LjUsNC4xLDYuNjUsMyw3djExLjVDNC4xLDE4LjE1LDUuMywxOCw2LjUsMTgKCQkJYzEuMzQsMCwzLjEzLDAuNDEsNC41LDAuOTlWNy40OXoiLz4KCTwvZz4KCTxnPgoJCTxwYXRoIGQ9Ik0xNy41LDEwLjVjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNlY5LjI0QzE5LjIxLDkuMDksMTguMzYsOSwxNy41LDljLTEuMjgsMC0yLjQ2LDAuMTYtMy41LDAuNDd2MS41NwoJCQlDMTQuOTksMTAuNjksMTYuMTgsMTAuNSwxNy41LDEwLjV6Ii8+CgkJPHBhdGggZD0iTTE3LjUsMTMuMTZjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNlYxMS45Yy0wLjc5LTAuMTUtMS42NC0wLjI0LTIuNS0wLjI0Yy0xLjI4LDAtMi40NiwwLjE2LTMuNSwwLjQ3djEuNTcKCQkJQzE0Ljk5LDEzLjM2LDE2LjE4LDEzLjE2LDE3LjUsMTMuMTZ6Ii8+CgkJPHBhdGggZD0iTTE3LjUsMTUuODNjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNnYtMS41MmMtMC43OS0wLjE1LTEuNjQtMC4yNC0yLjUtMC4yNGMtMS4yOCwwLTIuNDYsMC4xNi0zLjUsMC40N3YxLjU3CgkJCUMxNC45OSwxNi4wMiwxNi4xOCwxNS44MywxNy41LDE1LjgzeiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
label="Documentation" />
<Button
href="https://github.com/marmelab/react-admin/tree/master/examples"
img="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJub25lIiBkPSJNMCAwaDI0djI0SDBWMHoiLz48cGF0aCBkPSJNOS40IDE2LjZMNC44IDEybDQuNi00LjZMOCA2bC02IDYgNiA2IDEuNC0xLjR6bTUuMiAwbDQuNi00LjYtNC42LTQuNkwxNiA2bDYgNi02IDYtMS40LTEuNHoiLz48L3N2Zz4="
label="Examples" />
<Button
href="https://stackoverflow.com/questions/tagged/react-admin"
img="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IgoJIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJCb3VuZGluZ19Cb3giPgoJPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CjwvZz4KPGcgaWQ9IkZsYXQiPgoJPGcgaWQ9InVpX3g1Rl9zcGVjX3g1Rl9oZWFkZXJfY29weV8yIj4KCTwvZz4KCTxnPgoJCTxjaXJjbGUgb3BhY2l0eT0iMC4zIiBjeD0iOSIgY3k9IjgiIHI9IjIiLz4KCQk8cGF0aCBvcGFjaXR5PSIwLjMiIGQ9Ik05LDE1Yy0yLjcsMC01LjgsMS4yOS02LDIuMDFMMywxOGgxMnYtMUMxNC44LDE2LjI5LDExLjcsMTUsOSwxNXoiLz4KCQk8cGF0aCBkPSJNMTYuNjcsMTMuMTNDMTguMDQsMTQuMDYsMTksMTUuMzIsMTksMTd2M2g0di0zQzIzLDE0LjgyLDE5LjQzLDEzLjUzLDE2LjY3LDEzLjEzeiIvPgoJCTxwYXRoIGQ9Ik0xNSwxMmMyLjIxLDAsNC0xLjc5LDQtNGMwLTIuMjEtMS43OS00LTQtNGMtMC40NywwLTAuOTEsMC4xLTEuMzMsMC4yNEMxNC41LDUuMjcsMTUsNi41OCwxNSw4cy0wLjUsMi43My0xLjMzLDMuNzYKCQkJQzE0LjA5LDExLjksMTQuNTMsMTIsMTUsMTJ6Ii8+CgkJPHBhdGggZD0iTTksMTJjMi4yMSwwLDQtMS43OSw0LTRjMC0yLjIxLTEuNzktNC00LTRTNSw1Ljc5LDUsOEM1LDEwLjIxLDYuNzksMTIsOSwxMnogTTksNmMxLjEsMCwyLDAuOSwyLDJjMCwxLjEtMC45LDItMiwyCgkJCVM3LDkuMSw3LDhDNyw2LjksNy45LDYsOSw2eiIvPgoJCTxwYXRoIGQ9Ik05LDEzYy0yLjY3LDAtOCwxLjM0LTgsNHYzaDE2di0zQzE3LDE0LjM0LDExLjY3LDEzLDksMTN6IE0xNSwxOEgzbDAtMC45OUMzLjIsMTYuMjksNi4zLDE1LDksMTVzNS44LDEuMjksNiwyVjE4eiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
label="Community" />
</div>
</div>
);
};

@ -0,0 +1,5 @@
import { ReactNode } from "react";
export function RoutesWithoutLayout(_props: { children?: ReactNode }) {
return null;
}

@ -6,10 +6,12 @@ export * from "./CoreAdminRoutes";
export * from "./CoreAdminUI"; export * from "./CoreAdminUI";
export * from "./DefaultError"; export * from "./DefaultError";
export * from "./DefaultLayout"; export * from "./DefaultLayout";
export * from "./DefaultReady";
export * from "./DefaultTitleContextProvider"; export * from "./DefaultTitleContextProvider";
export * from "./ErrorBoundary"; export * from "./ErrorBoundary";
export * from "./ErrorContextProvider"; export * from "./ErrorContextProvider";
export * from "./HasDashboardContextProvider"; export * from "./HasDashboardContextProvider";
export * from "./RoutesWithoutLayout";
export * from "./types"; export * from "./types";
export * from "./useBasename"; export * from "./useBasename";
export * from "./useConfigureAdminRouterFromChildren"; export * from "./useConfigureAdminRouterFromChildren";

@ -1,9 +1,12 @@
import { import {
Children, Children,
Dispatch,
Fragment, Fragment,
isValidElement, isValidElement,
PropsWithChildren,
ReactElement, ReactElement,
ReactNode, ReactNode,
SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useState useState
@ -11,6 +14,7 @@ import {
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import { useLogout, usePermissions } from "../auth"; import { useLogout, usePermissions } from "../auth";
import { useSafeSetState } from "../util"; import { useSafeSetState } from "../util";
import { RoutesWithoutLayout } from "./RoutesWithoutLayout";
import { import {
AdminChildren, AdminChildren,
AdminRouterStatus, AdminRouterStatus,
@ -47,20 +51,16 @@ function getRenderRoutesFunctions(children: AdminChildren): RenderRoutesFunction
return []; return [];
} }
export function useConfigureAdminRouterFromChildren(children: AdminChildren): [ReactElement[], AdminRouterStatus] { export function useConfigureAdminRouterFromChildren(children: AdminChildren) {
// Gather custom routes that were declared as direct children of AppRouter // Gather custom routes that were declared as direct children of AppRouter
// e.g. Not returned from the child function (if any) // e.g. Not returned from the child function (if any)
// We need to know right away whether some resources were declared to correctly // We need to know right away whether some resources were declared to correctly
// initialize the status at the next stop // initialize the status at the next stop
const doLogout = useLogout(); const doLogout = useLogout();
const { permissions, isPending } = usePermissions(); const { permissions, isPending } = usePermissions();
const [routes, setRoutes] = useState(getRoutesFromNodes(children)); const [routes, setRoutes, mergeRoutes] = useRoutesState(getRoutesFromNodes(children));
const [status, setStatus] = useSafeSetState<AdminRouterStatus>(() => getStatus(children, routes)); const [status, setStatus] = useSafeSetState<AdminRouterStatus>(() => getStatus(children, routes));
const mergeRoutes = useCallback((newRoutes: ReactElement[]) => {
setRoutes(previous => previous.concat(newRoutes));
}, [setRoutes]);
if (!status) { if (!status) {
throw new Error('Status should be defined'); throw new Error('Status should be defined');
} }
@ -70,9 +70,15 @@ export function useConfigureAdminRouterFromChildren(children: AdminChildren): [R
const onResolve = () => { const onResolve = () => {
funcCounts -= 1; funcCounts -= 1;
setTimeout(() => {
if (funcCounts <= 0) { if (funcCounts <= 0) {
setStatus('ready'); setStatus(
routes.withLayout.length > 0 || routes.withoutLayout.length > 0
? 'ready'
: 'empty'
);
} }
})
} }
const resolveChildFunction = async (childFunc: RenderRoutesFunction) => { const resolveChildFunction = async (childFunc: RenderRoutesFunction) => {
@ -99,11 +105,11 @@ export function useConfigureAdminRouterFromChildren(children: AdminChildren): [R
const updateFromChildren = async () => { const updateFromChildren = async () => {
const functionChild = getRenderRoutesFunctions(children); const functionChild = getRenderRoutesFunctions(children);
const newRoutes = getRoutesFromNodes(children); const newRoutes = getRoutesFromNodes(children);
mergeRoutes(newRoutes); setRoutes(newRoutes);
setStatus( setStatus(
functionChild.length > 0 functionChild.length > 0
? 'loading' ? 'loading'
: newRoutes.length > 0 : newRoutes.withLayout.length > 0 || newRoutes.withoutLayout.length > 0
? 'ready' ? 'ready'
: 'empty' : 'empty'
); );
@ -131,13 +137,52 @@ export function useConfigureAdminRouterFromChildren(children: AdminChildren): [R
setStatus, setStatus,
]); ]);
return [routes, status]; return {
routesWithLayout: routes.withLayout,
routesWithoutLayout: routes.withoutLayout,
status,
};
}
type Routes = {
withLayout: ReactNode[],
withoutLayout: ReactNode[],
} }
function getStatus(children: AdminChildren, routes: ReactNode[]): AdminRouterStatus { /*
* A hook that store the routes and resources just like setState but also provides an additional function
* to merge new routes and resources with the existing ones.
*/
const useRoutesState = (initialState: Routes): [
Routes,
Dispatch<SetStateAction<Routes>>,
(newRoutes: Routes) => void,
] => {
const [routes, setRoutes] = useState(initialState);
const mergeRoutes = useCallback(
(newRouteGroups: Routes) => {
setRoutes(previous => ({
withLayout: previous.withLayout.concat(
newRouteGroups.withLayout
),
withoutLayout:
previous.withoutLayout.concat(
newRouteGroups.withoutLayout
),
}));
},
[]
);
return [routes, setRoutes, mergeRoutes];
};
function getStatus(children: AdminChildren, routes: Routes): AdminRouterStatus {
return getRenderRoutesFunctions(children).length > 0 return getRenderRoutesFunctions(children).length > 0
? 'loading' ? 'loading'
: routes.length > 0 : routes.withLayout.length > 0 || routes.withoutLayout.length
? 'ready' ? 'ready'
: 'empty'; : 'empty';
} }
@ -145,11 +190,15 @@ function getStatus(children: AdminChildren, routes: ReactNode[]): AdminRouterSta
/** /**
* Inspect the children and return an array of routable elements * Inspect the children and return an array of routable elements
*/ */
function getRoutesFromNodes(children: AdminChildren): ReactElement[] { function getRoutesFromNodes(children: AdminChildren): Routes {
const routes: ReactElement[] = []; const withLayout: ReactNode[] = [];
const withoutLayout: ReactNode[] = [];
if (isRenderRoutesFunction(children)) { if (isRenderRoutesFunction(children)) {
return routes; return {
withLayout,
withoutLayout,
}
} }
if ( if (
@ -166,9 +215,16 @@ function getRoutesFromNodes(children: AdminChildren): ReactElement[] {
// conditionals in their route config. // conditionals in their route config.
return; return;
} else if (node.type === Fragment) { } else if (node.type === Fragment) {
routes.push(...getRoutesFromNodes(node.props.children)); const customRoutesFromFragment = getRoutesFromNodes(node.props.children);
withLayout.push(...customRoutesFromFragment.withLayout);
withoutLayout.push(...customRoutesFromFragment.withoutLayout);
} else if (node.type === Route) { } else if (node.type === Route) {
routes.push(node); withLayout.push(node);
} else if (node.type === RoutesWithoutLayout) {
const customRoutesElement = node as ReactElement<PropsWithChildren>;
if (customRoutesElement.props.children != null) {
withoutLayout.push(customRoutesElement.props.children);
}
} else if (process.env.NODE_ENV !== "production") { } else if (process.env.NODE_ENV !== "production") {
// TODO 获取 node.type 的 displayName // TODO 获取 node.type 的 displayName
const name = typeof node.type === "string" const name = typeof node.type === "string"
@ -176,10 +232,13 @@ function getRoutesFromNodes(children: AdminChildren): ReactElement[] {
: ((node.type as any).displayName || node.type.name); : ((node.type as any).displayName || node.type.name);
throw new Error( throw new Error(
`[${name}] is not a <Route> component. ` + `[${name}] is not a <Route> component. ` +
`All component children of <Routes> must be a <Route> or <React.Fragment>` `All component children of <Routes> must be a <Route> or <RoutesWithoutLayout> or <React.Fragment>`
); );
} }
}); });
return routes; return {
withLayout,
withoutLayout,
};
} }

Loading…
Cancel
Save