mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Error boundary (#7176)
* Create error boundary component - Ref: https://docs.sentry.io/platforms/javascript/guides/react/features/error-boundary/ - Keeps errors container to local components - Will be critical for plugin support * Add boundary to API forms
This commit is contained in:
parent
c7351c4064
commit
108bd28102
44
src/frontend/src/components/Boundary.tsx
Normal file
44
src/frontend/src/components/Boundary.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Alert } from '@mantine/core';
|
||||
import { ErrorBoundary, FallbackRender } from '@sentry/react';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
|
||||
function DefaultFallback({ title }: { title: String }): ReactNode {
|
||||
return (
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<IconExclamationCircle />}
|
||||
title={t`Error rendering component` + `: ${title}`}
|
||||
>
|
||||
{t`An error occurred while rendering this component. Refer to the console for more information.`}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export function Boundary({
|
||||
children,
|
||||
label,
|
||||
fallback
|
||||
}: {
|
||||
children: ReactNode;
|
||||
label: string;
|
||||
fallback?: React.ReactElement | FallbackRender | undefined;
|
||||
}): ReactNode {
|
||||
const onError = useCallback(
|
||||
(error: Error, componentStack: string, eventId: string) => {
|
||||
console.error(`Error rendering component: ${label}`);
|
||||
console.error(error, componentStack);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={fallback ?? <DefaultFallback title={label} />}
|
||||
onError={onError}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
@ -36,6 +36,7 @@ import {
|
||||
import { invalidResponse } from '../../functions/notifications';
|
||||
import { getDetailUrl } from '../../functions/urls';
|
||||
import { PathParams } from '../../states/ApiState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import {
|
||||
ApiFormField,
|
||||
ApiFormFieldSet,
|
||||
@ -472,81 +473,89 @@ export function ApiForm({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{/* Show loading overlay while fetching fields */}
|
||||
{/* zIndex used to force overlay on top of modal header bar */}
|
||||
<LoadingOverlay visible={isLoading} zIndex={1010} />
|
||||
<Boundary label={`ApiForm-${id}`}>
|
||||
{/* Show loading overlay while fetching fields */}
|
||||
{/* zIndex used to force overlay on top of modal header bar */}
|
||||
<LoadingOverlay visible={isLoading} zIndex={1010} />
|
||||
|
||||
{/* Attempt at making fixed footer with scroll area */}
|
||||
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
||||
<div>
|
||||
{/* Form Fields */}
|
||||
<Stack spacing="sm">
|
||||
{(!isValid || nonFieldErrors.length > 0) && (
|
||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||
{nonFieldErrors.length > 0 && (
|
||||
<Stack spacing="xs">
|
||||
{nonFieldErrors.map((message) => (
|
||||
<Text key={message}>{message}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
{/* Attempt at making fixed footer with scroll area */}
|
||||
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
||||
<div>
|
||||
{/* Form Fields */}
|
||||
<Stack spacing="sm">
|
||||
{(!isValid || nonFieldErrors.length > 0) && (
|
||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||
{nonFieldErrors.length > 0 && (
|
||||
<Stack spacing="xs">
|
||||
{nonFieldErrors.map((message) => (
|
||||
<Text key={message}>{message}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
<Boundary label={`ApiForm-${id}-PreFormContent`}>
|
||||
{props.preFormContent}
|
||||
{props.preFormSuccess && (
|
||||
<Alert color="green" radius="sm">
|
||||
{props.preFormSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{props.preFormContent}
|
||||
{props.preFormSuccess && (
|
||||
<Alert color="green" radius="sm">
|
||||
{props.preFormSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
{props.preFormWarning && (
|
||||
<Alert color="orange" radius="sm">
|
||||
{props.preFormWarning}
|
||||
</Alert>
|
||||
)}
|
||||
<FormProvider {...form}>
|
||||
<Stack spacing="xs">
|
||||
{!optionsLoading &&
|
||||
Object.entries(fields).map(([fieldName, field]) => (
|
||||
<ApiFormField
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
definition={field}
|
||||
control={form.control}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
{props.postFormContent}
|
||||
</Stack>
|
||||
</div>
|
||||
</Paper>
|
||||
{props.preFormWarning && (
|
||||
<Alert color="orange" radius="sm">
|
||||
{props.preFormWarning}
|
||||
</Alert>
|
||||
)}
|
||||
</Boundary>
|
||||
<Boundary label={`ApiForm-${id}-FormContent`}>
|
||||
<FormProvider {...form}>
|
||||
<Stack spacing="xs">
|
||||
{!optionsLoading &&
|
||||
Object.entries(fields).map(([fieldName, field]) => (
|
||||
<ApiFormField
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
definition={field}
|
||||
control={form.control}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</Boundary>
|
||||
<Boundary label={`ApiForm-${id}-PostFormContent`}>
|
||||
{props.postFormContent}
|
||||
</Boundary>
|
||||
</Stack>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
{/* Footer with Action Buttons */}
|
||||
<Divider />
|
||||
<div>
|
||||
<Group position="right">
|
||||
{props.actions?.map((action, i) => (
|
||||
{/* Footer with Action Buttons */}
|
||||
<Divider />
|
||||
<div>
|
||||
<Group position="right">
|
||||
{props.actions?.map((action, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant ?? 'outline'}
|
||||
radius="sm"
|
||||
color={action.color}
|
||||
>
|
||||
{action.text}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
key={i}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant ?? 'outline'}
|
||||
onClick={form.handleSubmit(submitForm, onFormError)}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
color={action.color}
|
||||
color={props.submitColor ?? 'green'}
|
||||
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
||||
>
|
||||
{action.text}
|
||||
{props.submitText ?? t`Submit`}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
onClick={form.handleSubmit(submitForm, onFormError)}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
color={props.submitColor ?? 'green'}
|
||||
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
||||
>
|
||||
{props.submitText ?? t`Submit`}
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
</Boundary>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { getActions } from '../../defaults/actions';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
|
||||
@ -29,17 +30,17 @@ export default function LayoutComponent() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const defaultactions = getActions(navigate);
|
||||
const [actions, setActions] = useState(defaultactions);
|
||||
const defaultActions = getActions(navigate);
|
||||
const [actions, setActions] = useState(defaultActions);
|
||||
const [customActions, setCustomActions] = useState<boolean>(false);
|
||||
|
||||
function actionsAreChanging(change: []) {
|
||||
if (change.length > defaultactions.length) setCustomActions(true);
|
||||
if (change.length > defaultActions.length) setCustomActions(true);
|
||||
setActions(change);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (customActions) {
|
||||
setActions(defaultactions);
|
||||
setActions(defaultActions);
|
||||
setCustomActions(false);
|
||||
}
|
||||
}, [location]);
|
||||
@ -57,7 +58,10 @@ export default function LayoutComponent() {
|
||||
<Flex direction="column" mih="100vh">
|
||||
<Header />
|
||||
<Container className={classes.layoutContent} size="100%">
|
||||
<Outlet />
|
||||
<Boundary label={'layout'}>
|
||||
<Outlet />
|
||||
</Boundary>
|
||||
{/* </ErrorBoundary> */}
|
||||
</Container>
|
||||
<Space h="xl" />
|
||||
<Footer />
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { PlaceholderPanel } from '../items/Placeholder';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
@ -103,77 +104,81 @@ function BasePanelGroup({
|
||||
const [expanded, setExpanded] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<Paper p="sm" radius="xs" shadow="xs">
|
||||
<Tabs
|
||||
value={panel}
|
||||
orientation="vertical"
|
||||
onTabChange={handlePanelChange}
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List position="left">
|
||||
<Boundary label={`PanelGroup-${pageKey}`}>
|
||||
<Paper p="sm" radius="xs" shadow="xs">
|
||||
<Tabs
|
||||
value={panel}
|
||||
orientation="vertical"
|
||||
onTabChange={handlePanelChange}
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List position="left">
|
||||
{panels.map(
|
||||
(panel) =>
|
||||
!panel.hidden && (
|
||||
<Tooltip
|
||||
label={panel.label}
|
||||
key={panel.name}
|
||||
disabled={expanded}
|
||||
>
|
||||
<Tabs.Tab
|
||||
p="xs"
|
||||
value={panel.name}
|
||||
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
||||
icon={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
disabled={panel.disabled}
|
||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
||||
>
|
||||
{expanded && panel.label}
|
||||
</Tabs.Tab>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
{collapsible && (
|
||||
<ActionIcon
|
||||
style={{
|
||||
paddingLeft: '10px'
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
||||
) : (
|
||||
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Tabs.List>
|
||||
{panels.map(
|
||||
(panel) =>
|
||||
!panel.hidden && (
|
||||
<Tooltip
|
||||
label={panel.label}
|
||||
<Tabs.Panel
|
||||
key={panel.name}
|
||||
disabled={expanded}
|
||||
value={panel.name}
|
||||
p="sm"
|
||||
style={{
|
||||
overflowX: 'scroll',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Tabs.Tab
|
||||
p="xs"
|
||||
value={panel.name}
|
||||
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
||||
icon={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
disabled={panel.disabled}
|
||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
||||
>
|
||||
{expanded && panel.label}
|
||||
</Tabs.Tab>
|
||||
</Tooltip>
|
||||
<Stack spacing="md">
|
||||
{panel.showHeadline !== false && (
|
||||
<>
|
||||
<StylishText size="xl">{panel.label}</StylishText>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Boundary label={`PanelContent-${panel.name}`}>
|
||||
{panel.content ?? <PlaceholderPanel />}
|
||||
</Boundary>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
)}
|
||||
{collapsible && (
|
||||
<ActionIcon
|
||||
style={{
|
||||
paddingLeft: '10px'
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
||||
) : (
|
||||
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Tabs.List>
|
||||
{panels.map(
|
||||
(panel) =>
|
||||
!panel.hidden && (
|
||||
<Tabs.Panel
|
||||
key={panel.name}
|
||||
value={panel.name}
|
||||
p="sm"
|
||||
style={{
|
||||
overflowX: 'scroll',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack spacing="md">
|
||||
{panel.showHeadline !== false && (
|
||||
<>
|
||||
<StylishText size="xl">{panel.label}</StylishText>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{panel.content ?? <PlaceholderPanel />}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
)}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
</Tabs>
|
||||
</Paper>
|
||||
</Boundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,7 @@ import { UserRoles } from '../../enums/Roles';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { RenderInstance } from '../render/Instance';
|
||||
import { ModelInformationDict } from '../render/ModelType';
|
||||
|
||||
@ -386,48 +387,50 @@ export function SearchDrawer({
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
{searchQuery.isFetching && (
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
)}
|
||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||
<Stack spacing="md">
|
||||
{queryResults.map((query, idx) => (
|
||||
<QueryResultGroup
|
||||
key={idx}
|
||||
query={query}
|
||||
onRemove={(query) => removeResults(query)}
|
||||
onResultClick={(query, pk) => onResultClick(query, pk)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{searchQuery.isError && (
|
||||
<Alert
|
||||
color="red"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
title={t`Error`}
|
||||
icon={<IconAlertCircle size="1rem" />}
|
||||
>
|
||||
<Trans>An error occurred during search query</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{searchText &&
|
||||
!searchQuery.isFetching &&
|
||||
!searchQuery.isError &&
|
||||
queryResults.length == 0 && (
|
||||
<Boundary label="SearchDrawer">
|
||||
{searchQuery.isFetching && (
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
)}
|
||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||
<Stack spacing="md">
|
||||
{queryResults.map((query, idx) => (
|
||||
<QueryResultGroup
|
||||
key={idx}
|
||||
query={query}
|
||||
onRemove={(query) => removeResults(query)}
|
||||
onResultClick={(query, pk) => onResultClick(query, pk)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{searchQuery.isError && (
|
||||
<Alert
|
||||
color="blue"
|
||||
color="red"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
title={t`No results`}
|
||||
icon={<IconSearch size="1rem" />}
|
||||
title={t`Error`}
|
||||
icon={<IconAlertCircle size="1rem" />}
|
||||
>
|
||||
<Trans>No results available for search query</Trans>
|
||||
<Trans>An error occurred during search query</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{searchText &&
|
||||
!searchQuery.isFetching &&
|
||||
!searchQuery.isError &&
|
||||
queryResults.length == 0 && (
|
||||
<Alert
|
||||
color="blue"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
title={t`No results`}
|
||||
icon={<IconSearch size="1rem" />}
|
||||
>
|
||||
<Trans>No results available for search query</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
</Boundary>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { api } from '../App';
|
||||
import { Boundary } from '../components/Boundary';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
import { ButtonMenu } from '../components/buttons/ButtonMenu';
|
||||
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||
@ -557,131 +558,135 @@ export function InvenTreeTable<T = any>({
|
||||
onClose={() => setFiltersVisible(false)}
|
||||
/>
|
||||
)}
|
||||
<Stack spacing="sm">
|
||||
<Group position="apart">
|
||||
<Group position="left" key="custom-actions" spacing={5}>
|
||||
{tableProps.tableActions?.map((group, idx) => (
|
||||
<Fragment key={idx}>{group}</Fragment>
|
||||
))}
|
||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="barcode-actions"
|
||||
icon={<IconBarcode />}
|
||||
label={t`Barcode actions`}
|
||||
tooltip={t`Barcode actions`}
|
||||
actions={tableProps.barcodeActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="printing-actions"
|
||||
icon={<IconPrinter />}
|
||||
label={t`Print actions`}
|
||||
tooltip={t`Print actions`}
|
||||
actions={tableProps.printingActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.enableBulkDelete ?? false) && (
|
||||
<ActionButton
|
||||
disabled={tableState.selectedRecords.length == 0}
|
||||
icon={<IconTrash />}
|
||||
color="red"
|
||||
tooltip={t`Delete selected records`}
|
||||
onClick={deleteSelectedRecords}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Space />
|
||||
<Group position="right" spacing={5}>
|
||||
{tableProps.enableSearch && (
|
||||
<TableSearchInput
|
||||
searchCallback={(term: string) =>
|
||||
tableState.setSearchTerm(term)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tableProps.enableRefresh && (
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Refresh data`}>
|
||||
<IconRefresh onClick={() => refetch()} />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
)}
|
||||
{hasSwitchableColumns && (
|
||||
<TableColumnSelect
|
||||
columns={dataColumns}
|
||||
onToggleColumn={toggleColumn}
|
||||
/>
|
||||
)}
|
||||
{tableProps.enableFilters && filters.length > 0 && (
|
||||
<Indicator
|
||||
size="xs"
|
||||
label={tableState.activeFilters?.length ?? 0}
|
||||
disabled={tableState.activeFilters?.length == 0}
|
||||
>
|
||||
<Boundary label="inventreetable">
|
||||
<Stack spacing="sm">
|
||||
<Group position="apart">
|
||||
<Group position="left" key="custom-actions" spacing={5}>
|
||||
{tableProps.tableActions?.map((group, idx) => (
|
||||
<Fragment key={idx}>{group}</Fragment>
|
||||
))}
|
||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="barcode-actions"
|
||||
icon={<IconBarcode />}
|
||||
label={t`Barcode actions`}
|
||||
tooltip={t`Barcode actions`}
|
||||
actions={tableProps.barcodeActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="printing-actions"
|
||||
icon={<IconPrinter />}
|
||||
label={t`Print actions`}
|
||||
tooltip={t`Print actions`}
|
||||
actions={tableProps.printingActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.enableBulkDelete ?? false) && (
|
||||
<ActionButton
|
||||
disabled={tableState.selectedRecords.length == 0}
|
||||
icon={<IconTrash />}
|
||||
color="red"
|
||||
tooltip={t`Delete selected records`}
|
||||
onClick={deleteSelectedRecords}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Space />
|
||||
<Group position="right" spacing={5}>
|
||||
{tableProps.enableSearch && (
|
||||
<TableSearchInput
|
||||
searchCallback={(term: string) =>
|
||||
tableState.setSearchTerm(term)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tableProps.enableRefresh && (
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Table filters`}>
|
||||
<IconFilter
|
||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||
/>
|
||||
<Tooltip label={t`Refresh data`}>
|
||||
<IconRefresh onClick={() => refetch()} />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
)}
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction
|
||||
key="download-action"
|
||||
downloadCallback={downloadData}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{hasSwitchableColumns && (
|
||||
<TableColumnSelect
|
||||
columns={dataColumns}
|
||||
onToggleColumn={toggleColumn}
|
||||
/>
|
||||
)}
|
||||
{tableProps.enableFilters && filters.length > 0 && (
|
||||
<Indicator
|
||||
size="xs"
|
||||
label={tableState.activeFilters?.length ?? 0}
|
||||
disabled={tableState.activeFilters?.length == 0}
|
||||
>
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Table filters`}>
|
||||
<IconFilter
|
||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
)}
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction
|
||||
key="download-action"
|
||||
downloadCallback={downloadData}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
<Box pos="relative">
|
||||
<LoadingOverlay
|
||||
visible={tableOptionQuery.isLoading || tableOptionQuery.isFetching}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
withBorder
|
||||
striped
|
||||
highlightOnHover
|
||||
loaderVariant="dots"
|
||||
pinLastColumn={tableProps.rowActions != undefined}
|
||||
idAccessor={tableProps.idAccessor}
|
||||
minHeight={300}
|
||||
totalRecords={tableState.recordCount}
|
||||
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
||||
page={tableState.page}
|
||||
onPageChange={tableState.setPage}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={handleSortStatusChange}
|
||||
selectedRecords={
|
||||
tableProps.enableSelection
|
||||
? tableState.selectedRecords
|
||||
: undefined
|
||||
}
|
||||
onSelectedRecordsChange={
|
||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||
}
|
||||
rowExpansion={tableProps.rowExpansion}
|
||||
rowStyle={tableProps.rowStyle}
|
||||
fetching={isFetching}
|
||||
noRecordsText={missingRecordsText}
|
||||
records={tableState.records}
|
||||
columns={dataColumns}
|
||||
onRowClick={handleRowClick}
|
||||
onCellClick={tableProps.onCellClick}
|
||||
defaultColumnProps={{
|
||||
noWrap: true,
|
||||
textAlignment: 'left',
|
||||
cellsStyle: {
|
||||
// TODO @SchrodingersGat : Need a better way of handling "wide" cells,
|
||||
overflow: 'hidden'
|
||||
<Box pos="relative">
|
||||
<LoadingOverlay
|
||||
visible={
|
||||
tableOptionQuery.isLoading || tableOptionQuery.isFetching
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
withBorder
|
||||
striped
|
||||
highlightOnHover
|
||||
loaderVariant="dots"
|
||||
pinLastColumn={tableProps.rowActions != undefined}
|
||||
idAccessor={tableProps.idAccessor}
|
||||
minHeight={300}
|
||||
totalRecords={tableState.recordCount}
|
||||
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
||||
page={tableState.page}
|
||||
onPageChange={tableState.setPage}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={handleSortStatusChange}
|
||||
selectedRecords={
|
||||
tableProps.enableSelection
|
||||
? tableState.selectedRecords
|
||||
: undefined
|
||||
}
|
||||
onSelectedRecordsChange={
|
||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||
}
|
||||
rowExpansion={tableProps.rowExpansion}
|
||||
rowStyle={tableProps.rowStyle}
|
||||
fetching={isFetching}
|
||||
noRecordsText={missingRecordsText}
|
||||
records={tableState.records}
|
||||
columns={dataColumns}
|
||||
onRowClick={handleRowClick}
|
||||
onCellClick={tableProps.onCellClick}
|
||||
defaultColumnProps={{
|
||||
noWrap: true,
|
||||
textAlignment: 'left',
|
||||
cellsStyle: {
|
||||
// TODO @SchrodingersGat : Need a better way of handling "wide" cells,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Boundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user