Compare commits
2 Commits
b4e047a10c
...
358b0b8c32
Author | SHA1 | Date |
---|---|---|
熊二 | 358b0b8c32 | 3 months ago |
熊二 | bf1d289e8e | 3 months ago |
@ -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" /> |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1,14 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "simple-admin", |
|
||||||
"type": "module", |
|
||||||
"description": "low level admin & dashboard scaffold", |
|
||||||
"dependencies": { |
|
||||||
"@rakit/core": "workspace:*", |
|
||||||
"@rakit/use-async": "workspace:*", |
|
||||||
"@rakit/use-fetch": "workspace:*", |
|
||||||
"@rakit/use-invariant": "workspace:*", |
|
||||||
"react": "^18.3.1", |
|
||||||
"react-dom": "^18.3.1", |
|
||||||
"react-router-dom": "^6.26.1" |
|
||||||
} |
|
||||||
} |
|
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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 = {} |
@ -1,40 +0,0 @@ |
|||||||
import { CssVarsProvider } from '@mui/joy/styles'; |
|
||||||
import CssBaseline from '@mui/joy/CssBaseline'; |
|
||||||
import Box from '@mui/joy/Box'; |
|
||||||
|
|
||||||
import { AppBar } from './AppBar'; |
|
||||||
import { Sidebar } from './Sidebar'; |
|
||||||
import { Outlet } from 'react-router-dom'; |
|
||||||
|
|
||||||
export function AdminRoot() { |
|
||||||
return ( |
|
||||||
<CssVarsProvider disableTransitionOnChange> |
|
||||||
<CssBaseline /> |
|
||||||
<Box sx={{ display: 'flex', minHeight: '100dvh' }}> |
|
||||||
<AppBar /> |
|
||||||
<Sidebar /> |
|
||||||
<Box |
|
||||||
component="main" |
|
||||||
className="MainContent" |
|
||||||
sx={{ |
|
||||||
px: { xs: 2, md: 6 }, |
|
||||||
pt: { |
|
||||||
xs: 'calc(12px + var(--Header-height))', |
|
||||||
sm: 'calc(12px + var(--Header-height))', |
|
||||||
md: 3, |
|
||||||
}, |
|
||||||
pb: { xs: 2, sm: 2, md: 3 }, |
|
||||||
flex: 1, |
|
||||||
display: 'flex', |
|
||||||
flexDirection: 'column', |
|
||||||
minWidth: 0, |
|
||||||
height: '100dvh', |
|
||||||
gap: 1, |
|
||||||
}} |
|
||||||
> |
|
||||||
<Outlet /> |
|
||||||
</Box> |
|
||||||
</Box> |
|
||||||
</CssVarsProvider> |
|
||||||
); |
|
||||||
} |
|
@ -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,35 @@ |
|||||||
|
import Box from '@mui/joy/Box'; |
||||||
|
|
||||||
|
import { AppBar } from './AppBar'; |
||||||
|
import { Sidebar } from './Sidebar'; |
||||||
|
import { Outlet } from 'react-router-dom'; |
||||||
|
|
||||||
|
export function Layout() { |
||||||
|
return ( |
||||||
|
<Box sx={{ display: 'flex', minHeight: '100dvh' }}> |
||||||
|
<AppBar /> |
||||||
|
<Sidebar /> |
||||||
|
<Box |
||||||
|
component="main" |
||||||
|
className="MainContent" |
||||||
|
sx={{ |
||||||
|
px: { xs: 2, md: 6 }, |
||||||
|
pt: { |
||||||
|
xs: 'calc(12px + var(--Header-height))', |
||||||
|
sm: 'calc(12px + var(--Header-height))', |
||||||
|
md: 3, |
||||||
|
}, |
||||||
|
pb: { xs: 2, sm: 2, md: 3 }, |
||||||
|
flex: 1, |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
minWidth: 0, |
||||||
|
height: '100dvh', |
||||||
|
gap: 1, |
||||||
|
}} |
||||||
|
> |
||||||
|
<Outlet /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
} |
@ -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" |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import { Portlet } from "@rakit/core"; |
||||||
|
import Box, { BoxProps } from "@mui/joy/Box"; |
||||||
|
|
||||||
|
export interface PageDockProps extends BoxProps { } |
||||||
|
|
||||||
|
export function PageDock(props: PageDockProps) { |
||||||
|
if (props.children == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Portlet to="page-dock"> |
||||||
|
<Box |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</Portlet> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
import { |
||||||
|
Portlet, |
||||||
|
useDefaultTitle, |
||||||
|
useRecordRepresentation, |
||||||
|
useTranslate |
||||||
|
} from "@rakit/core"; |
||||||
|
import { ReactElement, JSX } from "react"; |
||||||
|
|
||||||
|
export interface PageTitleProps extends JSX.ElementAttributesProperty { |
||||||
|
record?: any; |
||||||
|
preferenceKey?: string | false; |
||||||
|
title?: string | ReactElement; |
||||||
|
} |
||||||
|
|
||||||
|
export function PageTitle(props: PageTitleProps) { |
||||||
|
const { |
||||||
|
record, |
||||||
|
preferenceKey, |
||||||
|
title, |
||||||
|
...rest |
||||||
|
} = props; |
||||||
|
|
||||||
|
const translate = useTranslate(); |
||||||
|
const defaultTitle = useDefaultTitle(); |
||||||
|
const titleFromPreferences = useRecordRepresentation({ |
||||||
|
record, |
||||||
|
representation: preferenceKey === false ? undefined : preferenceKey, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!title && !titleFromPreferences && !defaultTitle) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Portlet to="page-title"> |
||||||
|
<span {...rest}> |
||||||
|
{ |
||||||
|
titleFromPreferences |
||||||
|
? translate( |
||||||
|
titleFromPreferences, |
||||||
|
{ ...record, _: titleFromPreferences }, |
||||||
|
) |
||||||
|
: !title |
||||||
|
? defaultTitle |
||||||
|
: typeof title === "string" |
||||||
|
? translate(title, { _: title }) |
||||||
|
: title |
||||||
|
} |
||||||
|
</span> |
||||||
|
</Portlet> |
||||||
|
); |
||||||
|
} |
@ -1,15 +0,0 @@ |
|||||||
import Typography, { TypographyProps } from '@mui/joy/Typography'; |
|
||||||
|
|
||||||
export function TitlePortal(props: TypographyProps) { |
|
||||||
return ( |
|
||||||
<Typography |
|
||||||
sx={{ color: "inherit" }} |
|
||||||
flex="1" |
|
||||||
textOverflow="ellipsis" |
|
||||||
whiteSpace="nowrap" |
|
||||||
overflow="hidden" |
|
||||||
level="title-md" |
|
||||||
id="react-admin-title" |
|
||||||
{...props} /> |
|
||||||
); |
|
||||||
} |
|
@ -1,12 +1,16 @@ |
|||||||
export * from "./AdminRoot"; |
|
||||||
export * from "./AppBar"; |
export * from "./AppBar"; |
||||||
export * from "./ColorSchemeToggle"; |
export * from "./ColorSchemeToggle"; |
||||||
export * from "./Error"; |
export * from "./Forbidden"; |
||||||
|
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 "./TitlePortal"; |
|
||||||
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); |
||||||
|
} |
@ -1,7 +0,0 @@ |
|||||||
import { createContext } from "react"; |
|
||||||
import { AccessControlContextValue } from "./types"; |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export const AccessControlContext = createContext<AccessControlContextValue | undefined>(undefined); |
|
@ -1,16 +0,0 @@ |
|||||||
import { ReactNode } from "react"; |
|
||||||
import { AccessControlContext } from "./AccessControlContext"; |
|
||||||
import { CanFunction } from "./types"; |
|
||||||
|
|
||||||
export type AccessControlProviderProps = { |
|
||||||
can?: CanFunction; |
|
||||||
children?: ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
export function AccessControlProvider(props: AccessControlProviderProps) { |
|
||||||
return ( |
|
||||||
<AccessControlContext.Provider value={props.value}> |
|
||||||
{props.children} |
|
||||||
</AccessControlContext.Provider> |
|
||||||
) |
|
||||||
} |
|
@ -1,96 +0,0 @@ |
|||||||
import { |
|
||||||
cloneElement, |
|
||||||
createElement, |
|
||||||
isValidElement, |
|
||||||
ReactNode, |
|
||||||
useEffect |
|
||||||
} from "react"; |
|
||||||
import { |
|
||||||
AccessControlOptions, |
|
||||||
AccessFallbackComponent, |
|
||||||
CanParams |
|
||||||
} from "./types"; |
|
||||||
import { useCan } from "./useCan"; |
|
||||||
import { useAccessControl } from "./useAccessControl"; |
|
||||||
|
|
||||||
type OnUnauthorizedProps = { |
|
||||||
reason?: string; |
|
||||||
params: CanParams; |
|
||||||
}; |
|
||||||
|
|
||||||
export interface CanAccessProps extends CanParams { |
|
||||||
children: ReactNode; |
|
||||||
|
|
||||||
/** |
|
||||||
* Content to show if access control returns `false` |
|
||||||
*/ |
|
||||||
fallback?: AccessFallbackComponent; |
|
||||||
|
|
||||||
loading?: ReactNode |
|
||||||
|
|
||||||
queryOptions?: AccessControlOptions; |
|
||||||
|
|
||||||
/** |
|
||||||
* Callback function to be called if access control returns `can: false` |
|
||||||
*/ |
|
||||||
onUnauthorized?: (props: OnUnauthorizedProps) => void; |
|
||||||
} |
|
||||||
|
|
||||||
export function CanAccess(props: CanAccessProps) { |
|
||||||
const { |
|
||||||
children, |
|
||||||
fallback, |
|
||||||
loading, |
|
||||||
queryOptions, |
|
||||||
onUnauthorized, |
|
||||||
...params |
|
||||||
} = props; |
|
||||||
|
|
||||||
const { |
|
||||||
isExecuting: isLoading, |
|
||||||
reason, |
|
||||||
can, |
|
||||||
error, |
|
||||||
} = useCan({ |
|
||||||
...params, |
|
||||||
queryOptions, |
|
||||||
}); |
|
||||||
|
|
||||||
const { |
|
||||||
fallback: fallbackElement, |
|
||||||
} = useAccessControl({}); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (onUnauthorized && can === false && !isLoading) { |
|
||||||
onUnauthorized({ reason, params }); |
|
||||||
} |
|
||||||
}, [can, isLoading]); |
|
||||||
|
|
||||||
if (isLoading) { |
|
||||||
return loading; |
|
||||||
} |
|
||||||
|
|
||||||
if (can) { |
|
||||||
return children; |
|
||||||
} |
|
||||||
|
|
||||||
return resolveFallback( |
|
||||||
fallback ?? fallbackElement, |
|
||||||
reason, |
|
||||||
error, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function resolveFallback( |
|
||||||
fallback: AccessFallbackComponent | undefined | null, |
|
||||||
reason: string | undefined, |
|
||||||
error: unknown, |
|
||||||
) { |
|
||||||
if (fallback == null) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
if (isValidElement(fallback)) { |
|
||||||
return cloneElement(fallback, { reason, error }) |
|
||||||
} |
|
||||||
return createElement(fallback, { reason, error }) |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export * from "./AccessControlProvider"; |
|
||||||
export * from "./CanAccess"; |
|
||||||
export * from "./types"; |
|
||||||
export * from "./useAccessControl"; |
|
||||||
export * from "./useCan"; |
|
@ -1,52 +0,0 @@ |
|||||||
import { UseAsyncOptions } from "@rakit/use-async"; |
|
||||||
import { ComponentType, ReactElement } from "react"; |
|
||||||
|
|
||||||
export type CanReturn = { |
|
||||||
can: boolean; |
|
||||||
reason?: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export interface AccessParamsCustoms {} |
|
||||||
|
|
||||||
export type AccessParams = Record<string, any> & AccessParamsCustoms; |
|
||||||
|
|
||||||
export interface CanParams { |
|
||||||
/** |
|
||||||
* Resource name for API data interactions |
|
||||||
*/ |
|
||||||
on?: string; |
|
||||||
/** |
|
||||||
* Intended action on resource |
|
||||||
*/ |
|
||||||
key: string; |
|
||||||
/** |
|
||||||
* Parameters associated with the resource |
|
||||||
* @type { |
|
||||||
* resource?: [IResourceItem](https://refine.dev/docs/api-reference/core/interfaceReferences/#canparams),
|
|
||||||
* id?: [BaseKey](https://refine.dev/docs/api-reference/core/interfaceReferences/#basekey), [key: string]: any
|
|
||||||
* } |
|
||||||
*/ |
|
||||||
params?: AccessParams; |
|
||||||
} |
|
||||||
|
|
||||||
export type CanFunction = (params: CanParams) => Promise<CanReturn> | CanReturn; |
|
||||||
|
|
||||||
export type AccessControlOptions = Omit< |
|
||||||
UseAsyncOptions<CanReturn, CanParams, unknown>, |
|
||||||
'executor' | 'variables' |
|
||||||
>; |
|
||||||
|
|
||||||
export type AccessFallbackProps = { |
|
||||||
reason?: string; |
|
||||||
error?: unknown; |
|
||||||
} |
|
||||||
|
|
||||||
export type AccessFallbackComponent = ComponentType<AccessFallbackProps> | ReactElement<AccessFallbackProps>; |
|
||||||
|
|
||||||
export interface AccessControlContextCustomValue {} |
|
||||||
|
|
||||||
export interface AccessControlContextValue extends AccessControlContextCustomValue { |
|
||||||
can?: CanFunction; |
|
||||||
queryOptions?: AccessControlOptions; |
|
||||||
fallback?: AccessFallbackComponent; |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
import { useContext } from "react"; |
|
||||||
import { AccessControlContext } from "./AccessControlContext"; |
|
||||||
import { AccessControlContextValue } from "./types"; |
|
||||||
|
|
||||||
export function useAccessControl(): AccessControlContextValue | undefined; |
|
||||||
export function useAccessControl(overrides: AccessControlContextValue): AccessControlContextValue; |
|
||||||
export function useAccessControl(overrides?: AccessControlContextValue) { |
|
||||||
const fromContext = useContext(AccessControlContext) |
|
||||||
|
|
||||||
if (fromContext != null) { |
|
||||||
return { |
|
||||||
...fromContext, |
|
||||||
...overrides, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return overrides; |
|
||||||
} |
|
@ -1,70 +0,0 @@ |
|||||||
import { |
|
||||||
useAsync, |
|
||||||
UseAsyncResult, |
|
||||||
useAutomatic |
|
||||||
} from "@rakit/use-async"; |
|
||||||
import { |
|
||||||
AccessControlOptions, |
|
||||||
CanParams, |
|
||||||
CanReturn |
|
||||||
} from "./types"; |
|
||||||
import { useAccessControl } from "./useAccessControl"; |
|
||||||
import { useMemo } from "react"; |
|
||||||
|
|
||||||
export type UseCanProps = CanParams & { |
|
||||||
queryOptions?: AccessControlOptions; |
|
||||||
} |
|
||||||
|
|
||||||
export type UseCanResult = |
|
||||||
& Omit< |
|
||||||
UseAsyncResult<CanReturn, CanParams, unknown>, |
|
||||||
'execute' | 'abort' | 'data' | 'isExecuting' |
|
||||||
> |
|
||||||
& { isLoading: boolean } |
|
||||||
& CanReturn; |
|
||||||
|
|
||||||
export function useCan(options: UseCanProps): UseCanResult { |
|
||||||
const { queryOptions, ...params } = options; |
|
||||||
const context = useAccessControl({}); |
|
||||||
const can = context?.can |
|
||||||
|
|
||||||
const asyncOptions = { |
|
||||||
...context?.queryOptions, |
|
||||||
...queryOptions, |
|
||||||
} |
|
||||||
|
|
||||||
const { |
|
||||||
data, |
|
||||||
error, |
|
||||||
isExecuting: isLoading, |
|
||||||
execute, |
|
||||||
} = useAsync<CanReturn, CanParams, unknown>({ |
|
||||||
...asyncOptions, |
|
||||||
variables: params, |
|
||||||
executor: (params) => can?.(params) ?? ({ can: true }), |
|
||||||
immediate: typeof can !== "undefined", |
|
||||||
}); |
|
||||||
|
|
||||||
useAutomatic({ |
|
||||||
execute, |
|
||||||
params, |
|
||||||
asyncOptions, |
|
||||||
}); |
|
||||||
|
|
||||||
return useMemo(() => { |
|
||||||
if (typeof can === "undefined") { |
|
||||||
return { |
|
||||||
can: true, |
|
||||||
isLoading: false, |
|
||||||
error: undefined, |
|
||||||
reason: undefined, |
|
||||||
} |
|
||||||
} |
|
||||||
return { |
|
||||||
can: data?.can ?? false, |
|
||||||
error, |
|
||||||
isLoading, |
|
||||||
reason: data?.reason, |
|
||||||
} |
|
||||||
}, [can, data, error, isLoading]); |
|
||||||
} |
|
@ -0,0 +1,21 @@ |
|||||||
|
import { createContext } from "react"; |
||||||
|
import { |
||||||
|
AccessFallbackComponent, |
||||||
|
CanAccessOptions, |
||||||
|
CanAccessResult, |
||||||
|
Permission |
||||||
|
} from "./types"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @private |
||||||
|
*/ |
||||||
|
export interface AccessControlContextValue { |
||||||
|
canAccess?: (options: CanAccessOptions) => CanAccessResult; |
||||||
|
permissions?: Permission[]; |
||||||
|
accessFallback?: AccessFallbackComponent; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @private |
||||||
|
*/ |
||||||
|
export const AccessControlContext = createContext<AccessControlContextValue>({}); |
@ -0,0 +1,37 @@ |
|||||||
|
import { ReactNode } from "react"; |
||||||
|
import { AccessControlContext } from "./AccessControlContext"; |
||||||
|
import { |
||||||
|
AccessFallbackComponent, |
||||||
|
CanAccessOptions, |
||||||
|
CanAccessResult |
||||||
|
} from "./types"; |
||||||
|
import { useAuthProvider } from "./useAuthProvider"; |
||||||
|
import { usePermissions } from "./usePermissions"; |
||||||
|
|
||||||
|
interface AccessControlProviderProps { |
||||||
|
accessFallback?: AccessFallbackComponent; |
||||||
|
canAccess?: (options: CanAccessOptions) => CanAccessResult; |
||||||
|
children?: ReactNode; |
||||||
|
} |
||||||
|
|
||||||
|
export function AccessControlProvider(props: AccessControlProviderProps) { |
||||||
|
const authProvider = useAuthProvider(); |
||||||
|
const { |
||||||
|
accessFallback = authProvider?.accessFallback, |
||||||
|
canAccess = authProvider?.canAccess, |
||||||
|
children |
||||||
|
} = props; |
||||||
|
const { permissions } = usePermissions(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<AccessControlContext.Provider |
||||||
|
value={{ |
||||||
|
accessFallback, |
||||||
|
canAccess, |
||||||
|
permissions |
||||||
|
}} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</AccessControlContext.Provider> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
import { |
||||||
|
createElement, |
||||||
|
ReactNode, |
||||||
|
useContext |
||||||
|
} from "react"; |
||||||
|
import { AccessControlContext } from "./AccessControlContext"; |
||||||
|
import { |
||||||
|
AccessFallbackComponent, |
||||||
|
PermissionIdentifier, |
||||||
|
PermissionTarget |
||||||
|
} from "./types"; |
||||||
|
import { useCan } from "./useCan"; |
||||||
|
|
||||||
|
export interface CanAccessProps { |
||||||
|
target: PermissionTarget; |
||||||
|
identifier: PermissionIdentifier; |
||||||
|
fallback?: AccessFallbackComponent; |
||||||
|
children?: ReactNode; |
||||||
|
} |
||||||
|
|
||||||
|
export function CanAccess(props: CanAccessProps) { |
||||||
|
const { target, identifier } = props; |
||||||
|
const { can, reason } = useCan(target, identifier); |
||||||
|
const { accessFallback } = useContext(AccessControlContext); |
||||||
|
const { children, fallback = accessFallback } = props; |
||||||
|
|
||||||
|
if (can) { |
||||||
|
return children; |
||||||
|
} |
||||||
|
|
||||||
|
if (fallback == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return createElement(fallback, { |
||||||
|
reason, |
||||||
|
target, |
||||||
|
identifier |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
import { |
||||||
|
useContext, |
||||||
|
useEffect, |
||||||
|
useMemo, |
||||||
|
useState |
||||||
|
} from "react"; |
||||||
|
import { |
||||||
|
CanAccessResult, |
||||||
|
PermissionIdentifier, |
||||||
|
PermissionTarget |
||||||
|
} from "./types"; |
||||||
|
import { AccessControlContext } from "./AccessControlContext"; |
||||||
|
|
||||||
|
export function useCan( |
||||||
|
target: PermissionTarget, |
||||||
|
identifier: PermissionIdentifier, |
||||||
|
): CanAccessResult { |
||||||
|
const { canAccess, permissions } = useContext(AccessControlContext); |
||||||
|
const [can, setCan] = useState(false); |
||||||
|
const [reason, setReason] = useState<string>(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
let can = false; |
||||||
|
let reason: string | undefined = "forbidden"; |
||||||
|
if (permissions?.length) { |
||||||
|
for (const perm of permissions) { |
||||||
|
if (perm.identifier !== identifier) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (perm.target !== target) { |
||||||
|
break; |
||||||
|
} |
||||||
|
if (canAccess) { |
||||||
|
({ can, reason } = canAccess({ |
||||||
|
target, |
||||||
|
permissions, |
||||||
|
identifier, |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
setCan(can) |
||||||
|
setReason(reason) |
||||||
|
}, [ |
||||||
|
permissions, |
||||||
|
canAccess, |
||||||
|
target, |
||||||
|
identifier, |
||||||
|
]); |
||||||
|
|
||||||
|
return useMemo(() => { |
||||||
|
return { |
||||||
|
can, |
||||||
|
reason, |
||||||
|
} |
||||||
|
}, [can, reason]); |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
import { CoreAdminContext, CoreAdminContextProps } from './CoreAdminContext'; |
||||||
|
import { CoreAdminUI, CoreAdminUIProps } from './CoreAdminUI'; |
||||||
|
|
||||||
|
export type CoreAdminProps = CoreAdminContextProps & CoreAdminUIProps; |
||||||
|
|
||||||
|
/** |
||||||
|
* Main admin component, entry point to the application. |
||||||
|
* |
||||||
|
* Initializes the various contexts (auth, data, i18n, router) |
||||||
|
* and defines the main routes. |
||||||
|
* |
||||||
|
* Expects a list of resources as children, or a function returning a list of |
||||||
|
* resources based on the permissions. |
||||||
|
* |
||||||
|
* @example |
||||||
|
* |
||||||
|
* // static list of resources
|
||||||
|
* |
||||||
|
* import { |
||||||
|
* CoreAdmin, |
||||||
|
* Resource, |
||||||
|
* ListGuesser, |
||||||
|
* useDataProvider, |
||||||
|
* } from 'ra-core'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <CoreAdmin dataProvider={myDataProvider}> |
||||||
|
* <Resource name="posts" list={ListGuesser} /> |
||||||
|
* </CoreAdmin> |
||||||
|
* ); |
||||||
|
* |
||||||
|
* // dynamic list of resources based on permissions
|
||||||
|
* |
||||||
|
* import { |
||||||
|
* CoreAdmin, |
||||||
|
* Resource, |
||||||
|
* ListGuesser, |
||||||
|
* useDataProvider, |
||||||
|
* } from 'ra-core'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <CoreAdmin dataProvider={myDataProvider}> |
||||||
|
* {permissions => [ |
||||||
|
* <Resource name="posts" key="posts" list={ListGuesser} />, |
||||||
|
* ]} |
||||||
|
* </CoreAdmin> |
||||||
|
* ); |
||||||
|
* |
||||||
|
* // If you have to build a dynamic list of resources using a side effect,
|
||||||
|
* // you can't use <CoreAdmin>. But as it delegates to sub components,
|
||||||
|
* // it's relatively straightforward to replace it:
|
||||||
|
* |
||||||
|
* import * as React from 'react'; |
||||||
|
* import { useEffect, useState } from 'react'; |
||||||
|
* import { |
||||||
|
* CoreAdminContext, |
||||||
|
* CoreAdminUI, |
||||||
|
* Resource, |
||||||
|
* ListGuesser, |
||||||
|
* useDataProvider, |
||||||
|
* } from 'ra-core'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <CoreAdminContext dataProvider={myDataProvider}> |
||||||
|
* <UI /> |
||||||
|
* </CoreAdminContext> |
||||||
|
* ); |
||||||
|
* |
||||||
|
* const UI = () => { |
||||||
|
* const [resources, setResources] = useState([]); |
||||||
|
* const dataProvider = useDataProvider(); |
||||||
|
* useEffect(() => { |
||||||
|
* dataProvider.introspect().then(r => setResources(r)); |
||||||
|
* }, []); |
||||||
|
* |
||||||
|
* return ( |
||||||
|
* <CoreAdminUI> |
||||||
|
* {resources.map(resource => ( |
||||||
|
* <Resource name={resource.name} key={resource.key} list={ListGuesser} /> |
||||||
|
* ))} |
||||||
|
* </CoreAdminUI> |
||||||
|
* ); |
||||||
|
* }; |
||||||
|
*/ |
||||||
|
export const CoreAdmin = (props: CoreAdminProps) => { |
||||||
|
const { |
||||||
|
authCallbackPage, |
||||||
|
authProvider, |
||||||
|
basename, |
||||||
|
catchAll, |
||||||
|
children, |
||||||
|
dashboard, |
||||||
|
dataProvider, |
||||||
|
disableTelemetry, |
||||||
|
error, |
||||||
|
i18nProvider, |
||||||
|
initialLocation, |
||||||
|
queryClient, |
||||||
|
layout, |
||||||
|
loading, |
||||||
|
loginPage, |
||||||
|
ready, |
||||||
|
requireAuth, |
||||||
|
store, |
||||||
|
title = 'React Admin', |
||||||
|
} = props; |
||||||
|
return ( |
||||||
|
<CoreAdminContext |
||||||
|
authProvider={authProvider} |
||||||
|
basename={basename} |
||||||
|
dataProvider={dataProvider} |
||||||
|
i18nProvider={i18nProvider} |
||||||
|
queryClient={queryClient} |
||||||
|
store={store} |
||||||
|
> |
||||||
|
<CoreAdminUI |
||||||
|
authCallbackPage={authCallbackPage} |
||||||
|
catchAll={catchAll} |
||||||
|
dashboard={dashboard} |
||||||
|
disableTelemetry={disableTelemetry} |
||||||
|
error={error} |
||||||
|
initialLocation={initialLocation} |
||||||
|
layout={layout} |
||||||
|
loading={loading} |
||||||
|
loginPage={loginPage} |
||||||
|
ready={ready} |
||||||
|
requireAuth={requireAuth} |
||||||
|
title={title} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</CoreAdminUI> |
||||||
|
</CoreAdminContext> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,293 @@ |
|||||||
|
import { ComponentType, ReactElement } from "react"; |
||||||
|
import { Route, Routes, To } from "react-router-dom"; |
||||||
|
import { CoreAdminRoutes } from "./CoreAdminRoutes"; |
||||||
|
import { DefaultError } from "./DefaultError"; |
||||||
|
import { DefaultLayout } from "./DefaultLayout"; |
||||||
|
import { DefaultTitleContextProvider } from "./DefaultTitleContextProvider"; |
||||||
|
import { ErrorBoundary } from "./ErrorBoundary"; |
||||||
|
import { AdminChildren, CoreLayoutProps } from "./types"; |
||||||
|
import { DefaultReady } from "./DefaultReady"; |
||||||
|
|
||||||
|
export interface CoreAdminUIProps { |
||||||
|
/** |
||||||
|
* The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#authcallbackpage
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* import { authProvider } from './authProvider'; |
||||||
|
* import MyAuthCallbackPage from './MyAuthCallbackPage'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin |
||||||
|
* authCallbackPage={MyAuthCallbackPage} |
||||||
|
* authProvider={authProvider} |
||||||
|
* dataProvider={dataProvider} |
||||||
|
* > |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
authCallbackPage?: ComponentType<any> | null; |
||||||
|
|
||||||
|
/** |
||||||
|
* A catch-all react component to display when the URL does not match any |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#catchall
|
||||||
|
* @example |
||||||
|
* // in src/NotFound.js
|
||||||
|
* import Card from '@mui/material/Card'; |
||||||
|
* import CardContent from '@mui/material/CardContent'; |
||||||
|
* import { Title } from 'react-admin'; |
||||||
|
* |
||||||
|
* export const NotFound = () => ( |
||||||
|
* <Card> |
||||||
|
* <Title title="Not Found" /> |
||||||
|
* <CardContent> |
||||||
|
* <h1>404: Page not found</h1> |
||||||
|
* </CardContent> |
||||||
|
* </Card> |
||||||
|
* ); |
||||||
|
* |
||||||
|
* // in src/App.js
|
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* import { NotFound } from './NotFound'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin catchAll={NotFound} dataProvider={dataProvider}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
catchAll?: ComponentType<any> | null; |
||||||
|
|
||||||
|
children?: AdminChildren; |
||||||
|
|
||||||
|
/** |
||||||
|
* The component to use for the dashboard page (displayed on the `/` route). |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#dashboard
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import Dashboard from './Dashboard'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin dashboard={Dashboard} dataProvider={dataProvider}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
dashboard?: ComponentType | null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Set to true to disable anonymous telemetry collection |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#disabletelemetry
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin disableTelemetry dataProvider={dataProvider}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
disableTelemetry?: boolean; |
||||||
|
|
||||||
|
/** |
||||||
|
* The component displayed when an error is caught in a child component |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#error
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { MyError } from './error'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin error={MyError}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
error?: ComponentType<any> | null; |
||||||
|
|
||||||
|
initialLocation?: To; |
||||||
|
|
||||||
|
/** |
||||||
|
* The main app layout component |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#layout
|
||||||
|
* @example |
||||||
|
* import { Admin, Layout } from 'react-admin'; |
||||||
|
* |
||||||
|
* const MyLayout = ({ children }) => ( |
||||||
|
* <Layout appBarAlwaysOn> |
||||||
|
* {children} |
||||||
|
* </Layout> |
||||||
|
* ); |
||||||
|
* |
||||||
|
* export const App = () => ( |
||||||
|
* <Admin dataProvider={dataProvider} layout={MyLayout}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
layout?: ComponentType<CoreLayoutProps>; |
||||||
|
|
||||||
|
/** |
||||||
|
* The component displayed while fetching the auth provider if the admin child is an async function |
||||||
|
*/ |
||||||
|
loading?: ComponentType<any> | null; |
||||||
|
|
||||||
|
/** |
||||||
|
* The component displayed when the user visits the /login page |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#loginpage
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* import { authProvider } from './authProvider'; |
||||||
|
* import MyLoginPage from './MyLoginPage'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin |
||||||
|
* loginPage={MyLoginPage} |
||||||
|
* authProvider={authProvider} |
||||||
|
* dataProvider={dataProvider} |
||||||
|
* > |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
loginPage?: ComponentType<any> | null; |
||||||
|
|
||||||
|
/** |
||||||
|
* The page to display when the admin has no Resource children |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#ready
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* |
||||||
|
* const Ready = () => ( |
||||||
|
* <div> |
||||||
|
* <h1>Admin ready</h1> |
||||||
|
* <p>You can now add resources</p> |
||||||
|
* </div> |
||||||
|
* ) |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin ready={Ready}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
ready?: ComponentType<any> | null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Flag to require authentication for all routes. Defaults to false. |
||||||
|
* |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#requireauth
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* import { authProvider } from './authProvider'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin |
||||||
|
* requireAuth |
||||||
|
* authProvider={authProvider} |
||||||
|
* dataProvider={dataProvider} |
||||||
|
* > |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
requireAuth?: boolean; |
||||||
|
|
||||||
|
/** |
||||||
|
* The title of the error page |
||||||
|
* @see https://marmelab.com/react-admin/Admin.html#title
|
||||||
|
* @example |
||||||
|
* import { Admin } from 'react-admin'; |
||||||
|
* import { dataProvider } from './dataProvider'; |
||||||
|
* |
||||||
|
* const App = () => ( |
||||||
|
* <Admin title="My Admin" dataProvider={dataProvider}> |
||||||
|
* ... |
||||||
|
* </Admin> |
||||||
|
* ); |
||||||
|
*/ |
||||||
|
title?: string | ReactElement | null; |
||||||
|
} |
||||||
|
|
||||||
|
export const CoreAdminUI = (props: CoreAdminUIProps) => { |
||||||
|
const { |
||||||
|
authCallbackPage: AuthCallbackPage, |
||||||
|
catchAll, |
||||||
|
children, |
||||||
|
dashboard, |
||||||
|
// disableTelemetry = false,
|
||||||
|
error = DefaultError, |
||||||
|
initialLocation, |
||||||
|
layout = DefaultLayout, |
||||||
|
loading, |
||||||
|
loginPage: LoginPage, |
||||||
|
ready = DefaultReady, |
||||||
|
requireAuth = false, |
||||||
|
title = 'React Admin', |
||||||
|
} = props; |
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (
|
||||||
|
// disableTelemetry ||
|
||||||
|
// process.env.NODE_ENV !== 'production' ||
|
||||||
|
// typeof window === 'undefined' ||
|
||||||
|
// typeof window.location === 'undefined' ||
|
||||||
|
// typeof Image === 'undefined'
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const img = new Image();
|
||||||
|
// img.src = `https://react-admin-telemetry.marmelab.com/react-admin-telemetry?domain=${window.location.hostname}`;
|
||||||
|
// }, [disableTelemetry]);
|
||||||
|
|
||||||
|
return ( |
||||||
|
<DefaultTitleContextProvider value={title}> |
||||||
|
<ErrorBoundary error={error}> |
||||||
|
<Routes> |
||||||
|
{LoginPage != null ? ( |
||||||
|
<Route |
||||||
|
path="/login" |
||||||
|
element={<LoginPage />} |
||||||
|
/> |
||||||
|
) : null} |
||||||
|
|
||||||
|
{AuthCallbackPage != null ? ( |
||||||
|
<Route |
||||||
|
path="/auth-callback" |
||||||
|
element={<AuthCallbackPage />} |
||||||
|
/> |
||||||
|
) : null} |
||||||
|
|
||||||
|
<Route |
||||||
|
path="/*" |
||||||
|
element={ |
||||||
|
<CoreAdminRoutes |
||||||
|
catchAll={catchAll} |
||||||
|
dashboard={dashboard} |
||||||
|
initialLocation={initialLocation} |
||||||
|
layout={layout} |
||||||
|
loading={loading} |
||||||
|
ready={ready} |
||||||
|
requireAuth={requireAuth} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</CoreAdminRoutes> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Routes> |
||||||
|
</ErrorBoundary> |
||||||
|
</DefaultTitleContextProvider> |
||||||
|
); |
||||||
|
}; |
@ -1,271 +0,0 @@ |
|||||||
import { ComponentType, ReactElement, useMemo } from "react"; |
|
||||||
import { Route as ReactRoute, Routes } from "react-router-dom"; |
|
||||||
import { CoreAppRoutes, CoreAppRoutesProps } from "./CoreAppRoutes"; |
|
||||||
import { defaultStore, Store, StoreContextProvider } from "../store"; |
|
||||||
import { AuthContext, AuthProvider } from "../auth"; |
|
||||||
import { I18nContextProvider, I18nProvider } from "../i18n"; |
|
||||||
import { NotificationContextProvider } from "../notification"; |
|
||||||
import { AppRouter } from "../routing"; |
|
||||||
import { DefaultTitleContextProvider } from "../title"; |
|
||||||
import { ErrorBoundary } from "../errorBoundary"; |
|
||||||
import { DataProvider, DataProviderContext } from "../data"; |
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
|
||||||
|
|
||||||
export interface CoreAppContextProps extends CoreAppRoutesProps { |
|
||||||
/** |
|
||||||
* The authentication provider for security and permissions |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Authentication.html
|
|
||||||
* @example |
|
||||||
* import authProvider from './authProvider'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin authProvider={authProvider}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
authProvider?: AuthProvider; |
|
||||||
|
|
||||||
/** |
|
||||||
* The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#authcallbackpage
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* import { authProvider } from './authProvider'; |
|
||||||
* import MyAuthCallbackPage from './MyAuthCallbackPage'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin |
|
||||||
* authCallbackPage={MyAuthCallbackPage} |
|
||||||
* authProvider={authProvider} |
|
||||||
* dataProvider={dataProvider} |
|
||||||
* > |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
authCallbackPage?: ComponentType<any> | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* The base path for all URLs generated by react-admin. |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#using-react-admin-in-a-sub-path
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { BrowserRouter } from 'react-router-dom'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <BrowserRouter> |
|
||||||
* <Admin basename="/admin" dataProvider={dataProvider}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* </BrowserRouter> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
basename?: string; |
|
||||||
|
|
||||||
dataProvider?: DataProvider; |
|
||||||
|
|
||||||
/** |
|
||||||
* The component displayed when an error is caught in a child component |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#error
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { MyError } from './error'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin error={MyError}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
error?: ComponentType<any> | ReactElement | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* The internationalization provider for translations |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Translation.html
|
|
||||||
* @example |
|
||||||
* // in src/i18nProvider.js
|
|
||||||
* import polyglotI18nProvider from 'ra-i18n-polyglot'; |
|
||||||
* import fr from 'ra-language-french'; |
|
||||||
* |
|
||||||
* export const i18nProvider = polyglotI18nProvider(() => fr, 'fr'); |
|
||||||
* |
|
||||||
* // in src/App.js
|
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* import { i18nProvider } from './i18nProvider'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
i18nProvider?: I18nProvider; |
|
||||||
|
|
||||||
/** |
|
||||||
* The component displayed when the user visits the /login page |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#loginpage
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* import { authProvider } from './authProvider'; |
|
||||||
* import MyLoginPage from './MyLoginPage'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin |
|
||||||
* loginPage={MyLoginPage} |
|
||||||
* authProvider={authProvider} |
|
||||||
* dataProvider={dataProvider} |
|
||||||
* > |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
loginPage?: ComponentType<any> | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* The react-query client |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#queryclient
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { QueryClient } from '@tanstack/react-query'; |
|
||||||
* |
|
||||||
* const queryClient = new QueryClient({ |
|
||||||
* defaultOptions: { |
|
||||||
* queries: { |
|
||||||
* retry: false, |
|
||||||
* structuralSharing: false, |
|
||||||
* }, |
|
||||||
* mutations: { |
|
||||||
* retryDelay: 10000, |
|
||||||
* }, |
|
||||||
* }, |
|
||||||
* }); |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin queryClient={queryClient} dataProvider={...}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
queryClient?: QueryClient; |
|
||||||
|
|
||||||
/** |
|
||||||
* The adapter for storing user preferences |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#store
|
|
||||||
* @example |
|
||||||
* import { Admin, memoryStore } from 'react-admin'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin dataProvider={dataProvider} store={memoryStore()}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
store?: Store; |
|
||||||
|
|
||||||
/** |
|
||||||
* The title of the error page |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#title
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin title="My Admin" dataProvider={dataProvider}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
title?: string | ReactElement | null; |
|
||||||
} |
|
||||||
|
|
||||||
export function CoreAppContext(props: CoreAppContextProps) { |
|
||||||
const { |
|
||||||
authProvider, |
|
||||||
authCallbackPage: LoginCallbackPage, |
|
||||||
basename, |
|
||||||
catchAll, |
|
||||||
children, |
|
||||||
dashboard, |
|
||||||
dataProvider, |
|
||||||
error, |
|
||||||
i18nProvider, |
|
||||||
initialLocation, |
|
||||||
layout, |
|
||||||
loading, |
|
||||||
loginPage: LoginPage, |
|
||||||
queryClient, |
|
||||||
ready, |
|
||||||
requireAuth, |
|
||||||
store = defaultStore, |
|
||||||
title, |
|
||||||
} = props; |
|
||||||
|
|
||||||
const finalQueryClient = useMemo( |
|
||||||
() => queryClient || new QueryClient(), |
|
||||||
[queryClient] |
|
||||||
); |
|
||||||
|
|
||||||
return ( |
|
||||||
<AuthContext.Provider value={authProvider}> |
|
||||||
<DataProviderContext.Provider value={dataProvider}> |
|
||||||
<I18nContextProvider value={i18nProvider}> |
|
||||||
<NotificationContextProvider> |
|
||||||
<StoreContextProvider value={store}> |
|
||||||
<QueryClientProvider client={finalQueryClient}> |
|
||||||
<AppRouter basename={basename}> |
|
||||||
<DefaultTitleContextProvider value={title}> |
|
||||||
<ErrorBoundary error={error}> |
|
||||||
<Routes> |
|
||||||
{LoginPage != null ? ( |
|
||||||
<ReactRoute |
|
||||||
path="/login" |
|
||||||
element={<LoginPage />} |
|
||||||
/> |
|
||||||
) : null} |
|
||||||
|
|
||||||
{LoginCallbackPage != null ? ( |
|
||||||
<ReactRoute |
|
||||||
path="/auth-callback" |
|
||||||
element={<LoginCallbackPage />} |
|
||||||
/> |
|
||||||
) : null} |
|
||||||
|
|
||||||
<ReactRoute |
|
||||||
path="/*" |
|
||||||
element={ |
|
||||||
<CoreAppRoutes |
|
||||||
catchAll={catchAll} |
|
||||||
dashboard={dashboard} |
|
||||||
initialLocation={initialLocation} |
|
||||||
layout={layout} |
|
||||||
loading={loading} |
|
||||||
requireAuth={requireAuth} |
|
||||||
ready={ready} |
|
||||||
> |
|
||||||
{children} |
|
||||||
</CoreAppRoutes> |
|
||||||
} |
|
||||||
/> |
|
||||||
</Routes> |
|
||||||
</ErrorBoundary> |
|
||||||
</DefaultTitleContextProvider> |
|
||||||
</AppRouter> |
|
||||||
</QueryClientProvider> |
|
||||||
</StoreContextProvider> |
|
||||||
</NotificationContextProvider> |
|
||||||
</I18nContextProvider> |
|
||||||
</DataProviderContext.Provider> |
|
||||||
</AuthContext.Provider> |
|
||||||
); |
|
||||||
} |
|
@ -1,277 +0,0 @@ |
|||||||
import { |
|
||||||
ComponentType, |
|
||||||
ReactElement, |
|
||||||
ReactNode, |
|
||||||
useEffect, |
|
||||||
useState |
|
||||||
} from 'react'; |
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'; |
|
||||||
import { LogoutOnMount, useCheckAuth } from '../auth'; |
|
||||||
import { InitialLocationContextProvider } from '../routing'; |
|
||||||
import { getReactElement } from '../util'; |
|
||||||
import { DefaultLayout } from './DefaultLayout'; |
|
||||||
import { CoreLayoutProps } from './types'; |
|
||||||
import { useConfigureRoutesFromChildren } from './useConfigureRoutesFromChildren'; |
|
||||||
import { useScrollToTop } from '../scrollPosition'; |
|
||||||
import { HasDashboardContextProvider } from './HasDashboardContextProvider'; |
|
||||||
|
|
||||||
export interface CoreAppRoutesProps { |
|
||||||
/** |
|
||||||
* A catch-all react component to display when the URL does not match any |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#catchall
|
|
||||||
* @example |
|
||||||
* // in src/NotFound.js
|
|
||||||
* import Card from '@mui/material/Card'; |
|
||||||
* import CardContent from '@mui/material/CardContent'; |
|
||||||
* import { Title } from 'react-admin'; |
|
||||||
* |
|
||||||
* export const NotFound = () => ( |
|
||||||
* <Card> |
|
||||||
* <Title title="Not Found" /> |
|
||||||
* <CardContent> |
|
||||||
* <h1>404: Page not found</h1> |
|
||||||
* </CardContent> |
|
||||||
* </Card> |
|
||||||
* ); |
|
||||||
* |
|
||||||
* // in src/App.js
|
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* import { NotFound } from './NotFound'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin catchAll={NotFound} dataProvider={dataProvider}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
catchAll?: ComponentType<any> | ReactElement | null; |
|
||||||
|
|
||||||
children?: ReactNode; |
|
||||||
|
|
||||||
/** |
|
||||||
* @example |
|
||||||
* import { WithPermissions } from "rwas"; |
|
||||||
* import HomepageView from "./views/Homepage"; |
|
||||||
* |
|
||||||
* const authParams = { |
|
||||||
* params: { route: 'homepage' }, |
|
||||||
* }; |
|
||||||
* |
|
||||||
* const firstpage = ( |
|
||||||
* <WithPermissions |
|
||||||
* authParams={authParams} |
|
||||||
* component={HomepageView} |
|
||||||
* /> |
|
||||||
* ) |
|
||||||
*/ |
|
||||||
dashboard?: ComponentType | ReactElement | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* @example |
|
||||||
* import { WithPermissions } from "rwas"; |
|
||||||
* import HomepageView from "./views/Homepage"; |
|
||||||
* |
|
||||||
* const authParams = { |
|
||||||
* params: { route: 'homepage' }, |
|
||||||
* }; |
|
||||||
* |
|
||||||
* const firstpage = ( |
|
||||||
* <WithPermissions |
|
||||||
* authParams={authParams} |
|
||||||
* component={HomepageView} |
|
||||||
* /> |
|
||||||
* ) |
|
||||||
*/ |
|
||||||
homepage?: ComponentType<any> | ReactElement | null; |
|
||||||
|
|
||||||
initialLocation?: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* The main app layout component |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#layout
|
|
||||||
* @example |
|
||||||
* import { Admin, Layout } from 'react-admin'; |
|
||||||
* |
|
||||||
* const MyLayout = ({ children }) => ( |
|
||||||
* <Layout appBarAlwaysOn> |
|
||||||
* {children} |
|
||||||
* </Layout> |
|
||||||
* ); |
|
||||||
* |
|
||||||
* export const App = () => ( |
|
||||||
* <Admin dataProvider={dataProvider} layout={MyLayout}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
layout?: ComponentType<CoreLayoutProps>; |
|
||||||
|
|
||||||
/** |
|
||||||
* The component displayed while fetching the auth provider if the admin child is an async function |
|
||||||
*/ |
|
||||||
loading?: ComponentType<any> | ReactElement | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* The page to display when the admin has no Resource children |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#ready
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* |
|
||||||
* const Ready = () => ( |
|
||||||
* <div> |
|
||||||
* <h1>Admin ready</h1> |
|
||||||
* <p>You can now add resources</p> |
|
||||||
* </div> |
|
||||||
* ) |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin ready={Ready}> |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
ready?: ComponentType<any> | ReactElement | null; |
|
||||||
|
|
||||||
/** |
|
||||||
* Flag to require authentication for all routes. Defaults to false. |
|
||||||
* |
|
||||||
* @see https://marmelab.com/react-admin/Admin.html#requireauth
|
|
||||||
* @example |
|
||||||
* import { Admin } from 'react-admin'; |
|
||||||
* import { dataProvider } from './dataProvider'; |
|
||||||
* import { authProvider } from './authProvider'; |
|
||||||
* |
|
||||||
* const App = () => ( |
|
||||||
* <Admin |
|
||||||
* requireAuth |
|
||||||
* authProvider={authProvider} |
|
||||||
* dataProvider={dataProvider} |
|
||||||
* > |
|
||||||
* ... |
|
||||||
* </Admin> |
|
||||||
* ); |
|
||||||
*/ |
|
||||||
requireAuth?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export function CoreAppRoutes(props: CoreAppRoutesProps) { |
|
||||||
useScrollToTop(); |
|
||||||
|
|
||||||
const [routes, status] = useConfigureRoutesFromChildren(props.children); |
|
||||||
|
|
||||||
const { |
|
||||||
catchAll: catchAllElement, |
|
||||||
dashboard, |
|
||||||
homepage: homepageElement, |
|
||||||
initialLocation, |
|
||||||
layout: Layout = DefaultLayout, |
|
||||||
loading: loadingElement, |
|
||||||
requireAuth, |
|
||||||
ready: readyElement, |
|
||||||
} = props; |
|
||||||
|
|
||||||
const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth); |
|
||||||
const [checkAuthLoading, setCheckAuthLoading] = useState(requireAuth); |
|
||||||
const checkAuth = useCheckAuth(); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (requireAuth) { |
|
||||||
// do not log the user out on failure to allow access to custom routes with no layout
|
|
||||||
// for other routes, the LogoutOnMount component will log the user out
|
|
||||||
checkAuth(undefined, false) |
|
||||||
.then(() => { |
|
||||||
setOnlyAnonymousRoutes(false); |
|
||||||
}) |
|
||||||
.catch(() => { }) |
|
||||||
.finally(() => { |
|
||||||
setCheckAuthLoading(false); |
|
||||||
}); |
|
||||||
} |
|
||||||
}, [checkAuth, requireAuth]); |
|
||||||
|
|
||||||
if (status === 'empty') { |
|
||||||
if (!readyElement) { |
|
||||||
throw new Error( |
|
||||||
'The admin is empty. Please provide an empty component, ' + |
|
||||||
'or pass Route or CustomRoutes as children.' |
|
||||||
); |
|
||||||
} |
|
||||||
return getReactElement(readyElement); |
|
||||||
} |
|
||||||
|
|
||||||
if (status === 'loading' || checkAuthLoading) { |
|
||||||
return ( |
|
||||||
<Routes> |
|
||||||
<Route |
|
||||||
path="*" |
|
||||||
element={ |
|
||||||
<div style={{ height: '100vh' }}> |
|
||||||
{loadingElement ? getReactElement(loadingElement) : 'loading...'} |
|
||||||
</div> |
|
||||||
} |
|
||||||
/> |
|
||||||
</Routes> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
if (onlyAnonymousRoutes) { |
|
||||||
return ( |
|
||||||
<Routes> |
|
||||||
<Route |
|
||||||
path="*" |
|
||||||
element={<LogoutOnMount />} |
|
||||||
/> |
|
||||||
</Routes> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<InitialLocationContextProvider |
|
||||||
location={initialLocation} |
|
||||||
target="app" |
|
||||||
> |
|
||||||
<Routes> |
|
||||||
<Route |
|
||||||
path="/*" |
|
||||||
element={ |
|
||||||
<HasDashboardContextProvider value={!!dashboard}> |
|
||||||
<Layout> |
|
||||||
<Routes> |
|
||||||
{routes} |
|
||||||
|
|
||||||
<Route |
|
||||||
path="/" |
|
||||||
element={ |
|
||||||
homepageElement |
|
||||||
? (getReactElement(homepageElement)) |
|
||||||
: initialLocation |
|
||||||
? (<Navigate to={initialLocation} />) |
|
||||||
: null |
|
||||||
} |
|
||||||
/> |
|
||||||
|
|
||||||
{dashboard |
|
||||||
? (<Route path="/" element={getReactElement(dashboard)} />) |
|
||||||
: (initialLocation && initialLocation !== "/") |
|
||||||
? (<Route path="/" element={<Navigate to={initialLocation} />} />) |
|
||||||
: null} |
|
||||||
|
|
||||||
{catchAllElement ? ( |
|
||||||
<Route |
|
||||||
path="*" |
|
||||||
element={getReactElement(catchAllElement)} |
|
||||||
/> |
|
||||||
) : null} |
|
||||||
</Routes> |
|
||||||
</Layout> |
|
||||||
</HasDashboardContextProvider> |
|
||||||
} |
|
||||||
/> |
|
||||||
</Routes> |
|
||||||
</InitialLocationContextProvider> |
|
||||||
); |
|
||||||
} |
|
@ -0,0 +1,18 @@ |
|||||||
|
import { useErrorContext } from "./useErrorContext"; |
||||||
|
import { useResetErrorBoundaryOnLocationChange } from "./useResetErrorBoundaryOnLocationChange"; |
||||||
|
|
||||||
|
export function DefaultError() { |
||||||
|
const { error, errorInfo, resetErrorBoundary } = useErrorContext(); |
||||||
|
|
||||||
|
useResetErrorBoundaryOnLocationChange(resetErrorBoundary); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<h1>Error</h1> |
||||||
|
<pre> |
||||||
|
{error.message} |
||||||
|
{errorInfo?.componentStack} |
||||||
|
</pre> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -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 <Route> as child of |
||||||
|
<Admin>. |
||||||
|
</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> |
||||||
|
); |
||||||
|
}; |
@ -1,3 +1,6 @@ |
|||||||
import { createContext, ReactElement } from "react"; |
import { createContext, ReactElement } from "react"; |
||||||
|
|
||||||
export const DefaultTitleContext = createContext<string | ReactElement>('React WebApp Scaffold'); |
/** |
||||||
|
* @private |
||||||
|
*/ |
||||||
|
export const DefaultTitleContext = createContext<string | ReactElement>('Rakit'); |
@ -0,0 +1,7 @@ |
|||||||
|
import { createContext } from "react"; |
||||||
|
import { ErrorContextValue } from "./types"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @private |
||||||
|
*/ |
||||||
|
export const ErrorContext = createContext<ErrorContextValue | null>(null) |
@ -0,0 +1,13 @@ |
|||||||
|
import { ProviderProps, ReactElement } from "react"; |
||||||
|
import { ErrorContext } from "./ErrorContext"; |
||||||
|
import { ErrorContextValue } from "./types"; |
||||||
|
|
||||||
|
export function ErrorContextProvider( |
||||||
|
props: ProviderProps<ErrorContextValue> |
||||||
|
): ReactElement { |
||||||
|
return ( |
||||||
|
<ErrorContext.Provider value={props.value}> |
||||||
|
{props.children} |
||||||
|
</ErrorContext.Provider> |
||||||
|
); |
||||||
|
} |
@ -1,3 +1,6 @@ |
|||||||
import { createContext } from "react"; |
import { createContext } from "react"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @private |
||||||
|
*/ |
||||||
export const HasDashboardContext = createContext<boolean>(false); |
export const HasDashboardContext = createContext<boolean>(false); |
||||||
|
@ -1,21 +0,0 @@ |
|||||||
import { createContext, PropsWithChildren, useContext } from "react"; |
|
||||||
|
|
||||||
const InAdmin = createContext(false); |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export function InAdminContext(props: PropsWithChildren) { |
|
||||||
return ( |
|
||||||
<InAdmin.Provider value={true}> |
|
||||||
{props.children} |
|
||||||
</InAdmin.Provider> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export function useInAdminContext() { |
|
||||||
return useContext(InAdmin); |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
import { createContext, PropsWithChildren, useContext } from "react"; |
|
||||||
|
|
||||||
const InApp = createContext(false); |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export function InAppContext(props: PropsWithChildren) { |
|
||||||
return ( |
|
||||||
<InApp.Provider value={true}> |
|
||||||
{props.children} |
|
||||||
</InApp.Provider> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export function useInAppContext() { |
|
||||||
return useContext(InApp); |
|
||||||
} |
|
@ -0,0 +1,5 @@ |
|||||||
|
import { ReactNode } from "react"; |
||||||
|
|
||||||
|
export function RoutesWithoutLayout(_props: { children?: ReactNode }) { |
||||||
|
return null; |
||||||
|
} |
@ -1,10 +1,22 @@ |
|||||||
|
export * from "./AdminRouter"; |
||||||
|
export * from "./BasenameContextProvider"; |
||||||
|
export * from "./CoreAdmin"; |
||||||
export * from "./CoreAdminContext"; |
export * from "./CoreAdminContext"; |
||||||
export * from "./CoreAdminRoutes"; |
export * from "./CoreAdminRoutes"; |
||||||
export * from "./CoreAppContext"; |
export * from "./CoreAdminUI"; |
||||||
export * from "./CoreAppRoutes"; |
export * from "./DefaultError"; |
||||||
export * from "./DefaultLayout"; |
export * from "./DefaultLayout"; |
||||||
export * from "./HasDashboardContext"; |
export * from "./DefaultReady"; |
||||||
|
export * from "./DefaultTitleContextProvider"; |
||||||
|
export * from "./ErrorBoundary"; |
||||||
|
export * from "./ErrorContextProvider"; |
||||||
export * from "./HasDashboardContextProvider"; |
export * from "./HasDashboardContextProvider"; |
||||||
|
export * from "./RoutesWithoutLayout"; |
||||||
export * from "./types"; |
export * from "./types"; |
||||||
export * from "./useConfigureRoutesFromChildren"; |
export * from "./useBasename"; |
||||||
|
export * from "./useConfigureAdminRouterFromChildren"; |
||||||
|
export * from "./useDefaultTitle"; |
||||||
|
export * from "./useErrorContext"; |
||||||
export * from "./useHasDashboard"; |
export * from "./useHasDashboard"; |
||||||
|
export * from "./useRedirect"; |
||||||
|
export * from "./useResetErrorBoundaryOnLocationChange"; |
||||||
|
@ -1,5 +1,23 @@ |
|||||||
import { ReactNode } from "react"; |
import { ErrorInfo, ReactNode } from "react"; |
||||||
|
import { FallbackProps } from "react-error-boundary"; |
||||||
|
|
||||||
export interface CoreLayoutProps { |
export interface CoreLayoutProps { |
||||||
children: ReactNode; |
children: ReactNode; |
||||||
} |
} |
||||||
|
|
||||||
|
export type RenderRoutesFunction = (permissions: any) => |
||||||
|
| ReactNode // (permissions) => <><Route /><Route /><Route /></>
|
||||||
|
| Promise<ReactNode> // (permissions) => fetch().then(() => <><Route /><Route /><Route /></>)
|
||||||
|
|
||||||
|
export type AdminChildren = |
||||||
|
| RenderRoutesFunction |
||||||
|
| Iterable<ReactNode | RenderRoutesFunction> |
||||||
|
| ReactNode; |
||||||
|
|
||||||
|
export type AdminRouterStatus = 'loading' | 'empty' | 'ready'; |
||||||
|
|
||||||
|
export interface ErrorContextValue { |
||||||
|
errorInfo?: ErrorInfo; |
||||||
|
error: Error; |
||||||
|
resetErrorBoundary: FallbackProps['resetErrorBoundary']; |
||||||
|
} |
||||||
|
@ -1,5 +1,6 @@ |
|||||||
import { useContext } from "react"; |
import { useContext } from "react"; |
||||||
import { ErrorContext, ErrorContextValue } from "./ErrorContext"; |
import { ErrorContext } from "./ErrorContext"; |
||||||
|
import { ErrorContextValue } from "./types"; |
||||||
|
|
||||||
export function useErrorContext(): ErrorContextValue { |
export function useErrorContext(): ErrorContextValue { |
||||||
const errorContext = useContext(ErrorContext); |
const errorContext = useContext(ErrorContext); |
@ -1,10 +0,0 @@ |
|||||||
import { createContext, ErrorInfo } from "react"; |
|
||||||
import { FallbackProps } from "react-error-boundary"; |
|
||||||
|
|
||||||
export interface ErrorContextValue { |
|
||||||
errorInfo?: ErrorInfo; |
|
||||||
error: Error; |
|
||||||
resetErrorBoundary: FallbackProps['resetErrorBoundary']; |
|
||||||
} |
|
||||||
|
|
||||||
export const ErrorContext = createContext<ErrorContextValue | null>(null) |
|
@ -1,15 +0,0 @@ |
|||||||
import * as React from "react"; |
|
||||||
import { |
|
||||||
ErrorContext, |
|
||||||
ErrorContextValue, |
|
||||||
} from "./ErrorContext"; |
|
||||||
|
|
||||||
export const ErrorContextProvider = ( |
|
||||||
props: React.ProviderProps<ErrorContextValue> |
|
||||||
): React.ReactElement => { |
|
||||||
return ( |
|
||||||
<ErrorContext.Provider value={props.value}> |
|
||||||
{props.children} |
|
||||||
</ErrorContext.Provider> |
|
||||||
) |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
export * from "./ErrorBoundary"; |
|
||||||
export * from "./ErrorContext"; |
|
||||||
export * from "./ErrorContextProvider"; |
|
||||||
export * from "./useErrorContext"; |
|
@ -1,14 +1,10 @@ |
|||||||
export * from "./accessControl"; |
|
||||||
export * from "./auth"; |
export * from "./auth"; |
||||||
export * from "./core"; |
export * from "./core"; |
||||||
export * from "./data"; |
export * from "./data"; |
||||||
export * from "./errorBoundary"; |
|
||||||
export * from "./i18n"; |
export * from "./i18n"; |
||||||
export * from "./notification"; |
export * from "./notification"; |
||||||
export * from "./portal"; |
export * from "./portal"; |
||||||
export * from "./record"; |
export * from "./record"; |
||||||
export * from "./routing"; |
|
||||||
export * from "./scrollPosition"; |
export * from "./scrollPosition"; |
||||||
export * from "./store"; |
export * from "./store"; |
||||||
export * from "./title"; |
|
||||||
export * from "./util"; |
export * from "./util"; |
||||||
|
@ -1,12 +0,0 @@ |
|||||||
import { createContext } from "react"; |
|
||||||
import { To } from "react-router-dom"; |
|
||||||
|
|
||||||
export interface InitialLocationContextValue { |
|
||||||
app?: To; |
|
||||||
admin?: To; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export const InitialLocationContext = createContext<InitialLocationContextValue | undefined>(undefined); |
|
@ -1,32 +0,0 @@ |
|||||||
import { ReactNode } from "react"; |
|
||||||
import { To } from "react-router-dom"; |
|
||||||
import { |
|
||||||
InitialLocationContext, |
|
||||||
InitialLocationContextValue |
|
||||||
} from "./InitialLocationContext"; |
|
||||||
import { useInitialLocationContext } from "./useInitialLocationContext"; |
|
||||||
|
|
||||||
export interface InitialLocationContextProviderProps { |
|
||||||
location?: To; |
|
||||||
target: keyof InitialLocationContextValue |
|
||||||
children?: ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
export function InitialLocationContextProvider(props: InitialLocationContextProviderProps) { |
|
||||||
const { children, target, location } = props; |
|
||||||
const fromContext = useInitialLocationContext({}); |
|
||||||
const value = { |
|
||||||
...fromContext, |
|
||||||
[target]: location ?? fromContext[target] |
|
||||||
} |
|
||||||
|
|
||||||
if (Object.values(value).some(v => v == null)) { |
|
||||||
return children; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<InitialLocationContext.Provider value={value}> |
|
||||||
{children} |
|
||||||
</InitialLocationContext.Provider> |
|
||||||
) |
|
||||||
} |
|
@ -1,75 +0,0 @@ |
|||||||
import { |
|
||||||
IndexRouteProps as IndexRoutePropsRaw, |
|
||||||
PathRouteProps as PathRoutePropsRaw, |
|
||||||
} from "react-router-dom"; |
|
||||||
import { Authenticated } from "../auth"; |
|
||||||
import { RestoreScrollPosition } from "../scrollPosition"; |
|
||||||
import { ComponentType, ReactNode } from "react"; |
|
||||||
import { AccessParams, CanAccess } from "../accessControl"; |
|
||||||
|
|
||||||
export type RoutePropsBase = { |
|
||||||
accessParams?: AccessParams; |
|
||||||
authorised?: boolean; |
|
||||||
name: string; |
|
||||||
remembeScrollPosition?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export type IndexRouteProps = IndexRoutePropsRaw & RoutePropsBase; |
|
||||||
export type PathRouteProps = PathRoutePropsRaw & RoutePropsBase; |
|
||||||
export type RouteProps = IndexRouteProps | PathRouteProps; |
|
||||||
|
|
||||||
export function Route(props: RouteProps) { |
|
||||||
const { |
|
||||||
accessParams, |
|
||||||
authorised, |
|
||||||
Component, |
|
||||||
element, |
|
||||||
name, |
|
||||||
remembeScrollPosition, |
|
||||||
} = props; |
|
||||||
|
|
||||||
let children = getElement(element, Component); |
|
||||||
|
|
||||||
if (authorised) { |
|
||||||
children = ( |
|
||||||
<CanAccess |
|
||||||
on="route" |
|
||||||
key={name} |
|
||||||
params={accessParams} |
|
||||||
> |
|
||||||
{getElement(element, Component)} |
|
||||||
</CanAccess> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
if (remembeScrollPosition) { |
|
||||||
children = ( |
|
||||||
<RestoreScrollPosition storeKey={name}> |
|
||||||
{children} |
|
||||||
</RestoreScrollPosition> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
if (authorised) { |
|
||||||
children = ( |
|
||||||
<Authenticated> |
|
||||||
{children} |
|
||||||
</Authenticated> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return children; |
|
||||||
} |
|
||||||
|
|
||||||
function getElement( |
|
||||||
element: ReactNode | undefined | null, |
|
||||||
Component: ComponentType | undefined | null |
|
||||||
) { |
|
||||||
if (element != null) { |
|
||||||
return element; |
|
||||||
} |
|
||||||
if (Component != null) { |
|
||||||
return (<Component />) |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
export * from "./AppRouter"; |
|
||||||
export * from "./BasenameContextProvider"; |
|
||||||
export * from "./InitialLocationContextProvider"; |
|
||||||
export * from "./Route"; |
|
||||||
export * from "./useBasename"; |
|
||||||
export * from "./useInitialLocation"; |
|
||||||
export * from "./useInitialLocationContext"; |
|
||||||
export * from "./useRedirect"; |
|
||||||
export * from "./useResetErrorBoundaryOnLocationChange"; |
|
@ -1,17 +0,0 @@ |
|||||||
import { To } from "react-router-dom"; |
|
||||||
import { useInAdminContext } from "../core/InAdminContext"; |
|
||||||
import { useInAppContext } from "../core/InAppContext"; |
|
||||||
import { useInitialLocationContext } from "./useInitialLocationContext"; |
|
||||||
|
|
||||||
export function useInitialLocation(fallback?: To): To | undefined { |
|
||||||
const inAdmin = useInAdminContext(); |
|
||||||
const inApp = useInAppContext(); |
|
||||||
const { app, admin } = useInitialLocationContext({}); |
|
||||||
if (inAdmin) { |
|
||||||
return admin; |
|
||||||
} |
|
||||||
if (inApp) { |
|
||||||
return app; |
|
||||||
} |
|
||||||
return fallback; |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
import { useContext } from "react"; |
|
||||||
import { |
|
||||||
InitialLocationContext, |
|
||||||
InitialLocationContextValue |
|
||||||
} from "./InitialLocationContext"; |
|
||||||
|
|
||||||
export function useInitialLocationContext(): InitialLocationContextValue; |
|
||||||
export function useInitialLocationContext(overrides: InitialLocationContextValue): InitialLocationContextValue; |
|
||||||
export function useInitialLocationContext(overrides?: InitialLocationContextValue) { |
|
||||||
const fromContext = useContext(InitialLocationContext); |
|
||||||
|
|
||||||
if (fromContext != null) { |
|
||||||
return { |
|
||||||
...fromContext, |
|
||||||
...overrides, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return overrides; |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
import { useTranslate } from "../i18n"; |
|
||||||
import { TitleProps } from "./types"; |
|
||||||
|
|
||||||
export function PageTitle(props: TitleProps) { |
|
||||||
const { |
|
||||||
title, |
|
||||||
defaultTitle, |
|
||||||
className, |
|
||||||
...rest |
|
||||||
} = props; |
|
||||||
const translate = useTranslate(); |
|
||||||
|
|
||||||
if (!title && !defaultTitle) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<span className={className}> |
|
||||||
{!title ? ( |
|
||||||
<span {...rest}>{defaultTitle}</span> |
|
||||||
) : typeof title === 'string' ? ( |
|
||||||
<span {...rest}>{translate(title, { _: title })}</span> |
|
||||||
) : ( |
|
||||||
title |
|
||||||
)} |
|
||||||
</span> |
|
||||||
); |
|
||||||
} |
|
@ -1,37 +0,0 @@ |
|||||||
import { PageTitle } from './PageTitle'; |
|
||||||
import { useRecordRepresentation } from '../record'; |
|
||||||
import { useTranslate } from '../i18n'; |
|
||||||
import { TitleProps } from './types'; |
|
||||||
|
|
||||||
export const PageTitleConfigurable = ({ |
|
||||||
preferenceKey, |
|
||||||
title, |
|
||||||
defaultTitle, |
|
||||||
record, |
|
||||||
...props |
|
||||||
}: TitleProps) => { |
|
||||||
const translate = useTranslate(); |
|
||||||
const titleFromPreferences = useRecordRepresentation({ |
|
||||||
record, |
|
||||||
representation: preferenceKey === false ? undefined : preferenceKey, |
|
||||||
}); |
|
||||||
|
|
||||||
if (titleFromPreferences) { |
|
||||||
return ( |
|
||||||
<span className={props.className} {...props}> |
|
||||||
{translate(titleFromPreferences, { |
|
||||||
...record, |
|
||||||
_: titleFromPreferences, |
|
||||||
})} |
|
||||||
</span> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<PageTitle |
|
||||||
title={title} |
|
||||||
defaultTitle={defaultTitle} |
|
||||||
{...props} |
|
||||||
/> |
|
||||||
); |
|
||||||
}; |
|
@ -1,41 +0,0 @@ |
|||||||
import { Portlet } from '../portal'; |
|
||||||
import { titlePortalName } from './constants'; |
|
||||||
import { PageTitle } from './PageTitle'; |
|
||||||
import { PageTitleConfigurable } from './PageTitleConfigurable'; |
|
||||||
import { TitleProps } from './types'; |
|
||||||
|
|
||||||
export const Title = (props: TitleProps) => { |
|
||||||
const { |
|
||||||
defaultTitle, |
|
||||||
title, |
|
||||||
preferenceKey, |
|
||||||
...rest |
|
||||||
} = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Portlet to={titlePortalName}> |
|
||||||
{() => { |
|
||||||
if (!defaultTitle && !title) { |
|
||||||
console.warn('Missing title prop in <Title> element'); |
|
||||||
} |
|
||||||
if (preferenceKey === false) { |
|
||||||
return ( |
|
||||||
<PageTitle |
|
||||||
title={title} |
|
||||||
defaultTitle={defaultTitle} |
|
||||||
{...rest} |
|
||||||
/> |
|
||||||
); |
|
||||||
} |
|
||||||
return ( |
|
||||||
<PageTitleConfigurable |
|
||||||
title={title} |
|
||||||
defaultTitle={defaultTitle} |
|
||||||
preferenceKey={preferenceKey} |
|
||||||
{...rest} |
|
||||||
/> |
|
||||||
); |
|
||||||
}} |
|
||||||
</Portlet> |
|
||||||
); |
|
||||||
}; |
|
@ -1,19 +0,0 @@ |
|||||||
import { ReactNode } from "react"; |
|
||||||
import { PortalProvider } from "../portal"; |
|
||||||
import { titlePortalName } from "./constants"; |
|
||||||
|
|
||||||
export interface TitleContainerProviderProps { |
|
||||||
children?: ReactNode; |
|
||||||
value: string | Element | null; |
|
||||||
} |
|
||||||
|
|
||||||
export function TitlePortalProvider(props: TitleContainerProviderProps) { |
|
||||||
return ( |
|
||||||
<PortalProvider |
|
||||||
name={titlePortalName} |
|
||||||
container={props.value} |
|
||||||
> |
|
||||||
{props.children} |
|
||||||
</PortalProvider> |
|
||||||
); |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
/** |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
export const titlePortalName = "@@rakit-title-portal"; |
|
@ -1,8 +0,0 @@ |
|||||||
export * from "./DefaultTitleContext"; |
|
||||||
export * from "./DefaultTitleContextProvider"; |
|
||||||
export * from "./PageTitle"; |
|
||||||
export * from "./PageTitleConfigurable"; |
|
||||||
export * from "./Title"; |
|
||||||
export * from "./TitlePortalProvider"; |
|
||||||
export * from "./types"; |
|
||||||
export * from "./useDefaultTitle"; |
|
@ -1,9 +0,0 @@ |
|||||||
import { ReactElement } from "react"; |
|
||||||
|
|
||||||
export interface TitleProps { |
|
||||||
className?: string; |
|
||||||
defaultTitle?: ReactElement; |
|
||||||
record?: any; |
|
||||||
title?: string | ReactElement; |
|
||||||
preferenceKey?: string | false; |
|
||||||
} |
|
Loading…
Reference in new issue