Compare commits
No commits in common. '358b0b8c32551816cebdc2ae8400d67ec4af0b9c' and 'b4e047a10cd6502292947188dc87b7fa79409e21' have entirely different histories.
358b0b8c32
...
b4e047a10c
@ -0,0 +1,7 @@ |
||||
import { Navigate } from "react-router-dom"; |
||||
|
||||
export default function CatchAll() { |
||||
return ( |
||||
<Navigate to="/error/404" /> |
||||
); |
||||
} |
@ -1,20 +1,10 @@ |
||||
import { |
||||
Forbidden, |
||||
NotFound, |
||||
PageError as Error |
||||
} from "@rakit/joy-ui"; |
||||
import { StatusError } from "@rakit/joy-ui"; |
||||
import { useParams } from "react-router-dom"; |
||||
|
||||
export default function PageError() { |
||||
const { status } = useParams<'status'>(); |
||||
|
||||
switch (status) { |
||||
case "403": |
||||
return <Forbidden key="error403" /> |
||||
case "404": |
||||
return <NotFound key="error404" /> |
||||
case "500": |
||||
default: |
||||
return <Error key="error500" /> |
||||
} |
||||
return ( |
||||
<StatusError status={status} /> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,14 @@ |
||||
{ |
||||
"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" |
||||
} |
||||
} |
@ -1,77 +0,0 @@ |
||||
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,39 +0,0 @@ |
||||
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?", |
||||
}, |
||||
}, |
||||
}; |
@ -1,2 +0,0 @@ |
||||
export * from "./zh_CN"; |
||||
export * from "./en_US"; |
@ -1 +0,0 @@ |
||||
export const zhCN = {} |
@ -0,0 +1,40 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,11 +0,0 @@ |
||||
import { PageError } from "./PageError"; |
||||
|
||||
export function Forbidden() { |
||||
return ( |
||||
<PageError |
||||
image="forbidden" |
||||
title="ra.page.forbidden" |
||||
message="ra.message.forbidden" |
||||
/> |
||||
); |
||||
} |
@ -1,35 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,18 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,11 +0,0 @@ |
||||
import { PageError } from "./PageError"; |
||||
|
||||
export function NotFound() { |
||||
return ( |
||||
<PageError |
||||
image="not_found" |
||||
title="ra.page.not_found" |
||||
message="ra.message.not_found" |
||||
/> |
||||
); |
||||
} |
@ -1,18 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,52 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -0,0 +1,15 @@ |
||||
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,16 +1,12 @@ |
||||
export * from "./AdminRoot"; |
||||
export * from "./AppBar"; |
||||
export * from "./ColorSchemeToggle"; |
||||
export * from "./Forbidden"; |
||||
export * from "./Layout"; |
||||
export * from "./Error"; |
||||
export * from "./Loading"; |
||||
export * from "./LoadingIndicator"; |
||||
export * from "./NotFound"; |
||||
export * from "./Notification"; |
||||
export * from "./PageActions"; |
||||
export * from "./PageDock"; |
||||
export * from "./PageError"; |
||||
export * from "./PageRoot"; |
||||
export * from "./PageTitle"; |
||||
export * from "./Sidebar"; |
||||
export * from "./RuntimeError"; |
||||
export * from "./StatusError"; |
||||
export * from "./TitlePortal"; |
||||
export * from "./utils"; |
||||
|
@ -1 +0,0 @@ |
||||
export * from "./useMediaQuery" |
@ -1,9 +0,0 @@ |
||||
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); |
||||
} |
@ -0,0 +1,7 @@ |
||||
import { createContext } from "react"; |
||||
import { AccessControlContextValue } from "./types"; |
||||
|
||||
/** |
||||
* @private |
||||
*/ |
||||
export const AccessControlContext = createContext<AccessControlContextValue | undefined>(undefined); |
@ -0,0 +1,16 @@ |
||||
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> |
||||
) |
||||
} |
@ -0,0 +1,96 @@ |
||||
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 }) |
||||
} |
@ -0,0 +1,5 @@ |
||||
export * from "./AccessControlProvider"; |
||||
export * from "./CanAccess"; |
||||
export * from "./types"; |
||||
export * from "./useAccessControl"; |
||||
export * from "./useCan"; |
@ -0,0 +1,52 @@ |
||||
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; |
||||
} |
@ -0,0 +1,18 @@ |
||||
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; |
||||
} |
@ -0,0 +1,70 @@ |
||||
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]); |
||||
} |
@ -1,21 +0,0 @@ |
||||
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>({}); |
@ -1,37 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,40 +0,0 @@ |
||||
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 |
||||
}) |
||||
} |
@ -1,57 +0,0 @@ |
||||
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]); |
||||
} |
@ -1,134 +0,0 @@ |
||||
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> |
||||
); |
||||
}; |
@ -1,293 +0,0 @@ |
||||
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> |
||||
); |
||||
}; |
@ -0,0 +1,271 @@ |
||||
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> |
||||
); |
||||
} |
@ -0,0 +1,277 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,18 +0,0 @@ |
||||
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> |
||||
); |
||||
} |
@ -1,111 +0,0 @@ |
||||
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,7 +0,0 @@ |
||||
import { createContext } from "react"; |
||||
import { ErrorContextValue } from "./types"; |
||||
|
||||
/** |
||||
* @private |
||||
*/ |
||||
export const ErrorContext = createContext<ErrorContextValue | null>(null) |
@ -1,13 +0,0 @@ |
||||
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,6 +1,3 @@ |
||||
import { createContext } from "react"; |
||||
|
||||
/** |
||||
* @private |
||||
*/ |
||||
export const HasDashboardContext = createContext<boolean>(false); |
||||
|
@ -0,0 +1,21 @@ |
||||
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); |
||||
} |
@ -0,0 +1,21 @@ |
||||
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); |
||||
} |
@ -1,5 +0,0 @@ |
||||
import { ReactNode } from "react"; |
||||
|
||||
export function RoutesWithoutLayout(_props: { children?: ReactNode }) { |
||||
return null; |
||||
} |
@ -0,0 +1,10 @@ |
||||
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) |
@ -0,0 +1,15 @@ |
||||
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> |
||||
) |
||||
} |
@ -0,0 +1,4 @@ |
||||
export * from "./ErrorBoundary"; |
||||
export * from "./ErrorContext"; |
||||
export * from "./ErrorContextProvider"; |
||||
export * from "./useErrorContext"; |
@ -1,6 +1,5 @@ |
||||
import { useContext } from "react"; |
||||
import { ErrorContext } from "./ErrorContext"; |
||||
import { ErrorContextValue } from "./types"; |
||||
import { ErrorContext, ErrorContextValue } from "./ErrorContext"; |
||||
|
||||
export function useErrorContext(): ErrorContextValue { |
||||
const errorContext = useContext(ErrorContext); |
@ -1,10 +1,14 @@ |
||||
export * from "./accessControl"; |
||||
export * from "./auth"; |
||||
export * from "./core"; |
||||
export * from "./data"; |
||||
export * from "./errorBoundary"; |
||||
export * from "./i18n"; |
||||
export * from "./notification"; |
||||
export * from "./portal"; |
||||
export * from "./record"; |
||||
export * from "./routing"; |
||||
export * from "./scrollPosition"; |
||||
export * from "./store"; |
||||
export * from "./title"; |
||||
export * from "./util"; |
||||
|
@ -0,0 +1,12 @@ |
||||
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); |
@ -0,0 +1,32 @@ |
||||
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> |
||||
) |
||||
} |
@ -0,0 +1,75 @@ |
||||
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; |
||||
} |
@ -0,0 +1,9 @@ |
||||
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"; |
@ -0,0 +1,17 @@ |
||||
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; |
||||
} |
@ -0,0 +1,20 @@ |
||||
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,6 +1,3 @@ |
||||
import { createContext, ReactElement } from "react"; |
||||
|
||||
/** |
||||
* @private |
||||
*/ |
||||
export const DefaultTitleContext = createContext<string | ReactElement>('Rakit'); |
||||
export const DefaultTitleContext = createContext<string | ReactElement>('React WebApp Scaffold'); |
@ -0,0 +1,28 @@ |
||||
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> |
||||
); |
||||
} |
@ -0,0 +1,37 @@ |
||||
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} |
||||
/> |
||||
); |
||||
}; |
@ -0,0 +1,41 @@ |
||||
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> |
||||
); |
||||
}; |
@ -0,0 +1,19 @@ |
||||
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> |
||||
); |
||||
} |
@ -0,0 +1,4 @@ |
||||
/** |
||||
* @private |
||||
*/ |
||||
export const titlePortalName = "@@rakit-title-portal"; |
@ -0,0 +1,8 @@ |
||||
export * from "./DefaultTitleContext"; |
||||
export * from "./DefaultTitleContextProvider"; |
||||
export * from "./PageTitle"; |
||||
export * from "./PageTitleConfigurable"; |
||||
export * from "./Title"; |
||||
export * from "./TitlePortalProvider"; |
||||
export * from "./types"; |
||||
export * from "./useDefaultTitle"; |
@ -0,0 +1,9 @@ |
||||
import { ReactElement } from "react"; |
||||
|
||||
export interface TitleProps { |
||||
className?: string; |
||||
defaultTitle?: ReactElement; |
||||
record?: any; |
||||
title?: string | ReactElement; |
||||
preferenceKey?: string | false; |
||||
} |
Loading…
Reference in new issue